简体   繁体   English

Python - 如何在Mac OS X上使用GUI应用程序制作守护程序?

[英]Python - How to make a daemon out of GUI Application on Mac OS X?

On Windows it is easy. 在Windows上很容易。 Just run your program with pythonw instead with python and code will be executed in the background. 只需用pythonw运行你的程序而不是python,代码将在后台执行。

So, the thing I wish to achieve is easily arranged. 所以,我希望实现的目标很容易安排。

I have an application which is really a service doing underground stuff. 我有一个应用程序,它真的是一个做地下工作的服务。 But this service needs a control panel. 但是这项服务需要一个控制面板。

So, on Windows I use wxPython to create a GUI, even some wx stuff to provide needed service, and when user is done with adjustments she/he clicks Hide and Show(False) is called on main window. 所以,在Windows上我使用wxPython来创建一个GUI,甚至一些wx的东西来提供所需的服务,当用户完成调整后,她/他点击隐藏和显示(False)在主窗口上调用。

Thus the GUI disappears and the service continues its work in the background. 因此,GUI消失,服务在后台继续工作。 User can always bring it back using a hotkey. 用户可以随时使用热键将其恢复。

The trouble is that on Mac OS X this strategy works only to some degree. 麻烦的是,在Mac OS X上,这种策略只在某种程度上起作用。

When wx.Frame.Show(False) is called, the window disappears along with its menu bar and service works fine, but the Application is still visible there. 当调用wx.Frame.Show(False)时,窗口随其菜单栏一起消失,服务工作正常,但应用程序仍在那里可见。

You can switch to it regardless the fact that you cannot do anything with it. 无论您无法对其进行任何操作,都可以切换到它。 It is still present in the Dock etc. etc. 它仍然存在于Dock等等。

This happens when program is using python or pythonw or when it is bundled with Py2App. 当程序使用python或pythonw或与Py2App捆绑时会发生这种情况。

No matter what I do, the icon stays there. 无论我做什么,图标都会停留在那里。

There must be some trick that allows a programmer to remove this naughty icon and thus stop bothering poor little user when she/he doesn't want to be bothered. 必须有一些技巧,允许程序员删除这个顽皮的图标,从而当他/他不想被打扰时,停止打扰可怜的小用户。

Hiding window is obviously not enough. 隐藏窗口显然是不够的。 Anyone knows the trick? 谁知道诀窍?

NB: I would really like to do it the way I described above and not mess with two separate processes and IPC. 注意:我真的很想按照上面描述的方式进行,而不是混淆两个独立的进程和IPC。

Edit: 编辑:

After much digging I found these: 经过多次挖掘,我发现了这些:

How to hide application icon from Mac OS X dock 如何从Mac OS X dock隐藏应用程序图标

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

How to hide the Dock icon 如何隐藏Dock图标

According to last link the proper way to do it is to use: 根据最后一个链接,正确的方法是使用:

[NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory];

or 要么

[NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];

So what I want (runtime switching from background to foreground and back) is possible. 所以我想要的(运行时从后台切换到前台和后台)是可能的。

But how to do it from Python??? 但是如何从Python中做到这一点???

Constants: NSApplicationActivationPolicyProhibited and NSApplicationActivationPolicyAccessory are present in AppKit, but I cannot find setApplicationActivationPolicy function anywhere. 常量:NSApplicationActivationPolicyProhibited和NSApplicationActivationPolicyAccessory存在于AppKit中,但我无法在任何地方找到setApplicationActivationPolicy函数。

NSApp() doesn't have it. NSApp()没有它。

I know there is a way of doing it by loading objc dylib with ctypes, delegating to NSApp and sending "setApplicationActivationPolicy: <constant_value>", but I don't know how much will this mess with wx.App(). 我知道有一种方法可以通过使用ctypes加载objc dylib,委托给NSApp并发送“setApplicationActivationPolicy:<constant_value>”,但我不知道wx.App()会造成多大的麻烦。 And it is a bit much work for something that should be available already. 对于应该已经可用的东西来说,它有点多功。

In my experience, NSApp() and wx.App() active at the same time dislike eachother pretty much. 根据我的经验,同时活跃的NSApp()和wx.App()非常不喜欢彼此。

Perhaps we can get the NSApp() instance that wx is using somehow and use wx's delegate??? 也许我们可以得到wx以某种方式使用的NSApp()实例并使用wx的委托???

Remember please, already suggested solutions with starting as agent and switching to foreground or running multiple processes and doing IPC is very undesirable in my case. 请记住,已经建议的解决方案是启动代理并切换到前台或运行多个进程并且在我的情况下执行IPC是非常不受欢迎的。

So, ideally, using setApplicationActivationPolicy is my goal, but how? 所以,理想情况下,使用setApplicationActivationPolicy是我的目标,但如何? (Simple and easy and no messup to wx.App() please.) (简单易行,wx.App()请不要搞砸。)

Any ideas??? 有任何想法吗???

OK people, there is a good, nice and correct solution without any messing around. 好的人,有一个好的,好的和正确的解决方案,没有任何混乱。

Firstly, I want to explain why Windows GUI process goes to background when wx.Frame.Show(MyFrame, False) is called. 首先,我想解释为什么在调用wx.Frame.Show(MyFrame,False)时Windows GUI进程转到后台。

Very short explanation and skipping over details is that Windows consider the Window and an application the same thing. 非常简短的解释和跳过细节是Windows认为Window和应用程序是一回事。

Ie The main element of the MS Windows application is your main GUI window. 即MS Windows应用程序的主要元素是您的主GUI窗口。

