Coverage for gws-app/gws/server/manager.py: 58%

109 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Server manager, which creates the server configuration files and the start script.""" 

2 

3from typing import cast 

4 

5import os 

6import re 

7 

8import gws 

9import gws.config 

10import gws.config.util 

11import gws.lib.osx 

12 

13from . import core 

14 

15 

16class TemplateArgs(gws.TemplateArgs): 

17 """Arguments for configuration templates.""" 

18 

19 root: gws.Root 

20 """Root object.""" 

21 serverDir: str 

22 """Absolute path to the ``gws/server`` directory.""" 

23 gwsEnv: dict 

24 """Environment variables whose names start with ``GWS_``.""" 

25 inContainer: bool 

26 """True if running in a container.""" 

27 uwsgi: str 

28 """uWSGI backend name, ``web`` or ``spool``, for the uWSGI config template.""" 

29 userName: str 

30 """Name of the user the servers run as.""" 

31 groupName: str 

32 """Group name of that user.""" 

33 homeDir: str 

34 """Home directory of that user.""" 

35 nginxConfig: str 

36 """nginx config path.""" 

37 nginxPid: str 

38 """nginx pid path.""" 

39 spoolConfig: str 

40 """Spooler config path.""" 

41 spoolPid: str 

42 """Spooler pid path.""" 

43 spoolSocket: str 

44 """Spooler socket path.""" 

45 syslogConfig: str 

46 """Syslog config path.""" 

47 syslogPid: str 

48 """Syslog pid path.""" 

49 webConfig: str 

50 """Web server config path.""" 

51 webPid: str 

52 """Web server pid path.""" 

53 webSocket: str 

54 """Web server socket path.""" 

55 

56 

57_DEFAULT_BASE_TIMEOUT = 60 

58_DEFAULT_SPOOL_TIMEOUT = 300 

59 

60_SERVER_DIR = gws.u.dirname(__file__) 

61 

62_DEFAULT_TEMPLATES = [ 

63 gws.Config(type='text', subject='server.rsyslog_config', path=f'{_SERVER_DIR}/templates/rsyslog_config.cx.txt'), 

64 gws.Config(type='text', subject='server.nginx_config', path=f'{_SERVER_DIR}/templates/nginx_config.cx.txt'), 

65 gws.Config(type='text', subject='server.uwsgi_config', path=f'{_SERVER_DIR}/templates/uwsgi_config.cx.txt'), 

66 gws.Config(type='text', subject='server.start_script', path=f'{_SERVER_DIR}/templates/start_script.cx.txt'), 

67] 

68 

69 

70class Object(gws.ServerManager): 

71 """Server manager.""" 

72 

73 def configure(self): 

74 self.config = self._add_defaults(self.config, 'gws.server.core.Config') 

75 self.config.log = self._add_defaults(self.config.log, 'gws.server.core.LogConfig') 

76 self.config.monitor = self._add_defaults(self.config.monitor, 'gws.server.core.MonitorConfig') 

77 self.config.qgis = self._add_defaults(self.config.qgis, 'gws.server.core.QgisConfig') 

78 self.config.spool = self._add_defaults(self.config.spool, 'gws.server.core.SpoolConfig') 

79 self.config.web = self._add_defaults(self.config.web, 'gws.server.core.WebConfig') 

80 

81 # deprecated 'enabled' keys 

82 if self.config.spool.enabled is not None: 

83 self.root.config_warning('"server.spool.enabled" is deprecated, use "server.withSpool"') 

84 if self.config.spool.enabled is False: 

85 self.config.withSpool = False 

86 if self.config.web.enabled is not None: 

87 self.root.config_warning('"server.web.enabled" is deprecated, use "server.withWeb"') 

88 if self.config.web.enabled is False: 

89 self.config.withWeb = False 

90 if self.config.monitor.enabled is not None: 

91 self.root.config_warning('"server.monitor.enabled" is deprecated, use "server.withMonitor"') 

92 if self.config.monitor.enabled is False: 

93 self.config.withMonitor = False 

94 if self.config.mapproxy is not None: 

95 self.root.config_warning('"server.mapproxy" is deprecated and ignored') 

96 if self.config.withMapproxy is not None: 

97 self.root.config_warning('"server.withMapproxy" is deprecated and ignored') 

98 

99 self.configure_environment() 

100 self.configure_templates() 

101 

102 def _add_defaults(self, value, type_name): 

103 """Merge a config value into the defaults of the given config type.""" 

104 return gws.u.merge(self.root.specs.read({}, type_name), value) 

105 

106 def configure_environment(self): 

107 """Overwrite the log level and the numbers of workers from the environment.""" 

108 

109 cfg = cast(core.Config, self.config) 

110 p = gws.env.GWS_LOG_LEVEL 

111 if p: 

112 cfg.log.level = p 

113 p = gws.env.GWS_WEB_WORKERS 

114 if p: 

115 cfg.web.workers = int(p) 

116 p = gws.env.GWS_SPOOL_WORKERS 

117 if p: 

118 cfg.spool.workers = int(p) 

119 

120 def configure_templates(self): 

121 """Create the templates from the ``templates`` config and add the default templates.""" 

122 gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES) 

123 

124 def create_server_configs(self, target_dir, script_path, pid_paths): 

125 ui = gws.lib.osx.user_info(gws.c.UID, gws.c.GID) 

126 

127 args = TemplateArgs( 

128 root=self.root, 

129 serverDir=_SERVER_DIR, 

130 gwsEnv={k: v for k, v in sorted(os.environ.items()) if k.startswith('GWS_')}, 

131 inContainer=gws.c.env.GWS_IN_CONTAINER, 

132 uwsgi='', 

133 userName=ui['pw_name'], 

134 groupName=ui['gr_name'], 

135 homeDir=ui['pw_dir'], 

136 nginxConfig='', 

137 nginxPid=pid_paths['nginx'], 

138 spoolConfig='', 

139 spoolPid=pid_paths['spool'], 

140 spoolSocket=f'{gws.c.TMP_DIR}/spool.uwsgi.sock', 

141 syslogConfig='', 

142 syslogPid='', 

143 webConfig='', 

144 webPid=pid_paths['web'], 

145 webSocket=f'{gws.c.TMP_DIR}/web.uwsgi.sock', 

146 ) 

147 

148 if args.inContainer: 

149 args.syslogPid = f'{gws.c.PIDS_DIR}/rsyslogd.pid' 

150 args.syslogConfig = self._create_config('server.rsyslog_config', f'{target_dir}/syslog.conf', args) 

151 

152 if self.cfg('withWeb'): 

153 args.uwsgi = 'web' 

154 args.webConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_web.ini', args) 

155 

156 if self.cfg('withSpool'): 

157 args.uwsgi = 'spool' 

158 args.spoolConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_spool.ini', args) 

159 

160 args.nginxConfig = self._create_config('server.nginx_config', f'{target_dir}/nginx.conf', args, True) 

161 

162 self._create_config('server.start_script', script_path, args) 

163 

164 def _create_config(self, subject: str, path: str, args: TemplateArgs, is_nginx=False) -> str: 

165 """Render a template, normalize the whitespace and write the result to a file, return the path. 

166 

167 The NGINX config is reformatted with one statement per line and indented blocks. 

168 """ 

169 tpl = gws.u.require(self.root.app.templateMgr.find_template(subject, where=[self])) 

170 res = tpl.render(gws.TemplateRenderInput(args=args)) 

171 

172 text = str(res.content) 

173 if is_nginx: 

174 text = re.sub(r'\s*{\s*', ' {\n', text) 

175 text = re.sub(r'\s*}\s*', '\n}\n', text) 

176 

177 lines = [] 

178 indent = 0 

179 

180 for line in text.split('\n'): 

181 line = re.sub(r'\s+', ' ', line.strip()) 

182 if not line: 

183 continue 

184 if is_nginx: 

185 if line == '}': 

186 indent -= 1 

187 lines.append((' ' * (indent * 4)) + line) 

188 if '{' in line: 

189 indent += 1 

190 else: 

191 lines.append(line) 

192 

193 text = '\n'.join(lines) + '\n' 

194 gws.u.write_file(path, text) 

195 return path