Графический интерфейс приложения Mac Cocoa зависает при запуске процесса - PullRequest
0 голосов
/ 29 июня 2018

Я сделал базовое приложение Mac OS X Cocoa с Swift в Xcode 9. Приложение собирает источник и пункт назначения от пользователя, а затем передает данные из источника в пункт назначения. Передача данных выполняется путем запуска сценария rsync в качестве процесса:

let path = "/bin/bash"
let arguments = ["/path/to/backup.sh", sourcePath, destinationPath]
task = Process.launchedProcess(launchPath: path, arguments: arguments as! [String])

Проблема в том, что во время выполнения передачи приложение получает вращающееся радужное колесо и графический интерфейс пользователя не может быть использован. Это делает невозможной кнопку «Отмена» или функциональный индикатор выполнения.

Как только передача заканчивается, приложение снова становится функциональным, и любой тестовый вывод (например, операторы печати, индикатор выполнения) проходит (вместо того, чтобы выполнять, пока резервное копирование выполнялось, как и предполагалось).

Я думал, что темы могут помочь решить эту проблему, поэтому я посмотрел этот пост о темах от Apple: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html

Однако код в статье, похоже, не имеет правильного синтаксиса в XCode, поэтому я не уверен, как продолжить работу с потоками.

Я в тупике, любая помощь будет признательна!

После запуска приложения, приостановки в отладчике и ввода «bt» в консоли отладчика это вывод:

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff65fc120a libsystem_kernel.dylib`mach_msg_trap + 10
frame #1: 0x00007fff65fc0724 libsystem_kernel.dylib`mach_msg + 60
frame #2: 0x00007fff3dac4045 CoreFoundation`__CFRunLoopServiceMachPort + 341
frame #3: 0x00007fff3dac3397 CoreFoundation`__CFRunLoopRun + 1783
frame #4: 0x00007fff3dac2a07 CoreFoundation`CFRunLoopRunSpecific + 487
frame #5: 0x00007fff3cda0d96 HIToolbox`RunCurrentEventLoopInMode + 286
frame #6: 0x00007fff3cda0b06 HIToolbox`ReceiveNextEventCommon + 613
frame #7: 0x00007fff3cda0884 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 64
frame #8: 0x00007fff3b053a73 AppKit`_DPSNextEvent + 2085
frame #9: 0x00007fff3b7e9e34 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044
frame #10: 0x00007fff3b048885 AppKit`-[NSApplication run] + 764
frame #11: 0x00007fff3b017a72 AppKit`NSApplicationMain + 804
* frame #12: 0x000000010000a88d Mac Syncy`main at AppDelegate.swift:12
frame #13: 0x00007fff65e7a015 libdyld.dylib`start + 1
frame #14: 0x00007fff65e7a015 libdyld.dylib`start + 1

1 Ответ

0 голосов
/ 01 июля 2018

Итак, я обнаружил несколько вещей:

  1. Фактически, launchedProcess вызывает немедленный запуск процесса. Вместо этого используйте init, чтобы получить больше контроля
  2. Если вы позвоните waitForExit, то текущий поток будет ждать до конца процесса.
  3. Когда вы запускаете процесс, он запускается независимо от вашего приложения. Поэтому, если вы выйдете из приложения, запущенный процесс все равно продолжит работать.

Итак, давайте начнем (полностью рабочий контроллер вида в самом конце):

1. Создание процесса

task = Process.init()
task.launchPath = path
task.arguments = arguments
task.currentDirectoryPath = workDir
// not running yet, so let's start it:
task.lauch()

2. Ожидание завершения дочернего процесса асинхронно

DispatchQueue.global().async {
    print ("wating for exit")
    self.task.waitUntilExit()
}

3. Убить ребенка процесс

Вам придется завершить задачу, например, в applicationWillTerminate в приложении делегат.

Тем не менее, вы должны знать, что это может привести к тому, что ваша (rsync) операция останется в неопределенном состоянии - файл / каталоги будут только наполовину скопированы и т. Д.

4. Бонус: индикатор прогресса

Я думаю, что единственный способ предоставить индикатор прогресса - это проанализировать выходные данные процесса (task.standardOutput) и проверить, предоставляет ли rsync полезную информацию здесь. Но это совершенно новая история, поэтому здесь нет кода, извините.

код

Это контроллер вида с кнопкой запуска и отмены. Имейте в виду, что для поставляемого приложения вам нужно будет обеспечить дополнительную проверку ошибок.

class ViewController: NSViewController {

    var task:Process!
    var out:FileHandle?
    var outputTimer: Timer?


    @IBAction func startPressed(_ sender: NSButton) {
        print("** starting **")

        let path = "/bin/bash"
        let workDir = "/path/to/working/folder"
        let sourcePath = "source"
        let destinationPath = "destination"

        let arguments = ["backup.sh", sourcePath, destinationPath]
        task = Process.init()
        task.launchPath = path
        task.arguments = arguments
        task.currentDirectoryPath = workDir

        self.task.launch()
        DispatchQueue.global().async {
            // this runs in a worker thread, so the UI remains responsive
            print ("wating for exit")
            self.task.waitUntilExit()
            DispatchQueue.main.async {
                // do so in the main thread
                if let timer = self.outputTimer {
                    timer.invalidate()
                    self.outputTimer = nil
                }
            }
        }

    }


    @IBAction func cancelPressed(_ sender: NSButton) {
        print("** cancelling **")
        if let timer = self.outputTimer {
            timer.invalidate()
            self.outputTimer = nil
        }
        task.interrupt()
    }
}
...