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 , Dict , Set
7
9
8
10
import aw_core
11
13
12
14
logger = logging .getLogger (__name__ )
13
15
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 ]
14
19
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 )]
26
20
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 ]
29
24
30
- for exec_path in exec_paths :
25
+ # Look for it in the installation path
26
+ for exec_path in _exec_paths :
31
27
if os .path .isfile (exec_path ):
32
28
# 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
35
50
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
40
83
41
84
42
85
class Module :
43
86
def __init__ (self , name : str , testing : bool = False ) -> None :
44
87
self .name = name
45
- self .started = False
88
+ self .started = False # Should be True if module is supposed to be running, else False
46
89
self .testing = testing
90
+ self .location = "system" if _is_system_module (name ) else "bundled"
47
91
self ._process = None # type: Optional[subprocess.Popen]
48
92
self ._last_process = None # type: Optional[subprocess.Popen]
49
93
50
94
def start (self ) -> None :
51
95
logger .info ("Starting module {}" .format (self .name ))
52
96
53
97
# Create a process group, become its leader
98
+ # TODO: This shouldn't go here
54
99
if platform .system () != "Windows" :
55
100
os .setpgrp () # type: ignore
56
101
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))
61
110
62
111
# Don't display a console window on Windows
63
112
# See: https://github.com/ActivityWatch/activitywatch/issues/212
@@ -74,7 +123,6 @@ def start(self) -> None:
74
123
# See: https://github.com/ActivityWatch/aw-server/issues/27
75
124
self ._process = subprocess .Popen (exec_cmd , universal_newlines = True , startupinfo = startupinfo )
76
125
77
- # Should be True if module is supposed to be running, else False
78
126
self .started = True
79
127
80
128
def stop (self ) -> None :
@@ -138,6 +186,18 @@ def __init__(self, testing: bool = False) -> None:
138
186
self .modules [name ] = Module (name , testing = testing )
139
187
else :
140
188
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 )
141
201
142
202
def get_unexpected_stops (self ):
143
203
return list (filter (lambda x : x .started and not x .is_alive (), self .modules .values ()))
0 commit comments