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