from gi.repository import GObject
from .common import ST_ACTIVE, ST_TRANSITION, OnoffDevice
+from .gobject import spawn_child
logger = logging.getLogger("onoff.process")
class OnoffProcess(OnoffDevice):
+ """A device that in activated by starting a process and deactivated by
+ killing the process.
+
+ @type pid: int or None
+ @ivar pid: is either None if there is no process or the pid of the
+ spawned process
+ @ivar starting: is either None or a GObject event source id of the
+ callback sigalling the end of the activation transition.
+ @ivar watch: is either None or a GObject event source id of the
+ callback waiting for the termination of the spawned process.
+ @type killed: bool
+ @ivar killed: indicates whether the termination signal has been sent
+ to the spawned process.
+ """
def __init__(self, command, start_wait=0, termsig=signal.SIGTERM):
+ """
+ @type command: [str]
+ @param command: an argument vector to be executed. The first element
+ is used as executable and looked up in $PATH.
+ @param start_wait: duration of the transition period from inactive to
+ active in seconds.
+ @param termsig: termination signal to be sent to the process to
+ deactivate it. The process must exit in response to this
+ signal.
+ """
OnoffDevice.__init__(self)
self.command = command
self.start_wait = start_wait
self.termsig = termsig
- self.state = 0
+ self.desired_state = 0 # bit mask of just ST_ACTIVE
self.pid = None
self.starting = None # timeout event during start
self.watch = None # watch event
+ self.killed = False
+
+ @property
+ def state(self):
+ if self.starting or self.killed:
+ return self.desired_state | ST_TRANSITION
+ return self.desired_state
def start_process(self):
- assert self.state == 0
assert self.pid is None
assert self.starting is None
logger.info("starting command %s", " ".join(self.command))
- self.state = ST_ACTIVE|ST_TRANSITION
- ret = GObject.spawn_async(self.command, flags=GObject.SPAWN_SEARCH_PATH | GObject.SPAWN_DO_NOT_REAP_CHILD)
- self.pid = ret[0]
+ self.pid, self.watch = spawn_child(self.command, self.process_died)
logger.debug("started as pid %d", self.pid)
- self.watch = GObject.child_watch_add(self.pid, self.process_died)
- self.starting = GObject.timeout_add(1000 * self.start_wait,
+ self.starting = GObject.timeout_add(int(1000 * self.start_wait),
self.process_started)
self.changestate(self.state)
+ def cancel_start_wait(self):
+ if self.starting is None:
+ return
+ logger.debug("cancelling start notification")
+ ret = GObject.source_remove(self.starting)
+ assert ret
+ self.starting = None
+
def process_died(self, pid, condition):
- assert self.state != 0
assert self.pid == pid
assert self.watch is not None
- self.state = 0
self.watch = None
self.pid = None
+ self.killed = False
logger.info("process %d died", pid)
- self.changestate(self.state)
+ self.cancel_start_wait()
+ if self.desired_state == ST_ACTIVE:
+ self.start_process()
+ else:
+ self.changestate(self.state)
def process_started(self):
- assert self.state == ST_ACTIVE|ST_TRANSITION
+ assert self.desired_state == ST_ACTIVE
assert self.starting is not None
- self.state = ST_ACTIVE
self.starting = None
logger.debug("process started")
self.changestate(self.state)
def stop_process(self):
- assert self.state & ST_ACTIVE
- if self.state & ST_TRANSITION:
- logger.debug("cancelling start notification")
- assert self.starting is not None
- ret = GObject.source_remove(self.starting)
- assert ret
- self.starting = None
- self.state = ST_TRANSITION
+ assert self.pid is not None
+ assert self.watch is not None
+ self.cancel_start_wait()
logger.info("killing process %d", self.pid)
os.kill(self.pid, self.termsig)
+ self.killed = True
self.changestate(self.state)
def activate(self):
- if self.state == 0:
- self.start_process()
- else:
- logger.debug("not activating from state %d", self.state)
+ if self.desired_state != ST_ACTIVE:
+ self.desired_state = ST_ACTIVE
+ if self.pid is None:
+ self.start_process()
+ else:
+ logger.debug("already activated. nothing to do")
def deactivate(self):
- if self.state & ST_ACTIVE:
- self.stop_process()
- else:
- logger.debug("not deactivating from state %d", self.state)
+ if self.desired_state != 0:
+ self.desired_state = 0
+ if self.pid is not None and not self.killed:
+ self.stop_process()
+ else:
+ logger.debug("already deactivated. nothing to do.")
def close(self):
self.deactivate()