【问题标题】:Python - How to make a daemon out of GUI Application on Mac OS X?Python - 如何在 Mac OS X 上使用 GUI 应用程序制作守护程序?
【发布时间】:2016-03-18 17:44:53
【问题描述】:

在 Windows 上很容易。只需使用 pythonw 而不是 python 运行您的程序,代码将在后台执行。

所以,我想要实现的事情很容易安排。

我有一个应用程序,它实际上是一个做地下工作的服务。但是这个服务需要一个控制面板。

因此,在 Windows 上,我使用 wxPython 创建 GUI,甚至使用一些 wx 东西来提供所需的服务,当用户完成调整后,她/他单击 Hide 并在主窗口上调用 Show(False)。

因此,GUI 消失,服务在后台继续工作。用户可以随时使用热键将其恢复。

问题在于,在 Mac OS X 上,这种策略只能在一定程度上起作用。

当调用 wx.Frame.Show(False) 时,窗口连同其菜单栏一起消失,服务正常工作,但应用程序仍然可见。

你可以切换到它,不管你不能用它做任何事情。它仍然存在于 Dock 等中。

当程序使用 python 或 pythonw 或与 Py2App 捆绑时会发生这种情况。

无论我做什么,图标都会停留在那里。

一定有一些技巧可以让程序员删除这个顽皮的图标,从而在她/他不想被打扰时停止打扰可怜的小用户。

隐藏窗口显然是不够的。有人知道诀窍吗?

N.B.:我真的很想按照我上面描述的方式来做,而不是混淆两个独立的进程和 IPC。

编辑:

经过大量挖掘,我发现了这些:

How to hide application icon from Mac OS X dock

http://codesorcery.net/2008/02/06/feature-requests-versus-the-right-way-to-do-it

How to hide the Dock icon

根据最后一个链接,正确的方法是使用:

[NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory];

[NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];

所以我想要的(运行时从后台切换到前台再返回)是可能的。

但是如何从 Python 中做到这一点???

常量:AppKit 中存在 NSApplicationActivationPolicyProhibited 和 NSApplicationActivationPolicyAccessory,但我在任何地方都找不到 setApplicationActivationPolicy 函数。

NSApp() 没有。

我知道有一种方法可以通过使用 ctypes 加载 objc dylib,委托给 NSApp 并发送“setApplicationActivationPolicy: ”,但我不知道这会对 wx.App() 造成多大影响。对于应该已经可用的东西来说,这需要做很多工作。

根据我的经验,同时激活的 NSApp() 和 wx.App() 非常讨厌对方。

也许我们可以通过某种方式获取 wx 正在使用的 NSApp() 实例并使用 wx 的委托???

请记住,在我的情况下,已经提出了从代理开始并切换到前台或运行多个进程并进行 IPC 的建议解决方案。

所以,理想情况下,使用 setApplicationActivationPolicy 是我的目标,但如何? (请简单易用,不要弄乱 wx.App()。)

有什么想法吗???

【问题讨论】:

  • P.S 为什么不使用info.plist 解决方案?
  • 因为这意味着进程在后台启动,我想显示的任何 GUI 都可以,但是在我失去焦点后我将无法切换回这个应用程序,即 Dock 图标不会展示。这可以通过使用 TransformProcessType() 来纠正,我不知道如何再次从 Python 调用它,以及它隐藏在哪里,如果它完全实现的话。但是,即使我这样做,也意味着我必须有 2 个进程。一个守护进程,一个守护进程,一个 GUI,在请求时再次调用应用程序并告诉它作为守护进程运行。
  • 如果我这样做,我将不得不使用一些 IPC 方法来更改应用程序守护程序部分的参数,或者在用户更改 GUI 部分中的某些设置时将其杀死并使用新参数再次启动它.更不用说错误报告了。我将不得不使用 IPC 或系统退出代码或信号,否则我不知道如何实现这一点。很多并发症,包括我需要 wx 才能使“守护程序”服务正常运行。这并非不可能,如果必须,我会做类似的事情,但我会先尝试你的答案。我会让你知道发生了什么。

