I'm trying to send signals from my pyobjc gui (a menu in the osx status bar) to the main process of my app. Specifically, I'm running the gui wrapped in a class and this inside a process, and I'm trying to send messages from the gui to the main process via a pipe.
When I use a simple method to put data into the pipe, my code works. The message gets passed to the main process, yielding main process... recv(): foo
When I start the gui in a subprocess and attempt to put data into the pipe, say when I click on the menu option 'start', nothing happens. The main process line never gets printed, and as far as I can tell the main process is blocked.
I'm assuming this has something to do with the event loop in pyobjc. What can I do to make this work? How can I run the pyobjc code as a subprocess?
main.py
import sys
from multiprocessing import Process, Pipe
from userinterface import OSXstatusbaritem
def f2(pipe):
print "starting subprocess f2"
print pipe.send("foo")
pipe.close()
def main():
pipeUI, pipeServer = Pipe()
# p = Process(target=f2, args=(pipeUI,)) # <---------------------- This works
p = Process(target=OSXstatusbaritem.start(pipeUI), args=()) # <----This doesn't
p.start()
print "main process... recv():", pipeServer.recv()
p.join()
if __name__ == "__main__": sys.exit(main())
userinterface.py
import objc, re, os
from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper
from multiprocessing import Pipe
status_images = {'idle':'./ghost.png'}
class OSXstatusbaritem(NSObject):
images = {}
statusbar = None
state = 'idle'
@classmethod
def start(self, pipe):
self.pipe = pipe
self.start_time = NSDate.date()
app = NSApplication.sharedApplication()
delegate = self.alloc().init()
app.setDelegate_(delegate)
AppHelper.runEventLoop()
def applicationDidFinishLaunching_(self, notification):
statusbar = NSStatusBar.systemStatusBar()
# Create the statusbar item
self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength)
# Load all images
for i in status_images.keys():
self.images[i] = NSImage.alloc().initByReferencingFile_(status_images[i])
# Set initial image
self.statusitem.setImage_(self.images['idle'])
# self.statusitem.setAlternateImage_(self.images['highlight'])
# Let it highlight upon clicking
self.statusitem.setHighlightMode_(1)
# Set a tooltip
self.statusitem.setToolTip_('Sample app')
# Build a very simple menu
self.menu = NSMenu.alloc().init()
# Start and stop service
menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Start Service', 'startService:', '')
self.menu.addItem_(menuitem)
menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Stop Service', 'stopService:', '')
self.menu.addItem_(menuitem)
# Add a separator
menuitem = NSMenuItem.separatorItem()
self.menu.addItem_(menuitem)
# Terminate event
menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Quit', 'terminate:', '')
self.menu.addItem_(menuitem)
# Bind it to the status item
self.statusitem.setMenu_(self.menu)
# Get the timer going
self.timer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_(self.start_time, 5.0, self, 'tick:', None, True)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSDefaultRunLoopMode)
self.timer.fire()
def tick_(self, notification):
print self.state
def startService_(self, notification):
self.pipe.send(["foobar", None])
print "starting service"
def stopService_(self, notification):
print "stopping service"
Your code creates the GUI objects in the main process (main.py) then uses that object in a subprocess created using fork. This in not supported by most of Apple's frameworks.
Furthermore the call to OSXstatusbaritem.start creates and runs the eventloop in the main process.
You might have more success by creating the GUI object in the child process, but even that is not guaranteed to work (if you're unlucky the GUI framework has already initialised and causes a crash when using it in the child process):
p = Process(target=OSXstatusbaritem.start, args=(pipeUI,))
The safest way to start the status bar item process it to use subprocess, to avoid creating a new process without calling execv(2). There was some talk on Python's tracker to add an option to the multiprocessing module to start new processes using fork+exec but that hasn't led to a commit yet.