ominfra 0.0.0.dev157__py3-none-any.whl → 0.0.0.dev159__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.
- ominfra/clouds/aws/journald2aws/main.py +1 -1
- ominfra/journald/tailer.py +2 -2
- ominfra/manage/bootstrap_.py +1 -1
- ominfra/manage/commands/subprocess.py +4 -4
- ominfra/manage/deploy/apps.py +23 -21
- ominfra/manage/deploy/atomics.py +207 -0
- ominfra/manage/deploy/config.py +3 -0
- ominfra/manage/deploy/git.py +27 -47
- ominfra/manage/deploy/inject.py +11 -0
- ominfra/manage/deploy/paths.py +89 -51
- ominfra/manage/deploy/specs.py +42 -0
- ominfra/manage/deploy/tmp.py +46 -0
- ominfra/manage/deploy/types.py +1 -0
- ominfra/manage/deploy/venvs.py +16 -6
- ominfra/manage/remote/spawning.py +3 -3
- ominfra/manage/system/packages.py +1 -1
- ominfra/pyremote.py +26 -26
- ominfra/scripts/journald2aws.py +467 -354
- ominfra/scripts/manage.py +1426 -1037
- ominfra/scripts/supervisor.py +359 -336
- ominfra/supervisor/http.py +1 -1
- ominfra/supervisor/main.py +2 -2
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/RECORD +28 -25
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev157.dist-info → ominfra-0.0.0.dev159.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@ import dataclasses as dc
|
|
5
5
|
import os.path
|
6
6
|
import sys
|
7
7
|
|
8
|
-
from omlish.
|
8
|
+
from omlish.logs.standard import configure_standard_logging
|
9
9
|
|
10
10
|
from ....configs import read_config_file
|
11
11
|
from .driver import JournalctlToAwsDriver
|
ominfra/journald/tailer.py
CHANGED
@@ -410,8 +410,8 @@ import typing as ta
|
|
410
410
|
from omlish.lite.cached import cached_nullary
|
411
411
|
from omlish.lite.check import check
|
412
412
|
from omlish.lite.logs import log
|
413
|
-
from omlish.
|
414
|
-
from omlish.
|
413
|
+
from omlish.subprocesses import subprocess_close
|
414
|
+
from omlish.subprocesses import subprocess_shell_wrap_exec
|
415
415
|
|
416
416
|
from ..threadworkers import ThreadWorker
|
417
417
|
from .messages import JournalctlMessage # noqa
|
ominfra/manage/bootstrap_.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
from omlish.lite.inject import Injector
|
3
3
|
from omlish.lite.inject import inj
|
4
|
-
from omlish.
|
4
|
+
from omlish.logs.standard import configure_standard_logging
|
5
5
|
|
6
6
|
from .bootstrap import MainBootstrap
|
7
7
|
from .inject import bind_main
|
@@ -6,11 +6,11 @@ import subprocess
|
|
6
6
|
import time
|
7
7
|
import typing as ta
|
8
8
|
|
9
|
-
from omlish.
|
9
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
10
10
|
from omlish.lite.check import check
|
11
|
-
from omlish.
|
12
|
-
from omlish.
|
13
|
-
from omlish.
|
11
|
+
from omlish.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
|
12
|
+
from omlish.subprocesses import SubprocessChannelOption
|
13
|
+
from omlish.subprocesses import subprocess_maybe_shell_wrap_exec
|
14
14
|
|
15
15
|
from .base import Command
|
16
16
|
from .base import CommandExecutor
|
ominfra/manage/deploy/apps.py
CHANGED
@@ -3,14 +3,16 @@ import datetime
|
|
3
3
|
import os.path
|
4
4
|
import typing as ta
|
5
5
|
|
6
|
+
from omlish.lite.cached import cached_nullary
|
7
|
+
from omlish.lite.check import check
|
8
|
+
|
6
9
|
from .git import DeployGitManager
|
7
|
-
from .git import DeployGitRepo
|
8
|
-
from .git import DeployGitSpec
|
9
10
|
from .paths import DeployPath
|
10
11
|
from .paths import DeployPathOwner
|
11
|
-
from .
|
12
|
+
from .specs import DeploySpec
|
12
13
|
from .types import DeployAppTag
|
13
14
|
from .types import DeployHome
|
15
|
+
from .types import DeployKey
|
14
16
|
from .types import DeployRev
|
15
17
|
from .types import DeployTag
|
16
18
|
from .venvs import DeployVenvManager
|
@@ -18,20 +20,22 @@ from .venvs import DeployVenvManager
|
|
18
20
|
|
19
21
|
def make_deploy_tag(
|
20
22
|
rev: DeployRev,
|
21
|
-
|
23
|
+
key: DeployKey,
|
24
|
+
*,
|
25
|
+
utcnow: ta.Optional[datetime.datetime] = None,
|
22
26
|
) -> DeployTag:
|
23
|
-
if
|
24
|
-
|
25
|
-
now_fmt = '%Y%m%dT%H%M%
|
26
|
-
now_str =
|
27
|
-
return DeployTag('-'.join([rev,
|
27
|
+
if utcnow is None:
|
28
|
+
utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
|
29
|
+
now_fmt = '%Y%m%dT%H%M%SZ'
|
30
|
+
now_str = utcnow.strftime(now_fmt)
|
31
|
+
return DeployTag('-'.join([now_str, rev, key]))
|
28
32
|
|
29
33
|
|
30
34
|
class DeployAppManager(DeployPathOwner):
|
31
35
|
def __init__(
|
32
36
|
self,
|
33
37
|
*,
|
34
|
-
deploy_home: DeployHome,
|
38
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
35
39
|
git: DeployGitManager,
|
36
40
|
venvs: DeployVenvManager,
|
37
41
|
) -> None:
|
@@ -41,29 +45,27 @@ class DeployAppManager(DeployPathOwner):
|
|
41
45
|
self._git = git
|
42
46
|
self._venvs = venvs
|
43
47
|
|
44
|
-
|
48
|
+
@cached_nullary
|
49
|
+
def _dir(self) -> str:
|
50
|
+
return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
|
45
51
|
|
46
|
-
def
|
52
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
47
53
|
return {
|
48
54
|
DeployPath.parse('apps/@app/@tag'),
|
49
55
|
}
|
50
56
|
|
51
57
|
async def prepare_app(
|
52
58
|
self,
|
53
|
-
|
54
|
-
rev: DeployRev,
|
55
|
-
repo: DeployGitRepo,
|
59
|
+
spec: DeploySpec,
|
56
60
|
):
|
57
|
-
app_tag = DeployAppTag(app, make_deploy_tag(rev))
|
58
|
-
app_dir = os.path.join(self._dir, app, app_tag.tag)
|
61
|
+
app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev, spec.key()))
|
62
|
+
app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
|
59
63
|
|
60
64
|
#
|
61
65
|
|
62
66
|
await self._git.checkout(
|
63
|
-
|
64
|
-
|
65
|
-
rev=rev,
|
66
|
-
),
|
67
|
+
spec.repo,
|
68
|
+
spec.rev,
|
67
69
|
app_dir,
|
68
70
|
)
|
69
71
|
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
import tempfile
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.check import check
|
9
|
+
from omlish.lite.strings import attr_repr
|
10
|
+
|
11
|
+
|
12
|
+
DeployAtomicPathSwapKind = ta.Literal['dir', 'file']
|
13
|
+
DeployAtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
class DeployAtomicPathSwap(abc.ABC):
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
kind: DeployAtomicPathSwapKind,
|
23
|
+
dst_path: str,
|
24
|
+
*,
|
25
|
+
auto_commit: bool = False,
|
26
|
+
) -> None:
|
27
|
+
super().__init__()
|
28
|
+
|
29
|
+
self._kind = kind
|
30
|
+
self._dst_path = dst_path
|
31
|
+
self._auto_commit = auto_commit
|
32
|
+
|
33
|
+
self._state: DeployAtomicPathSwapState = 'open'
|
34
|
+
|
35
|
+
def __repr__(self) -> str:
|
36
|
+
return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
|
37
|
+
|
38
|
+
@property
|
39
|
+
def kind(self) -> DeployAtomicPathSwapKind:
|
40
|
+
return self._kind
|
41
|
+
|
42
|
+
@property
|
43
|
+
def dst_path(self) -> str:
|
44
|
+
return self._dst_path
|
45
|
+
|
46
|
+
@property
|
47
|
+
@abc.abstractmethod
|
48
|
+
def tmp_path(self) -> str:
|
49
|
+
raise NotImplementedError
|
50
|
+
|
51
|
+
#
|
52
|
+
|
53
|
+
@property
|
54
|
+
def state(self) -> DeployAtomicPathSwapState:
|
55
|
+
return self._state
|
56
|
+
|
57
|
+
def _check_state(self, *states: DeployAtomicPathSwapState) -> None:
|
58
|
+
if self._state not in states:
|
59
|
+
raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
|
60
|
+
|
61
|
+
#
|
62
|
+
|
63
|
+
@abc.abstractmethod
|
64
|
+
def _commit(self) -> None:
|
65
|
+
raise NotImplementedError
|
66
|
+
|
67
|
+
def commit(self) -> None:
|
68
|
+
if self._state == 'committed':
|
69
|
+
return
|
70
|
+
self._check_state('open')
|
71
|
+
try:
|
72
|
+
self._commit()
|
73
|
+
except Exception: # noqa
|
74
|
+
self._abort()
|
75
|
+
raise
|
76
|
+
else:
|
77
|
+
self._state = 'committed'
|
78
|
+
|
79
|
+
#
|
80
|
+
|
81
|
+
@abc.abstractmethod
|
82
|
+
def _abort(self) -> None:
|
83
|
+
raise NotImplementedError
|
84
|
+
|
85
|
+
def abort(self) -> None:
|
86
|
+
if self._state == 'aborted':
|
87
|
+
return
|
88
|
+
self._abort()
|
89
|
+
self._state = 'aborted'
|
90
|
+
|
91
|
+
#
|
92
|
+
|
93
|
+
def __enter__(self) -> 'DeployAtomicPathSwap':
|
94
|
+
return self
|
95
|
+
|
96
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
97
|
+
if (
|
98
|
+
exc_type is None and
|
99
|
+
self._auto_commit and
|
100
|
+
self._state == 'open'
|
101
|
+
):
|
102
|
+
self.commit()
|
103
|
+
else:
|
104
|
+
self.abort()
|
105
|
+
|
106
|
+
|
107
|
+
#
|
108
|
+
|
109
|
+
|
110
|
+
class DeployAtomicPathSwapping(abc.ABC):
|
111
|
+
@abc.abstractmethod
|
112
|
+
def begin_atomic_path_swap(
|
113
|
+
self,
|
114
|
+
kind: DeployAtomicPathSwapKind,
|
115
|
+
dst_path: str,
|
116
|
+
*,
|
117
|
+
name_hint: ta.Optional[str] = None,
|
118
|
+
make_dirs: bool = False,
|
119
|
+
**kwargs: ta.Any,
|
120
|
+
) -> DeployAtomicPathSwap:
|
121
|
+
raise NotImplementedError
|
122
|
+
|
123
|
+
|
124
|
+
##
|
125
|
+
|
126
|
+
|
127
|
+
class OsRenameDeployAtomicPathSwap(DeployAtomicPathSwap):
|
128
|
+
def __init__(
|
129
|
+
self,
|
130
|
+
kind: DeployAtomicPathSwapKind,
|
131
|
+
dst_path: str,
|
132
|
+
tmp_path: str,
|
133
|
+
**kwargs: ta.Any,
|
134
|
+
) -> None:
|
135
|
+
if kind == 'dir':
|
136
|
+
check.state(os.path.isdir(tmp_path))
|
137
|
+
elif kind == 'file':
|
138
|
+
check.state(os.path.isfile(tmp_path))
|
139
|
+
else:
|
140
|
+
raise TypeError(kind)
|
141
|
+
|
142
|
+
super().__init__(
|
143
|
+
kind,
|
144
|
+
dst_path,
|
145
|
+
**kwargs,
|
146
|
+
)
|
147
|
+
|
148
|
+
self._tmp_path = tmp_path
|
149
|
+
|
150
|
+
@property
|
151
|
+
def tmp_path(self) -> str:
|
152
|
+
return self._tmp_path
|
153
|
+
|
154
|
+
def _commit(self) -> None:
|
155
|
+
os.rename(self._tmp_path, self._dst_path)
|
156
|
+
|
157
|
+
def _abort(self) -> None:
|
158
|
+
shutil.rmtree(self._tmp_path, ignore_errors=True)
|
159
|
+
|
160
|
+
|
161
|
+
class TempDirDeployAtomicPathSwapping(DeployAtomicPathSwapping):
|
162
|
+
def __init__(
|
163
|
+
self,
|
164
|
+
*,
|
165
|
+
temp_dir: ta.Optional[str] = None,
|
166
|
+
root_dir: ta.Optional[str] = None,
|
167
|
+
) -> None:
|
168
|
+
super().__init__()
|
169
|
+
|
170
|
+
if root_dir is not None:
|
171
|
+
root_dir = os.path.abspath(root_dir)
|
172
|
+
self._root_dir = root_dir
|
173
|
+
self._temp_dir = temp_dir
|
174
|
+
|
175
|
+
def begin_atomic_path_swap(
|
176
|
+
self,
|
177
|
+
kind: DeployAtomicPathSwapKind,
|
178
|
+
dst_path: str,
|
179
|
+
*,
|
180
|
+
name_hint: ta.Optional[str] = None,
|
181
|
+
make_dirs: bool = False,
|
182
|
+
**kwargs: ta.Any,
|
183
|
+
) -> DeployAtomicPathSwap:
|
184
|
+
dst_path = os.path.abspath(dst_path)
|
185
|
+
if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
|
186
|
+
raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
|
187
|
+
|
188
|
+
dst_dir = os.path.dirname(dst_path)
|
189
|
+
if make_dirs:
|
190
|
+
os.makedirs(dst_dir, exist_ok=True)
|
191
|
+
if not os.path.isdir(dst_dir):
|
192
|
+
raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
|
193
|
+
|
194
|
+
if kind == 'dir':
|
195
|
+
tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
|
196
|
+
elif kind == 'file':
|
197
|
+
fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
|
198
|
+
os.close(fd)
|
199
|
+
else:
|
200
|
+
raise TypeError(kind)
|
201
|
+
|
202
|
+
return OsRenameDeployAtomicPathSwap(
|
203
|
+
kind,
|
204
|
+
dst_path,
|
205
|
+
tmp_path,
|
206
|
+
**kwargs,
|
207
|
+
)
|
ominfra/manage/deploy/config.py
CHANGED
ominfra/manage/deploy/git.py
CHANGED
@@ -8,17 +8,17 @@ git/github.com/wrmsr/omlish <- bootstrap repo
|
|
8
8
|
|
9
9
|
github.com/wrmsr/omlish@rev
|
10
10
|
"""
|
11
|
-
import dataclasses as dc
|
12
11
|
import functools
|
13
12
|
import os.path
|
14
13
|
import typing as ta
|
15
14
|
|
16
|
-
from omlish.
|
15
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
17
16
|
from omlish.lite.cached import async_cached_nullary
|
18
17
|
from omlish.lite.check import check
|
19
18
|
|
20
|
-
from .
|
21
|
-
from .paths import
|
19
|
+
from .atomics import DeployAtomicPathSwapping
|
20
|
+
from .paths import SingleDirDeployPathOwner
|
21
|
+
from .specs import DeployGitRepo
|
22
22
|
from .types import DeployHome
|
23
23
|
from .types import DeployRev
|
24
24
|
|
@@ -26,44 +26,22 @@ from .types import DeployRev
|
|
26
26
|
##
|
27
27
|
|
28
28
|
|
29
|
-
|
30
|
-
class DeployGitRepo:
|
31
|
-
host: ta.Optional[str] = None
|
32
|
-
username: ta.Optional[str] = None
|
33
|
-
path: ta.Optional[str] = None
|
34
|
-
|
35
|
-
def __post_init__(self) -> None:
|
36
|
-
check.not_in('..', check.non_empty_str(self.host))
|
37
|
-
check.not_in('.', check.non_empty_str(self.path))
|
38
|
-
|
39
|
-
|
40
|
-
@dc.dataclass(frozen=True)
|
41
|
-
class DeployGitSpec:
|
42
|
-
repo: DeployGitRepo
|
43
|
-
rev: DeployRev
|
44
|
-
|
45
|
-
|
46
|
-
##
|
47
|
-
|
48
|
-
|
49
|
-
class DeployGitManager(DeployPathOwner):
|
29
|
+
class DeployGitManager(SingleDirDeployPathOwner):
|
50
30
|
def __init__(
|
51
31
|
self,
|
52
32
|
*,
|
53
|
-
deploy_home: DeployHome,
|
33
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
34
|
+
atomics: DeployAtomicPathSwapping,
|
54
35
|
) -> None:
|
55
|
-
super().__init__(
|
36
|
+
super().__init__(
|
37
|
+
owned_dir='git',
|
38
|
+
deploy_home=deploy_home,
|
39
|
+
)
|
56
40
|
|
57
|
-
self.
|
58
|
-
self._dir = os.path.join(deploy_home, 'git')
|
41
|
+
self._atomics = atomics
|
59
42
|
|
60
43
|
self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
|
61
44
|
|
62
|
-
def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
63
|
-
return {
|
64
|
-
DeployPath.parse('git'),
|
65
|
-
}
|
66
|
-
|
67
45
|
class RepoDir:
|
68
46
|
def __init__(
|
69
47
|
self,
|
@@ -75,7 +53,7 @@ class DeployGitManager(DeployPathOwner):
|
|
75
53
|
self._git = git
|
76
54
|
self._repo = repo
|
77
55
|
self._dir = os.path.join(
|
78
|
-
self._git.
|
56
|
+
self._git._make_dir(), # noqa
|
79
57
|
check.non_empty_str(repo.host),
|
80
58
|
check.non_empty_str(repo.path),
|
81
59
|
)
|
@@ -112,18 +90,20 @@ class DeployGitManager(DeployPathOwner):
|
|
112
90
|
|
113
91
|
async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
|
114
92
|
check.state(not os.path.exists(dst_dir))
|
93
|
+
with self._git._atomics.begin_atomic_path_swap( # noqa
|
94
|
+
'dir',
|
95
|
+
dst_dir,
|
96
|
+
auto_commit=True,
|
97
|
+
make_dirs=True,
|
98
|
+
) as dst_swap:
|
99
|
+
await self.fetch(rev)
|
115
100
|
|
116
|
-
|
117
|
-
|
118
|
-
# FIXME: temp dir swap
|
119
|
-
os.makedirs(dst_dir)
|
120
|
-
|
121
|
-
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
|
122
|
-
await dst_call('git', 'init')
|
101
|
+
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
102
|
+
await dst_call('git', 'init')
|
123
103
|
|
124
|
-
|
125
|
-
|
126
|
-
|
104
|
+
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
105
|
+
await dst_call('git', 'fetch', '--depth=1', 'local', rev)
|
106
|
+
await dst_call('git', 'checkout', rev)
|
127
107
|
|
128
108
|
def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
|
129
109
|
try:
|
@@ -132,5 +112,5 @@ class DeployGitManager(DeployPathOwner):
|
|
132
112
|
repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
|
133
113
|
return repo_dir
|
134
114
|
|
135
|
-
async def checkout(self,
|
136
|
-
await self.get_repo_dir(
|
115
|
+
async def checkout(self, repo: DeployGitRepo, rev: DeployRev, dst_dir: str) -> None:
|
116
|
+
await self.get_repo_dir(repo).checkout(rev, dst_dir)
|
ominfra/manage/deploy/inject.py
CHANGED
@@ -8,12 +8,14 @@ from omlish.lite.inject import inj
|
|
8
8
|
|
9
9
|
from ..commands.inject import bind_command
|
10
10
|
from .apps import DeployAppManager
|
11
|
+
from .atomics import DeployAtomicPathSwapping
|
11
12
|
from .commands import DeployCommand
|
12
13
|
from .commands import DeployCommandExecutor
|
13
14
|
from .config import DeployConfig
|
14
15
|
from .git import DeployGitManager
|
15
16
|
from .interp import InterpCommand
|
16
17
|
from .interp import InterpCommandExecutor
|
18
|
+
from .tmp import DeployTmpManager
|
17
19
|
from .types import DeployHome
|
18
20
|
from .venvs import DeployVenvManager
|
19
21
|
|
@@ -25,10 +27,19 @@ def bind_deploy(
|
|
25
27
|
lst: ta.List[InjectorBindingOrBindings] = [
|
26
28
|
inj.bind(deploy_config),
|
27
29
|
|
30
|
+
#
|
31
|
+
|
28
32
|
inj.bind(DeployAppManager, singleton=True),
|
33
|
+
|
29
34
|
inj.bind(DeployGitManager, singleton=True),
|
35
|
+
|
36
|
+
inj.bind(DeployTmpManager, singleton=True),
|
37
|
+
inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
|
38
|
+
|
30
39
|
inj.bind(DeployVenvManager, singleton=True),
|
31
40
|
|
41
|
+
#
|
42
|
+
|
32
43
|
bind_command(DeployCommand, DeployCommandExecutor),
|
33
44
|
bind_command(InterpCommand, InterpCommandExecutor),
|
34
45
|
]
|