ominfra 0.0.0.dev190__py3-none-any.whl → 0.0.0.dev192__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
+ import json
3
4
  import os.path
4
5
  import typing as ta
5
6
 
@@ -13,6 +14,7 @@ from .git import DeployGitManager
13
14
  from .paths.owners import DeployPathOwner
14
15
  from .paths.paths import DeployPath
15
16
  from .specs import DeployAppSpec
17
+ from .tags import DeployAppRev
16
18
  from .tags import DeployTagMap
17
19
  from .types import DeployHome
18
20
  from .venvs import DeployVenvManager
@@ -57,9 +59,21 @@ class DeployAppManager(DeployPathOwner):
57
59
 
58
60
  #
59
61
 
62
+ def _make_tags(self, spec: DeployAppSpec) -> DeployTagMap:
63
+ return DeployTagMap(
64
+ spec.app,
65
+ spec.key(),
66
+ DeployAppRev(spec.git.rev),
67
+ )
68
+
69
+ #
70
+
60
71
  @dc.dataclass(frozen=True)
61
72
  class PreparedApp:
62
- app_dir: str
73
+ spec: DeployAppSpec
74
+ tags: DeployTagMap
75
+
76
+ dir: str
63
77
 
64
78
  git_dir: ta.Optional[str] = None
65
79
  venv_dir: ta.Optional[str] = None
@@ -70,21 +84,30 @@ class DeployAppManager(DeployPathOwner):
70
84
  spec: DeployAppSpec,
71
85
  home: DeployHome,
72
86
  tags: DeployTagMap,
87
+ *,
88
+ conf_string_ns: ta.Optional[ta.Mapping[str, ta.Any]] = None,
73
89
  ) -> PreparedApp:
74
90
  spec_json = json_dumps_pretty(self._msh.marshal_obj(spec))
75
91
 
76
92
  #
77
93
 
94
+ app_tags = tags.add(*self._make_tags(spec))
95
+
96
+ #
97
+
78
98
  check.non_empty_str(home)
79
99
 
80
- app_dir = os.path.join(home, self.APP_DIR.render(tags))
100
+ app_dir = os.path.join(home, self.APP_DIR.render(app_tags))
81
101
 
82
102
  os.makedirs(app_dir, exist_ok=True)
83
103
 
84
104
  #
85
105
 
86
106
  rkw: ta.Dict[str, ta.Any] = dict(
87
- app_dir=app_dir,
107
+ spec=spec,
108
+ tags=app_tags,
109
+
110
+ dir=app_dir,
88
111
  )
89
112
 
90
113
  #
@@ -119,11 +142,67 @@ class DeployAppManager(DeployPathOwner):
119
142
  if spec.conf is not None:
120
143
  conf_dir = os.path.join(app_dir, 'conf')
121
144
  rkw.update(conf_dir=conf_dir)
145
+
146
+ conf_ns: ta.Dict[str, ta.Any] = dict(
147
+ **(conf_string_ns or {}),
148
+ app=spec.app.s,
149
+ app_dir=app_dir.rstrip('/'),
150
+ )
151
+
122
152
  await self._conf.write_app_conf(
123
153
  spec.conf,
124
154
  conf_dir,
155
+ string_ns=conf_ns,
125
156
  )
126
157
 
127
158
  #
128
159
 
129
160
  return DeployAppManager.PreparedApp(**rkw)
