从启动服务中隐藏 NSDocument 子类

Posted

技术标签:

【中文标题】从启动服务中隐藏 NSDocument 子类【英文标题】:Hide NSDocument subclass from Launch Services 【发布时间】:2020-04-09 09:38:14 【问题描述】:

我有一对基本上是双胞胎的应用程序——一个是客户端,另一个是服务器。它们共享许多相同的代码,并且都使用相同的 NSDocument 子类来实现它们共享的文档格式。客户端应用程序有一个用户界面,允许用户以可视方式处理文档,但服务器应用程序没有(尽管它确实作为常规应用程序运行,而不是作为守护程序运行),它被设计为不可见地运行。

问题在于,当文档放在其图标上时,服务器应用程序会被触发。如果服务器应用程序正在运行而客户端应用程序未运行,并且用户双击文档,也会触发服务器应用程序。在这种情况下,我希望启动服务启动客户端应用程序并打开文档,而是尝试使用服务器应用程序打开文档。我已经自定义了 NSApplicationDelegate application:openFile: 方法,以便服务器应用程序拒绝在这种情况下实际打开文档,但我想要的是 NSApplicationDelegate 不使用文档打开事件调用它。这让用户感到困惑,因为他们希望双击文档来打开客户端应用程序,无论服务器应用程序是否正在运行。

Apple 的 Core Foundation Keys 文档似乎表明这样做的方法是使用 LSHandlerRank 属性。

https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/TP40009249-SW1

对于客户端应用,我已将其设置为“所有者”。

<key>LSHandlerRank</key>
<string>Owner</string>

对于服务器应用程序,我将其设置为“无”。

<key>LSHandlerRank</key>
<string>None</string>

不幸的是,这没有效果。如果客户端应用程序没有运行,我仍然可以将文档放在服务器应用程序图标上,或者双击文档以使服务器应用程序前进。

我认为可能有希望的另一个属性是 CFBundleTypeRole。客户端应用程序的 plist 将此设置为“编辑器”。

<key>CFBundleTypeRole</key>
<string>Editor</string>

该属性的文档非常少,但确实说“无”是一个选项。所以我在服务器应用程序 plist 中进行了尝试,然后我无法再以编程方式打开我的 NSDocument 文件。另一方面,将文档放在服务器应用程序图标上仍然会导致图标亮起,因此 Launch Services 显然仍然认为服务器应用程序可以处理这种类型的文件。

总而言之,我需要对 plist 进行一些更改,以便我仍然可以以编程方式处理与我的 NSDocument 子类关联的文件,但我不希望 Launch Services 知道我的(服务器)应用程序可以使用这些文件。这可能吗?

---- 跟进彼得·霍西的回答----

感谢您的回答。我不知道 lsregister shell 命令,它看起来很方便。转储会在我的机器上产生 46 兆字节的数据!

但是,我很确定这不是缓存问题。虽然我的电脑有多个服务器应用程序副本,但这个问题是由客户发现的。他只有一个应用程序的副本,而且他只有很短的时间。我已经一年多没有更改 plist 的内容了,所以他的系统上不会有任何缓存。

您建议不要使用 Launch Services,但我没有说清楚,我没有利用它,并且不希望 Launch Services 打开我的文档(至少不使用我的应用程序的服务器版本)。事实上,我已经成功地修改了这个应用程序,如果 Launch Services 确实请求服务器应用程序打开一个文档,它会忽略这个请求。通过使用openDocumenWithContentsOfURL 的内部 TCP/IP 服务器来打开文档,如下所示:

[sharedDocumentController openDocumentWithContentsOfURL:databaseURL display:openWindows error:&err];

openDocumenWithContentsOfURL 方法似乎要求为文档类型设置 plist。我的问题是,这也告诉 Finder 这个应用程序可以处理我不想要的这种类型的文档。所以我正在寻找一种可以在应用程序中使用 NSDocument 的方法,但不将其暴露给 Finder(所以我认为不要将它暴露给 Launch Services)。也许这是不可能的。

是否有一些方法可以打开没有 URL 的文档,这样扩展名是什么或 plist 的设置方式都无关紧要?我在 NSDocumentController 类中没有看到任何这样的记录方法。在我看来,测试似乎证实了 NSDocument 类依赖于 plist 将文件扩展名链接到 NSDocument 子类。

如果有办法改变 plist 来做到这一点,那么我肯定需要使用 lsregister 来清除缓存来测试这个:)

【问题讨论】:

【参考方案1】:

哦,所以您没有使用启动服务(或 Apple 事件)将“文档”发送到服务器,但服务器通过使用 NSDocumentController“打开”“文档”来处理请求(通过其他方式接收)。您会发现 NSDocumentController 需要 Info.plist 中的文档类型才能知道要使用哪个 NSDocument 子类。

