混合图形用户界面和命令行的 OS X 应用程序

7
有没有可能创建一个单一的Mac OS X应用程序,严格从*nix命令行(使用stdin / stdout,从终端控制台或通过ssh等)运行,但也可以通过应用程序图标启动并使用Mac GUI进行所有用户交互(不需要终端)? 如果是这样,怎么做?
我已经使用两个可执行文件实现了类似的功能,一个用于另一个的GUI前端,使用管道、套接字、共享内存或Applescript(等等)进行通信。这个问题是是否可以使用单个可执行文件和单个应用程序进程空间来完成这个操作。
有没有一种方法可以在不向命令行可执行文件传递新的命令行参数的情况下完成此操作?(因为与命令行应用程序的使用相关的遗留问题)。
7个回答

4
我相信你所寻找的是一种方法来确定你是从GUI还是命令行启动的。一个简单的方法是获取你的父进程标识符。如果你是GUI进程,那么你是由launchd(用户会话launchd)启动的。如果你是命令行,那么一个shell或脚本启动了你(无论如何,不是launchd)。现在,如何做到这一点 - 一个简单的方法是使用proc_info系统调用(#336),它方便地被libproc包装(检查)。具体来说,你需要proc_bsdinfo(参见),它有pbi_ppid字段来指定你的父进程(而父进程的pbi_comm将告诉你它的名称-以查看它是否是launchd)。你可以通过在进入NSApplicationMain之前检查父进程来实现命令行功能。(就像其他答案中一样,你的二进制文件最终会出现在你的.app/Contents/MacOS/中,所以是的,符号链接是有意义的)。

我在使用 getppid() 来确定这是一个“UI”启动还是“shell”启动的技术上遇到了问题,因为我需要让我的“命令行”作为 launchDaemon 运行... 因此,它的父进程 ID 将会是 1,甚至(尤其是)当意图作为没有 UI 的命令行运行时也是如此。我需要另一个技巧。 - Motti Shneor

3
我不确定这个方法是否有效,但是在Info.plistLSEnvironment键的文档中,指定的环境变量只有在通过Finder启动应用程序时才会定义。
如果你在LSEnvironment中定义一个变量(我们称之为MYAPP_GUI),并在启动时检查它的值,那么在使用Finder时它应该存在,而在使用Terminal时则不存在。这将告诉您是否显示GUI或使用stdio。

这是一个很好的想法,我没有想到。但是,有些恶意人可能会读取我的info-plist并在从shell运行时模拟该Env变量,你觉得呢? - Motti Shneor
谢谢,它像魔法一样运行良好。即使有人可能会搞乱它,这仍然是我首选的方法,除非有更健壮的方式(来自苹果的某些API)告诉您启动的上下文。 - Motti Shneor
1
此外,您的答案也适用于通过命令行中的“open”启动,从“登录项”,从“LaunchPad”以及所有其他类似用户的启动方式(即由LaunchServices完成)。所有其他“编程/命令行/launchd”启动都不会有该环境变量。这是我首选的答案。 - Motti Shneor

1
尽管@3doubloons提供了正确的答案,但我仍想分享一下我的代码片段来演示这个问题。
首先,在您的Info.plist中添加一个LSEnvironment键。它的值是一个env-var-names字典(作为键)和它们的字符串(只允许字符串!)值。在我的情况下,我添加了LaunchUI键,其值为"Yup"。

enter image description here

然后,我的 main() 函数看起来像这样:

int main(int argc, const char * argv[]) {
    int status = -1;
    @autoreleasepool {
        
        do {
            NSProcessInfo *procInfo = [NSProcessInfo processInfo];

            // deterine "UI" launch versus "command-line
            id uiEnvVar = [[procInfo environment] objectForKey:@"LaunchUI"];
            if([uiEnvVar isKindOfClass:[NSString class]] && [uiEnvVar isEqualToString:@"Yup"] ) {    
                status = NSApplicationMain(argc, argv); //launch app normally with UI
                break;
            }
            
            id arguments = [procInfo arguments];
            if (arguments == nil) {
                printUsage();
                break;
            }
            if ([arguments count] == 1 || [arguments containsObject:@"-h"] || [arguments containsObject:@"-help"]) {
                printUsage();
                status = 0;
                break;
            }
            
            //process further arguments
            if(processArgs(arguments) == NO) {
                printUsage();
                break;
            }

            // kick off protection.

            if(YES != do_work()) {
                break;
            }
            
            status = 0; //

            // if your command-line should stay alive and handle events - you can do the following to block it from exiting.
            [[NSRunLoop currentRunLoop] run];
        } while(false);
    }
    return status;
}

我认为这差不多就是了。享受吧!


0

是的,只需在程序中添加一个开关--command-line。除非在运行时传递了--command-line参数,否则启动正常的GUI。

./myprog --command-line

这基本上就是全部内容了。


如果我猜的话,我会说这个问题是如何在代码中实现像 --command-line 开关一样的东西。 - Corey Ogburn
如果是这样的话,我建议将问题重命名为“请为我解释基本逻辑”。问题是“可能吗?”除非原帖编辑他的问题或告诉我们他正在使用哪种编程语言,否则我认为这个答案已经足够了。 - ZnArK
很好的回答,但请查看编辑以获取额外要求。在启动后打开/关闭stdio与GUI处理并不是问题所在。 - hotpaw2
+1 我同意。我猜我已经提供了详细的阐述了。:) 但实际上,我同意,我们需要更多来自OP的信息,比如他使用哪种编程语言,也许还有他main()函数的一些示例代码。 - ZnArK
Xcode构建了几乎相同的泛型Objective C应用程序(目前有两个版本,一个使用stdio,一个使用Cocoa)。如果可能的话,我更喜欢分发一个可执行文件而不是两个。 - hotpaw2
2
GUI应用程序不是可执行文件,而是一个目录。里面有很多东西,包括可执行文件。您可以使用主函数的标准参数来检查是否需要启动GUI,然后仅分发应用程序包,如@ZnArK所建议的那样。至于“没有新参数”,我只能建议您在Unix环境中进行操作,或者使用脚本添加参数(它是一个单独的文件,但不是单独的二进制文件,并且可以在首次执行时由应用程序创建,如果您不想分发它)。 - Analog File