161
+
162
+ async def prepare_app_link(
163
+ self,
164
+ tags: DeployTagMap,
165
+ app_dir: str,
166
+ ) -> PreparedApp:
167
+ spec_file = os.path.join(app_dir, 'spec.json')
168
+ with open(spec_file) as f: # noqa
169
+ spec_json = f.read()
170
+
171
+ spec: DeployAppSpec = self._msh.unmarshal_obj(json.loads(spec_json), DeployAppSpec)
172
+
173
+ #
174
+
175
+ app_tags = tags.add(*self._make_tags(spec))
176
+
177
+ #
178
+
179
+ rkw: ta.Dict[str, ta.Any] = dict(
180
+ spec=spec,
181
+ tags=app_tags,
182
+
183
+ dir=app_dir,
184
+ )
185
+
186
+ #
187
+
188
+ git_dir = os.path.join(app_dir, 'git')
189
+ check.state(os.path.isdir(git_dir))
190
+ rkw.update(git_dir=git_dir)
191
+
192
+ #
193
+
194
+ if spec.venv is not None:
195
+ venv_dir = os.path.join(app_dir, 'venv')
196
+ check.state(os.path.isdir(venv_dir))
197
+ rkw.update(venv_dir=venv_dir)
198
+
199
+ #
200
+
201
+ if spec.conf is not None:
202
+ conf_dir = os.path.join(app_dir, 'conf')
203
+ check.state(os.path.isdir(conf_dir))
204
+ rkw.update(conf_dir=conf_dir)
205
+
206
+ #
207
+
208
+ return DeployAppManager.PreparedApp(**rkw)
@@ -16,6 +16,8 @@ TODO:
16
16
  - some things (venvs) cannot be moved, thus the /deploy/venvs dir
17
17
  - ** ensure (enforce) equivalent relpath nesting
