【问题标题】:Find out location of an executable file in Cocoa在 Cocoa 中找出可执行文件的位置
【发布时间】:2008-10-16 14:55:27
【问题描述】:

简单的问题是:如何在 Cocoa 应用程序中找出可执行文件的位置。

请记住,在许多类 Unix 操作系统中,人们使用 PATH 环境为其可执行文件分配首选位置,尤其是当他们的系统中有多个版本的相同应用程序时。作为一个好的实践,我们的 Cocoa 应用程序应该找到它需要的可执行文件的首选位置。

例如,Leopard 默认配置中的 SVN 1.4 位于 /usr/bin,而您安装了一个更新的版本,例如通过 MacPorts 在 /opt/local/bin 安装的 SVN 1.5.3。然后你使用 /etc/path.d 或 .bash_profile 或 .zshrc 设置你的 PATH :

导出 PATH=/opt/local/bin:$PATH

因此您可以使用新版本的 svn 而不是系统中的旧版本。它适用于任何终端环境。但不是在 Cocoa 应用程序中。据我所知,Cocoa 应用程序只有一个默认的 PATH 环境,如下所示:

导出 PATH="/usr/bin:/bin:/usr/sbin:/sbin"

默认不会使用/etc/path.d、.bash_profile、.profile、.zshrc等中的配置

那么我们到底该怎么做呢?

附言我们有a semi-solution here,但它不能完全满足这个问题的目标。