标签: python macos user-interface icons wxpython


【解决方案1】:

好的人们,有一个好的、好的和正确的解决方案,没有任何麻烦。

首先,我想解释一下为什么在调用 wx.Frame.Show(MyFrame, False) 时,Windows GUI 进程会进入后台。

非常简短的解释和略过细节是 Windows 将 Window 和应用程序视为同一事物。

即MS Windows 应用程序的主要元素是您的主 GUI 窗口。

因此,当隐藏此窗口时,应用程序将不再有 GUI,并继续在后台运行。

Mac OS X 将应用程序视为您的应用程序,您选择放入其中的任何窗口都是它的子级。

这允许您在运行应用程序时不显示窗口,只显示一个菜单栏,您可以从中选择一个操作,然后生成所需的窗口。

对于编辑器来说非常方便,您可能一次打开多个文件,每个文件都在自己的窗口中,当您关闭最后一个文件时,您仍然可以打开一个新文件或创建一个空白文件等。

因此,Mac OS X 应用程序的主要元素是应用程序本身,这就是逻辑上在最后一个窗口隐藏后它保持打开状态的原因。破坏它的菜单栏也无济于事。应用程序的名称将保留在 Dock 和应用程序切换器以及 Force Quit 中。您将能够切换到它并且什么都不做。 :D 但是,幸运的是,Mac 为我们提供了将其置于后台的功能。而这个函数在 NSApp 对象中已经提到了 setApplicationActivationPolicy()。

问题在于它在 Python 的 AppKit 中的命名,即 NSApp.setActivationPolicy_()。更复杂的是,它不能直接从 Python 的交互式 shell 中使用,但必须至少从导入的模块中调用。

为什么?我不知道。无论如何,这里是一个完整的例子,用于将一个应用程序放到后台,可以在 Mac 和 Windows 上运行。

我没有在 Linux 上尝试过,它结合了 Mac 和 Windows 在呈现应用方面的行为,因此,仅隐藏一个窗口是否足够还有待观察。

请随时尝试提交修改以使示例更具跨平台性。

例子:



"""
This app will show you small window with the randomly generated code that will confirm that reopened window is still the same app returned from background,
and the button allowing you to send it to background.
After you send it to background, wait 8 seconds and application will return to foreground again.
Too prove that the application is continuing its work in the background, the app will call wx.Bell() every second.
You should hear the sound while app is in the foreground and when it is in background too.

Merry Christmas and a happy New Year!
"""

import wx
import random, sys

if sys.platform=="darwin":
    from AppKit import NSBundle, NSApp, NSAutoreleasePool, NSApplicationActivationPolicyRegular, NSApplicationActivationPolicyProhibited

    # Use Info.plist values to know whether our process started as daemon
    # Also, change this dict in case anyone is later checking it (e.g. some module)
    # Note: Changing this dict doesn't change Info.plist file
    info = NSBundle.mainBundle().infoDictionary()

    def SendToBackground ():
        # Change info, just in case someone checks it later
        info["LSUIElement"] = "1"
        NSApp.setActivationPolicy_(NSApplicationActivationPolicyProhibited)

    def ReturnToForeground ():
        # Change info, just in case someone checks it later
        info["LSUIElement"] = "0"
        NSApp.setActivationPolicy_(NSApplicationActivationPolicyRegular)

else:
    # Simulate Mac OS X App - Info.plist
    info = {"LSUIElement": "0"} # Assume non background at startup
                                # If programmer chose not to display GUI at startup then she/he should change this before calling ReturnToForeground()
                                # To preserve consistency and allow correct IsDaemon() answer
    def SendToBackground ():
        info["LSUIElement"] = "1"

    def ReturnToForeground ():
        info["LSUIElement"] = "0"

def IsDaemon ():
    return info["LSUIElement"]=="1"