这是您可以在 NSDocumentController 子类中覆盖的内容:

-typeForContentsOfURL:error::给定一个 URL,返回一个字符串,指示该 URL 所指的文档类型。默认行为是通过查找在其标签中包含 URL 的 pathExtension 的类型来实现的。根据您想要的严格程度,您可以进行类似的检查,并且仅在它是由您的客户端发送的文档时才返回类型名称,或者您可能是懒惰的并且只返回一个常量字符串(如果您的服务器只处理一种类型的“文件”)。 -documentClassForType::给定一个文档类型名称,返回一个 NSDocument 子类来实例化以管理该类型的文档。只要您只处理一种类型并因此只有一个 NSDocument 子类,您就可以无条件地返回您的 NSDocument 子类;如果您有多个这样的子类或想为将来可能的情况做计划,请将类型名称与每个已知类型名称进行比较并返回相应的子类。 -displayNameForType::给定一个文档类型名称,返回一个包含该类型用户可呈现名称的本地化字符串(例如,英文中的“web page”,而不是“public.html”)。在您的情况下可能不需要。 -documentClassNames:返回 NSDocument 子类的名称数组,这些子类可能被实例化以管理应用程序可以处理的文档。对于您的目的可能不是必需的,但默认实现会参考您的 Info.plist。 -defaultType:用于新文档的文档类型的名称(如在newDocument: 操作中)。如果您不从服务器创建新文档,则可能不相关,但默认实现会参考您的 Info.plist(它返回您已声明自己为编辑器的第一个类型)。

在您的 NSDocumentController 子类中重写这些方法后,您的文档控制器将不再参考 Info.plist,并且您可以删除 Info.plist 中有关您不希望 LS 知道的文档类型的信息。

如果您还没有使用 NSDocumentController 子类,则需要创建一个,并在程序的早期调用 [MyDocumentController sharedDocumentController](您甚至可能必须在 main 中执行此操作才能在 nib 加载之前执行此操作;自从我查看文档控制器实例化的时间和地点以来已经有一段时间了)。将该消息发送到您的子类将确保从该子类创建文档控制器,从而具有您在该子类中实现的行为。

【讨论】:

覆盖 -typeForContentsOfURL: 和 -documentClassForType: 以返回硬编码值(我复制了 Apple 的逻辑,但没有引用 plist),然后从 plist 中删除 CFBundleDocumentTypes 条目就可以了。如另一个答案中所述,在进行这些更改后,我还必须使用 lsregister 清除 LS 缓存。【参考方案2】:

使用 LS 的挑战之一是缓存失效(众所周知,这是计算机科学中最难解决的两个问题之一,还有命名和非一个错误)。 LS 出于性能原因想要缓存所有内容,因此当您更改某些内容时,确保从 LS 的数据库中清除旧信息变得很重要。

首先要检查的是您拥有多少个服务器应用程序副本。如果您曾经将它从构建产品文件夹中复制出来,那么旧的副本可能就是 LS 试图用来处理文档的那个。

要检查的第二件事是 LS 认为您拥有哪些服务器应用程序副本,以及它认为它们可以处理什么。

使用lsregister -dump 实现这两种方法。 lsregister 隐藏在 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister 中,它的转储将为您提供有关注册了哪些类型 (UTI)、注册了哪些应用程序包的大量信息(包括不同位置不同版本、不同能力)、服务等。

lsregister -help 会告诉您它的其他选项,其中一些可能有助于从 LS 的数据库中清除过时/无用的记录。

我可能会避免为此使用 LS。可以在服务端应用的Info.plist中导入该类型,但不要将其列为文档类型,也不要使用客户端的LS通过服务端应用打开文档。

相反,使用 NSAppleEventDescriptor 创建您自己的 Open Documents 事件,然后使用 AESendMessage 将其发送到服务器应用程序。事件类为kCoreEventClass,事件ID为kAEOpenDocuments。目标描述符应该是一个 typeApplicationBundleID 类型的 NSAppleEventDescriptor,包含作为字符串的服务器应用程序包 ID。

将事件的 keyDirectObject 关键字参数设置为至少一个 typeFileURL 描述符的列表描述符,该描述符包含要打开的文档的 URL(也表示为字符串)。

【讨论】:

以上是关于从启动服务中隐藏 NSDocument 子类的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中隐藏控制台应用程序

NSDocument 子类是不是是模型对象?

使用 NSDocument 时如何隐藏保存面板

如何获取 NSDocument 子类来打印自定义视图

`attemptRecovery(fromError:optionIndex:)` 在 NSDocument 的 Swift 子类的超类中找不到

华云数据推动政府服务数字化:西企服平台政企服务大厅正式启动