【问题讨论】:

    标签: cocoa macos


    【解决方案1】:

    尝试执行此操作的棘手之处在于,用户可以将其 shell 设置为任何类型:sh、bash、csh、tcsh 等,并且每个 shell 设置其终端环境的方式不同。我不确定自己是否会为此自找麻烦,但如果您真的愿意,这就是我会采取的路线。

    第一步是弄清楚用户的shell。在 OS X 上,此信息存储在目录服务中,可以通过 DirectoryService.framework 中的 API 或使用dscl 命令行工具访问。 DirectoryService API 实在是太让人头疼了,所以我可能会选择 CLI 路线。在 Cocoa 中,您可以使用 NSTask 执行带有参数的工具以获取用户的 shell(我将在其他地方详细说明)。该命令看起来像:

    dscl -plist localhost -read /Local/Default/Users/username UserShell

    这将返回 XML 文本,您可以将其解释为 plist 并转换为 NSDictionary,或者您可以省略 -plist 选项并自己解析文本输出。

    一旦你知道了用户shell的路径,下一步就是执行那个shell并告诉它运行env命令来打印出用户的环境。看起来大多数 shell 都接受 -c 命令行选项,让你传入一个字符串来执行 - 我想你只需要假设它是用户选择的任何 shell 的通用接口。

    一旦您有了用户的环境,您就可以从中获取他们的路径列表,并从中搜索您正在寻找的任何可执行文件。就像我说的,我真的不知道这是否值得麻烦,但如果我要实现它,这就是我的方向。

    【讨论】:

    • 我认为您已经为标准解决方案提供了一个简短而清晰的途径。我知道这不是很常见的东西,但在某些情况下确实很有用。我会试试看。谢谢!
    • 顺便说一句,我认为在 Leopard 中处理 PATH 的建议方法是通过 /etc/paths.d ,它适用于任何 shell。但似乎没有多少人使用它:(
    • 确保检查 shell 是否在 /etc/shells 中,如果是则只运行它。 shell 可以是任何程序,而不仅仅是实际的 shell,而且人们可能正在使用 /sbin/nologin 或其他程序作为其 shell 的特殊用户帐户运行您的应用程序。
    • 几个月前我发布了执行此操作的确切代码。我现在描述了我在生产中使用它时发现的注意事项。请参阅我自己对这个问题的回答。
    【解决方案2】:

    与 Brian Webster 的回答有关:

    获取用户外壳的更简单方法是使用 NSProcessInfo 类。例如

    NSDictionary *environmentDict = [[NSProcessInfo processInfo] environment];
    NSString *shellString = [environmentDict objectForKey:@"SHELL"];
    

    这比使用 dscl 和解析 XML 输入更容易。

    【讨论】:

      【解决方案3】:

      这是我基于上述答案的实现,从 applicationDidFinishLaunching 调用:

      // from http://cocoawithlove.com/2009/05/invoking-other-processes-in-cocoa.html
      #import "NSTask+OneLineTasksWithOutput.h"
      
      void FixUnixPath() {
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){
              NSString *userShell = [[[NSProcessInfo processInfo] environment] objectForKey:@"SHELL"];
              NSLog(@"User's shell is %@", userShell);
      
              // avoid executing stuff like /sbin/nologin as a shell
              BOOL isValidShell = NO;
              for (NSString *validShell in [[NSString stringWithContentsOfFile:@"/etc/shells" encoding:NSUTF8StringEncoding error:nil] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) {
                  if ([[validShell stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString:userShell]) {
                      isValidShell = YES;
                      break;
                  }
              }
      
              if (!isValidShell) {
                  NSLog(@"Shell %@ is not in /etc/shells, won't continue.", userShell);
                  return;
              }
              NSString *userPath = [[NSTask stringByLaunchingPath:userShell withArguments:[NSArray arrayWithObjects:@"-c", @"echo $PATH", nil] error:nil] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
              if (userPath.length > 0 && [userPath rangeOfString:@":"].length > 0 && [userPath rangeOfString:@"/usr/bin"].length > 0) {
                  // BINGO!
                  NSLog(@"User's PATH as reported by %@ is %@", userShell, userPath);
                  setenv("PATH", [userPath fileSystemRepresentation], 1);
              }
          });
      }
      

      附:之所以有效,是因为它捕获了 shell 所做的环境更改。例如。 RVM 在安装时将PATH=$PATH:$HOME/.rvm/bin 添加到 .bashrc。 Cocoa 应用程序是从 launchd 启动的,因此它们的 PATH 中没有这些更改。

      我对这段代码不是 100% 满意,因为它不能捕获所有内容。我的初衷是专门处理 RVM,所以我这里不得不使用非登录 shell,但实际上人们随机把 PATH 修改放到 .bashrc 和 .bash_profile 中,所以最好同时运行。

      我的一个用户甚至在他的 shell 配置文件中有一个交互式菜单 (!!!),这自然会导致这段代码挂起,我只为他导出一个 shell env 标志。 :-) 添加超时可能是个好主意。

      这还假设 shell 与 bourne 兼容,因此不适用于在黑客社区中越来越流行的 fish 2.0。 (Fish 认为 $PATH 是一个数组,而不是一个以冒号分隔的字符串。因此默认情况下它使用空格作为分隔符来打印它。人们可能会想出一个简单的修复方法,比如运行 for i in $PATH; echo "PATH=$i"; end 然后只取以开头的行PATH=。过滤在任何情况下都是一个好主意,因为配置文件脚本通常会自己打印一些东西。)

      最后一点,一年多来,此代码一直是发布应用程序的重要组成部分(Mac App Store 在一年中的大部分时间里排名前 10 位的付费开发者工具)。但是,我现在正在实施沙盒并将其取出;自然,您不能从沙盒应用程序中执行此操作。我用对 RVM 和朋友的明确支持来替换它,并手动复制他们各自的环境变化。

      对于那些希望从沙盒应用程序使用系统 Git 之类的东西的人,请注意,虽然您无权读取文件和枚举目录,但您可以访问 stat — [[NSFileManager defaultManager] fileExistsAtPath:path]。您可以使用它来探测查找二进制文件的典型文件夹的硬编码列表,当您找到位置(如 /usr/local 或 /opt/local 或其他)时,请用户通过 NSOpenPanel 授予您访问权限。这不会涵盖所有情况,但可以处理 90% 的用例,并且是开箱即用您可以为用户做的最好的事情。

      【讨论】:

      • 这是错误的,它只是设置了与从[NSProcessInfo processInfo] environment][@"PATH"] 获得的相同的 PATH 值。记住执行的 shell 命令 (echo $PATH) 在当前进程的环境变量下运行。
      • @Zdenek 看起来确实是这样,但不是,因为登录 shell 执行的配置文件会更改 $PATH。曾经在你的 .bash_profile 中设置过一些东西吗?而且 Cocoa 应用程序不是从登录 shell 中启动的,因此它们的环境中没有 .bash_profile/.zprofile/etc 的结果。
      • 我发现这几乎是正确的。但是您需要添加“-l”参数以使 shell 像登录 shell 一样工作。从这个示例中获得的 PATH 与 NSProcessInfo 的路径相同,因为 bash 从父(Cocoa)进程继承环境变量 - 除非您提供 -l"
      • @gngrwzrd 我刚刚查看了我的生产代码,我在那里使用了--login -i 组合(在-c 之前)。我记得它需要几次迭代才能达到。 (请注意,即使没有 -l,bash 仍然会执行 .bashrc,因此上面的代码会看到那里所做的任何修改。)
      【解决方案4】:

      您的用户拥有您正在使用的工具的自定义版本的可能性有多大(以及您的应用与该工具的任意版本兼容的可能性有多大)?如果答案是“不太”,那么考虑默认使用系统提供的工具的路径,并为高级用户提供一种指定自己的路径作为首选项的方法。

      【讨论】:

        【解决方案5】:

        Finder 的路径(以及任何 GUI 启动的 Cocoa 应用程序)不是从您的登录 shell 设置的吗?如果您的登录 shell 和您在 Terminal.app 中使用的 shell 不同,那可能会导致一些混乱。

        此信息可能会有所帮助: http://lists.apple.com/archives/cocoa-dev/2005/Oct/msg00528.html

        显然,为 GUI 进程设置环境变量的“正确”方法是在隐藏的 .plist 文件中。我确信我曾经知道这一点,然后很快就忘记了。

        【讨论】:

        • 这可能是一个解决方案:~/.MacOSX/environment.plist 我还找到了一种自动制作这个 .plist 的方法。感谢您的信息。
        • 需要澄清的一些事实: (1) Finder 的路径不是从登录 shell 设置的。 (2) 没有“因此”;其他 GUI 启动的 Cocoa 应用程序实际上是由 launchd 启动的,而不是由 Finder 本身启动的。 (3) 是的,plist 是处理它的正确方法,但如果你有机会向用户解释,你不妨让他们找到二进制文件,这可能是对于 10 个用户中的 9 个来说更容易。
        猜你喜欢
        • 1970-01-01
        • 2016-06-13
        • 1970-01-01
        • 2010-10-30
        • 1970-01-01
        相关资源
        最近更新 更多