11import os
2- import platform
3- from time import sleep
2+ import sys
43import logging
54import subprocess
6- from typing import Optional , List
5+ import shutil
6+ from glob import glob
7+ from time import sleep
8+ from typing import Optional , List , Dict , Tuple
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.
17-
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- program_dir = os .path .abspath (os .path .join (curr_dir , os .pardir ))
23- search_paths = [curr_dir , program_dir , os .path .join (program_dir , name )]
2418
25- exec_end = ".exe" if platform .system () == "Windows" else ""
26- exec_paths = [os .path .join (path , name + exec_end ) 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 ]
2722
28- for exec_path in exec_paths :
23+ # Look for it in the installation path
24+ for exec_path in _exec_paths :
2925 if os .path .isfile (exec_path ):
3026 # logger.debug("Found executable for {} in: {}".format(name, exec_path))
31- return [exec_path ]
32- break # this break is redundant, but kept due to for-else semantics
27+ return exec_path
28+ return None
29+
30+
31+ def _is_system_module (name : str ) -> bool :
32+ """Checks if a module with a particular name exists in PATH"""
33+ return shutil .which (name ) is not None
34+
35+
36+ def _locate_executable (name : str ) -> Tuple [Optional [str ], Optional [str ]]:
37+ """
38+ Will return the path to the executable if bundled,
39+ otherwise returns the name if it is available in PATH.
40+
41+ Used when calling Popen.
42+ """
43+ exec_path = _locate_bundled_executable (name )
44+ if exec_path is not None : # Check if it exists in bundle
45+ return exec_path , "bundle"
46+ elif _is_system_module (name ): # Check if it's in PATH
47+ return name , "system"
3348 else :
34- # TODO: Actually check if it is in PATH
35- logger .debug ("Trying to start {} using PATH (executable not found in: {})"
36- .format (name , exec_paths ))
37- return [name ]
49+ logger .warning (
50+ "Could not find module '{}' in installation directory or PATH" .format (name )
51+ )
52+ return None , None
53+
54+
55+ def _discover_modules_in_directory (search_path : str ) -> List [str ]:
56+ """Look for modules in given directory path and recursively in subdirs matching aw-*"""
57+ modules = []
58+ matches = glob (os .path .join (search_path , "aw-*" ))
59+ for match in matches :
60+ if os .path .isfile (match ) and os .access (match , os .X_OK ):
61+ name = os .path .basename (match )
62+ modules .append (name )
63+ elif os .path .isdir (match ) and os .access (match , os .X_OK ):
64+ modules .extend (_discover_modules_in_directory (match ))
65+ else :
66+ logger .warning (
67+ "Found matching file but was not executable: {}" .format (match )
68+ )
69+ return modules
70+
71+
72+ def _discover_modules_bundled () -> List [str ]:
73+ """Use ``_discover_modules_in_directory`` to find all bundled modules """
74+ cwd = os .getcwd ()
75+ modules = _discover_modules_in_directory (cwd )
76+ logger .info ("Found bundled modules: {}" .format (set (modules )))
77+ return modules
78+
79+
80+ def _discover_modules_system () -> List [str ]:
81+ """Find all aw- modules in PATH"""
82+ search_paths = os .get_exec_path ()
83+ modules = []
84+ for path in search_paths :
85+ if os .path .isdir (path ):
86+ files = os .listdir (path )
87+ for filename in files :
88+ if filename .startswith ("aw-" ):
89+ modules .append (filename )
90+
91+ logger .info ("Found system modules: {}" .format (set (modules )))
92+ return modules
3893
3994
4095class Module :
4196 def __init__ (self , name : str , testing : bool = False ) -> None :
4297 self .name = name
43- self .started = False
98+ self .started = (
99+ False # Should be True if module is supposed to be running, else False
100+ )
44101 self .testing = testing
45- self ._process = None # type: Optional[subprocess.Popen]
46- self ._last_process = None # type: Optional[subprocess.Popen]
102+ self .location = "system" if _is_system_module (name ) else "bundled"
103+ self ._process : Optional [subprocess .Popen [str ]] = None
104+ self ._last_process : Optional [subprocess .Popen [str ]] = None
47105
48106 def start (self ) -> None :
49107 logger .info ("Starting module {}" .format (self .name ))
50108
51109 # Create a process group, become its leader
52- if platform .system () != "Windows" :
53- os .setpgrp () # type: ignore
110+ # TODO: This shouldn't go here
111+ if sys .platform != "win32" :
112+ os .setpgrp ()
54113
55- exec_cmd = _locate_executable (self .name )
56- if self .testing :
57- exec_cmd .append ("--testing" )
58- # logger.debug("Running: {}".format(exec_cmd))
114+ exec_path , location = _locate_executable (self .name )
115+ if exec_path is None :
116+ logger .error ("Tried to start nonexistent module {}" .format (self .name ))
117+ return
118+ else :
119+ exec_cmd = [exec_path ]
120+ if self .testing :
121+ exec_cmd .append ("--testing" )
122+ # logger.debug("Running: {}".format(exec_cmd))
59123
60124 # Don't display a console window on Windows
61125 # See: https://github.com/ActivityWatch/activitywatch/issues/212
62126 startupinfo = None
63- if platform . system () == "Windows " :
64- startupinfo = subprocess .STARTUPINFO () # type: ignore
65- startupinfo .dwFlags |= subprocess .STARTF_USESHOWWINDOW # type: ignore
66- elif platform . system () == "Darwin " :
67- logger .info ("Macos : Disable dock icon" )
127+ if sys . platform == "win32" or sys . platform == "cygwin " :
128+ startupinfo = subprocess .STARTUPINFO ()
129+ startupinfo .dwFlags |= subprocess .STARTF_USESHOWWINDOW
130+ elif sys . platform == "darwin " :
131+ logger .info ("macOS : Disable dock icon" )
68132 import AppKit
133+
69134 AppKit .NSBundle .mainBundle ().infoDictionary ()["LSBackgroundOnly" ] = "1"
70135
71136 # There is a very good reason stdout and stderr is not PIPE here
72137 # See: https://github.com/ActivityWatch/aw-server/issues/27
73- self ._process = subprocess .Popen (exec_cmd , universal_newlines = True , startupinfo = startupinfo )
74-
75- # Should be True if module is supposed to be running, else False
138+ self ._process = subprocess .Popen (
139+ exec_cmd , universal_newlines = True , startupinfo = startupinfo
140+ )
76141 self .started = True
77142
78143 def stop (self ) -> None :
@@ -81,10 +146,14 @@ def stop(self) -> None:
81146 """
82147 # TODO: What if a module doesn't stop? Add timeout to p.wait() and then do a p.kill() if timeout is hit
83148 if not self .started :
84- logger .warning ("Tried to stop module {}, but it hasn't been started" .format (self .name ))
149+ logger .warning (
150+ "Tried to stop module {}, but it hasn't been started" .format (self .name )
151+ )
85152 return
86153 elif not self .is_alive ():
87- logger .warning ("Tried to stop module {}, but it wasn't running" .format (self .name ))
154+ logger .warning (
155+ "Tried to stop module {}, but it wasn't running" .format (self .name )
156+ )
88157 else :
89158 if not self ._process :
90159 logger .error ("No reference to process object" )
@@ -126,42 +195,57 @@ def read_log(self) -> str:
126195
127196
128197class Manager :
129- def __init__ (self , testing : bool = False ) -> None :
130- # TODO: Fetch these from somewhere appropriate (auto detect or a config file)
131- # Save to config wether they should autostart or not.
132- _possible_modules = [
133- "aw-server" ,
134- "aw-server-rust" ,
135- "aw-watcher-afk" ,
136- "aw-watcher-window" ,
137- # "aw-watcher-spotify",
138- # "aw-watcher-network"
139- ]
140-
141- # TODO: Filter away all modules not available on system
142- self .modules = {name : Module (name , testing = testing ) for name in _possible_modules }
143-
144- def get_unexpected_stops (self ):
145- return list (filter (lambda x : x .started and not x .is_alive (), self .modules .values ()))
146-
147- def start (self , module_name ):
198+ def __init__ (self , testing : bool = False ) -> None :
199+ self .modules : Dict [str , Module ] = {}
200+ self .testing = testing
201+
202+ self .discover_modules ()
203+
204+ def discover_modules (self ) -> None :
205+ # These should always be bundled with aw-qt
206+ found_modules = set (_discover_modules_bundled ())
207+ found_modules |= set (_discover_modules_system ())
208+ found_modules ^= {"aw-qt" , "aw-client" } # Exclude self
209+
210+ for m_name in found_modules :
211+ if m_name not in self .modules :
212+ self .modules [m_name ] = Module (m_name , testing = self .testing )
213+
214+ def get_unexpected_stops (self ) -> List [Module ]:
215+ return list (
216+ filter (lambda x : x .started and not x .is_alive (), self .modules .values ())
217+ )
218+
219+ def start (self , module_name : str ) -> None :
148220 if module_name in self .modules .keys ():
149221 self .modules [module_name ].start ()
150222 else :
151- logger .error ("Unable to start module '{}': No such module" .format (module_name ))
223+ logger .debug (
224+ "Manager tried to start nonexistent module {}" .format (module_name )
225+ )
152226
153- def autostart (self , autostart_modules ):
154- # Always start aw-server first
155- if "aw-server" in autostart_modules :
156- self .start ("aw-server" )
157- elif "aw-server-rust" in autostart_modules :
227+ def autostart (self , autostart_modules : List [str ]) -> None :
228+ # We only want to autostart modules that are both in found modules and are asked to autostart.
229+ not_found = []
230+ for name in autostart_modules :
231+ if name not in self .modules .keys ():
232+ logger .error (f"Module { name } not found" )
233+ not_found .append (name )
234+ autostart_modules = list (set (autostart_modules ) - set (not_found ))
235+
236+ # Start aw-server-rust first
237+ if "aw-server-rust" in autostart_modules :
158238 self .start ("aw-server-rust" )
239+ elif "aw-server" in autostart_modules :
240+ self .start ("aw-server" )
159241
160- autostart_modules = set (autostart_modules ) - {"aw-server" , "aw-server-rust" }
161- for module_name in autostart_modules :
162- self .start (module_name )
242+ autostart_modules = list (
243+ set (autostart_modules ) - {"aw-server" , "aw-server-rust" }
244+ )
245+ for name in autostart_modules :
246+ self .start (name )
163247
164- def stop_all (self ):
248+ def stop_all (self ) -> None :
165249 for module in filter (lambda m : m .is_alive (), self .modules .values ()):
166250 module .stop ()
167251
0 commit comments