Mercurial > libervia-backend
annotate sat/plugins/plugin_misc_app_manager.py @ 3998:402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Application may be long to start (e.g. a Docker app may have to download images first,
and even without the downloading, the starting could be long), which may lead to UI
blocking or bridge time out.
To prevent that, `start` is now returning immediately, and 2 new signals are used to
indicate when the application is started, of if something wrong happened.
`start` now returns initial app data, including exposed data without the computed exposed
data. The computed data must be retrieved after the app has been started.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 04 Mar 2023 18:30:47 +0100 |
parents | d66a8453b02b |
children | 524856bd7b19 |
rev | line source |
---|---|
3372 | 1 #!/usr/bin/env python3 |
2 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
3 # Libervia plugin to manage external applications |
3479 | 4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) |
3372 | 5 |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from pathlib import Path | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
20 from typing import Optional, List, Callable |
3372 | 21 from functools import partial, reduce |
22 import tempfile | |
23 import secrets | |
24 import string | |
25 import shortuuid | |
26 from twisted.internet import defer | |
27 from twisted.python.procutils import which | |
28 from sat.core.i18n import _ | |
29 from sat.core import exceptions | |
30 from sat.core.constants import Const as C | |
31 from sat.core.log import getLogger | |
32 from sat.tools.common import data_format | |
33 from sat.tools.common import async_process | |
34 | |
35 log = getLogger(__name__) | |
36 | |
37 try: | |
38 import yaml | |
39 except ImportError: | |
40 raise exceptions.MissingModule( | |
41 'Missing module PyYAML, please download/install it. You can use ' | |
42 '"pip install pyyaml"' | |
43 ) | |
44 | |
45 try: | |
46 from yaml import CLoader as Loader, CDumper as Dumper | |
47 except ImportError: | |
48 log.warning( | |
49 "Can't use LibYAML binding (is libyaml installed?), pure Python version will be " | |
50 "used, but it is slower" | |
51 ) | |
52 from yaml import Loader, Dumper | |
53 | |
54 from yaml.constructor import ConstructorError | |
55 | |
56 | |
57 PLUGIN_INFO = { | |
58 C.PI_NAME: "Applications Manager", | |
59 C.PI_IMPORT_NAME: "APP_MANAGER", | |
60 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
61 C.PI_MODES: C.PLUG_MODE_BOTH, | |
62 C.PI_MAIN: "AppManager", | |
63 C.PI_HANDLER: "no", | |
64 C.PI_DESCRIPTION: _( | |
65 """Applications Manager | |
66 | |
67 Manage external applications using packagers, OS virtualization/containers or other | |
68 software management tools. | |
69 """), | |
70 } | |
71 | |
72 APP_FILE_PREFIX = "sat_app_" | |
73 | |
74 | |
75 class AppManager: | |
76 load = partial(yaml.load, Loader=Loader) | |
77 dump = partial(yaml.dump, Dumper=Dumper) | |
78 | |
79 def __init__(self, host): | |
80 log.info(_("plugin Applications Manager initialization")) | |
81 self.host = host | |
82 self._managers = {} | |
83 self._apps = {} | |
84 self._started = {} | |
85 # instance id to app data map | |
86 self._instances = {} | |
87 host.bridge.addMethod( | |
88 "applicationsList", | |
89 ".plugin", | |
90 in_sign="as", | |
91 out_sign="as", | |
92 method=self.list_applications, | |
93 ) | |
94 host.bridge.addMethod( | |
95 "applicationStart", | |
96 ".plugin", | |
97 in_sign="ss", | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
98 out_sign="s", |
3372 | 99 method=self._start, |
100 async_=True, | |
101 ) | |
102 host.bridge.addMethod( | |
103 "applicationStop", | |
104 ".plugin", | |
105 in_sign="sss", | |
106 out_sign="", | |
107 method=self._stop, | |
108 async_=True, | |
109 ) | |
110 host.bridge.addMethod( | |
111 "applicationExposedGet", | |
112 ".plugin", | |
113 in_sign="sss", | |
114 out_sign="s", | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
115 method=self._get_exposed, |
3372 | 116 async_=True, |
117 ) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
118 # application has been started succeesfully, |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
119 # args: name, instance_id, extra |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
120 host.bridge.addSignal( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
121 "application_started", ".plugin", signature="sss" |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
122 ) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
123 # application went wrong with the application |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
124 # args: name, instance_id, extra |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
125 host.bridge.addSignal( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
126 "application_error", ".plugin", signature="sss" |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
127 ) |
3372 | 128 yaml.add_constructor( |
129 "!sat_conf", self._sat_conf_constr, Loader=Loader) | |
130 yaml.add_constructor( | |
131 "!sat_generate_pwd", self._sat_generate_pwd_constr, Loader=Loader) | |
132 yaml.add_constructor( | |
133 "!sat_param", self._sat_param_constr, Loader=Loader) | |
134 | |
135 def unload(self): | |
136 log.debug("unloading applications manager") | |
137 for instances in self._started.values(): | |
138 for instance in instances: | |
139 data = instance['data'] | |
140 if not data['single_instance']: | |
141 log.debug( | |
142 f"cleaning temporary directory at {data['_instance_dir_path']}") | |
143 data['_instance_dir_obj'].cleanup() | |
144 | |
145 def _sat_conf_constr(self, loader, node): | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
146 """Get a value from Libervia configuration |
3372 | 147 |
148 A list is expected with either "name" of a config parameter, a one or more of | |
149 those parameters: | |
150 - section | |
151 - name | |
152 - default value | |
153 - filter | |
154 filter can be: | |
155 - "first": get the first item of the value | |
156 """ | |
157 config_data = loader.construct_sequence(node) | |
158 if len(config_data) == 1: | |
159 section, name, default, filter_ = "", config_data[0], None, None | |
160 if len(config_data) == 2: | |
161 (section, name), default, filter_ = config_data, None, None | |
162 elif len(config_data) == 3: | |
163 (section, name, default), filter_ = config_data, None | |
164 elif len(config_data) == 4: | |
165 section, name, default, filter_ = config_data | |
166 else: | |
167 raise ValueError( | |
168 f"invalid !sat_conf value ({config_data!r}), a list of 1 to 4 items is " | |
169 "expected" | |
170 ) | |
171 | |
172 value = self.host.memory.getConfig(section, name, default) | |
3564
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
173 # FIXME: "public_url" is used only here and doesn't take multi-sites into account |
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
174 if name == "public_url" and (not value or value.startswith('http')): |
3372 | 175 if not value: |
176 log.warning(_( | |
3564
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
177 'No value found for "public_url", using "example.org" for ' |
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
178 'now, please set the proper value in libervia.conf')) |
3372 | 179 else: |
180 log.warning(_( | |
3564
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
181 'invalid value for "public_url" ({value}), it musts not start with ' |
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
182 'schema ("http"), ignoring it and using "example.org" ' |
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
183 'instead') |
3372 | 184 .format(value=value)) |
3564
2c9e95796371
plugin app manager: "public_url" must NOT have a schema
Goffi <goffi@goffi.org>
parents:
3479
diff
changeset
|
185 value = "example.org" |
3372 | 186 |
187 if filter_ is None: | |
188 pass | |
189 elif filter_ == 'first': | |
190 value = value[0] | |
191 else: | |
192 raise ValueError(f"unmanaged filter: {filter_}") | |
193 | |
194 return value | |
195 | |
196 def _sat_generate_pwd_constr(self, loader, node): | |
197 alphabet = string.ascii_letters + string.digits | |
198 return ''.join(secrets.choice(alphabet) for i in range(30)) | |
199 | |
200 def _sat_param_constr(self, loader, node): | |
201 """Get a parameter specified when starting the application | |
202 | |
203 The value can be either the name of the parameter to get, or a list as | |
204 [name, default_value] | |
205 """ | |
206 try: | |
207 name, default = loader.construct_sequence(node) | |
208 except ConstructorError: | |
209 name, default = loader.construct_scalar(node), None | |
210 return self._params.get(name, default) | |
211 | |
212 def register(self, manager): | |
213 name = manager.name | |
214 if name in self._managers: | |
215 raise exceptions.ConflictError( | |
216 f"There is already a manager with the name {name}") | |
217 self._managers[manager.name] = manager | |
218 if hasattr(manager, "discover_path"): | |
219 self.discover(manager.discover_path, manager) | |
220 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
221 def get_manager(self, app_data: dict) -> object: |
3372 | 222 """Get manager instance needed for this app |
223 | |
224 @raise exceptions.DataError: something is wrong with the type | |
225 @raise exceptions.NotFound: manager is not registered | |
226 """ | |
227 try: | |
228 app_type = app_data["type"] | |
229 except KeyError: | |
230 raise exceptions.DataError( | |
231 "app file doesn't have the mandatory \"type\" key" | |
232 ) | |
233 if not isinstance(app_type, str): | |
234 raise exceptions.DataError( | |
235 f"invalid app data type: {app_type!r}" | |
236 ) | |
237 app_type = app_type.strip() | |
238 try: | |
239 return self._managers[app_type] | |
240 except KeyError: | |
241 raise exceptions.NotFound( | |
242 f"No manager found to manage app of type {app_type!r}") | |
243 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
244 def get_app_data( |
3372 | 245 self, |
246 id_type: Optional[str], | |
247 identifier: str | |
248 ) -> dict: | |
249 """Retrieve instance's app_data from identifier | |
250 | |
251 @param id_type: type of the identifier, can be: | |
252 - "name": identifier is a canonical application name | |
253 the first found instance of this application is returned | |
254 - "instance": identifier is an instance id | |
255 @param identifier: identifier according to id_type | |
256 @return: instance application data | |
257 @raise exceptions.NotFound: no instance with this id can be found | |
258 @raise ValueError: id_type is invalid | |
259 """ | |
260 if not id_type: | |
261 id_type = 'name' | |
262 if id_type == 'name': | |
263 identifier = identifier.lower().strip() | |
264 try: | |
265 return next(iter(self._started[identifier])) | |
266 except (KeyError, StopIteration): | |
267 raise exceptions.NotFound( | |
268 f"No instance of {identifier!r} is currently running" | |
269 ) | |
270 elif id_type == 'instance': | |
271 instance_id = identifier | |
272 try: | |
273 return self._instances[instance_id] | |
274 except KeyError: | |
275 raise exceptions.NotFound( | |
276 f"There is no application instance running with id {instance_id!r}" | |
277 ) | |
278 else: | |
279 raise ValueError(f"invalid id_type: {id_type!r}") | |
280 | |
281 def discover( | |
282 self, | |
283 dir_path: Path, | |
284 manager: Optional = None | |
285 ) -> None: | |
286 for file_path in dir_path.glob(f"{APP_FILE_PREFIX}*.yaml"): | |
287 if manager is None: | |
288 try: | |
289 app_data = self.parse(file_path) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
290 manager = self.get_manager(app_data) |
3372 | 291 except (exceptions.DataError, exceptions.NotFound) as e: |
292 log.warning( | |
293 f"Can't parse {file_path}, skipping: {e}") | |
294 app_name = file_path.stem[len(APP_FILE_PREFIX):].strip().lower() | |
295 if not app_name: | |
296 log.warning( | |
297 f"invalid app file name at {file_path}") | |
298 continue | |
299 app_dict = self._apps.setdefault(app_name, {}) | |
300 manager_set = app_dict.setdefault(manager, set()) | |
301 manager_set.add(file_path) | |
302 log.debug( | |
303 f"{app_name!r} {manager.name} application found" | |
304 ) | |
305 | |
306 def parse(self, file_path: Path, params: Optional[dict] = None) -> dict: | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
307 """Parse Libervia application file |
3372 | 308 |
309 @param params: parameters for running this instance | |
310 @raise exceptions.DataError: something is wrong in the file | |
311 """ | |
312 if params is None: | |
313 params = {} | |
314 with file_path.open() as f: | |
315 # we set parameters to be used only with this instance | |
316 # no async method must used between this assignation and `load` | |
317 self._params = params | |
318 app_data = self.load(f) | |
319 self._params = None | |
320 if "name" not in app_data: | |
321 # note that we don't use lower() here as we want human readable name and | |
322 # uppercase may be set on purpose | |
323 app_data['name'] = file_path.stem[len(APP_FILE_PREFIX):].strip() | |
324 single_instance = app_data.setdefault("single_instance", True) | |
325 if not isinstance(single_instance, bool): | |
326 raise ValueError( | |
327 f'"single_instance" must be a boolean, but it is {type(single_instance)}' | |
328 ) | |
329 return app_data | |
330 | |
331 def list_applications(self, filters: Optional[List[str]]) -> List[str]: | |
332 """List available application | |
333 | |
334 @param filters: only show applications matching those filters. | |
335 using None will list all known applications | |
336 a filter can be: | |
337 - available: applications available locally | |
338 - running: only show launched applications | |
339 """ | |
340 if not filters: | |
341 return list(self.apps) | |
342 found = set() | |
343 for filter_ in filters: | |
344 if filter_ == "available": | |
345 found.update(self._apps) | |
346 elif filter_ == "running": | |
347 found.update(self._started) | |
348 else: | |
349 raise ValueError(f"Unknown filter: {filter_}") | |
350 return list(found) | |
351 | |
352 def _start(self, app_name, extra): | |
353 extra = data_format.deserialise(extra) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
354 d = defer.ensureDeferred(self.start(str(app_name), extra)) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
355 d.addCallback(data_format.serialise) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
356 return d |
3372 | 357 |
358 async def start( | |
359 self, | |
360 app_name: str, | |
361 extra: Optional[dict] = None, | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
362 ) -> dict: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
363 """Start an application |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
364 |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
365 @param app_name: name of the application to start |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
366 @param extra: extra parameters |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
367 @return: data with following keys: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
368 - name (str): canonical application name |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
369 - instance (str): instance ID |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
370 - started (bool): True if the application is already started |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
371 if False, the "application_started" signal should be used to get notificed |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
372 when the application is actually started |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
373 - expose (dict): exposed data as given by [self.get_exposed] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
374 exposed data which need to be computed are NOT returned, they will |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
375 available when the app will be fully started, throught the |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
376 [self.get_exposed] method. |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
377 """ |
3372 | 378 # FIXME: for now we use the first app manager available for the requested app_name |
379 # TODO: implement running multiple instance of the same app if some metadata | |
380 # to be defined in app_data allows explicitly it. | |
381 app_name = app_name.lower().strip() | |
382 try: | |
383 app_file_path = next(iter(next(iter(self._apps[app_name].values())))) | |
384 except KeyError: | |
385 raise exceptions.NotFound( | |
386 f"No application found with the name {app_name!r}" | |
387 ) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
388 log.info(f"starting {app_name!r}") |
3372 | 389 started_data = self._started.setdefault(app_name, []) |
390 app_data = self.parse(app_file_path, extra) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
391 app_data["_started"] = False |
3372 | 392 app_data['_file_path'] = app_file_path |
393 app_data['_name_canonical'] = app_name | |
394 single_instance = app_data['single_instance'] | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
395 ret_data = { |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
396 "name": app_name, |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
397 "started": False |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
398 } |
3372 | 399 if single_instance: |
400 if started_data: | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
401 instance_data = started_data[0] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
402 instance_id = instance_data["_instance_id"] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
403 ret_data["instance"] = instance_id |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
404 ret_data["started"] = instance_data["_started"] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
405 ret_data["expose"] = await self.get_exposed( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
406 instance_id, "instance", {"skip_compute": True} |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
407 ) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
408 log.info(f"{app_name!r} is already started or being started") |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
409 return ret_data |
3372 | 410 else: |
411 cache_path = self.host.memory.getCachePath( | |
412 PLUGIN_INFO[C.PI_IMPORT_NAME], app_name | |
413 ) | |
414 cache_path.mkdir(0o700, parents=True, exist_ok=True) | |
415 app_data['_instance_dir_path'] = cache_path | |
416 else: | |
417 dest_dir_obj = tempfile.TemporaryDirectory(prefix="sat_app_") | |
418 app_data['_instance_dir_obj'] = dest_dir_obj | |
419 app_data['_instance_dir_path'] = Path(dest_dir_obj.name) | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
420 instance_id = ret_data["instance"] = app_data['_instance_id'] = shortuuid.uuid() |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
421 manager = self.get_manager(app_data) |
3372 | 422 app_data['_manager'] = manager |
423 started_data.append(app_data) | |
424 self._instances[instance_id] = app_data | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
425 # we retrieve exposed data such as url_prefix which can be useful computed exposed |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
426 # data must wait for the app to be started, so we skip them for now |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
427 ret_data["expose"] = await self.get_exposed( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
428 instance_id, "instance", {"skip_compute": True} |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
429 ) |
3372 | 430 |
431 try: | |
432 start = manager.start | |
433 except AttributeError: | |
434 raise exceptions.InternalError( | |
435 f"{manager.name} doesn't have the mandatory \"start\" method" | |
436 ) | |
437 else: | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
438 defer.ensureDeferred(self.start_app(start, app_data)) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
439 return ret_data |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
440 |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
441 async def start_app(self, start_cb: Callable, app_data: dict) -> None: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
442 app_name = app_data["_name_canonical"] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
443 instance_id = app_data["_instance_id"] |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
444 try: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
445 await start_cb(app_data) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
446 except Exception as e: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
447 log.exception(f"Can't start libervia app {app_name!r}") |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
448 self.host.bridge.application_error( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
449 app_name, |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
450 instance_id, |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
451 data_format.serialise( |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
452 { |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
453 "class": str(type(e)), |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
454 "msg": str(e) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
455 } |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
456 )) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
457 else: |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
458 app_data["_started"] = True |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
459 self.host.bridge.application_started(app_name, instance_id, "") |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
460 log.info(f"{app_name!r} started") |
3372 | 461 |
462 def _stop(self, identifier, id_type, extra): | |
463 extra = data_format.deserialise(extra) | |
464 return defer.ensureDeferred( | |
465 self.stop(str(identifier), str(id_type) or None, extra)) | |
466 | |
467 async def stop( | |
468 self, | |
469 identifier: str, | |
470 id_type: Optional[str] = None, | |
471 extra: Optional[dict] = None, | |
472 ) -> None: | |
473 if extra is None: | |
474 extra = {} | |
475 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
476 app_data = self.get_app_data(id_type, identifier) |
3372 | 477 |
478 log.info(f"stopping {app_data['name']!r}") | |
479 | |
480 app_name = app_data['_name_canonical'] | |
481 instance_id = app_data['_instance_id'] | |
482 manager = app_data['_manager'] | |
483 | |
484 try: | |
485 stop = manager.stop | |
486 except AttributeError: | |
487 raise exceptions.InternalError( | |
488 f"{manager.name} doesn't have the mandatory \"stop\" method" | |
489 ) | |
490 else: | |
491 try: | |
492 await stop(app_data) | |
493 except Exception as e: | |
494 log.warning( | |
495 f"Instance {instance_id} of application {app_name} can't be stopped " | |
496 f"properly: {e}" | |
497 ) | |
498 return | |
499 | |
500 try: | |
501 del self._instances[instance_id] | |
502 except KeyError: | |
503 log.error( | |
504 f"INTERNAL ERROR: {instance_id!r} is not present in self._instances") | |
505 | |
506 try: | |
507 self._started[app_name].remove(app_data) | |
508 except ValueError: | |
509 log.error( | |
510 "INTERNAL ERROR: there is no app data in self._started with id " | |
511 f"{instance_id!r}" | |
512 ) | |
513 | |
514 log.info(f"{app_name!r} stopped") | |
515 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
516 def _get_exposed(self, identifier, id_type, extra): |
3372 | 517 extra = data_format.deserialise(extra) |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
518 d = defer.ensureDeferred(self.get_exposed(identifier, id_type, extra)) |
3372 | 519 d.addCallback(lambda d: data_format.serialise(d)) |
520 return d | |
521 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
522 async def get_exposed( |
3372 | 523 self, |
524 identifier: str, | |
525 id_type: Optional[str] = None, | |
526 extra: Optional[dict] = None, | |
527 ) -> dict: | |
528 """Get data exposed by the application | |
529 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
530 The manager's "compute_expose" method will be called if it exists. It can be used |
3372 | 531 to handle manager specific conventions. |
532 """ | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
533 app_data = self.get_app_data(id_type, identifier) |
3372 | 534 if app_data.get('_exposed_computed', False): |
535 return app_data['expose'] | |
536 if extra is None: | |
537 extra = {} | |
538 expose = app_data.setdefault("expose", {}) | |
539 if "passwords" in expose: | |
540 passwords = expose['passwords'] | |
541 for name, value in list(passwords.items()): | |
542 if isinstance(value, list): | |
543 # if we have a list, is the sequence of keys leading to the value | |
544 # to expose. We use "reduce" to retrieve the desired value | |
545 try: | |
546 passwords[name] = reduce(lambda l, k: l[k], value, app_data) | |
547 except Exception as e: | |
548 log.warning( | |
549 f"Can't retrieve exposed value for password {name!r}: {e}") | |
550 del passwords[name] | |
551 | |
552 url_prefix = expose.get("url_prefix") | |
553 if isinstance(url_prefix, list): | |
554 try: | |
555 expose["url_prefix"] = reduce(lambda l, k: l[k], url_prefix, app_data) | |
556 except Exception as e: | |
557 log.warning( | |
558 f"Can't retrieve exposed value for url_prefix: {e}") | |
559 del expose["url_prefix"] | |
560 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
561 if extra.get("skip_compute", False): |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
562 return expose |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
563 |
3372 | 564 try: |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
565 compute_expose = app_data['_manager'].compute_expose |
3372 | 566 except AttributeError: |
567 pass | |
568 else: | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
569 await compute_expose(app_data) |
3372 | 570 |
571 app_data['_exposed_computed'] = True | |
572 return expose | |
573 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
574 async def _do_prepare( |
3372 | 575 self, |
576 app_data: dict, | |
577 ) -> None: | |
578 name = app_data['name'] | |
579 dest_path = app_data['_instance_dir_path'] | |
580 if next(dest_path.iterdir(), None) != None: | |
581 log.debug(f"There is already a prepared dir at {dest_path}, nothing to do") | |
582 return | |
583 try: | |
584 prepare = app_data['prepare'].copy() | |
585 except KeyError: | |
586 prepare = {} | |
587 | |
588 if not prepare: | |
589 log.debug("Nothing to prepare for {name!r}") | |
590 return | |
591 | |
592 for action, value in list(prepare.items()): | |
593 log.debug(f"[{name}] [prepare] running {action!r} action") | |
594 if action == "git": | |
595 try: | |
596 git_path = which('git')[0] | |
597 except IndexError: | |
598 raise exceptions.NotFound( | |
599 "Can't find \"git\" executable, {name} can't be started without it" | |
600 ) | |
601 await async_process.run(git_path, "clone", value, str(dest_path)) | |
602 log.debug(f"{value!r} git repository cloned at {dest_path}") | |
603 else: | |
604 raise NotImplementedError( | |
605 f"{action!r} is not managed, can't start {name}" | |
606 ) | |
607 del prepare[action] | |
608 | |
609 if prepare: | |
610 raise exceptions.InternalError('"prepare" should be empty') | |
611 | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
612 async def _do_create_files( |
3565
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
613 self, |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
614 app_data: dict, |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
615 ) -> None: |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
616 dest_path = app_data['_instance_dir_path'] |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
617 files = app_data.get('files') |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
618 if not files: |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
619 return |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
620 if not isinstance(files, dict): |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
621 raise ValueError('"files" must be a dictionary') |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
622 for filename, data in files.items(): |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
623 path = dest_path / filename |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
624 if path.is_file(): |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
625 log.info(f"{path} already exists, skipping") |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
626 with path.open("w") as f: |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
627 f.write(data.get("content", "")) |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
628 log.debug(f"{path} created") |
d66a8453b02b
plugin app manager: add a way to create files:
Goffi <goffi@goffi.org>
parents:
3564
diff
changeset
|
629 |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
630 async def start_common(self, app_data: dict) -> None: |
3372 | 631 """Method running common action when starting a manager |
632 | |
633 It should be called by managers in "start" method. | |
634 """ | |
3998
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
635 await self._do_prepare(app_data) |
402d31527af4
plugin app manager: `start` doesn't wait anymore for actual app start:
Goffi <goffi@goffi.org>
parents:
3565
diff
changeset
|
636 await self._do_create_files(app_data) |