dcc-mcp-renderdoc 0.3.1__tar.gz → 0.3.3__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.
Files changed (38) hide show
  1. dcc_mcp_renderdoc-0.3.3/.release-please-manifest.json +2 -0
  2. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/CHANGELOG.md +20 -0
  3. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/PKG-INFO +23 -12
  4. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/README.md +22 -11
  5. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/pyproject.toml +1 -1
  6. dcc_mcp_renderdoc-0.3.3/src/dcc_mcp_renderdoc/__version__.py +3 -0
  7. dcc_mcp_renderdoc-0.3.3/src/dcc_mcp_renderdoc/_target_control.py +185 -0
  8. dcc_mcp_renderdoc-0.3.3/src/dcc_mcp_renderdoc/runtime.py +751 -0
  9. dcc_mcp_renderdoc-0.3.3/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +40 -0
  10. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +2 -2
  11. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tests/live_renderdoc_smoke.py +18 -4
  12. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tests/renderdoc_smoke.c +11 -10
  13. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tests/test_package.py +1 -0
  14. dcc_mcp_renderdoc-0.3.3/tests/test_runtime.py +1686 -0
  15. dcc_mcp_renderdoc-0.3.1/.release-please-manifest.json +0 -2
  16. dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/__version__.py +0 -3
  17. dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/runtime.py +0 -510
  18. dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +0 -30
  19. dcc_mcp_renderdoc-0.3.1/tests/test_runtime.py +0 -267
  20. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/.gitignore +0 -0
  21. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/LICENSE +0 -0
  22. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/release-please-config.json +0 -0
  23. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/__init__.py +0 -0
  24. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/server.py +0 -0
  25. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +0 -0
  26. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
  27. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
  28. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
  29. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
  30. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +0 -0
  31. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
  32. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
  33. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
  34. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
  35. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tests/test_download.py +0 -0
  36. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tools/__init__.py +0 -0
  37. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tools/download_latest_renderdoc.py +0 -0
  38. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.3}/tools/lint_skills.py +0 -0
