abstract away ScheduledFunction
[~helmut/onoff.git] / onoff / process.py
1 import logging
2 import os
3 import signal
4
5 from .common import ST_ACTIVE, ST_TRANSITION, OnoffDevice
6 from .gobject import spawn_child, ScheduledFunction
7
8 logger = logging.getLogger("onoff.process")
9
10 class OnoffProcess(OnoffDevice):
11     """A device that in activated by starting a process and deactivated by
12     killing the process.
13
14     @type pid: int or None
15     @ivar pid: is either None if there is no process or the pid of the
16             spawned process
17     @ivar starting: is either None or a ScheduledFunction representing the
18             callback signalling the end of the activation transition.
19     @type killed: bool
20     @ivar killed: indicates whether the termination signal has been sent
21             to the spawned process.
22     """
23     def __init__(self, command, start_wait=0, termsig=signal.SIGTERM):
24         """
25         @type command: [str]
26         @param command: an argument vector to be executed. The first element
27                 is used as executable and looked up in $PATH.
28         @param start_wait: duration of the transition period from inactive to
29                 active in seconds.
30         @param termsig: termination signal to be sent to the process to
31                 deactivate it. The process must exit in response to this
32                 signal.
33         """
34         OnoffDevice.__init__(self)
35         self.command = command
36         self.start_wait = start_wait
37         self.termsig = termsig
38         self.desired_state = 0 # bit mask of just ST_ACTIVE
39         self.pid = None
40         self.starting = None # timeout event during start
41         self.killed = False
42
43     @property
44     def state(self):
45         if self.starting or self.killed:
46             return self.desired_state | ST_TRANSITION
47         return self.desired_state
48
49     def start_process(self):
50         assert self.pid is None
51         assert self.starting is None
52         logger.info("starting command %s", " ".join(self.command))
53         self.pid = spawn_child(self.command, self.process_died)
54         logger.debug("started as pid %d", self.pid)
55         self.starting = ScheduledFunction(self.start_wait, self.process_started)
56         self.changestate(self.state)
57
58     def cancel_start_wait(self):
59         if self.starting is None:
60             return
61         logger.debug("cancelling start notification")
62         self.starting.cancel()
63         self.starting = None
64
65     def process_died(self, pid, condition):
66         assert self.pid == pid
67         self.pid = None
68         self.killed = False
69         logger.info("process %d died", pid)
70         self.cancel_start_wait()
71         if self.desired_state == ST_ACTIVE:
72             self.start_process()
73         else:
74             self.changestate(self.state)
75
76     def process_started(self):
77         assert self.desired_state == ST_ACTIVE
78         assert self.starting is not None
79         self.starting = None
80         logger.debug("process started")
81         self.changestate(self.state)
82
83     def stop_process(self):
84         assert self.pid is not None
85         self.cancel_start_wait()
86         logger.info("killing process %d", self.pid)
87         os.kill(self.pid, self.termsig)
88         self.killed = True
89         self.changestate(self.state)
90
91     def activate(self):
92         if self.desired_state != ST_ACTIVE:
93             self.desired_state = ST_ACTIVE
94             if self.pid is None:
95                 self.start_process()
96             else:
97                 logger.debug("already activated. nothing to do")
98
99     def deactivate(self):
100         if self.desired_state != 0:
101             self.desired_state = 0
102             if self.pid is not None and not self.killed:
103                 self.stop_process()
104             else:
105                 logger.debug("already deactivated. nothing to do.")
106
107     def close(self):
108         self.deactivate()