Coverage for gws-app/gws/server/monitor.py: 33%

115 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1import os 

2 

3import gws 

4import gws.lib.watcher 

5import gws.server.uwsgi_module 

6 

7from . import control 

8 

9_LOCK_FILE = '/tmp/monitor.lock' 

10_RELOAD_FILE = '/tmp/monitor.reload' 

11_RECONFIGURE_FILE = '/tmp/monitor.reconfigure' 

12_TICK_FREQUENCY = 3 

13 

14DEFAULT_FREQUENCY = 30 

15 

16 

17class _Task(gws.Data): 

18 obj: gws.Node 

19 frequency: int 

20 lastTime: int 

21 

22 

23class Object(gws.ServerMonitor): 

24 watchPaths: set[str] 

25 enabled: bool 

26 frequency: int 

27 watcher: gws.lib.watcher.Watcher 

28 dirs: list 

29 files: list 

30 tasks: list[_Task] 

31 

32 def configure(self): 

33 self.frequency = self.cfg('frequency', default=DEFAULT_FREQUENCY) 

34 self.dirs = [] 

35 self.files = [] 

36 self.tasks = [] 

37 

38 def watch_directory(self, dirname, pattern, recursive=False): 

39 self.dirs.append((dirname, pattern, recursive)) 

40 

41 def watch_file(self, path): 

42 self.files.append(path) 

43 

44 def register_periodic_task(self, obj, frequency=0): 

45 if not hasattr(obj, 'periodic_task'): 

46 raise gws.Error(f'MONITOR: {obj!r} has no periodic_task') 

47 self.tasks.append( 

48 _Task( 

49 obj=obj, 

50 frequency=frequency or self.frequency, 

51 lastTime=0, 

52 ) 

53 ) 

54 

55 def schedule_reload(self, with_reconfigure=False): 

56 gws.log.info(f'MONITOR: reload scheduled {with_reconfigure=}') 

57 self._touch(_RECONFIGURE_FILE if with_reconfigure else _RELOAD_FILE) 

58 

59 def start(self): 

60 self._check_unlink(_LOCK_FILE) 

61 self._check_unlink(_RELOAD_FILE) 

62 self._check_unlink(_RECONFIGURE_FILE) 

63 

64 for t in self.tasks: 

65 t.lastTime = gws.u.stime() 

66 

67 if not self.cfg('disableWatch'): 

68 

69 def notify(evt, path): 

70 self._touch(_RECONFIGURE_FILE) 

71 

72 self.watcher = gws.lib.watcher.new(notify) 

73 

74 for d in self.dirs: 

75 self.watcher.add_directory(*d) 

76 for f in self.files: 

77 self.watcher.add_file(f) 

78 

79 self.watcher.start() 

80 

81 uwsgi = gws.server.uwsgi_module.load() 

82 uwsgi.register_signal(42, 'worker2', self._tick) 

83 uwsgi.add_timer(42, _TICK_FREQUENCY) 

84 

85 gws.log.info(f'MONITOR: started') 

86 

87 def _tick(self, signo): 

88 do_reconfigure = self._check_unlink(_RECONFIGURE_FILE) 

89 do_reload = self._check_unlink(_RELOAD_FILE) 

90 

91 tasks = [t for t in self.tasks if gws.u.stime() - t.lastTime >= t.frequency] 

92 

93 if not do_reconfigure and not do_reload and not tasks: 

94 # gws.log.debug(f'MONITOR: tick skip') 

95 return 

96 

97 gws.log.debug(f'MONITOR: tick {do_reconfigure=} {do_reload=} tasks={len(tasks)}') 

98 

99 try: 

100 self._touch(_LOCK_FILE, excl=True) 

101 except FileExistsError: 

102 gws.log.debug(f'MONITOR: locked') 

103 return 

104 

105 try: 

106 if do_reconfigure: 

107 self._reload(True) 

108 elif do_reload: 

109 self._reload(False) 

110 elif tasks: 

111 self._run_periodic_tasks(tasks) 

112 finally: 

113 self._check_unlink(_LOCK_FILE) 

114 

115 def _reload(self, with_reconfigure): 

116 gws.log.info(f'MONITOR: reloading...') 

117 

118 if not self._reload2(with_reconfigure): 

119 return 

120 

121 # ok, reload ourselves 

122 gws.log.info(f'MONITOR: bye bye') 

123 control.reload_app('spool') 

124 

125 def _reload2(self, with_reconfigure): 

126 if with_reconfigure: 

127 try: 

128 control.configure_and_store() 

129 except Exception as exc: 

130 gws.log.exception(f'MONITOR: configuration error: {exc!r}') 

131 return False 

132 

133 try: 

134 control.reload_app('mapproxy') 

135 control.reload_app('web') 

136 return True 

137 except Exception as exc: 

138 gws.log.exception(f'MONITOR: reload error: {exc!r}') 

139 return False 

140 

141 def _run_periodic_tasks(self, tasks): 

142 for t in tasks: 

143 try: 

144 t.obj.periodic_task() 

145 t.lastTime = gws.u.stime() 

146 except Exception as exc: 

147 gws.log.exception(f'MONITOR: periodic task failed {t.obj}: {exc!r}') 

148 

149 def _touch(self, path, excl=False): 

150 flags = os.O_CREAT | os.O_WRONLY 

151 if excl: 

152 flags |= os.O_EXCL 

153 os.close(os.open(path, flags)) 

154 

155 def _check_unlink(self, path): 

156 try: 

157 os.unlink(path) 

158 return True 

159 except FileNotFoundError: 

160 return False