dirigent-block-execute 0.17.2__tar.gz → 0.18.0__tar.gz
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.
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/PKG-INFO +4 -4
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/pyproject.toml +4 -4
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/pyproject.toml.orig +4 -4
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/build.py +2 -6
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/compose.py +11 -22
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/docker.py +21 -11
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/git.py +3 -6
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/messages.py +1 -1
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/shell.py +2 -6
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/subprocess.py +27 -20
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/LICENSE +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/README.md +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/__init__.py +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/capture.py +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/environment.py +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/py.typed +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/reap.py +0 -0
- {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/secrets.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-block-execute
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: The execute block family for dirigent: shell, docker, compose, buildx, and a checkout.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Dist: dirigent-block-http==0.
|
|
10
|
-
Requires-Dist: dirigent-common==0.
|
|
11
|
-
Requires-Dist: dirigent-plugin==0.
|
|
9
|
+
Requires-Dist: dirigent-block-http==0.18.0
|
|
10
|
+
Requires-Dist: dirigent-common==0.18.0
|
|
11
|
+
Requires-Dist: dirigent-plugin==0.18.0
|
|
12
12
|
Requires-Dist: httpx2>=2.12.0
|
|
13
13
|
Requires-Python: >=3.13
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-block-execute"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.18.0"
|
|
4
4
|
description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,9 +11,9 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-block-http==0.
|
|
15
|
-
"dirigent-common==0.
|
|
16
|
-
"dirigent-plugin==0.
|
|
14
|
+
"dirigent-block-http==0.18.0",
|
|
15
|
+
"dirigent-common==0.18.0",
|
|
16
|
+
"dirigent-plugin==0.18.0",
|
|
17
17
|
"httpx2>=2.12.0",
|
|
18
18
|
]
|
|
19
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-block-execute"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.18.0"
|
|
4
4
|
description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,9 +11,9 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-block-http==0.
|
|
15
|
-
"dirigent-common==0.
|
|
16
|
-
"dirigent-plugin==0.
|
|
14
|
+
"dirigent-block-http==0.18.0",
|
|
15
|
+
"dirigent-common==0.18.0",
|
|
16
|
+
"dirigent-plugin==0.18.0",
|
|
17
17
|
"httpx2>=2.12.0",
|
|
18
18
|
]
|
|
19
19
|
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/build.py
RENAMED
|
@@ -176,22 +176,18 @@ class DockerBuildOperator(Operator[DockerBuildConfig, DockerBuildOutput]):
|
|
|
176
176
|
if config.push and (settings is None or not settings.authenticates):
|
|
177
177
|
raise BlockFailure(NO_REGISTRY_CREDENTIAL, error_class=ErrorClass.REJECTED)
|
|
178
178
|
timeout = config.timeout.total_seconds()
|
|
179
|
-
artifacts = subprocess.prefix(ctx, "build")
|
|
180
|
-
stdout_uri = f"{artifacts}-stdout.txt"
|
|
181
|
-
build_log_uri = f"{artifacts}-build.log"
|
|
182
179
|
|
|
183
180
|
with sealed(settings, root, isolate_config=config.push) as material:
|
|
184
181
|
environ = daemon_environment(
|
|
185
182
|
subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
|
|
186
183
|
)
|
|
187
184
|
write_cli_config(root / ".docker")
|
|
188
|
-
code, out, err = await subprocess.run(
|
|
185
|
+
code, out, err, stdout_uri, build_log_uri = await subprocess.run(
|
|
189
186
|
directory=root,
|
|
190
187
|
ctx=ctx,
|
|
191
|
-
stdout_uri=stdout_uri,
|
|
192
|
-
stderr_uri=build_log_uri,
|
|
193
188
|
timeout_seconds=timeout,
|
|
194
189
|
environ=environ,
|
|
190
|
+
stderr_name="build.log",
|
|
195
191
|
argv=build_argv(config, context, iidfile),
|
|
196
192
|
what="docker buildx build",
|
|
197
193
|
redact=material.secrets,
|
|
@@ -141,10 +141,9 @@ class DockerComposeUpConfig(BlockModel):
|
|
|
141
141
|
pipeline document itself."""
|
|
142
142
|
|
|
143
143
|
project_name: str | None = None
|
|
144
|
-
"""The compose project (``-p``). Defaults to a deterministic name derived from the run id,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
and two runs never collide."""
|
|
144
|
+
"""The compose project (``-p``). Defaults to a deterministic name derived from the run id, so
|
|
145
|
+
this ``up``, the steps that drive it, and a later ``down`` all address the same project, and
|
|
146
|
+
two runs never collide."""
|
|
148
147
|
|
|
149
148
|
profiles: list[str] = Field(default_factory=list[str])
|
|
150
149
|
"""Compose profiles to activate (``--profile``)."""
|
|
@@ -213,13 +212,11 @@ class DockerComposeDownConfig(BlockModel):
|
|
|
213
212
|
|
|
214
213
|
project_name: str | None = None
|
|
215
214
|
"""The compose project (``-p``). Defaults to the same deterministic name ``up`` derives from
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
no wiring from the author."""
|
|
215
|
+
the run id, so a teardown step in the same run addresses the stack the ``up`` created with no
|
|
216
|
+
wiring from the author."""
|
|
219
217
|
|
|
220
218
|
file: str | None = None
|
|
221
219
|
"""A compose file, only if the CLI needs ``-f`` to resolve the project; a project tears down
|
|
222
|
-
|
|
223
220
|
by its label alone otherwise. As a path inside the run's work directory."""
|
|
224
221
|
|
|
225
222
|
content: str | None = None
|
|
@@ -342,7 +339,6 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
|
|
|
342
339
|
assert compose_file is not None
|
|
343
340
|
project = config.project_name or _default_project(ctx)
|
|
344
341
|
timeout = config.timeout.total_seconds()
|
|
345
|
-
stdout_uri, stderr_uri = _artifact_uris(ctx, "up")
|
|
346
342
|
|
|
347
343
|
with sealed(_connection(config.connection, ctx), root) as material:
|
|
348
344
|
environ = daemon_environment(
|
|
@@ -350,13 +346,13 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
|
|
|
350
346
|
)
|
|
351
347
|
write_cli_config(root / ".docker")
|
|
352
348
|
try:
|
|
353
|
-
code, out, err = await subprocess.run(
|
|
349
|
+
code, out, err, stdout_uri, stderr_uri = await subprocess.run(
|
|
354
350
|
directory=root,
|
|
355
351
|
ctx=ctx,
|
|
356
|
-
stdout_uri=stdout_uri,
|
|
357
|
-
stderr_uri=stderr_uri,
|
|
358
352
|
timeout_seconds=timeout,
|
|
359
353
|
environ=environ,
|
|
354
|
+
stdout_name="up-stdout",
|
|
355
|
+
stderr_name="up-stderr",
|
|
360
356
|
argv=up_argv(config, project, compose_file, root),
|
|
361
357
|
what="docker compose up",
|
|
362
358
|
)
|
|
@@ -411,20 +407,19 @@ class DockerComposeDownOperator(Operator[DockerComposeDownConfig, DockerComposeD
|
|
|
411
407
|
compose_file = _compose_file(config.file, config.content, ctx, root)
|
|
412
408
|
project = config.project_name or _default_project(ctx)
|
|
413
409
|
timeout = config.timeout.total_seconds()
|
|
414
|
-
stdout_uri, stderr_uri = _artifact_uris(ctx, "down")
|
|
415
410
|
|
|
416
411
|
with sealed(_connection(config.connection, ctx), root) as material:
|
|
417
412
|
environ = daemon_environment(
|
|
418
413
|
subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
|
|
419
414
|
)
|
|
420
415
|
write_cli_config(root / ".docker")
|
|
421
|
-
code, out, err = await subprocess.run(
|
|
416
|
+
code, out, err, stdout_uri, stderr_uri = await subprocess.run(
|
|
422
417
|
directory=root,
|
|
423
418
|
ctx=ctx,
|
|
424
|
-
stdout_uri=stdout_uri,
|
|
425
|
-
stderr_uri=stderr_uri,
|
|
426
419
|
timeout_seconds=timeout,
|
|
427
420
|
environ=environ,
|
|
421
|
+
stdout_name="down-stdout",
|
|
422
|
+
stderr_name="down-stderr",
|
|
428
423
|
argv=down_argv(config, project, compose_file, root),
|
|
429
424
|
what="docker compose down",
|
|
430
425
|
)
|
|
@@ -656,12 +651,6 @@ def _work_relative(path: Path, root: Path) -> str:
|
|
|
656
651
|
return str(path.relative_to(root))
|
|
657
652
|
|
|
658
653
|
|
|
659
|
-
def _artifact_uris(ctx: StepContext, action: str) -> tuple[str, str]:
|
|
660
|
-
"""Where the CLI's two streams are written for this attempt."""
|
|
661
|
-
root = f"{subprocess.prefix(ctx, 'compose')}-{action}"
|
|
662
|
-
return f"{root}-stdout.txt", f"{root}-stderr.txt"
|
|
663
|
-
|
|
664
|
-
|
|
665
654
|
def _default_project(ctx: StepContext) -> str:
|
|
666
655
|
"""A project name that is the same across a run's up, drive and down, and unique between runs."""
|
|
667
656
|
return f"dirigent-{ctx.run_id.hex}"
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/docker.py
RENAMED
|
@@ -109,8 +109,8 @@ NOT_FOUND = 404
|
|
|
109
109
|
CONFLICT = 409
|
|
110
110
|
|
|
111
111
|
|
|
112
|
-
#: The schemes a daemon URL may be written in, which are the ones the
|
|
113
|
-
DAEMON_SCHEMES = ("tcp://", "
|
|
112
|
+
#: The schemes a daemon URL may be written in, which are the ones the worker speaks.
|
|
113
|
+
DAEMON_SCHEMES = ("tcp://", "unix://")
|
|
114
114
|
|
|
115
115
|
#: What a registry credential that names no registry authenticates to.
|
|
116
116
|
DOCKER_HUB = "docker.io"
|
|
@@ -123,7 +123,7 @@ class DockerConnectionConfig(BlockModel):
|
|
|
123
123
|
"""A daemon to reach, a registry to authenticate to, or both."""
|
|
124
124
|
|
|
125
125
|
host: str = ""
|
|
126
|
-
"""The daemon, as ``tcp://host:2376
|
|
126
|
+
"""The daemon, as ``tcp://host:2376`` or ``unix:///var/run/docker.sock``.
|
|
127
127
|
|
|
128
128
|
Left empty, a step using this connection reaches the daemon the worker's own environment
|
|
129
129
|
names, which is what a step naming no connection does."""
|
|
@@ -157,7 +157,7 @@ class DockerConnectionConfig(BlockModel):
|
|
|
157
157
|
def _check_shape(self) -> "DockerConnectionConfig":
|
|
158
158
|
"""Refuse a daemon URL of an unknown form, half a TLS triple, and half a credential."""
|
|
159
159
|
if self.host and not self.host.startswith(DAEMON_SCHEMES):
|
|
160
|
-
raise ValueError(NOT_A_DAEMON_SCHEME.render(schemes="
|
|
160
|
+
raise ValueError(NOT_A_DAEMON_SCHEME.render(schemes=" or ".join(DAEMON_SCHEMES), host=repr(self.host)))
|
|
161
161
|
pems = [self.tls_ca, self.tls_cert, self.tls_key]
|
|
162
162
|
if any(pem is not None for pem in pems) and not all(pem is not None for pem in pems):
|
|
163
163
|
raise ValueError(TLS_IS_ALL_THREE.render())
|
|
@@ -493,6 +493,7 @@ class DockerRunOutput(BlockModel):
|
|
|
493
493
|
"""What the container did, with its full streams and produced files addressable as artifacts."""
|
|
494
494
|
|
|
495
495
|
exit_code: int
|
|
496
|
+
"""The status the container exited with."""
|
|
496
497
|
|
|
497
498
|
stdout: str
|
|
498
499
|
"""The head of what the container printed, cut at the instance's inline capture size.
|
|
@@ -521,7 +522,11 @@ class DockerRunOutput(BlockModel):
|
|
|
521
522
|
"""Whether ``stderr`` above is short of the stream, which is an independent question."""
|
|
522
523
|
|
|
523
524
|
container_id: str
|
|
525
|
+
"""The container that ran, by the id the daemon gave it."""
|
|
526
|
+
|
|
524
527
|
image: str
|
|
528
|
+
"""The image the container ran."""
|
|
529
|
+
|
|
525
530
|
outputs: dict[str, str] = Field(default_factory=dict[str, str])
|
|
526
531
|
"""Where each declared output was written, by the name the config gave it: the storage URI,
|
|
527
532
|
or the path relative to the run's work directory it landed at."""
|
|
@@ -582,21 +587,16 @@ class DockerRunOperator(Operator[DockerRunConfig, DockerRunOutput]):
|
|
|
582
587
|
|
|
583
588
|
async def fetch(self, handle: RemoteHandle, config: DockerRunConfig, ctx: StepContext) -> DockerRunOutput:
|
|
584
589
|
"""Collect the exit code, the streams, and the declared files, then drop the container."""
|
|
585
|
-
artifacts = subprocess.prefix(ctx, "docker")
|
|
586
|
-
stdout_uri = f"{artifacts}-stdout.txt"
|
|
587
|
-
stderr_uri = f"{artifacts}-stderr.txt"
|
|
588
590
|
with _material(config, ctx) as environ:
|
|
589
591
|
async with _daemon(config, environ) as daemon:
|
|
590
592
|
inspected = await daemon.inspect(handle.ref)
|
|
591
593
|
if inspected is None:
|
|
592
594
|
raise BlockFailure(CONTAINER_GONE, error_class=ErrorClass.TRANSIENT, container=handle.ref[:12])
|
|
593
|
-
async with (
|
|
594
|
-
ctx.storage.open_write(stdout_uri) as out_sink,
|
|
595
|
-
ctx.storage.open_write(stderr_uri) as err_sink,
|
|
596
|
-
):
|
|
595
|
+
async with ctx.capture("stdout") as out_sink, ctx.capture("stderr") as err_sink:
|
|
597
596
|
stdout, stderr = await drain_logs(
|
|
598
597
|
daemon, handle.ref, ctx.inline_capture, sinks=(out_sink, err_sink)
|
|
599
598
|
)
|
|
599
|
+
stdout_uri, stderr_uri = out_sink.uri, err_sink.uri
|
|
600
600
|
collected = await _collect_outputs(config, handle, ctx)
|
|
601
601
|
# The run log gets only what the probes have not already streamed: the segment
|
|
602
602
|
# between the last committed cursor and the exit. The full read above is for
|
|
@@ -1153,6 +1153,9 @@ def resolve_endpoint(explicit_socket: str | None, environ: Mapping[str, str] | N
|
|
|
1153
1153
|
pipeline's containers off the host. ``DOCKER_TLS_VERIFY`` with ``DOCKER_CERT_PATH`` turns
|
|
1154
1154
|
that into https with client certificates, the way the docker CLI reads the same variables.
|
|
1155
1155
|
|
|
1156
|
+
A ``DOCKER_HOST`` in any other scheme is refused instead of resolving to the local socket:
|
|
1157
|
+
the daemon is reached over HTTP here, and nothing tunnels an ``ssh://`` one.
|
|
1158
|
+
|
|
1156
1159
|
``environ`` is what a step with a ``docker`` connection reads instead of the worker's own.
|
|
1157
1160
|
"""
|
|
1158
1161
|
source = os.environ if environ is None else environ
|
|
@@ -1173,6 +1176,13 @@ def resolve_endpoint(explicit_socket: str | None, environ: Mapping[str, str] | N
|
|
|
1173
1176
|
if host.startswith("unix://"):
|
|
1174
1177
|
socket = host.removeprefix("unix://") or DEFAULT_SOCKET_PATH
|
|
1175
1178
|
return DaemonEndpoint(DAEMON_BASE_URL, socket, True, None, socket)
|
|
1179
|
+
if host:
|
|
1180
|
+
raise BlockFailure(
|
|
1181
|
+
NOT_A_DAEMON_SCHEME,
|
|
1182
|
+
error_class=ErrorClass.REJECTED,
|
|
1183
|
+
schemes=" or ".join(DAEMON_SCHEMES),
|
|
1184
|
+
host=repr(host),
|
|
1185
|
+
)
|
|
1176
1186
|
return DaemonEndpoint(DAEMON_BASE_URL, DEFAULT_SOCKET_PATH, True, None, DEFAULT_SOCKET_PATH)
|
|
1177
1187
|
|
|
1178
1188
|
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/git.py
RENAMED
|
@@ -381,16 +381,13 @@ class _Git:
|
|
|
381
381
|
artifact rather than a buffer in the worker. Each command writes its own pair, named for
|
|
382
382
|
its subcommand; the pair the last one wrote is what the output carries.
|
|
383
383
|
"""
|
|
384
|
-
|
|
385
|
-
stdout_uri = f"{artifacts}-stdout.txt"
|
|
386
|
-
stderr_uri = f"{artifacts}-stderr.txt"
|
|
387
|
-
code, out, err = await subprocess.run(
|
|
384
|
+
code, out, err, stdout_uri, stderr_uri = await subprocess.run(
|
|
388
385
|
directory=directory,
|
|
389
386
|
ctx=self.ctx,
|
|
390
|
-
stdout_uri=stdout_uri,
|
|
391
|
-
stderr_uri=stderr_uri,
|
|
392
387
|
timeout_seconds=self.timeout,
|
|
393
388
|
environ=self.environ,
|
|
389
|
+
stdout_name=f"{argv[0]}-stdout",
|
|
390
|
+
stderr_name=f"{argv[0]}-stderr",
|
|
394
391
|
argv=[self.binary, *argv],
|
|
395
392
|
what=f"git {argv[0]}",
|
|
396
393
|
redact=self.secrets,
|
|
@@ -125,7 +125,7 @@ CWD_STAYS_INSIDE = EXECUTE.define(
|
|
|
125
125
|
|
|
126
126
|
NOT_A_DAEMON_SCHEME = EXECUTE.define(
|
|
127
127
|
"not_a_daemon_scheme",
|
|
128
|
-
"a docker host is
|
|
128
|
+
"a docker host is {schemes}, and {host} is not one of them",
|
|
129
129
|
)
|
|
130
130
|
|
|
131
131
|
TLS_IS_ALL_THREE = EXECUTE.define(
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/shell.py
RENAMED
|
@@ -77,6 +77,7 @@ class ShellRunOutput(BlockModel):
|
|
|
77
77
|
"""What the command did, with its full streams addressable as artifacts."""
|
|
78
78
|
|
|
79
79
|
exit_code: int
|
|
80
|
+
"""The status the command exited with."""
|
|
80
81
|
|
|
81
82
|
stdout: str
|
|
82
83
|
"""The head of what the command printed, cut at the instance's inline capture size.
|
|
@@ -136,14 +137,9 @@ class ShellRunOperator(Operator[ShellRunConfig, ShellRunOutput]):
|
|
|
136
137
|
workspace = subprocess.workspace(ctx, "shell")
|
|
137
138
|
directory = workspace / config.cwd if config.cwd else workspace
|
|
138
139
|
directory.mkdir(parents=True, exist_ok=True)
|
|
139
|
-
|
|
140
|
-
stdout_uri = f"{artifacts}-stdout.txt"
|
|
141
|
-
stderr_uri = f"{artifacts}-stderr.txt"
|
|
142
|
-
code, out, err = await subprocess.run(
|
|
140
|
+
code, out, err, stdout_uri, stderr_uri = await subprocess.run(
|
|
143
141
|
directory=directory,
|
|
144
142
|
ctx=ctx,
|
|
145
|
-
stdout_uri=stdout_uri,
|
|
146
|
-
stderr_uri=stderr_uri,
|
|
147
143
|
timeout_seconds=config.timeout.total_seconds(),
|
|
148
144
|
environ=_environment(config, directory),
|
|
149
145
|
argv=config.argv or None,
|
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import signal
|
|
15
15
|
from collections.abc import Sequence
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from typing import NamedTuple
|
|
17
18
|
|
|
18
19
|
from dirigent_block_execute.capture import Drained, LiveLines, drain
|
|
19
20
|
from dirigent_block_execute.environment import allowed
|
|
@@ -24,22 +25,26 @@ from dirigent_plugin import BlockFailure, ErrorClass, StepContext
|
|
|
24
25
|
BASELINE_ENV = ("PATH", "LANG", "LC_ALL", "TZ")
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
class Ran(NamedTuple):
|
|
29
|
+
"""What one child process left behind: how it exited, both streams, and where they were written."""
|
|
30
|
+
|
|
31
|
+
code: int
|
|
32
|
+
out: Drained
|
|
33
|
+
err: Drained
|
|
34
|
+
stdout_uri: str
|
|
35
|
+
stderr_uri: str
|
|
36
|
+
|
|
37
|
+
|
|
27
38
|
def segment(ctx: StepContext, name: str) -> str:
|
|
28
|
-
"""The path this attempt's files sit at,
|
|
39
|
+
"""The path this attempt's files sit at, inside the run's work directory.
|
|
29
40
|
|
|
30
|
-
``{name}/{step}[/{item}]/attempt-{n}``.
|
|
31
|
-
|
|
32
|
-
out of each other's files and working directories.
|
|
41
|
+
``{name}/{step}[/{item}]/attempt-{n}``. The step key, and inside a fan-out the item id,
|
|
42
|
+
are what keep two steps of one run and two items of one step out of each other's files.
|
|
33
43
|
"""
|
|
34
44
|
item = f"/{ctx.run_item_id}" if ctx.run_item_id is not None else ""
|
|
35
45
|
return f"{name}/{ctx.step}{item}/attempt-{ctx.attempt}"
|
|
36
46
|
|
|
37
47
|
|
|
38
|
-
def prefix(ctx: StepContext, name: str) -> str:
|
|
39
|
-
"""The storage URI this attempt's artifact names are built on, under the run's scratch prefix."""
|
|
40
|
-
return f"{ctx.scratch.rstrip('/')}/{segment(ctx, name)}"
|
|
41
|
-
|
|
42
|
-
|
|
43
48
|
def workspace(ctx: StepContext, name: str) -> Path:
|
|
44
49
|
"""This attempt's working directory inside the run's work directory, made if absent."""
|
|
45
50
|
directory = ctx.work / segment(ctx, name)
|
|
@@ -59,22 +64,24 @@ async def run(
|
|
|
59
64
|
*,
|
|
60
65
|
directory: Path,
|
|
61
66
|
ctx: StepContext,
|
|
62
|
-
stdout_uri: str,
|
|
63
|
-
stderr_uri: str,
|
|
64
67
|
timeout_seconds: float,
|
|
65
68
|
environ: dict[str, str],
|
|
69
|
+
stdout_name: str = "stdout",
|
|
70
|
+
stderr_name: str = "stderr",
|
|
66
71
|
argv: list[str] | None = None,
|
|
67
72
|
command: str | None = None,
|
|
68
73
|
what: str = "the command",
|
|
69
74
|
redact: Sequence[str] = (),
|
|
70
|
-
) ->
|
|
75
|
+
) -> Ran:
|
|
71
76
|
"""Start the process, drain it under the timeout, and leave nothing of it behind.
|
|
72
77
|
|
|
73
|
-
Both pipes are read as the process writes them and written straight through to
|
|
74
|
-
so what is held is two bounded ends of each stream rather
|
|
75
|
-
prints more than the worker has memory for is a file, not
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
Both pipes are read as the process writes them and written straight through to a capture
|
|
79
|
+
the context opens and names, so what is held is two bounded ends of each stream rather
|
|
80
|
+
than all of it: a process that prints more than the worker has memory for is a file, not
|
|
81
|
+
a dead worker. The two names distinguish one command's streams from another's where a
|
|
82
|
+
step runs more than one. Each stream's first lines go into the run log where they are
|
|
83
|
+
printed, so a long command is visible working; ``redact`` names the strings it was
|
|
84
|
+
handed that must not reach one of them.
|
|
78
85
|
|
|
79
86
|
The process leads a session of its own, so what it starts is killable with it: a command
|
|
80
87
|
is usually ``sh -c``, and killing the shell alone leaves the children doing the work.
|
|
@@ -106,7 +113,7 @@ async def run(
|
|
|
106
113
|
pgid = group_of(process)
|
|
107
114
|
try:
|
|
108
115
|
async with asyncio.timeout(timeout_seconds):
|
|
109
|
-
async with ctx.
|
|
116
|
+
async with ctx.capture(stdout_name) as out_sink, ctx.capture(stderr_name) as err_sink:
|
|
110
117
|
out, err = await asyncio.gather(
|
|
111
118
|
drain(
|
|
112
119
|
process.stdout,
|
|
@@ -126,7 +133,7 @@ async def run(
|
|
|
126
133
|
raise BlockFailure(TIMED_OUT, error_class=ErrorClass.TRANSIENT, what=what, seconds=timeout_seconds) from error
|
|
127
134
|
finally:
|
|
128
135
|
await end(process, pgid)
|
|
129
|
-
return process.returncode or 0, out, err
|
|
136
|
+
return Ran(process.returncode or 0, out, err, out_sink.uri, err_sink.uri)
|
|
130
137
|
|
|
131
138
|
|
|
132
139
|
async def output(
|
|
@@ -207,12 +214,12 @@ def kill(pgid: int, pid: int) -> None:
|
|
|
207
214
|
|
|
208
215
|
__all__ = [
|
|
209
216
|
"BASELINE_ENV",
|
|
217
|
+
"Ran",
|
|
210
218
|
"end",
|
|
211
219
|
"environment",
|
|
212
220
|
"group_of",
|
|
213
221
|
"kill",
|
|
214
222
|
"output",
|
|
215
|
-
"prefix",
|
|
216
223
|
"run",
|
|
217
224
|
"segment",
|
|
218
225
|
"workspace",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/py.typed
RENAMED
|
File without changes
|
{dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/reap.py
RENAMED
|
File without changes
|
|
File without changes
|