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 , Dict , Set
79
810import aw_core
1113
1214logger = logging .getLogger (__name__ )
1315
16+ _module_dir = os .path .dirname (os .path .realpath (__file__ ))
17+ _parent_dir = os .path .abspath (os .path .join (_module_dir , os .pardir ))
18+ _search_paths = [_module_dir , _parent_dir ]
1419
15- def _locate_executable (name : str ) -> List [str ]:
16- """
17- Will start module from localdir if present there,
18- otherwise will try to call what is available in PATH.
19-
20- Returns it as a Popen cmd list.
21- """
22- curr_filepath = os .path .realpath (__file__ )
23- curr_dir = os .path .dirname (curr_filepath )
24- program_dir = os .path .abspath (os .path .join (curr_dir , os .pardir ))
25- search_paths = [curr_dir , program_dir , os .path .join (program_dir , name )]
2620
27- exec_end = ".exe" if platform .system () == "Windows" else ""
28- exec_paths = [os .path .join (path , name + exec_end ) for path in search_paths ]
21+ def _locate_bundled_executable (name : str ) -> Optional [str ]:
22+ """Returns the path to the module executable if it exists in the bundle, else None."""
23+ _exec_paths = [os .path .join (path , name ) for path in _search_paths ]
2924
30- for exec_path in exec_paths :
25+ # Look for it in the installation path
26+ for exec_path in _exec_paths :
3127 if os .path .isfile (exec_path ):
3228 # logger.debug("Found executable for {} in: {}".format(name, exec_path))
33- return [exec_path ]
34- break # this break is redundant, but kept due to for-else semantics
29+ return exec_path
30+ return None
31+
32+
33+ def _is_system_module (name ) -> bool :
34+ """Checks if a module with a particular name exists in PATH"""
35+ return shutil .which (name ) is not None
36+
37+
38+ def _locate_executable (name : str ) -> Optional [str ]:
39+ """
40+ Will return the path to the executable if bundled,
41+ otherwise returns the name if it is available in PATH.
42+
43+ Used when calling Popen.
44+ """
45+ exec_path = _locate_bundled_executable (name )
46+ if exec_path is not None : # Check if it exists in bundle
47+ return exec_path
48+ elif _is_system_module (name ): # Check if it's in PATH
49+ return name
3550 else :
36- # TODO: Actually check if it is in PATH
37- logger .debug ("Trying to start {} using PATH (executable not found in: {})"
38- .format (name , exec_paths ))
39- return [name ]
51+ logger .warning ("Could not find module '{}' in installation directory or PATH" .format (name ))
52+ return None
53+
54+
55+ def _discover_modules_bundled () -> List [str ]:
56+ # Look for modules in source dir and parent dir
57+ modules = []
58+ for path in _search_paths :
59+ matches = glob (os .path .join (path , "aw-*" ))
60+ for match in matches :
61+ if os .path .isfile (match ) and os .access (match , os .X_OK ):
62+ name = os .path .basename (match )
63+ modules .append (name )
64+ else :
65+ logger .warning ("Found matching file but was not executable: {}" .format (path ))
66+
67+ logger .info ("Found bundled modules: {}" .format (set (modules )))
68+ return modules
69+
70+
71+ def _discover_modules_system () -> List [str ]:
72+ search_paths = os .environ ["PATH" ].split (":" )
73+ modules = []
74+ for path in search_paths :
75+ if os .path .isdir (path ):
76+ files = os .listdir (path )
77+ for filename in files :
78+ if "aw-" in filename :
79+ modules .append (filename )
80+
81+ logger .info ("Found system modules: {}" .format (set (modules )))
82+ return modules
4083
4184
4285class Module :
4386 def __init__ (self , name : str , testing : bool = False ) -> None :
4487 self .name = name
45- self .started = False
88+ self .started = False # Should be True if module is supposed to be running, else False
4689 self .testing = testing
90+ self .location = "system" if _is_system_module (name ) else "bundled"
4791 self ._process = None # type: Optional[subprocess.Popen]
4892 self ._last_process = None # type: Optional[subprocess.Popen]
4993
5094 def start (self ) -> None :
5195 logger .info ("Starting module {}" .format (self .name ))
5296
5397 # Create a process group, become its leader
98+ # TODO: This shouldn't go here
5499 if platform .system () != "Windows" :
55100 os .setpgrp () # type: ignore
56101
57- exec_cmd = _locate_executable (self .name )
58- if self .testing :
59- exec_cmd .append ("--testing" )
60- # logger.debug("Running: {}".format(exec_cmd))
102+ exec_path = _locate_executable (self .name )
103+ if exec_path is None :
104+ return
105+ else :
106+ exec_cmd = [exec_path ]
107+ if self .testing :
108+ exec_cmd .append ("--testing" )
109+ # logger.debug("Running: {}".format(exec_cmd))
61110
62111 # Don't display a console window on Windows
63112 # See: https://github.com/ActivityWatch/activitywatch/issues/212
@@ -74,7 +123,6 @@ def start(self) -> None:
74123 # See: https://github.com/ActivityWatch/aw-server/issues/27
75124 self ._process = subprocess .Popen (exec_cmd , universal_newlines = True , startupinfo = startupinfo )
76125
77- # Should be True if module is supposed to be running, else False
78126 self .started = True
79127
80128 def stop (self ) -> None :
@@ -138,6 +186,18 @@ def __init__(self, testing: bool = False) -> None:
138186 self .modules [name ] = Module (name , testing = testing )
139187 else :
140188 logger .warning ("Module '{}' not found but was in possible modules" .format (name ))
189+ # Is this actually a good way to do this? merged from dev/autodetect-modules
190+ self .discover_modules ()
191+
192+ def discover_modules (self ):
193+ # These should always be bundled with aw-qt
194+ found_modules = set (_discover_modules_bundled ())
195+ found_modules |= set (_discover_modules_system ())
196+ found_modules ^= {"aw-qt" } # Exclude self
197+
198+ for m_name in found_modules :
199+ if m_name not in self .modules :
200+ self .modules [m_name ] = Module (m_name , testing = self .testing )
141201
142202 def get_unexpected_stops (self ):
143203 return list (filter (lambda x : x .started and not x .is_alive (), self .modules .values ()))
0 commit comments