Coverage for gws-app/gws/server/monitor.py: 0%
114 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1import os
2import re
4import gws
5import gws.config
6import gws.lib.lock
7import gws.lib.osx
8import gws.server.uwsgi_module
10from . import control
12_LOCK_FILE = '/tmp/monitor.lock'
15class Object(gws.ServerMonitor):
16 watchDirs: dict
17 watchFiles: dict
18 pathStats: dict
20 enabled: bool
21 frequency: int
22 ignore: list[str]
24 def configure(self):
25 self.enabled = self.cfg('enabled', default=True)
26 self.frequency = self.cfg('frequency', default=30)
27 self.ignore = self.cfg('ignore', default=[])
29 self.watchDirs = {}
30 self.watchFiles = {}
31 self.pathStats = {}
33 def add_directory(self, path, pattern):
34 self.watchDirs[path] = pattern
36 def add_file(self, path):
37 self.watchFiles[path] = 1
39 def start(self):
40 if not self.enabled:
41 gws.log.info(f'MONITOR: disabled')
42 return
44 # @TODO: use file monitor
45 # actually, we should be using uwsgi.add_file_monitor here, however I keep having problems
46 # getting inotify working on docker-mounted volumes (is that possible at all)?
48 self._cleanup()
50 for s in self.watchDirs:
51 gws.log.info(f'MONITOR: watching directory {s!r}')
52 for s in self.watchFiles:
53 gws.log.info(f'MONITOR: watching file {s!r}')
55 try:
56 os.unlink(_LOCK_FILE)
57 except OSError:
58 pass
60 self._poll()
62 uwsgi = gws.server.uwsgi_module.load()
64 # only one worker is allowed to do that
65 uwsgi.register_signal(42, 'worker2', self._worker)
66 uwsgi.add_timer(42, self.frequency)
67 gws.log.info(f'MONITOR: started, frequency={self.frequency}')
69 def _worker(self, signo):
70 with gws.lib.lock.SoftFileLock(_LOCK_FILE) as ok:
71 if not ok:
72 return
74 changed_paths = self._poll()
75 if not changed_paths:
76 return
78 for path in changed_paths:
79 gws.log.info(f'MONITOR: changed {path!r}')
81 # @TODO: smarter reload
83 needs_reconfigure = any(not path.endswith('.py') for path in changed_paths)
84 gws.log.info(f'MONITOR: begin reload {needs_reconfigure=}')
86 if not self._reload(needs_reconfigure):
87 return
89 # finally, reload ourselves
90 gws.log.info(f'MONITOR: bye bye')
91 control.reload_app('spool')
93 def _reload(self, needs_reconfigure):
94 if needs_reconfigure:
95 try:
96 control.configure_and_store()
97 except:
98 gws.log.exception('MONITOR: configuration error')
99 return False
101 try:
102 control.reload_app('mapproxy')
103 control.reload_app('web')
104 return True
105 except:
106 gws.log.exception('MONITOR: reload error')
107 return False
109 def _cleanup(self):
110 """Remove superfluous directory and file entries."""
112 ls = []
113 for d in sorted(self.watchDirs):
114 if d in ls or any(d.startswith(e + '/') for e in ls):
115 # if we watch /some/dir already, there's no need to watch /some/dir/subdir
116 continue
117 ls.append(d)
118 self.watchDirs = {d: self.watchDirs[d] for d in sorted(ls)}
120 ls = []
121 for f in sorted(self.watchFiles):
122 if any(f.startswith(e + '/') for e in self.watchDirs):
123 # if we watch /some/dir already, there's no need to watch /some/dir/some.file
124 continue
125 ls.append(f)
126 self.watchFiles = {f: 1 for f in sorted(ls)}
128 def _poll(self):
129 new_stats = {}
130 changed_paths = []
132 for dirpath, pattern in self.watchDirs.items():
133 if self._ignored(dirpath):
134 continue
135 if not gws.u.is_dir(dirpath):
136 continue
137 for path in gws.lib.osx.find_files(dirpath, pattern):
138 if not self._ignored(path):
139 new_stats[path] = self._stats(path)
141 for path, _ in self.watchFiles.items():
142 if path not in new_stats and not self._ignored(path):
143 new_stats[path] = self._stats(path)
145 for path in set(self.pathStats) | set(new_stats):
146 if self.pathStats.get(path) != new_stats.get(path):
147 changed_paths.append(path)
149 self.pathStats = new_stats
150 return changed_paths
152 def _stats(self, path):
153 try:
154 s = os.stat(path)
155 return s.st_size, s.st_mtime
156 except OSError:
157 return 0, 0
159 def _ignored(self, filename):
160 return self.ignore and any(re.search(p, filename) for p in self.ignore)