class Interface (wx.Frame):
    def __init__ (self):
        wx.Frame.__init__(self, None, -1, "Test", pos=(100, 100), size=(100, 100))
        wx.StaticText(self, -1, "Test code: "+str(random.randint(1000, 10000)), pos=(10, 10), size=(80, 20))
        b = wx.Button(self, -1, "DAEMONIZE ME", size=(80, 20), pos=(10, 50))
        wx.EVT_BUTTON(self, b.GetId(), self.OnDaemonize)
        self.belltimer = wx.Timer(self)
        wx.EVT_TIMER(self, self.belltimer.GetId(), self.OnBellTimer)
        self.belltimer.Start(1000)
        # On Mac OS X, you wouldn't be able to quit the app without the menu bar:
        if sys.platform=="darwin":
            self.SetMenuBar(wx.MenuBar())
        self.Show()

    def OnBellTimer (self, e):
        wx.Bell()

    def OnDaemonize (self, e):
        self.Show(False)
        SendToBackground()
        self.timer = wx.Timer(self)
        wx.EVT_TIMER(self, self.timer.GetId(), self.OnExorcize)
        self.timer.Start(8000)

    def OnExorcize (self, e):
        self.timer.Stop()
        ReturnToForeground()
        self.Show()
        self.Raise()

app = wx.App()
i = Interface()
app.MainLoop()

当然,这个例子可以从终端启动,也可以用 CLI 窗口启动。在这种情况下,您的程序的终端控件将保持打开状态,而仅应用程序会出现和消失。

要完成您的 GUI 守护程序,您应该使用 pythonw 启动它(在 Windows 上)或从 daemontest.pyw 文件启动它,

在 Mac 上你应该使用:

% nohup python daemontest.py &

或与py2app捆绑或使用python.org Python版本附带的Python启动器在没有终端的情况下启动daemontest.py。

注意:此示例在 Mac OS X 上存在与我在问题中提供的链接中提到的相同的缺陷。我指的是当应用程序来自后台时,焦点错误和菜单栏没有立即出现的问题。用户必须切换并返回到新返回的应用程序才能正常工作。我也希望有人能解决这个问题。等等。很烦人。

另一个注意事项:如果您的程序中有线程正在运行,请在守护进程和驱除线程时暂停它们。尤其是当他们使用 Apple 事件与另一个应用程序通信时。坦率地说,关于 wx.Timers 的一些事情也应该做。如果您不小心,您可能会在程序终止时遇到不存在的 NSAutoreleasePool 和/或 SegmentationFault 的泄漏问题。

【讨论】:

  • 非常详细而且很有帮助!
【解决方案2】:

好的。这是执行您想做的事情的代码:

import AppKit
info = AppKit.NSBundle.mainBundle().infoDictionary()
info["LSUIElement"] = "1"

这是您不想做的更混乱的答案,但无论如何我都会列出它。在info.plist 文件中添加这个键:

<key>LSUIElement</key>
<string>1</string>

另一个更守护进程的解决方案,但意味着它不能有 GUI,您将此密钥添加到 info.plist 文件:

<key>LSBackgroundOnly</key>
<string>1</string>

Source

【讨论】:

  • 不,当然不是!操作此 dict 中的值可能仅在 wx.App() 或任何其他 App 类初始化之前生效。 wx.App() 从那里抓取值,如果 LSUIElement = 1,则应用不会呈现给用户。
  • -1 表示错误答案。 :D!然而,你是那个甚至敢于尝试给予的人。如果所讨论的 dict() 不是 dict() 而是类似 dict 的对象,它甚至可以工作,它会将更改变为计数。然后更改值将触发显示/隐藏。为那次尝试喝彩。此外,您的回答帮助我在疯狂地谷歌搜索时偶然发现了解决方案。看我的回答。我还使用您的 A 作为建议,建议您在解决完整解决方案时如何做一些事情。因此,我认为您很好地获得了 +50 赏金。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-18
  • 1970-01-01
  • 2016-06-28
  • 2011-03-09
  • 1970-01-01
  • 2011-11-07
  • 2012-05-17
相关资源
最近更新 更多