@@ -0,0 +1,2 @@
1
+ {".":"0.3.3"}
2
+
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.2...v0.3.3) (2026-07-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * accept POSIX target ident exit status ([e35d14e](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/e35d14ed7dfdd429f5356ac8a5d5a98411a9dc28))
9
+ * harden target control lifecycle ([1a1eca8](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/1a1eca82fb453ec40b11a33791160d6074e70a6f))
10
+ * isolate target control sidecar ([2e16b2b](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/2e16b2b009c76136a5ad2889c4943c458401923a))
11
+ * normalize target names across platforms ([44602a0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/44602a0fca3fd2749688f56f169280d18ddcb989))
12
+ * trigger captures through RenderDoc Target Control ([68d93aa](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/68d93aa9c0629b61cac75275ca53a55a65f29729))
13
+ * trigger captures through target control ([f0a11c8](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/f0a11c844a5597c0ae6b022a8ec07c32d7a15806))
14
+ * verify target before capture trigger ([a392e9f](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a392e9fc974152d5d2dc183c1ed8b60ed15c2195))
15
+
16
+ ## [0.3.2](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.1...v0.3.2) (2026-07-22)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * keep capture controller alive through trigger ([#11](https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues/11)) ([1fb7bed](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/1fb7bed40dfb027b148a14cefd01c53028891d56))
22
+
3
23
  ## [0.3.1](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.0...v0.3.1) (2026-07-22)
4
24
 
5
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcc-mcp-renderdoc
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: RenderDoc capture and replay automation skills for DCC-MCP
5
5
  Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
6
6
  Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
@@ -78,8 +78,8 @@ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range
78
78
  RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
79
79
 
80
80
  The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
81
- conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
82
- open or installing a second bridge.
81
+ conversion. Delayed capture uses RenderDoc's official Target Control API through the sibling
82
+ `qrenderdoc` bundled Python runtime, without foreground focus or synthetic keyboard input.
83
83
 
84
84
  ## Install
85
85
 
@@ -103,8 +103,8 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
103
103
  ## Agent workflows
104
104
 
105
105
  - Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
106
- - Trigger F12 automatically after a configurable delay, with optional child-process window focus.
107
- - Inject into a visible Windows process that had to be launched by a platform client, then trigger and collect a capture.
106
+ - Trigger a capture through official Target Control after a configurable delay.
107
+ - Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
108
108
  - Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
109
109
  - Export a capture thumbnail for visual review.
110
110
  - Export Chrome trace JSON for timeline tooling.
@@ -112,16 +112,27 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
112
112
  The capture tool launches only the explicit executable and arguments supplied by the caller. It
113
113
  never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
114
114
 
115
- For interactive Windows programs, pass `trigger_after_secs` to `capture_program`. When a launcher
116
- creates the rendered child process, also pass `trigger_process_name`. Use `capture_process` only
117
- when the target is already running; late injection may not capture graphics devices created before
118
- RenderDoc was attached.
115
+ Pass `trigger_after_secs` to `capture_program` for a Target Control trigger. This requires
116
+ `qrenderdoc` beside `renderdoccmd`. The official RenderDoc runtime supports Windows and Linux;
117
+ macOS is covered only by this project's Python unit tests. Linux Target Control requires an X or
118
+ Wayland display, so headless hosts must run under Xvfb (or explicitly configure a working Qt
119
+ platform). The official Linux archive does not bundle Qt's `offscreen` platform plugin. Each
120
+ sidecar uses an isolated Qt data profile with RenderDoc analytics explicitly opted out, preventing
121
+ the first-run consent dialog without reading or changing the user's qrenderdoc configuration.
122
+
123
+ When a launcher creates the rendered child process, set `hook_children=true` and pass
124
+ `trigger_process_name`. The adapter first checks the launched target itself; if its name does not
125
+ match, it follows only that target's official `NewChild` messages to find a unique named child. A
126
+ child name without child hooking fails before launch. Use `capture_process` only when the target is
127
+ already running; late injection may not capture graphics devices created before RenderDoc was
128
+ attached.
119
129
 
120
130
  ## Real CI
121
131
 
122
- CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
123
- small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
124
- resulting `.rdc`, and verifies thumbnail and timeline exports.
132
+ CI discovers the current stable Linux RenderDoc build from the official downloads page. It
133
+ compiles a small OpenGL program, requests a real frame through Target Control under Xvfb using
134
+ Qt's bundled `xcb` platform, asserts the structured trigger mode, calls the MCP analysis tool
135
+ against the resulting `.rdc`, and verifies thumbnail and timeline exports.
125
136
 
126
137
  ## Development
127
138
 
@@ -50,8 +50,8 @@ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range
50
50
  RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
51
51
 
52
52
  The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
53
- conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
54
- open or installing a second bridge.
53
+ conversion. Delayed capture uses RenderDoc's official Target Control API through the sibling
54
+ `qrenderdoc` bundled Python runtime, without foreground focus or synthetic keyboard input.
55
55
 
56
56
  ## Install
57
57
 
@@ -75,8 +75,8 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
75
75
  ## Agent workflows
76
76
 
77
77
  - Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
78
- - Trigger F12 automatically after a configurable delay, with optional child-process window focus.
79
- - Inject into a visible Windows process that had to be launched by a platform client, then trigger and collect a capture.
78
+ - Trigger a capture through official Target Control after a configurable delay.
79
+ - Inject into a process that had to be launched by a platform client, then trigger and collect a capture.
80
80
  - Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
81
81
  - Export a capture thumbnail for visual review.
82
82
  - Export Chrome trace JSON for timeline tooling.
@@ -84,16 +84,27 @@ through the stable gateway at `http://127.0.0.1:9765/mcp`; set
84
84
  The capture tool launches only the explicit executable and arguments supplied by the caller. It
85
85
  never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
86
86
 
87
- For interactive Windows programs, pass `trigger_after_secs` to `capture_program`. When a launcher
88
- creates the rendered child process, also pass `trigger_process_name`. Use `capture_process` only
89
- when the target is already running; late injection may not capture graphics devices created before
90
- RenderDoc was attached.
87
+ Pass `trigger_after_secs` to `capture_program` for a Target Control trigger. This requires
88
+ `qrenderdoc` beside `renderdoccmd`. The official RenderDoc runtime supports Windows and Linux;
89
+ macOS is covered only by this project's Python unit tests. Linux Target Control requires an X or
90
+ Wayland display, so headless hosts must run under Xvfb (or explicitly configure a working Qt
91
+ platform). The official Linux archive does not bundle Qt's `offscreen` platform plugin. Each
92
+ sidecar uses an isolated Qt data profile with RenderDoc analytics explicitly opted out, preventing
93
+ the first-run consent dialog without reading or changing the user's qrenderdoc configuration.
94
+
95
+ When a launcher creates the rendered child process, set `hook_children=true` and pass
96
+ `trigger_process_name`. The adapter first checks the launched target itself; if its name does not
97
+ match, it follows only that target's official `NewChild` messages to find a unique named child. A
98
+ child name without child hooking fails before launch. Use `capture_process` only when the target is
99
+ already running; late injection may not capture graphics devices created before RenderDoc was
100
+ attached.
91
101
 
92
102
  ## Real CI
93
103
 
94
- CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
95
- small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
96
- resulting `.rdc`, and verifies thumbnail and timeline exports.
104
+ CI discovers the current stable Linux RenderDoc build from the official downloads page. It
105
+ compiles a small OpenGL program, requests a real frame through Target Control under Xvfb using
106
+ Qt's bundled `xcb` platform, asserts the structured trigger mode, calls the MCP analysis tool
107
+ against the resulting `.rdc`, and verifies thumbnail and timeline exports.
97
108
 
98
109
  ## Development
99
110
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dcc-mcp-renderdoc"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "RenderDoc capture and replay automation skills for DCC-MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,3 @@
1
+ """Package version."""
2
+
3
+ __version__ = "0.3.3" # x-release-please-version
@@ -0,0 +1,185 @@
1
+ """RenderDoc 1.45 bundled-Python bridge for headless Target Control capture."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+
7
+
8
+ def _positive_int(name):
9
+ value = int(os.environ[name])
10
+ if value <= 0:
11
+ raise ValueError("{} must be positive".format(name))
12
+ return value
13
+
14
+
15
+ def _nonnegative_float(name):
16
+ value = float(os.environ[name])
17
+ if value < 0:
18
+ raise ValueError("{} must not be negative".format(name))
19
+ return value
20
+
21
+
22
+ def _open_target(rd, ident):
23
+ target = rd.CreateTargetControl("", ident, "dcc-mcp-renderdoc", False)
24
+ if target is None:
25
+ raise RuntimeError("RenderDoc target ident {} is busy or unavailable".format(ident))
26
+ if not target.Connected():
27
+ busy_client = target.GetBusyClient()
28
+ detail = " (busy client: {})".format(busy_client) if busy_client else ""
29
+ target.Shutdown()
30
+ raise RuntimeError("could not connect to RenderDoc target ident {}{}".format(ident, detail))
31
+ return target
32
+
33
+
34
+ def _target_name_matches(actual, expected):
35
+ actual = str(actual).replace("\\", "/").rsplit("/", 1)[-1].casefold()
36
+ expected = str(expected).replace("\\", "/").rsplit("/", 1)[-1].casefold()
37
+ return (
38
+ actual == expected
39
+ or (actual.endswith(".exe") and actual[:-4] == expected)
40
+ or (expected.endswith(".exe") and expected[:-4] == actual)
41
+ )
42
+
43
+
44
+ def _wait_for_child_target(rd, parent, target_name, trigger_at, deadline):
45
+ matches = []
46
+ quiet_deadline = None
47
+ try:
48
+ while time.monotonic() < deadline:
49
+ now = time.monotonic()
50
+ if (
51
+ len(matches) == 1
52
+ and quiet_deadline is not None
53
+ and now >= trigger_at
54
+ and now >= quiet_deadline
55
+ ):
56
+ return matches.pop()
57
+ message = parent.ReceiveMessage(None)
58
+ if message is not None and message.type == rd.TargetControlMessageType.NewChild:
59
+ child_ident = int(message.newChild.ident)
60
+ child_pid = int(message.newChild.processId)
61
+ child = _open_target(rd, child_ident)
62
+ try:
63
+ if int(child.GetPID()) != child_pid:
64
+ raise RuntimeError(
65
+ "RenderDoc child target PID did not match NewChild message"
66
+ )
67
+ if _target_name_matches(child.GetTarget(), target_name):
68
+ matches.append(child)
69
+ child = None
70
+ finally:
71
+ if child is not None:
72
+ child.Shutdown()
73
+ if matches:
74
+ if len(matches) > 1:
75
+ raise RuntimeError("multiple child targets matched {}".format(target_name))
76
+ quiet_deadline = time.monotonic() + 0.25
77
+ if quiet_deadline > deadline:
78
+ raise RuntimeError("insufficient time to select a unique child target")
79
+ continue
80
+ if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
81
+ if len(matches) == 1:
82
+ return matches.pop()
83
+ raise RuntimeError("RenderDoc parent Target Control disconnected before child")
84
+ for match in matches:
85
+ child_message = match.ReceiveMessage(None)
86
+ if (
87
+ child_message is not None
88
+ and child_message.type == rd.TargetControlMessageType.Disconnected
89
+ ):
90
+ raise RuntimeError("RenderDoc child Target Control disconnected before trigger")
91
+ time.sleep(0.05)
92
+ if len(matches) == 1:
93
+ raise RuntimeError("insufficient time to select a unique child target")
94
+ raise RuntimeError("no child target matched {}".format(target_name))
95
+ finally:
96
+ for match in matches:
97
+ match.Shutdown()
98
+
99
+
100
+ def _pump_until(rd, target, deadline):
101
+ while time.monotonic() < deadline:
102
+ message = target.ReceiveMessage(None)
103
+ if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
104
+ raise RuntimeError("RenderDoc Target Control disconnected before trigger")
105
+ time.sleep(0.05)
106
+
107
+
108
+ def main():
109
+ status_path = os.environ.get("DCC_MCP_RENDERDOC_TARGET_STATUS")
110
+ status = {
111
+ "schema_version": 1,
112
+ "connected": False,
113
+ "triggered": False,
114
+ "shutdown": False,
115
+ "timed_out": False,
116
+ "target_pid": None,
117
+ "capture_path": None,
118
+ "error": None,
119
+ }
120
+ target = None
121
+ try:
122
+ import renderdoc as rd
123
+
124
+ ident = _positive_int("DCC_MCP_RENDERDOC_TARGET_IDENT")
125
+ capture_wait_secs = _positive_int("DCC_MCP_RENDERDOC_TARGET_TIMEOUT_SECS")
126
+ trigger_after_secs = _nonnegative_float("DCC_MCP_RENDERDOC_TRIGGER_AFTER_SECS")
127
+ target_name = os.environ.get("DCC_MCP_RENDERDOC_TARGET_NAME", "").strip()
128
+ expected_pid_value = os.environ.get("DCC_MCP_RENDERDOC_EXPECTED_PID", "").strip()
129
+ expected_pid = int(expected_pid_value) if expected_pid_value else None
130
+ if expected_pid is not None and expected_pid <= 0:
131
+ raise ValueError("DCC_MCP_RENDERDOC_EXPECTED_PID must be positive")
132
+ trigger_at = time.monotonic() + trigger_after_secs
133
+ capture_deadline = trigger_at + capture_wait_secs
134
+ target = _open_target(rd, ident)
135
+ if target_name and not _target_name_matches(target.GetTarget(), target_name):
136
+ child = _wait_for_child_target(rd, target, target_name, trigger_at, capture_deadline)
137
+ try:
138
+ target.Shutdown()
139
+ except BaseException:
140
+ child.Shutdown()
141
+ raise
142
+ target = child
143
+ status["connected"] = True
144
+ status["target_pid"] = int(target.GetPID())
145
+ if expected_pid is not None and status["target_pid"] != expected_pid:
146
+ raise RuntimeError(
147
+ "RenderDoc target PID {} did not match expected PID {}".format(
148
+ status["target_pid"], expected_pid
149
+ )
150
+ )
151
+ _pump_until(rd, target, trigger_at)
152
+ target.TriggerCapture(1)
153
+ status["triggered"] = True
154
+ while time.monotonic() < capture_deadline:
155
+ message = target.ReceiveMessage(None)
156
+ if message is not None and message.type == rd.TargetControlMessageType.NewCapture:
157
+ status["capture_path"] = str(message.newCapture.path)
158
+ break
159
+ if message is not None and message.type == rd.TargetControlMessageType.Disconnected:
160
+ raise RuntimeError("RenderDoc Target Control disconnected before capture")
161
+ time.sleep(0.05)
162
+ if status["capture_path"] is None:
163
+ status["timed_out"] = True
164
+ raise RuntimeError("timed out waiting for RenderDoc NewCapture")
165
+ except BaseException as exc:
166
+ status["error"] = str(exc)
167
+ finally:
168
+ if target is not None:
169
+ try:
170
+ target.Shutdown()
171
+ status["shutdown"] = True
172
+ target = None
173
+ except BaseException as exc:
174
+ status["error"] = status["error"] or ("Target Control shutdown failed: " + str(exc))
175
+ if status_path:
176
+ with open(status_path, "w") as status_file:
177
+ json.dump(status, status_file)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ try:
182
+ main()
183
+ except BaseException:
184
+ pass
185
+ raise SystemExit(0)