Cocoa macos应用程序中的陷阱SIGINT

时间:2018-05-08 03:51:12

标签: swift macos

我正在尝试为一个为MacOS制作的UI应用程序捕获一个SIGINT。在app delegate类中,我看到以下方法:

func applicationWillTerminate(_ aNotification: Notification) {

}

但是, Ctrl + C ,SIGINT,永远不会被捕到这里。在互联网上阅读,表明这个功能无法保证执行,特别是如果应用程序在后台运行。

我可以在app委托中做什么来捕获SIGINT?或者我是否有另一个地方来捕捉中断,以便我可以适当地关闭资源?

3 个答案:

答案 0 :(得分:5)

查尔斯的答案是正确的,但他的警告("确保只能在处理程序中调用可重入的函数")是一个极端的限制。可以使用kqueueCFFileDescriptor将信号处理重定向到更安全的环境。

Technical Note TN2050: Observing Process Lifetimes Without Polling是一个不同的主题,但说明了这项技术。在那里,Apple用这种方式描述了查尔斯的警告:

  

由于执行不力,听取信号可能会很棘手   与信号处理程序相关的环境。具体来说,如果你   安装一个信号处理程序(使用signalsigaction),你必须非常   小心你在那个处理程序中做了什么。很少有功能是安全的   从信号处理程序调用。例如,分配是不安全的   内存使用malloc

     

信号处理程序安全的函数( async-signal   安全功能)列在sigaction man page

     

在大多数情况下,您必须采取额外步骤来重定向传入信号   一个更明智的环境。

我已从那里获取代码插图并对其进行了修改以便处理SIGINT。对不起,这是Objective-C。这是一次性设置代码:

// Ignore SIGINT so it doesn't terminate the process.

signal(SIGINT, SIG_IGN);

// Create the kqueue and set it up to watch for SIGINT. Use the 
// EV_RECEIPT flag to ensure that we get what we expect.

int kq = kqueue();

struct kevent changes;
EV_SET(&changes, SIGINT, EVFILT_SIGNAL, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
(void) kevent(kq, &changes, 1, &changes, 1, NULL);

// Wrap the kqueue in a CFFileDescriptor. Then create a run-loop source
// from the CFFileDescriptor and add that to the runloop.

CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL };
CFFileDescriptorRef kqRef = CFFileDescriptorCreate(NULL, kq, true, sigint_handler, &context);
CFRunLoopSourceRef rls = CFFileDescriptorCreateRunLoopSource(NULL, kqRef, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);

CFFileDescriptorEnableCallBacks(kqRef, kCFFileDescriptorReadCallBack);
CFRelease(kqRef);

以下是如何实现上面引用的sigint_handler回调:

static void sigint_handler(CFFileDescriptorRef f,  CFOptionFlags callBackTypes, void *info)
{
    struct kevent event;

    (void) kevent(CFFileDescriptorGetNativeDescriptor(f), NULL, 0, &event, 1, NULL);
    CFFileDescriptorEnableCallBacks(f, kCFFileDescriptorReadCallBack);

    // You've been notified!
}

请注意,此技术要求您在线程上运行设置代码,只要您对处理SIGINT(可能是应用程序的生命周期)和服务/运行感兴趣,该线程就会生效它的运行循环。系统为其自身目的创建的线程(例如为Grand Central Dispatch队列提供服务的线程)适用于此目的。

该应用程序的主要线程将起作用,您可以使用它。 但是,如果主线程锁定或无响应,则它不会为其运行循环提供服务,并且SIGINT处理程序不会被调用。由于SIGINT通常用于中断这种卡住的过程,因此主线程可能不合适。

所以,您可能想要生成自己的线程来监视此信号。它应该什么都不做,因为其他任何东西都可能导致它卡住。即使在那里,也存在问题。你的处理函数将在你的后台线程上调用,主线程可能仍然被锁定。在系统库中有很多只是主线程的东西,而你却无法做到这一点。但是你比POSIX风格的信号处理程序具有更大的灵活性。

我应该补充一点,GCD的调度源也可以监视UNIX信号,并且更易于使用,特别是来自Swift。但是,他们没有预先创建专用线程来运行处理程序。处理程序将提交到队列。现在,您可以指定一个高优先级/高QOS队列,但是我不完全确定如果进程有许多已经运行的失控线程,那么处理程序将会运行。也就是说,实际在高优先级队列上运行的任务优先于优先级较低的线程或队列,但启动新任务可能不会。我不确定。

答案 1 :(得分:2)

您可以通过sigaction()功能执行此操作:

import Foundation

let handler: @convention(c) (Int32) -> () = { sig in
    // handle the signal somehow
}

var action = sigaction(__sigaction_u: unsafeBitCast(handler, to: __sigaction_u.self),
                        sa_mask: 0,
                        sa_flags: 0)

sigaction(SIGINT, &action, nil)

确保只从处理程序中调用可重入函数。

答案 2 :(得分:0)

尽管我不敢与上面的绝妙答案竞争,但我认为有一些重要的说法可以大大简化解决方案。

我认为您的问题混合了两个非常不同的MacOS程序生命周期管理级别。

是的,您可以在Mac上运行简单的posix风格的可执行文件-但是Mac Cocoa应用程序不是具有“附加UI”的posix应用程序。 Mac应用程序依赖于非常详细,完整且功能丰富的生命周期机制,该机制所能提供的功能远远超过posix进程所能提供的任何功能-从状态保存和恢复(被杀死/重新启动时)到自动启动以响应打开文件。能源管理,处理计算机睡眠,背景和前景转换,在附近的设备,模式等上附加到同一应用程序。

此外,我看不到有人期望键盘上的'ctrl-C'到达Cocoa(“ UI”)应用程序,因为在这种情况下键盘快捷键完全不同,因此您可以'不能从终端/外壳同步运行“ UI”应用。

现在可以使用“ command-preiod ”组合键来“中断”被“卡住”或需要太长时间才能执行某项操作的Cocoa应用程序,该方法仍被许多人广泛采用应用程序(Photoshop,视频编辑器,Finder等)。很抱歉,我无法在Apple用户界面指南中找到此定义-也许它不再是该标准的一部分。但是,Ctrl-C当然不是!

您可以在应用中实现Cmd-Period(⌘。)(即注册此高级“中断”操作,并妥善处理。)

有一个很好的理由为什么包裹可可应用程序的NSApplication对象会忽略SIGINT!这完全没有上下文。