11import os
22import platform
3+ from glob import glob
34from time import sleep
45import logging
56import subprocess
7+ import shutil
68from typing import Optional , List
79
810import aw_core
911
1012logger = logging .getLogger (__name__ )
1113
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 ]
1217
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.
1718
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 ]
2422
25- for exec_path in exec_paths :
23+ # Look for it in the installation path
24+ for exec_path in _exec_paths :
2625 if os .path .isfile (exec_path ):
2726 # 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
3047 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
3578
3679
3780class Module :
@@ -46,20 +89,25 @@ def start(self) -> None:
4689 logger .info ("Starting module {}" .format (self .name ))
4790
4891 # Create a process group, become its leader
92+ # TODO: This shouldn't go here
4993 if platform .system () != "Windows" :
5094 os .setpgrp ()
5195
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))
56104
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 )
60108
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
63111
64112 def stop (self ) -> None :
65113 """
@@ -108,19 +156,25 @@ def read_log(self) -> str:
108156
109157
110158class 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 = {
115168 "aw-server" ,
116169 "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 )
124178
125179 def get_unexpected_stops (self ):
126180 return list (filter (lambda x : x .started and not x .is_alive (), self .modules .values ()))
0 commit comments