0

需要注意的是,这是我在 .Net 应用程序中所做的事情,我认为这个想法可以在其他语言和平台上实现。

我会创建一个控制台应用程序,然后仍然使用所有 GUI 类。当程序启动时,根据确定它是 GUI 实例还是命令行实例的任何标准,我要么启动 GUI,要么相应地启动命令行界面。

最重要的是,您必须有一些条件来确定何时要显示 GUI 而不是命令行。也许通过关于会话的系统变量(它是否是 ssh 会话,然后是命令行,否则是 GUI)。


0

我认为以你期望的方式是不可能的。GUI OSX 应用实际上是应用程序包,即它们实际上是包含除了实际可执行文件之外的大量资源的目录。你肯定不能像命令行可执行文件一样在 shell 中执行这些包(例如,你不能在 shell 提示符上执行 ./MyApplication.app --some-option,因为你的 shell 不知道如何执行目录),尽管你可能知道你可以通过 open MyApplication.app 以 GUI 方式运行它们。现在,你可以访问包内的实际可执行文件,并且可能有可能使可执行文件作为 CLI 可执行文件工作——我在这里胡说八道,因为我没有使用 Xcode 工具及其构建系统的实际经验,但可能有可能使实际可执行文件在 CLI 和 GUI 调用中都能工作。

然而,这并不是非常有用,因为这些可执行文件被埋藏在应用程序包中,通常无法直接在 shell 中调用(即它们肯定不在您的 $PATH 中,因此您必须将它们作为类似 ./MyApplication.app/Contents/MacOS/actualbinaryname 的东西来调用)。我在现实世界中看到的通常方法是提供一个单独的 CLI 可执行文件和您的应用程序捆绑在一起,这通常会通过安装程序或通过单击应用程序首选项中的按钮放置在适当的位置(例如 /usr/local/bin)。如果您打算与 GUI 应用程序交互,那么这些 CLI 可执行文件会负责处理它(从您的问题中并不清楚您是否是这样做)。


是的。我现在使用两个可执行文件来完成这项工作。如果这2个可执行文件可以以某种方式构建为单个文件(具有适当的内部条件代码,用于 GUI vs. stdio 的大部分代码已经存在),那么在 /usr/local/bin 中创建一个符号链接将浪费更少的磁盘空间。 - hotpaw2
不需要双重可执行文件或符号链接。只有你的main()函数应该决定是调用NSApplicationMain(argc, argv);还是执行你的命令行代码。 - Motti Shneor

0

如果我理解有误请见谅,但是如果你正在使用Objective-C,那么其中一部分答案不是使用Objective-C中的NSProcessInfo类并检查“arguments”变量以确定是否传递了任何参数,然后在代码中决定下一步该做什么吗?


如果它不需要参数,或者是从“登录项”或Doc中启动的呢?根本区别在于是通过macOS的“LaunchServices”启动还是其他方式启动。目前最好的答案是在下面的Info.plist中通过LSEnvironment键启动。 - Motti Shneor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接