So, when this window is hidden, an application has no more GUI and continues to run in background. 因此,当隐藏此窗口时,应用程序不再有GUI并继续在后台运行。

Mac OS X considers the application to be your application and any windows you choose to put into it are its children so to speak. Mac OS X认为应用程序是您的应用程序,您选择放入其中的任何窗口都可以说是它的孩子。

This allows you to have an application running while presenting no windows but a menu bar, from which you may choose an action which would then generate a needed window. 这允许您在不显示窗口的情况下运行应用程序,而不是菜单栏,您可以从中选择一个操作,然后生成所需的窗口。

Very handy for editors where you may have more than one file opened at once, each in its own window and when you close the last one, you can still open a new one or create a blank one, etc. etc. 对于您可能同时打开多个文件的编辑器非常方便,每个文件都在自己的窗口中,当您关闭最后一个文件时,您仍然可以打开一个新文件或创建一个空白文件等。

Therefore a main element of Mac OS X application is the application itself, and that is why it stays opened after last window is hidden, logically. 因此,Mac OS X应用程序的一个主要元素是应用程序本身,这就是它在逻辑上隐藏最后一个窗口后保持打开的原因。 Destroying its menu bar also will not help. 销毁其菜单栏也无济于事。 The name of the app will stay present in Dock and in application switcher and in Force Quit. 该应用程序的名称将保留在Dock和应用程序切换器以及强制退出中。 You will be able to switch to it and do nothing. 您将能够切换到它并且什么都不做。 :D But, luckily, Mac provides us with function to put it to background though. :D但是,幸运的是,Mac为我们提供了将它放到后台的功能。 And this function is already mentioned setApplicationActivationPolicy() from NSApp object. 并且已经从NSApp对象中提到了此函数setApplicationActivationPolicy()。

The trouble was its naming in Python's AppKit, which is NSApp.setActivationPolicy_(). 麻烦的是它在Python的AppKit中命名,它是NSApp.setActivationPolicy_()。 To complicate matters further, it is not available directly from Python's interactive shell but it has to be called at least from an imported module. 更复杂的是,它不能直接从Python的交互式shell中获得,但必须至少从导入的模块中调用它。

Why? 为什么? I have no idea. 我不知道。 Anyway here is a complete example for throwing an application into background that will work on Mac and Windows. 无论如何,这里是一个完整的例子,可以将应用程序投入到适用于Mac和Windows的后台。

I didn't try it on Linux, which combines behaviour of Mac and Windows in matter of presenting an app, so, whether only hiding a window would be enough remains to be seen. 我没有在Linux上尝试它,它在呈现应用程序时结合了Mac和Windows的行为,因此,是否只隐藏窗口就足够了还有待观察。

Feel free to try and submit an edit to make the example more cross-platform. 随意尝试提交编辑,使示例更具跨平台性。

Example: 例:



"""
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()

Of course, this example may be started from terminal or with CLI window. 当然,此示例可以从终端或CLI窗口启动。 In this case the terminal control over your program will stay opened while app only will appear and disappear. 在这种情况下,对程序的终端控制将保持打开状态,而app只会出现并消失。

To complete your GUI daemon, you should start it with pythonw (on Windows) or launch it from daemontest.pyw file, 要完成GUI守护程序,您应该使用pythonw(在Windows上)启动它或从daemontest.pyw文件启动它,

and on Mac you should use: 在Mac上你应该使用:

% nohup python daemontest.py &

or bundle it with py2app or use Python launcher that comes with python.org Python version to start daemontest.py without terminal. 或者将它与py2app捆绑在一起,或者使用python.org Python版本附带的Python启动程序启动daemontest.py而不使用终端。

Note: This example suffers from the same flaw on Mac OS X that is mentioned on links I supplied in my question. 注意:此示例在我在我的问题中提供的链接上提到的Mac OS X上存在相同的缺陷。 I refer to the problem of wrong focusing and menu bar not instantly appearing when app comes from background. 我指的是当应用程序来自背景时,错误聚焦和菜单栏不会立即出现的问题。 User has to switch around and come back to newly returned app for it to work properly. 用户必须切换并返回新返回的应用程序才能正常工作。 I hope somebody will solve this too. 我希望有人也能解决这个问题。 And soon. 等等。 It is quite annoying. 这很烦人。

One more note: If you have threads running in your program, pause them while daemonizing and exorcizing. 还有一点需要注意:如果您的程序中有线程运行,请在进行守护和驱除时暂停它们。 Especially if they are communicating with another app using Apple events. 特别是如果他们使用Apple活动与另一个应用程序通信。 To be frank, something about wx.Timers should be done too. 坦率地说,关于wx.Timers的事情也应该做。 If you are not careful you may get leaking problems around non-existing NSAutoreleasePool and/or SegmentationFault upon program termination. 如果您不小心,在程序终止时,您可能会在不存在的NSAutoreleasePool和/或SegmentationFault周围出现泄漏问题。

Ok. 好。 Here is the code to do what you want to do: 以下是执行您要执行的操作的代码:

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

This the messier answer you do not want to do, but I will list it anyway. 这是你不想做的更糟糕的答案,但无论如何我会列出它。 In the info.plist file add in this key: info.plist文件中添加以下密钥:

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

Another more daemonish solution but means it can't have a GUI, you add in this key to the info.plist file: 另一个更符合守护程序的解决方案,但意味着它不能拥有GUI,您可以将此密钥添加到info.plist文件中:

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

Source 资源

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM