Coverage for gws-app/gws/server/manager.py: 57%
115 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""Server manager, which creates the server configuration files and the start script."""
3from typing import cast
5import os
6import re
8import gws
9import gws.config
10import gws.config.util
11import gws.lib.osx
13from . import core
16class TemplateArgs(gws.TemplateArgs):
17 """Arguments for configuration templates."""
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."""
57_DEFAULT_BASE_TIMEOUT = 60
58_DEFAULT_SPOOL_TIMEOUT = 300
60_SERVER_DIR = gws.u.dirname(__file__)
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]
70class Object(gws.ServerManager):
71 """Server manager."""
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')
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 if self.config.autoRun:
99 self.root.config_warning('"server.autoRun" is deprecated and ignored')
100 if self.config.timeZone is not None:
101 self.root.config_warning('"server.timeZone" is deprecated and ignored, set the "TZ" environment variable')
103 if self.config.withMonitor and not self.config.withSpool:
104 raise gws.ConfigurationError('"server.withMonitor" requires "server.withSpool"')
106 self.configure_environment()
107 self.configure_templates()
109 def _add_defaults(self, value, type_name):
110 """Merge a config value into the defaults of the given config type."""
111 return gws.u.merge(self.root.specs.read({}, type_name), value)
113 def configure_environment(self):
114 """Overwrite the log level and the numbers of workers from the environment."""
116 cfg = cast(core.Config, self.config)
117 p = gws.env.GWS_LOG_LEVEL
118 if p:
119 cfg.log.level = p
120 p = gws.env.GWS_WEB_WORKERS
121 if p:
122 cfg.web.workers = int(p)
123 p = gws.env.GWS_SPOOL_WORKERS
124 if p:
125 cfg.spool.workers = int(p)
127 def configure_templates(self):
128 """Create the templates from the ``templates`` config and add the default templates."""
129 gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES)
131 def create_server_configs(self, target_dir, script_path, pid_paths):
132 ui = gws.lib.osx.user_info(gws.c.UID, gws.c.GID)
134 args = TemplateArgs(
135 root=self.root,
136 serverDir=_SERVER_DIR,
137 gwsEnv={k: v for k, v in sorted(os.environ.items()) if k.startswith('GWS_')},
138 inContainer=gws.c.env.GWS_IN_CONTAINER,
139 uwsgi='',
140 userName=ui['pw_name'],
141 groupName=ui['gr_name'],
142 homeDir=ui['pw_dir'],
143 nginxConfig='',
144 nginxPid=pid_paths['nginx'],
145 spoolConfig='',
146 spoolPid=pid_paths['spool'],
147 spoolSocket=f'{gws.c.TMP_DIR}/spool.uwsgi.sock',
148 syslogConfig='',
149 syslogPid='',
150 webConfig='',
151 webPid=pid_paths['web'],
152 webSocket=f'{gws.c.TMP_DIR}/web.uwsgi.sock',
153 )
155 if args.inContainer:
156 args.syslogPid = f'{gws.c.PIDS_DIR}/rsyslogd.pid'
157 args.syslogConfig = self._create_config('server.rsyslog_config', f'{target_dir}/syslog.conf', args)
159 if self.cfg('withWeb'):
160 args.uwsgi = 'web'
161 args.webConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_web.ini', args)
163 if self.cfg('withSpool'):
164 args.uwsgi = 'spool'
165 args.spoolConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_spool.ini', args)
167 args.nginxConfig = self._create_config('server.nginx_config', f'{target_dir}/nginx.conf', args, True)
169 self._create_config('server.start_script', script_path, args)
171 def _create_config(self, subject: str, path: str, args: TemplateArgs, is_nginx=False) -> str:
172 """Render a template, normalize the whitespace and write the result to a file, return the path.
174 The NGINX config is reformatted with one statement per line and indented blocks.
175 """
176 tpl = gws.u.require(self.root.app.templateMgr.find_template(subject, where=[self]))
177 res = tpl.render(gws.TemplateRenderInput(args=args))
179 text = str(res.content)
180 if is_nginx:
181 text = re.sub(r'\s*{\s*', ' {\n', text)
182 text = re.sub(r'\s*}\s*', '\n}\n', text)
184 lines = []
185 indent = 0
187 for line in text.split('\n'):
188 line = re.sub(r'\s+', ' ', line.strip())
189 if not line:
190 continue
191 if is_nginx:
192 if line == '}':
193 indent -= 1
194 lines.append((' ' * (indent * 4)) + line)
195 if '{' in line:
196 indent += 1
197 else:
198 lines.append(line)
200 text = '\n'.join(lines) + '\n'
201 gws.u.write_file(path, text)
202 return path