Coverage for gws-app/gws/server/monitor.py: 33%
113 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Server monitor, which watches files and runs periodic tasks."""
3import os
5import gws
6import gws.lib.watcher
7import gws.server.uwsgi_module
9from . import control
11_LOCK_FILE = '/tmp/monitor.lock'
12_RELOAD_FILE = '/tmp/monitor.reload'
13_RECONFIGURE_FILE = '/tmp/monitor.reconfigure'
14_TICK_FREQUENCY = 3
16DEFAULT_FREQUENCY = 30
19class _Task(gws.Data):
20 """A registered periodic task."""
22 obj: gws.Node
23 """Object with a ``periodic_task`` method."""
24 frequency: int
25 """Interval between runs in seconds."""
26 lastTime: int
27 """Time of the last run, as a Unix timestamp."""
30class Object(gws.ServerMonitor):
31 """Server monitor."""
33 enabled: bool
34 """Not used."""
35 frequency: int
36 """Default interval of periodic tasks in seconds."""
37 watcher: gws.lib.watcher.Watcher
38 """File watcher, created on start unless ``disableWatch`` is set."""
39 dirs: list
40 """Watched directories, as tuples of directory, file pattern and recursive flag."""
41 files: list
42 """Watched files."""
43 tasks: list[_Task]
44 """Registered periodic tasks."""
46 def configure(self):
47 self.frequency = self.cfg('frequency', default=DEFAULT_FREQUENCY)
48 self.dirs = []
49 self.files = []
50 self.tasks = []
52 def watch_directory(self, dirname, pattern, recursive=False):
53 self.dirs.append((dirname, pattern, recursive))
55 def watch_file(self, path):
56 self.files.append(path)
58 def register_periodic_task(self, obj, frequency=0):
59 if not hasattr(obj, 'periodic_task'):
60 raise gws.Error(f'MONITOR: {obj!r} has no periodic_task')
61 self.tasks.append(
62 _Task(
63 obj=obj,
64 frequency=frequency or self.frequency,
65 lastTime=0,
66 )
67 )
69 def schedule_reload(self, with_reconfigure=False):
70 gws.log.info(f'MONITOR: reload scheduled {with_reconfigure=}')
71 self._touch(_RECONFIGURE_FILE if with_reconfigure else _RELOAD_FILE)
73 def start(self):
74 self._check_unlink(_LOCK_FILE)
75 self._check_unlink(_RELOAD_FILE)
76 self._check_unlink(_RECONFIGURE_FILE)
78 for t in self.tasks:
79 t.lastTime = gws.u.stime()
81 if not self.cfg('disableWatch'):
83 def notify(evt, path):
84 self._touch(_RECONFIGURE_FILE)
86 self.watcher = gws.lib.watcher.new(notify)
88 for d in self.dirs:
89 self.watcher.add_directory(*d)
90 for f in self.files:
91 self.watcher.add_file(f)
93 self.watcher.start()
95 uwsgi = gws.server.uwsgi_module.load()
96 uwsgi.register_signal(42, 'worker2', self._tick)
97 uwsgi.add_timer(42, _TICK_FREQUENCY)
99 gws.log.info(f'MONITOR: started')
101 def _tick(self, signo):
102 """Handle the timer signal: reconfigure, reload or run the periodic tasks that are due.
104 Reconfigure and reload requests are signalled through marker files in ``/tmp``,
105 so that they reach the monitor from any process. A lock file prevents overlapping runs.
106 """
107 do_reconfigure = self._check_unlink(_RECONFIGURE_FILE)
108 do_reload = self._check_unlink(_RELOAD_FILE)
110 tasks = [t for t in self.tasks if gws.u.stime() - t.lastTime >= t.frequency]
112 if not do_reconfigure and not do_reload and not tasks:
113 # gws.log.debug(f'MONITOR: tick skip')
114 return
116 gws.log.debug(f'MONITOR: tick {do_reconfigure=} {do_reload=} tasks={len(tasks)}')
118 try:
119 self._touch(_LOCK_FILE, excl=True)
120 except FileExistsError:
121 gws.log.debug(f'MONITOR: locked')
122 return
124 try:
125 if do_reconfigure:
126 self._reload(True)
127 elif do_reload:
128 self._reload(False)
129 elif tasks:
130 self._run_periodic_tasks(tasks)
131 finally:
132 self._check_unlink(_LOCK_FILE)
134 def _reload(self, with_reconfigure):
135 """Reload the web backend, then the spool backend, which restarts the monitor.
137 If the configuration fails, nothing is reloaded.
138 """
139 gws.log.info(f'MONITOR: reloading...')
141 if not self._reload2(with_reconfigure):
142 return
144 # ok, reload ourselves
145 gws.log.info(f'MONITOR: bye bye')
146 control.reload_app('spool')
148 def _reload2(self, with_reconfigure):
149 """Optionally reconfigure, then reload the web backend, return False on failure."""
150 if with_reconfigure:
151 try:
152 control.configure_and_store()
153 except Exception as exc:
154 gws.log.exception(f'MONITOR: configuration error: {exc!r}')
155 return False
157 try:
158 control.reload_app('web')
159 return True
160 except Exception as exc:
161 gws.log.exception(f'MONITOR: reload error: {exc!r}')
162 return False
164 def _run_periodic_tasks(self, tasks):
165 """Run the given tasks, logging and skipping failed ones."""
166 for t in tasks:
167 try:
168 t.obj.periodic_task()
169 t.lastTime = gws.u.stime()
170 except Exception as exc:
171 gws.log.exception(f'MONITOR: periodic task failed {t.obj}: {exc!r}')
173 def _touch(self, path, excl=False):
174 """Create an empty file, fail if it exists and ``excl`` is set."""
175 flags = os.O_CREAT | os.O_WRONLY
176 if excl:
177 flags |= os.O_EXCL
178 os.close(os.open(path, flags))
180 def _check_unlink(self, path):
181 """Delete a file, return True if it existed."""
182 try:
183 os.unlink(path)
184 return True
185 except FileNotFoundError:
186 return False