drop unused event id from spawn_child
[~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     @type killed: bool
22     @ivar killed: indicates whether the termination signal has been sent
23             to the spawned process.
24     """
25     def __init__(self, command, start_wait=0, termsig=signal.SIGTERM):
26         """
27         @type command: [str]
28         @param command: an argument vector to be executed. The first element
29                 is used as executable and looked up in $PATH.
30         @param start_wait: duration of the transition period from inactive to
31                 active in seconds.
32         @param termsig: termination signal to be sent to the process to
33                 deactivate it. The process must exit in response to this
34                 signal.
35         """
36         OnoffDevice.__init__(self)
37         self.command = command
38         self.start_wait = start_wait
39         self.termsig = termsig
40         self.desired_state = 0 # bit mask of just ST_ACTIVE
41         self.pid = None
42         self.starting = None # timeout event during start
43         self.killed = False
44
45     @property
46     def state(self):
47         if self.starting or self.killed:
48             return self.desired_state | ST_TRANSITION
49         return self.desired_state
50
51     def start_process(self):
52         assert self.pid is None
53         assert self.starting is None
54         logger.info("starting command %s", " ".join(self.command))
55         self.pid = spawn_child(self.command, self.process_died)
56         logger.debug("started as pid %d", self.pid)
57         self.starting = GObject.timeout_add(int(1000 * self.start_wait),
58                                             self.process_started)
59         self.changestate(self.state)
60
61     def cancel_start_wait(self):
62         if self.starting is None:
63             return
64         logger.debug("cancelling start notification")
65         ret = GObject.source_remove(self.starting)
66         assert ret
67         self.starting = None
68
69     def process_died(self, pid, condition):
70         assert self.pid == pid
71         self.pid = None
72         self.killed = False
73         logger.info("process %d died", pid)
74         self.cancel_start_wait()
75         if self.desired_state == ST_ACTIVE:
76             self.start_process()
77         else:
78             self.changestate(self.state)
79
80     def process_started(self):
81         assert self.desired_state == ST_ACTIVE
82         assert self.starting is not None
83         self.starting = None
84         logger.debug("process started")
85         self.changestate(self.state)
86
87     def stop_process(self):
88         assert self.pid is not None
89         self.cancel_start_wait()
90         logger.info("killing process %d", self.pid)
91         os.kill(self.pid, self.termsig)
92         self.killed = True
93         self.changestate(self.state)
94
95     def activate(self):
96         if self.desired_state != ST_ACTIVE:
97             self.desired_state = ST_ACTIVE
98             if self.pid is None:
99                 self.start_process()
100             else:
101                 logger.debug("already activated. nothing to do")
102
103     def deactivate(self):
104         if self.desired_state != 0:
105             self.desired_state = 0
106             if self.pid is not None and not self.killed:
107                 self.stop_process()
108             else:
109                 logger.debug("already deactivated. nothing to do.")
110
111     def close(self):
112         self.deactivate()