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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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