Skip to content

Commit 53f7dd0

Browse files
committed
added basic module autodetection
1 parent 52b2837 commit 53f7dd0

File tree

1 file changed

+91
-37
lines changed

1 file changed

+91
-37
lines changed

aw_qt/manager.py

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,80 @@
11
import os
22
import platform
3+
from glob import glob
34
from time import sleep
45
import logging
56
import subprocess
7+
import shutil
68
from typing import Optional, List
79

810
import aw_core
911

1012
logger = 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

3780
class 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

110158
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 = {
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

Comments
 (0)