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

1import os 

2import re 

3 

4import gws 

5import gws.config 

6import gws.lib.lock 

7import gws.lib.osx 

8import gws.server.uwsgi_module 

9 

10from . import control 

11 

12_LOCK_FILE = '/tmp/monitor.lock' 

13 

14 

15class Object(gws.ServerMonitor): 

16 watchDirs: dict 

17 watchFiles: dict 

18 pathStats: dict 

19 

20 enabled: bool 

21 frequency: int 

22 ignore: list[str] 

23 

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=[]) 

28 

29 self.watchDirs = {} 

30 self.watchFiles = {} 

31 self.pathStats = {} 

32 

33 def add_directory(self, path, pattern): 

34 self.watchDirs[path] = pattern 

35 

36 def add_file(self, path): 

37 self.watchFiles[path] = 1 

38 

39 def start(self): 

40 if not self.enabled: 

41 gws.log.info(f'MONITOR: disabled') 

42 return 

43 

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)? 

47 

48 self._cleanup() 

49 

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}') 

54 

55 try: 

56 os.unlink(_LOCK_FILE) 

57 except OSError: 

58 pass 

59 

60 self._poll() 

61 

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

63 

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}') 

68 

69 def _worker(self, signo): 

70 with gws.lib.lock.SoftFileLock(_LOCK_FILE) as ok: 

71 if not ok: 

72 return 

73 

74 changed_paths = self._poll() 

75 if not changed_paths: 

76 return 

77 

78 for path in changed_paths: 

79 gws.log.info(f'MONITOR: changed {path!r}') 

80 

81 # @TODO: smarter reload 

82 

83 needs_reconfigure = any(not path.endswith('.py') for path in changed_paths) 

84 gws.log.info(f'MONITOR: begin reload {needs_reconfigure=}') 

85 

86 if not self._reload(needs_reconfigure): 

87 return 

88 

89 # finally, reload ourselves 

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

91 control.reload_app('spool') 

92 

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 

100 

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 

108 

109 def _cleanup(self): 

110 """Remove superfluous directory and file entries.""" 

111 

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)} 

119 

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)} 

127 

128 def _poll(self): 

129 new_stats = {} 

130 changed_paths = [] 

131 

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) 

140 

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) 

144 

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) 

148 

149 self.pathStats = new_stats 

150 return changed_paths 

151 

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 

158 

159 def _ignored(self, filename): 

160 return self.ignore and any(re.search(p, filename) for p in self.ignore)