1
1
import os
2
2
import platform
3
+ from glob import glob
3
4
from time import sleep
4
5
import logging
5
6
import subprocess
7
+ import shutil
6
8
from typing import Optional , List
7
9
8
10
import aw_core
9
11
10
12
logger = logging .getLogger (__name__ )
11
13
14
+ _module_dir = os .path .dirname (os .path .realpath (__file__ ))
15
+ _parent_dir = os .path .abspath (os .path .join (_module_dir , os .pardir ))
16
+ _search_paths = [_module_dir , _parent_dir ]
12
17
13
- def _locate_executable (name : str ) -> List [str ]:
14
- """
15
- Will start module from localdir if present there,
16
- otherwise will try to call what is available in PATH.
17
18
18
- Returns it as a Popen cmd list.
19
- """
20
- curr_filepath = os .path .realpath (__file__ )
21
- curr_dir = os .path .dirname (curr_filepath )
22
- search_paths = [curr_dir , os .path .abspath (os .path .join (curr_dir , os .pardir ))]
23
- exec_paths = [os .path .join (path , name ) for path in search_paths ]
19
+ def _locate_bundled_executable (name : str ) -> Optional [str ]:
20
+ """Returns the path to the module executable if it exists in the bundle, else None."""
21
+ _exec_paths = [os .path .join (path , name ) for path in _search_paths ]
24
22
25
- for exec_path in exec_paths :
23
+ # Look for it in the installation path
24
+ for exec_path in _exec_paths :
26
25
if os .path .isfile (exec_path ):
27
26
# logger.debug("Found executable for {} in: {}".format(name, exec_path))
28
- return [exec_path ]
29
- break # this break is redundant, but kept due to for-else semantics
27
+ return exec_path
28
+
29
+
30
+ def _is_system_module (name ) -> bool :
31
+ """Checks if a module with a particular name exists in PATH"""
32
+ return shutil .which (name ) is not None
33
+
34
+
35
+ def _locate_executable (name : str ) -> Optional [str ]:
36
+ """
37
+ Will return the path to the executable if bundled,
38
+ otherwise returns the name if it is available in PATH.
39
+
40
+ Used when calling Popen.
41
+ """
42
+ exec_path = _locate_bundled_executable (name )
43
+ if exec_path is not None : # Check if it exists in bundle
44
+ return exec_path
45
+ elif _is_system_module (name ): # Check if it's in PATH
46
+ return name
30
47
else :
31
- # TODO: Actually check if it is in PATH
32
- # logger.debug("Trying to start {} using PATH (executable not found in: {})"
33
- # .format(name, exec_paths))
34
- return [name ]
48
+ logger .warning ("Could not find module '{}' in installation directory or PATH" .format (name ))
49
+ return None
50
+
51
+
52
+ def _discover_modules_bundled () -> List [str ]:
53
+ # Look for modules in source dir and parent dir
54
+ modules = []
55
+ for path in _search_paths :
56
+ matches = glob (os .path .join (path , "aw-*" ))
57
+ for match in matches :
58
+ if os .path .isfile (match ) and os .access (match , os .X_OK ):
59
+ modules .append (match )
60
+ else :
61
+ logger .warning ("Found matching file but was not executable: {}" .format (path ))
62
+
63
+ logger .info ("Found bundled modules: {}" .format (set (modules )))
64
+ return modules
65
+
66
+
67
+ def _discover_modules_system () -> List [str ]:
68
+ search_paths = os .environ ["PATH" ].split (":" )
69
+ modules = []
70
+ for path in search_paths :
71
+ files = os .listdir (path )
72
+ for filename in files :
73
+ if "aw-" in filename :
74
+ modules .append (filename )
75
+
76
+ logger .info ("Found system modules: {}" .format (set (modules )))
77
+ return modules
35
78
36
79
37
80
class Module :
@@ -46,20 +89,25 @@ def start(self) -> None:
46
89
logger .info ("Starting module {}" .format (self .name ))
47
90
48
91
# Create a process group, become its leader
92
+ # TODO: This shouldn't go here
49
93
if platform .system () != "Windows" :
50
94
os .setpgrp ()
51
95
52
- exec_cmd = _locate_executable (self .name )
53
- if self .testing :
54
- exec_cmd .append ("--testing" )
55
- # logger.debug("Running: {}".format(exec_cmd))
96
+ exec_path = _locate_executable (self .name )
97
+ if exec_path is None :
98
+ return
99
+ else :
100
+ exec_cmd = [exec_path ]
101
+ if self .testing :
102
+ exec_cmd .append ("--testing" )
103
+ # logger.debug("Running: {}".format(exec_cmd))
56
104
57
- # There is a very good reason stdout and stderr is not PIPE here
58
- # See: https://github.com/ActivityWatch/aw-server/issues/27
59
- self ._process = subprocess .Popen (exec_cmd , universal_newlines = True )
105
+ # There is a very good reason stdout and stderr is not PIPE here
106
+ # See: https://github.com/ActivityWatch/aw-server/issues/27
107
+ self ._process = subprocess .Popen (exec_cmd , universal_newlines = True )
60
108
61
- # Should be True if module is supposed to be running, else False
62
- self .started = True
109
+ # Should be True if module is supposed to be running, else False
110
+ self .started = True
63
111
64
112
def stop (self ) -> None :
65
113
"""
@@ -108,19 +156,25 @@ def read_log(self) -> str:
108
156
109
157
110
158
class Manager :
111
- def __init__ (self , testing : bool = False ):
112
- # TODO: Fetch these from somewhere appropriate (auto detect or a config file)
113
- # Save to config wether they should autostart or not.
114
- _possible_modules = [
159
+ def __init__ (self , testing : bool = False ) -> None :
160
+ self .testing = testing
161
+ self .modules = {} # type: Dict[str, Module]
162
+
163
+ self .discover_modules ()
164
+
165
+ def discover_modules (self ):
166
+ # These should always be bundled with aw-qt
167
+ found_modules = {
115
168
"aw-server" ,
116
169
"aw-watcher-afk" ,
117
- "aw-watcher-window" ,
118
- # "aw-watcher-spotify",
119
- # "aw-watcher-network"
120
- ]
121
-
122
- # TODO: Filter away all modules not available on system
123
- self .modules = {name : Module (name , testing = testing ) for name in _possible_modules }
170
+ "aw-watcher-window"
171
+ }
172
+ found_modules |= set (_discover_modules_bundled ())
173
+ found_modules |= set (_discover_modules_system ())
174
+
175
+ for m_name in found_modules :
176
+ if m_name not in self .modules :
177
+ self .modules [m_name ] = Module (m_name , testing = self .testing )
124
178
125
179
def get_unexpected_stops (self ):
126
180
return list (filter (lambda x : x .started and not x .is_alive (), self .modules .values ()))
0 commit comments