@junghanacs/entwurf 0.39.0 → 0.41.0
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.
- package/AGENTS.md +4 -2
- package/BASELINE.md +60 -5
- package/CHANGELOG.md +557 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +52 -2
- package/FAQ.md +8 -4
- package/README.md +37 -17
- package/VERIFY.md +23 -10
- package/docs/acp-backend-rail.md +5 -1
- package/docs/external-mcp-host.md +8 -6
- package/docs/fresh-cut-policy.md +9 -1
- package/docs/setup-clean-host.md +64 -9
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +100 -24
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +10 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +123 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +65 -0
- package/mcp/entwurf-bridge/src/index.ts +118 -24
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/mcp/entwurf-bridge/tsconfig.build.json +17 -1
- package/package.json +5 -5
- package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
- package/pi-extensions/entwurf-control.ts +55 -20
- package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
- package/pi-extensions/lib/cc-retire-request.ts +553 -0
- package/pi-extensions/lib/control-retire.ts +195 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
- package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
- package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
- package/pi-extensions/lib/fresh-call-composition.ts +15 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
- package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
- package/pi-extensions/lib/meta-session.ts +140 -16
- package/pi-extensions/lib/mux-fresh-call.ts +7 -27
- package/pi-extensions/lib/mux-launch.ts +29 -0
- package/pi-extensions/lib/mux-resume-call.ts +33 -12
- package/pi-extensions/lib/pi-retire-request.ts +111 -0
- package/pi-extensions/lib/remote-pair-receive.ts +184 -0
- package/pi-extensions/lib/remote-pair-send.ts +325 -0
- package/pi-extensions/lib/remote-pair-transport.ts +81 -0
- package/pi-extensions/lib/resume-launch-identity.ts +104 -18
- package/pi-extensions/lib/retirement-event.ts +253 -0
- package/pi-extensions/meta-bridge-hook.ts +142 -13
- package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
- package/run.sh +122 -7
- package/scripts/agy-bridge-config.py +4 -1
- package/scripts/build-bridge.sh +14 -0
- package/scripts/check-capability-bundle-reach.ts +135 -2
- package/scripts/check-entwurf-bridge-boot.ts +2 -1
- package/scripts/check-entwurf-fact-provider.ts +1 -0
- package/scripts/check-entwurf-v2-production.ts +2 -0
- package/scripts/check-entwurf-v2-surface.ts +7 -0
- package/scripts/check-gate-qualification.ts +20 -8
- package/scripts/check-herdr-activation.ts +2 -2
- package/scripts/check-herdr-placement.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +6 -15
- package/scripts/check-install-container.sh +2 -2
- package/scripts/check-meta-doctor-oracle.sh +27 -1
- package/scripts/check-meta-facts.ts +8 -3
- package/scripts/check-meta-hook-session-switch.ts +230 -13
- package/scripts/check-meta-identity-consumers.ts +1 -0
- package/scripts/check-meta-listing.ts +1 -0
- package/scripts/check-meta-manifest-schema.py +1 -1
- package/scripts/check-meta-session.ts +31 -0
- package/scripts/check-meta-v3-record.ts +55 -1
- package/scripts/check-mux-resume-call.ts +35 -9
- package/scripts/check-peer-facts.ts +1 -0
- package/scripts/check-pi-durable-contact.ts +3 -1
- package/scripts/check-pi-durable-send.ts +4 -2
- package/scripts/check-pi-mcp-bridge.ts +1 -0
- package/scripts/check-remote-pair-installed.ts +242 -0
- package/scripts/check-resume-launch-identity.ts +62 -23
- package/scripts/lib/meta-hook-bundle.ts +72 -0
- package/scripts/lib/remote-pair-installed-audit.ts +25 -0
- package/scripts/meta-bridge-claude-floor.sh +13 -9
- package/scripts/meta-bridge-doctor.sh +17 -0
- package/scripts/meta-bridge-install.sh +6 -0
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/bridge-boot-resume.json +5 -2
- package/scripts/mutants/cc-retire-pidfd.json +129 -0
- package/scripts/mutants/cc-retire-request.json +171 -0
- package/scripts/mutants/control-retire.json +105 -0
- package/scripts/mutants/control-send-receipt.json +2 -2
- package/scripts/mutants/copilot-receive.json +2 -6
- package/scripts/mutants/entwurf-v2-retire.json +476 -0
- package/scripts/mutants/fresh-call-dispatch.json +37 -0
- package/scripts/mutants/meta-hook-session-switch.json +88 -0
- package/scripts/mutants/meta-identity.json +57 -0
- package/scripts/mutants/mux-fresh-call.json +1 -1
- package/scripts/mutants/mux-resume-call.json +40 -4
- package/scripts/mutants/pi-durable.json +79 -0
- package/scripts/mutants/pi-retire-request.json +72 -0
- package/scripts/mutants/remote-pair-display.json +53 -0
- package/scripts/mutants/remote-pair-installed-audit.json +28 -0
- package/scripts/mutants/remote-pair-receive.json +126 -0
- package/scripts/mutants/remote-pair-roundtrip.json +83 -0
- package/scripts/mutants/remote-pair-send.json +194 -0
- package/scripts/mutants/remote-pair-transport.json +66 -0
- package/scripts/mutants/resume-launch-identity.json +35 -8
- package/scripts/mutants/retirement-event.json +195 -0
- package/scripts/mutants/v2-surface.json +16 -0
- package/scripts/mutants/v2-visible-resume.json +283 -13
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/remote-pair-receive.ts +73 -0
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +5 -4
- package/scripts/smoke-meta-install-state.sh +4 -4
- package/scripts/smoke-mux-lifecycle-live.ts +10 -10
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
- package/scripts/smoke-pi-provider-state.sh +3 -3
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""cc-retire-pidfd — terminate ONE expected Linux process through a pidfd, or say why not (#147 G3).
|
|
2
|
+
|
|
3
|
+
A numeric PID names whichever process holds that number NOW. Checking a PID and then signalling
|
|
4
|
+
it leaves a window in which the process can exit and the number be reused, so the signal lands on
|
|
5
|
+
a stranger. A pidfd names the process it was opened on, for as long as the fd is open: a signal
|
|
6
|
+
sent through it can never reach a later holder of the number. This helper closes exactly that
|
|
7
|
+
check -> signal race, and nothing else.
|
|
8
|
+
|
|
9
|
+
python3 -I -S cc-retire-pidfd.py <expected-pid> <expected-start-key>
|
|
10
|
+
|
|
11
|
+
Order, and why each step is where it is:
|
|
12
|
+
|
|
13
|
+
1. Validate the inputs: a decimal pid > 1 and a `linux:<starttime>` start key (the scheme
|
|
14
|
+
`processStartKey` in meta-session.ts mints from /proc/<pid>/stat field 22).
|
|
15
|
+
2. pidfd_open(pid). From here the fd pins the process the number named at this instant.
|
|
16
|
+
3. Re-read /proc/<pid>/stat and compare the start key. /proc is a NUMERIC path, so this read
|
|
17
|
+
could already describe a reused number...
|
|
18
|
+
4. ...which is why the fd is then asked whether ITS process has exited. Not exited + key match
|
|
19
|
+
ties the fd and the stat to the same live process.
|
|
20
|
+
5. Print `{"phase": "ready"}` and wait, bounded, for ONE complete control frame on stdin — at
|
|
21
|
+
most CONTROL_MAX bytes of ASCII ending in a newline. Only the exact frame `go\n` proceeds;
|
|
22
|
+
`cancel\n`, EOF (with or without partial bytes), an oversized or non-ASCII frame, any other
|
|
23
|
+
line, a read failure, or the deadline (which partial input never extends) sends nothing. The caller re-checks its own
|
|
24
|
+
record/marker binding while the fd is held; this helper reads no record, marker, role or
|
|
25
|
+
permission and makes no decision of its own about WHETHER to terminate.
|
|
26
|
+
6. On `go`: ask the fd again (exited -> no signal), then pidfd_send_signal(fd, SIGTERM, None, 0)
|
|
27
|
+
ONCE, then poll the same fd for its exit within a fixed bound.
|
|
28
|
+
|
|
29
|
+
What it does NOT do, ever: signal a numeric pid, a process group, any signal but SIGTERM, a second
|
|
30
|
+
signal, an escalation, a retry, or a fallback to os.kill / kill(1) / Node's process.kill. A process
|
|
31
|
+
that ignores SIGTERM is left running and reported as `exit-unobserved`.
|
|
32
|
+
|
|
33
|
+
What the result does NOT mean:
|
|
34
|
+
- `signal-requested` means the kernel accepted the signal for that process, nothing more.
|
|
35
|
+
- `exited` means THAT process exited (its pidfd became readable). It is not its exit code, not
|
|
36
|
+
proof it flushed anything, and not proof its children are gone.
|
|
37
|
+
- A pidfd pins a PROCESS, not a conversation: a process that switched its native session after
|
|
38
|
+
the caller's last check is still the process this terminates.
|
|
39
|
+
- The start key is the same tick-resolution, same-boot value meta-session.ts compares; it is not
|
|
40
|
+
a cryptographic identity. The helper, the caller and the target must share one host, one PID
|
|
41
|
+
namespace and one procfs.
|
|
42
|
+
|
|
43
|
+
Output: JSON lines on stdout. Every normal protocol end prints `{"outcome": ..., ...}` as its last
|
|
44
|
+
line; a crash or a broken pipe can end without one, and a caller must read a missing outcome as
|
|
45
|
+
unknown — after `signal-requested` it means the signal may have been sent. Exit codes:
|
|
46
|
+
0 exited — SIGTERM was requested through the fd and that process's exit was observed
|
|
47
|
+
2 refused — nothing was opened or nothing was signalled: bad input, capability absent,
|
|
48
|
+
pid not found, permission, /proc unreadable, start-key mismatch, open error
|
|
49
|
+
3 not-signalled — the fd was held and NO signal was sent: cancelled, EOF, truncated / oversized
|
|
50
|
+
/ bad control, control read failure, go deadline, or the process had already
|
|
51
|
+
exited
|
|
52
|
+
4 unknown-after-signal — SIGTERM was requested but the exit was not observed in the bound, or
|
|
53
|
+
observing it failed; nothing is retried
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
import errno
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
import re
|
|
60
|
+
import select
|
|
61
|
+
import signal
|
|
62
|
+
import sys
|
|
63
|
+
import time
|
|
64
|
+
|
|
65
|
+
# Fixed on purpose — no flag or environment knob, so an outcome means the same thing everywhere.
|
|
66
|
+
GO_DEADLINE_S = 10.0
|
|
67
|
+
EXIT_WAIT_S = 5.0
|
|
68
|
+
|
|
69
|
+
# Linux PID_MAX_LIMIT (2^22): no pid_t a Linux kernel hands out is larger, so anything above it is
|
|
70
|
+
# malformed input, refused before any syscall.
|
|
71
|
+
PID_MAX = 4194304
|
|
72
|
+
# PID_MAX has 7 digits; fullmatch on this bound runs BEFORE int(), so no input reaches int()'s
|
|
73
|
+
# string-length limit and every over-long decimal is the named malformed-pid refusal.
|
|
74
|
+
PID_RE = re.compile(r"[0-9]{1,7}")
|
|
75
|
+
KEY_RE = re.compile(r"linux:[0-9]{1,20}")
|
|
76
|
+
CONTROL_MAX = 16
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def emit(obj):
|
|
80
|
+
sys.stdout.write(json.dumps(obj, sort_keys=True) + "\n")
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def finish(code, outcome, **facts):
|
|
85
|
+
emit(dict(outcome=outcome, **facts))
|
|
86
|
+
sys.exit(code)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def start_key(pid):
|
|
90
|
+
"""`linux:<starttime>` from /proc/<pid>/stat, split after the LAST ')' (comm may hold parens)."""
|
|
91
|
+
with open("/proc/%d/stat" % pid, "r", encoding="ascii", errors="strict") as fh:
|
|
92
|
+
stat = fh.read()
|
|
93
|
+
fields = stat[stat.rindex(")") + 1:].split()
|
|
94
|
+
starttime = fields[19]
|
|
95
|
+
if not starttime.isdigit():
|
|
96
|
+
raise ValueError("starttime field is not numeric")
|
|
97
|
+
return "linux:" + starttime
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def exited(fd):
|
|
101
|
+
"""True when the pidfd's process has exited (readable); raises when the fd cannot be observed."""
|
|
102
|
+
poller = select.poll()
|
|
103
|
+
poller.register(fd, select.POLLIN)
|
|
104
|
+
events = poller.poll(0)
|
|
105
|
+
for _, mask in events:
|
|
106
|
+
if mask & (select.POLLNVAL | select.POLLERR):
|
|
107
|
+
raise OSError(errno.EBADF, "pidfd not observable")
|
|
108
|
+
if mask & select.POLLIN:
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def read_control(deadline):
|
|
114
|
+
"""ONE complete control frame from stdin before `deadline` (monotonic; partial input does not
|
|
115
|
+
extend it). Returns (kind, line): ("line", text) for a complete ASCII frame, or one of
|
|
116
|
+
("eof",), ("truncated",), ("oversize",), ("bad-encoding",), ("deadline",), ("read-failed",)."""
|
|
117
|
+
buf = b""
|
|
118
|
+
stdin = sys.stdin.fileno()
|
|
119
|
+
while True:
|
|
120
|
+
remaining = deadline - time.monotonic()
|
|
121
|
+
if remaining <= 0:
|
|
122
|
+
return ("deadline", None)
|
|
123
|
+
try:
|
|
124
|
+
ready, _, _ = select.select([stdin], [], [], remaining)
|
|
125
|
+
if not ready:
|
|
126
|
+
continue
|
|
127
|
+
chunk = os.read(stdin, CONTROL_MAX + 1)
|
|
128
|
+
except OSError:
|
|
129
|
+
return ("read-failed", None)
|
|
130
|
+
if not chunk:
|
|
131
|
+
return ("eof", None) if not buf else ("truncated", None)
|
|
132
|
+
buf += chunk
|
|
133
|
+
if b"\n" in buf:
|
|
134
|
+
frame = buf.split(b"\n", 1)[0]
|
|
135
|
+
if len(frame) > CONTROL_MAX:
|
|
136
|
+
return ("oversize", None)
|
|
137
|
+
try:
|
|
138
|
+
return ("line", frame.decode("ascii"))
|
|
139
|
+
except UnicodeDecodeError:
|
|
140
|
+
return ("bad-encoding", None)
|
|
141
|
+
if len(buf) > CONTROL_MAX:
|
|
142
|
+
return ("oversize", None)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv):
|
|
146
|
+
if len(argv) != 3:
|
|
147
|
+
finish(2, "refused", reason="usage")
|
|
148
|
+
raw_pid, key = argv[1], argv[2]
|
|
149
|
+
if not PID_RE.fullmatch(raw_pid) or not 1 < int(raw_pid) <= PID_MAX:
|
|
150
|
+
finish(2, "refused", reason="malformed-pid")
|
|
151
|
+
if not KEY_RE.fullmatch(key):
|
|
152
|
+
finish(2, "refused", reason="malformed-start-key")
|
|
153
|
+
pid = int(raw_pid)
|
|
154
|
+
if not sys.platform.startswith("linux") or not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
|
|
155
|
+
finish(2, "refused", reason="capability-unsupported")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
fd = os.pidfd_open(pid, 0)
|
|
159
|
+
except OSError as err:
|
|
160
|
+
if err.errno == errno.ESRCH:
|
|
161
|
+
finish(2, "refused", reason="pid-not-found")
|
|
162
|
+
if err.errno == errno.EPERM:
|
|
163
|
+
finish(2, "refused", reason="permission-denied")
|
|
164
|
+
if err.errno in (errno.ENOSYS, errno.EINVAL):
|
|
165
|
+
finish(2, "refused", reason="capability-unsupported", errno=errno.errorcode.get(err.errno, err.errno))
|
|
166
|
+
finish(2, "refused", reason="open-failed", errno=errno.errorcode.get(err.errno, err.errno))
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
try:
|
|
170
|
+
observed = start_key(pid)
|
|
171
|
+
except (OSError, ValueError, IndexError, UnicodeError):
|
|
172
|
+
finish(2, "refused", reason="proc-unreadable")
|
|
173
|
+
if observed != key:
|
|
174
|
+
finish(2, "refused", reason="start-key-mismatch")
|
|
175
|
+
try:
|
|
176
|
+
if exited(fd):
|
|
177
|
+
finish(3, "already-exited")
|
|
178
|
+
except OSError:
|
|
179
|
+
finish(2, "refused", reason="observe-failed")
|
|
180
|
+
|
|
181
|
+
emit({"phase": "ready", "pid": pid})
|
|
182
|
+
kind, line = read_control(time.monotonic() + GO_DEADLINE_S)
|
|
183
|
+
if kind == "deadline":
|
|
184
|
+
finish(3, "go-deadline")
|
|
185
|
+
if kind == "eof":
|
|
186
|
+
finish(3, "cancelled-eof")
|
|
187
|
+
if kind == "truncated":
|
|
188
|
+
finish(3, "truncated-control")
|
|
189
|
+
if kind == "oversize":
|
|
190
|
+
finish(3, "oversize-control")
|
|
191
|
+
if kind == "read-failed":
|
|
192
|
+
finish(3, "control-read-failed")
|
|
193
|
+
if kind != "line":
|
|
194
|
+
finish(3, "bad-control")
|
|
195
|
+
if line == "cancel":
|
|
196
|
+
finish(3, "cancelled")
|
|
197
|
+
if line != "go":
|
|
198
|
+
finish(3, "bad-control")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if exited(fd):
|
|
202
|
+
finish(3, "already-exited")
|
|
203
|
+
except OSError:
|
|
204
|
+
finish(3, "observe-failed-before-signal")
|
|
205
|
+
try:
|
|
206
|
+
signal.pidfd_send_signal(fd, signal.SIGTERM, None, 0)
|
|
207
|
+
except OSError as err:
|
|
208
|
+
if err.errno == errno.ESRCH:
|
|
209
|
+
finish(3, "already-exited")
|
|
210
|
+
if err.errno == errno.EPERM:
|
|
211
|
+
finish(2, "refused", reason="permission-denied")
|
|
212
|
+
finish(2, "refused", reason="signal-failed", errno=errno.errorcode.get(err.errno, err.errno))
|
|
213
|
+
emit({"phase": "signal-requested", "signal": "SIGTERM"})
|
|
214
|
+
|
|
215
|
+
poller = select.poll()
|
|
216
|
+
poller.register(fd, select.POLLIN)
|
|
217
|
+
deadline = time.monotonic() + EXIT_WAIT_S
|
|
218
|
+
while True:
|
|
219
|
+
remaining = deadline - time.monotonic()
|
|
220
|
+
if remaining <= 0:
|
|
221
|
+
finish(4, "exit-unobserved", waitedS=EXIT_WAIT_S)
|
|
222
|
+
try:
|
|
223
|
+
events = poller.poll(int(remaining * 1000) + 1)
|
|
224
|
+
except OSError:
|
|
225
|
+
finish(4, "observe-failed")
|
|
226
|
+
for _, mask in events:
|
|
227
|
+
if mask & (select.POLLNVAL | select.POLLERR):
|
|
228
|
+
finish(4, "observe-failed")
|
|
229
|
+
if mask & select.POLLIN:
|
|
230
|
+
finish(0, "exited")
|
|
231
|
+
finally:
|
|
232
|
+
os.close(fd)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main(sys.argv)
|