18
18
  """
19
+ import collections.abc
20
+ import functools
19
21
  import os.path
20
22
  import typing as ta
21
23
 
@@ -43,20 +45,66 @@ from .specs import NginxDeployAppConfContent
43
45
  from .specs import RawDeployAppConfContent
44
46
 
45
47
 
48
+ T = ta.TypeVar('T')
49
+
50
+
51
+ ##
52
+
53
+
46
54
  class DeployConfManager:
47
- def _render_app_conf_content(self, ac: DeployAppConfContent) -> str:
55
+ def _process_conf_content(
56
+ self,
57
+ content: T,
58
+ *,
59
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
60
+ ) -> T:
61
+ def rec(o):
62
+ if isinstance(o, str):
63
+ if str_processor is not None:
64
+ return type(o)(str_processor(o))
65
+
66
+ elif isinstance(o, collections.abc.Mapping):
67
+ return type(o)([ # type: ignore
68
+ (rec(k), rec(v))
69
+ for k, v in o.items()
70
+ ])
71
+
72
+ elif isinstance(o, collections.abc.Iterable):
73
+ return type(o)([ # type: ignore
74
+ rec(e) for e in o
75
+ ])
76
+
77
+ return o
78
+
79
+ return rec(content)
80
+
81
+ #
82
+
83
+ def _render_app_conf_content(
84
+ self,
85
+ ac: DeployAppConfContent,
86
+ *,
87
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
88
+ ) -> str:
89
+ pcc = functools.partial(
90
+ self._process_conf_content,
91
+ str_processor=str_processor,
92
+ )
93
+
48
94
  if isinstance(ac, RawDeployAppConfContent):
49
- return ac.body
95
+ return pcc(ac.body)
50
96
 
51
97
  elif isinstance(ac, JsonDeployAppConfContent):
52
- return strip_with_newline(json_dumps_pretty(ac.obj))
98
+ json_obj = pcc(ac.obj)
99
+ return strip_with_newline(json_dumps_pretty(json_obj))
53
100
 
54
101
  elif isinstance(ac, IniDeployAppConfContent):
55
- return strip_with_newline(render_ini_config(ac.sections))
102
+ ini_sections = pcc(ac.sections)
103
+ return strip_with_newline(render_ini_config(ini_sections))
56
104
 
57
105
  elif isinstance(ac, NginxDeployAppConfContent):
58
- ni = NginxConfigItems.of(ac.items)
59
- return strip_with_newline(render_nginx_config_str(ni))
106
+ nginx_items = NginxConfigItems.of(pcc(ac.items))
107
+ return strip_with_newline(render_nginx_config_str(nginx_items))
60
108
 
61
109
  else:
62
110
  raise TypeError(ac)
@@ -65,11 +113,16 @@ class DeployConfManager:
65
113
  self,
66
114
  acf: DeployAppConfFile,
67
115
  app_conf_dir: str,
116
+ *,
117
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
68
118
  ) -> None:
69
119
  conf_file = os.path.join(app_conf_dir, acf.path)
70
120
  check.arg(is_path_in_dir(app_conf_dir, conf_file))
71
121
 
72
- body = self._render_app_conf_content(acf.content)
122
+ body = self._render_app_conf_content(
123
+ acf.content,
124
+ str_processor=str_processor,
125
+ )
73
126
 
74
127
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
75
128
 
@@ -80,11 +133,21 @@ class DeployConfManager:
80
133
  self,
81
134
  spec: DeployAppConfSpec,
82
135
  app_conf_dir: str,
136
+ *,
137
+ string_ns: ta.Optional[ta.Mapping[str, ta.Any]] = None,
83
138
  ) -> None:
139
+ process_str: ta.Any
140
+ if string_ns is not None:
141
+ def process_str(s: str) -> str:
142
+ return s.format(**string_ns)
143
+ else:
144
+ process_str = None
145
+
84
146
  for acf in spec.files or []:
85
147
  await self._write_app_conf_file(
86
148
  acf,
87
149
  app_conf_dir,
150
+ str_processor=process_str,
88
151
  )
89
152
 
90
153
  #
@@ -9,6 +9,7 @@ from omlish.lite.json import json_dumps_pretty
9
9
  from omlish.lite.marshal import ObjMarshalerManager
10
10
  from omlish.lite.typing import Func0
11
11
  from omlish.lite.typing import Func1
12
+ from omlish.os.paths import abs_real_path
12
13
  from omlish.os.paths import relative_symlink
13
14
 
14
15
  from .apps import DeployAppManager
@@ -19,16 +20,17 @@ from .paths.paths import DeployPath
19
20
  from .specs import DeployAppSpec
20
21
  from .specs import DeploySpec
21
22
  from .systemd import DeploySystemdManager
22
- from .tags import DeployAppRev
23
+ from .tags import DeployApp
23
24
  from .tags import DeployTagMap
24
25
  from .tags import DeployTime
26
+ from .tmp import DeployHomeAtomics
25
27
  from .types import DeployHome
26
28
 
27
29
 
28
30
  ##
29
31
 
30
32
 
31
- DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
33
+ DEPLOY_TAG_DATETIME_FMT = '%Y-%m-%d-T-%H-%M-%S-%f-Z'
32
34
 
33
35
 
34
36
  DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
@@ -38,16 +40,24 @@ class DeployManager(DeployPathOwner):
38
40
  def __init__(
39
41
  self,
40
42
  *,
43
+ atomics: DeployHomeAtomics,
44
+
41
45
  utc_clock: ta.Optional[DeployManagerUtcClock] = None,
42
46
  ):
43
47
  super().__init__()
44
48
 
49
+ self._atomics = atomics
50
+
45
51
  self._utc_clock = utc_clock
46
52
 
47
53
  #
48
54
 
55
+ # Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
56
+ HOME_CURRENT_LINK = DeployPath.parse('current')
57
+
49
58
  DEPLOYS_DIR = DeployPath.parse('deploys/')
50
59
 
60
+ # Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
51
61
  CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
52
62
  DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
53
63
 
@@ -80,6 +90,11 @@ class DeployManager(DeployPathOwner):
80
90
 
81
91
  #
82
92
 
93
+ def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
94
+ return os.path.join(check.non_empty_str(home), pth.render(tags))
95
+
96
+ #
97
+
83
98
  def _utc_now(self) -> datetime.datetime:
84
99
  if self._utc_clock is not None:
85
100
  return self._utc_clock() # noqa
@@ -89,6 +104,22 @@ class DeployManager(DeployPathOwner):
89
104
  def make_deploy_time(self) -> DeployTime:
90
105
  return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
91
106
 
107
+ #
108
+
109
+ def make_home_current_link(self, home: DeployHome) -> None:
110
+ home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
111
+ current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
112
+ with self._atomics(home).begin_atomic_path_swap( # noqa
113
+ 'file',
114
+ home_current_link,
115
+ auto_commit=True,
116
+ ) as dst_swap:
117
+ os.unlink(dst_swap.tmp_path)
118
+ os.symlink(
119
+ os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
120
+ dst_swap.tmp_path,
121
+ )
122
+
92
123
 
93
124
  ##
94
125
 
@@ -130,18 +161,18 @@ class DeployDriver:
130
161
  #
131
162
 
132
163
  @property
133
- def deploy_tags(self) -> DeployTagMap:
164
+ def tags(self) -> DeployTagMap:
134
165
  return DeployTagMap(
135
166
  self._time,
136
167
  self._spec.key(),
137
168
  )
138
169
 
139
- def render_deploy_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
140
- return os.path.join(self._home, pth.render(tags if tags is not None else self.deploy_tags))
170
+ def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
171
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
141
172
 
142
173
  @property
143
- def deploy_dir(self) -> str:
144
- return self.render_deploy_path(self._deploys.DEPLOY_DIR)
174
+ def dir(self) -> str:
175
+ return self.render_path(self._deploys.DEPLOY_DIR)
145
176
 
146
177
  #
147
178
 
@@ -150,25 +181,37 @@ class DeployDriver:
150
181
 
151
182
  #
152
183
 
184
+ das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
185
+ las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
186
+ ras: ta.Set[DeployApp] = set(self._spec.app_links.removed_apps)
187
+ check.empty(das & (las | ras))
188
+ check.empty(las & ras)
189
+
190
+ #
191
+
153
192
  self._paths.validate_deploy_paths()
154
193
 
155
194
  #
156
195
 
157
- os.makedirs(self.deploy_dir)
196
+ os.makedirs(self.dir)
158
197
 
159
198
  #
160
199
 
161
- spec_file = self.render_deploy_path(self._deploys.DEPLOY_SPEC_FILE)
200
+ spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
162
201
  with open(spec_file, 'w') as f: # noqa
163
202
  f.write(spec_json)
164
203
 
165
204
  #
166
205
 
167
- deploying_link = self.render_deploy_path(self._deploys.DEPLOYING_DEPLOY_LINK)
206
+ deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
207
+ current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
208
+
209
+ #
210
+
168
211
  if os.path.exists(deploying_link):
169
212
  os.unlink(deploying_link)
170
213
  relative_symlink(
171
- self.deploy_dir,
214
+ self.dir,
172
215
  deploying_link,
173
216
  target_is_directory=True,
174
217
  make_dirs=True,
@@ -180,16 +223,30 @@ class DeployDriver:
180
223
  self._deploys.APPS_DEPLOY_DIR,
181
224
  self._deploys.CONFS_DEPLOY_DIR,
182
225
  ]:
183
- os.makedirs(self.render_deploy_path(md))
226
+ os.makedirs(self.render_path(md))
184
227
 
185
228
  #
186
229
 
230
+ if not self._spec.app_links.exclude_unspecified:
231
+ cad = abs_real_path(os.path.join(current_link, 'apps'))
232
+ if os.path.exists(cad):
233
+ for d in os.listdir(cad):
234
+ if (da := DeployApp(d)) not in das and da not in ras:
235
+ las.add(da)
236
+
237
+ for la in las:
238
+ await self._drive_app_link(
239
+ la,
240
+ current_link,
241
+ )
242
+
187
243
  for app in self._spec.apps:
188
- await self.drive_app_deploy(app)
244
+ await self._drive_app_deploy(
245
+ app,
246
+ )
189
247
 
190
248
  #
191
249
 
192
- current_link = self.render_deploy_path(self._deploys.CURRENT_DEPLOY_LINK)
193
250
  os.replace(deploying_link, current_link)
194
251
 
195
252
  #
@@ -197,31 +254,33 @@ class DeployDriver:
197
254
  await self._systemd.sync_systemd(
198
255
  self._spec.systemd,
199
256
  self._home,
200
- os.path.join(self.deploy_dir, 'conf', 'systemd'), # FIXME
257
+ os.path.join(self.dir, 'conf', 'systemd'), # FIXME
201
258
  )
202
259
 
203
- #
260
+ #
204
261
 
205
- async def drive_app_deploy(self, app: DeployAppSpec) -> None:
206
- app_tags = self.deploy_tags.add(
207
- app.app,
208
- app.key(),
209
- DeployAppRev(app.git.rev),
210
- )
262
+ self._deploys.make_home_current_link(self._home)
211
263
 
212
- #
264
+ #
265
+
266
+ async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
267
+ current_deploy_link = os.path.join(self._home, self._deploys.CURRENT_DEPLOY_LINK.render())
213
268
 
214
- da = await self._apps.prepare_app(
269
+ pa = await self._apps.prepare_app(
215
270
  app,
216
271
  self._home,
217
- app_tags,
272
+ self.tags,
273
+ conf_string_ns=dict(
274
+ deploy_home=self._home,
275
+ current_deploy_link=current_deploy_link,
276
+ ),
218
277
  )
219
278
 
220
279
  #
221
280
 
222
- app_link = self.render_deploy_path(self._deploys.APP_DEPLOY_LINK, app_tags)
281
+ app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
223
282
  relative_symlink(
224
- da.app_dir,
283
+ pa.dir,
225
284
  app_link,
226
285
  target_is_directory=True,
227
286
  make_dirs=True,
@@ -229,11 +288,47 @@ class DeployDriver:
229
288
 
230
289
  #
231
290
 
232
- deploy_conf_dir = self.render_deploy_path(self._deploys.CONFS_DEPLOY_DIR)
233
- if app.conf is not None:
291
+ await self._drive_app_configure(pa)
292
+
293
+ async def _drive_app_link(
294
+ self,
295
+ app: DeployApp,
296
+ current_link: str,
297
+ ) -> None:
298
+ app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
299
+ check.state(os.path.islink(app_link))
300
+
301
+ app_dir = abs_real_path(app_link)
302
+ check.state(os.path.isdir(app_dir))
303
+
304
+ #
305
+
306
+ pa = await self._apps.prepare_app_link(
307
+ self.tags,
308
+ app_dir,
309
+ )
310
+
311
+ #
312
+
313
+ relative_symlink(
314
+ app_dir,
315
+ os.path.join(self.dir, 'apps', app.s),
316
+ target_is_directory=True,
317
+ )
318
+
319
+ #
320
+
321
+ await self._drive_app_configure(pa)
322
+
323
+ async def _drive_app_configure(
324
+ self,
325
+ pa: DeployAppManager.PreparedApp,
326
+ ) -> None:
327
+ deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
328
+ if pa.spec.conf is not None:
234
329
  await self._conf.link_app_conf(
235
- app.conf,
236
- app_tags,
237
- check.non_empty_str(da.conf_dir),
330
+ pa.spec.conf,
331
+ pa.tags,
332
+ check.non_empty_str(pa.conf_dir),
238
333
  deploy_conf_dir,
239
334
  )
@@ -91,7 +91,9 @@ class DeployGitManager(SingleDirDeployPathOwner):
91
91
 
92
92
  async def fetch(self, rev: DeployRev) -> None:
93
93
  await self.init()
94
- await self._call('git', 'fetch', '--depth=1', 'origin', rev)
94
+
95
+ # This fetch shouldn't be depth=1 - git doesn't reuse local data with shallow fetches.
96
+ await self._call('git', 'fetch', 'origin', rev)
95
97
 
96
98
  #
97
99
 
@@ -91,6 +91,15 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
91
91
  return DeployAppKey(self._key_str())
92
92
 
93
93
 
94
+ @dc.dataclass(frozen=True)
95
+ class DeployAppLinksSpec:
96
+ apps: ta.Sequence[DeployApp] = ()
97
+
98
+ removed_apps: ta.Sequence[DeployApp] = ()
99
+
100
+ exclude_unspecified: bool = False
101
+
102
+
94
103
  ##
95
104
 
96
105
 
@@ -109,6 +118,8 @@ class DeploySpec(DeploySpecKeyed[DeployKey]):
109
118
 
110
119
  apps: ta.Sequence[DeployAppSpec] = ()
111
120
 
121
+ app_links: DeployAppLinksSpec = DeployAppLinksSpec()
122
+
112
123
  systemd: ta.Optional[DeploySystemdSpec] = None
113
124
 
114
125
  def __post_init__(self) -> None:
@@ -1,34 +1,18 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  """
3
- ~/.config/systemd/user/
4
-
5
- verify - systemd-analyze
6
-
7
- sudo loginctl enable-linger "$USER"
8
-
9
- cat ~/.config/systemd/user/sleep-infinity.service
10
- [Unit]
11
- Description=User-specific service to run 'sleep infinity'
12
- After=default.target
13
-
14
- [Service]
15
- ExecStart=/bin/sleep infinity
16
- Restart=always
17
- RestartSec=5
18
-
19
- [Install]
20
- WantedBy=default.target
21
-
22
- systemctl --user daemon-reload
23
-
24
- systemctl --user enable sleep-infinity.service
25
- systemctl --user start sleep-infinity.service
26
-
27
- systemctl --user status sleep-infinity.service
3
+ TODO:
4
+ - verify - systemd-analyze
5
+ - sudo loginctl enable-linger "$USER"
6
+ - idemp kill services that shouldn't be running, start ones that should
7
+ - ideally only those defined by links to deploy home
8
+ - ominfra.systemd / x.sd_orphans
28
9
  """
29
10
  import os.path
11
+ import shutil
12
+ import sys
30
13
  import typing as ta
31
14
 
15
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
32
16
  from omlish.lite.check import check
33
17
  from omlish.os.paths import abs_real_path
34
18
  from omlish.os.paths import is_path_in_dir
@@ -73,6 +57,8 @@ class DeploySystemdManager:
73
57
  if not spec:
74
58
  return
75
59
 
60
+ #
61
+
76
62
  if not (ud := spec.unit_dir):
77
63
  return
78
64
 
@@ -80,6 +66,8 @@ class DeploySystemdManager:
80
66
 
81
67
  os.makedirs(ud, exist_ok=True)
82
68
 
69
+ #
70
+
83
71
  uld = {
84
72
  n: p
85
73
  for n, p in self._scan_link_dir(ud).items()
@@ -91,8 +79,11 @@ class DeploySystemdManager:
91
79
  else:
92
80
  cld = {}
93
81
 
94
- for n in sorted(set(uld) | set(cld)):
95
- ul = uld.get(n) # noqa
82
+ #
83
+
84
+ ns = sorted(set(uld) | set(cld))
85
+
86
+ for n in ns:
96
87
  cl = cld.get(n)
97
88
  if cl is None:
98
89
  os.unlink(os.path.join(ud, n))
@@ -108,3 +99,34 @@ class DeploySystemdManager:
108
99
  os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
109
100
  dst_swap.tmp_path,
110
101
  )
102
+
103
+ #
104
+
105
+ if sys.platform == 'linux' and shutil.which('systemctl') is not None:
106
+ async def reload() -> None:
107
+ await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
108
+
109
+ await reload()
110
+
111
+ num_deleted = 0
112
+ for n in ns:
113
+ if n.endswith('.service'):
114
+ cl = cld.get(n)
115
+ ul = uld.get(n)
116
+ if cl is not None:
117
+ if ul is None:
118
+ cs = ['enable', 'start']
119
+ else:
120
+ cs = ['restart']
121
+ else: # noqa
122
+ if ul is not None:
123
+ cs = ['stop']
124
+ num_deleted += 1
125
+ else:
126
+ cs = []
127
+
128
+ for c in cs:
129
+ await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
130
+
131
+ if num_deleted:
132
+ await reload()
@@ -29,7 +29,7 @@ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
29
29
  ##
30
30
 
31
31
 
32
- @dc.dataclass(frozen=True)
32
+ @dc.dataclass(frozen=True, order=True)
33
33
  class DeployTag(abc.ABC): # noqa
34
34
  s: str
35
35