-
-
Notifications
You must be signed in to change notification settings - Fork 28
/
manager.py
169 lines (139 loc) · 5.92 KB
/
manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import os
import platform
from time import sleep
import logging
import subprocess
from typing import Optional, List
import aw_core
logger = logging.getLogger(__name__)
def _locate_executable(name: str) -> List[str]:
"""
Will start module from localdir if present there,
otherwise will try to call what is available in PATH.
Returns it as a Popen cmd list.
"""
curr_filepath = os.path.realpath(__file__)
curr_dir = os.path.dirname(curr_filepath)
search_paths = [curr_dir, os.path.abspath(os.path.join(curr_dir, os.pardir))]
exec_paths = [os.path.join(path, name) for path in search_paths]
for exec_path in exec_paths:
if os.path.isfile(exec_path):
# logger.debug("Found executable for {} in: {}".format(name, exec_path))
return [exec_path]
break # this break is redundant, but kept due to for-else semantics
else:
# TODO: Actually check if it is in PATH
# logger.debug("Trying to start {} using PATH (executable not found in: {})"
# .format(name, exec_paths))
return [name]
class Module:
def __init__(self, name: str, testing: bool = False) -> None:
self.name = name
self.started = False
self.testing = testing
self._process = None # type: Optional[subprocess.Popen]
self._last_process = None # type: Optional[subprocess.Popen]
def start(self) -> None:
logger.info("Starting module {}".format(self.name))
# Create a process group, become its leader
if platform.system() != "Windows":
os.setpgrp()
exec_cmd = _locate_executable(self.name)
if self.testing:
exec_cmd.append("--testing")
# logger.debug("Running: {}".format(exec_cmd))
# Don't display a console window on Windows
# See: https://github.com/ActivityWatch/activitywatch/issues/212
startupinfo = None
if platform.system() == "Windows":
startupinfo = subprocess.STARTUPINFO() # type: ignore
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
elif platform.system() == "Darwin":
logger.info("Macos: Disable dock icon")
import AppKit
AppKit.NSBundle.mainBundle().infoDictionary()["LSBackgroundOnly"] = "1"
# There is a very good reason stdout and stderr is not PIPE here
# See: https://github.com/ActivityWatch/aw-server/issues/27
self._process = subprocess.Popen(exec_cmd, universal_newlines=True, startupinfo=startupinfo)
# Should be True if module is supposed to be running, else False
self.started = True
def stop(self) -> None:
"""
Stops a module, and waits until it terminates.
"""
# TODO: What if a module doesn't stop? Add timeout to p.wait() and then do a p.kill() if timeout is hit
if not self.started:
logger.warning("Tried to stop module {}, but it hasn't been started".format(self.name))
return
elif not self.is_alive():
logger.warning("Tried to stop module {}, but it wasn't running".format(self.name))
else:
if not self._process:
logger.error("No reference to process object")
logger.debug("Stopping module {}".format(self.name))
if self._process:
self._process.terminate()
logger.debug("Waiting for module {} to shut down".format(self.name))
if self._process:
self._process.wait()
logger.info("Stopped module {}".format(self.name))
assert not self.is_alive()
self._last_process = self._process
self._process = None
self.started = False
def toggle(self) -> None:
if self.started:
self.stop()
else:
self.start()
def is_alive(self) -> bool:
if self._process is None:
return False
self._process.poll()
# If returncode is none after p.poll(), module is still running
return True if self._process.returncode is None else False
def read_log(self) -> str:
"""Useful if you want to retrieve the logs of a module"""
log_path = aw_core.log.get_latest_log_file(self.name, self.testing)
if log_path:
with open(log_path) as f:
return f.read()
else:
return "No log file found"
class Manager:
def __init__(self, testing: bool=False) -> None:
# TODO: Fetch these from somewhere appropriate (auto detect or a config file)
# Save to config wether they should autostart or not.
_possible_modules = [
"aw-server",
"aw-watcher-afk",
"aw-watcher-window",
# "aw-watcher-spotify",
# "aw-watcher-network"
]
# TODO: Filter away all modules not available on system
self.modules = {name: Module(name, testing=testing) for name in _possible_modules}
def get_unexpected_stops(self):
return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values()))
def start(self, module_name):
if module_name in self.modules.keys():
self.modules[module_name].start()
else:
logger.error("Unable to start module '{}': No such module".format(module_name))
def autostart(self, autostart_modules):
# Always start aw-server first
if "aw-server" in autostart_modules:
self.start("aw-server")
autostart_modules = set(autostart_modules) - {"aw-server"}
for module_name in autostart_modules:
self.start(module_name)
def stop_all(self):
for module in filter(lambda m: m.is_alive(), self.modules.values()):
module.stop()
if __name__ == "__main__":
manager = Manager()
for module in manager.modules.values():
module.start()
sleep(2)
assert module.is_alive()
module.stop()