Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

On Windows it is easy. Just run your program with pythonw instead with python and code will be executed in the background.

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.

Thus the GUI disappears and the service continues its work in the background. User can always bring it back using a hotkey.

The trouble is that on Mac OS X this strategy works only to some degree.

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.

You can switch to it regardless the fact that you cannot do anything with it. It is still present in the Dock etc. etc.

This happens when program is using python or pythonw or when it is bundled with 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?

N.B.: I would really like to do it the way I described above and not mess with two separate processes and IPC.

Edit:

After much digging I found these:

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

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???

Constants: NSApplicationActivationPolicyProhibited and NSApplicationActivationPolicyAccessory are present in AppKit, but I cannot find setApplicationActivationPolicy function anywhere.

NSApp() doesn't have it.

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

Perhaps we can get the NSApp() instance that wx is using somehow and use wx's delegate???

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.

So, ideally, using setApplicationActivationPolicy is my goal, but how? (Simple and easy and no messup to wx.App() please.)

Any ideas???

like image 761
Dalen Avatar asked Oct 18 '22 20:10

Dalen


1 Answers

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.

Very short explanation and skipping over details is that Windows consider the Window and an application the same thing.

I.e. The main element of the MS Windows application is your main GUI window.

So, when this window is hidden, an application has no more GUI and continues to run in background.

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.

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. 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. 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. And this function is already mentioned setApplicationActivationPolicy() from NSApp object.

The trouble was its naming in Python's AppKit, which is 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.

Why? I have no idea. Anyway here is a complete example for throwing an application into background that will work on Mac and 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.

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. In this case the terminal control over your program will stay opened while app only will appear and disappear.

To complete your GUI daemon, you should start it with pythonw (on Windows) or launch it from daemontest.pyw file,

and on Mac you should use:

% 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.

Note: This example suffers from the same flaw on Mac OS X that is mentioned on links I supplied in my question. 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. To be frank, something about wx.Timers should be done too. If you are not careful you may get leaking problems around non-existing NSAutoreleasePool and/or SegmentationFault upon program termination.

like image 159
Dalen Avatar answered Oct 23 '22 11:10

Dalen