dcc-mcp-renderdoc 0.3.0__tar.gz → 0.3.1__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.
- dcc_mcp_renderdoc-0.3.1/.release-please-manifest.json +2 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/CHANGELOG.md +14 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/PKG-INFO +48 -1
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/README.md +47 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/pyproject.toml +1 -1
- dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/__version__.py +3 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/runtime.py +63 -20
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +1 -1
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_package.py +16 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_runtime.py +86 -11
- dcc_mcp_renderdoc-0.3.0/.release-please-manifest.json +0 -2
- dcc_mcp_renderdoc-0.3.0/src/dcc_mcp_renderdoc/__version__.py +0 -3
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/.gitignore +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/LICENSE +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/release-please-config.json +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/server.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/live_renderdoc_smoke.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/renderdoc_smoke.c +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_download.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/__init__.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/download_latest_renderdoc.py +0 -0
- {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/lint_skills.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.0...v0.3.1) (2026-07-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* allow long-running frame trigger delays ([#10](https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues/10)) ([3212cf7](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/3212cf7c9f236b28b48bf0d28705e2b1939fbe21))
|
|
9
|
+
* capture relaunched child processes ([39e5603](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/39e5603112c8cad98807da3fee86248dd03fb824))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
|
|
14
|
+
* align agent workflow and branding ([ca8b2cf](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/ca8b2cf955a470a87efde24c12e9ebbdb284633c))
|
|
15
|
+
* document CLI install and updates ([599cc70](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/599cc70445e11c2fc53d2626c24f9912365b87e0))
|
|
16
|
+
|
|
3
17
|
## [0.3.0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.2.1...v0.3.0) (2026-07-16)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcc-mcp-renderdoc
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -28,6 +28,53 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
# dcc-mcp-renderdoc
|
|
30
30
|
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="docs/assets/dcc-mcp-renderdoc.svg" alt="DCC-MCP · RENDERDOC" width="600">
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
## Agent workflow
|
|
36
|
+
|
|
37
|
+
AI agents should use the shared gateway through `dcc-mcp-cli`; IDE users may
|
|
38
|
+
continue to use the MCP endpoint. Prefer typed skills and tools over raw scripts.
|
|
39
|
+
|
|
40
|
+
### Install or update the CLI
|
|
41
|
+
|
|
42
|
+
`dcc-mcp-cli` is the preferred control path for every shell-capable agent. If
|
|
43
|
+
it is missing, ask the user before installing the latest official release:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Linux/macOS
|
|
47
|
+
curl -fsSL https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.sh | sh
|
|
48
|
+
|
|
49
|
+
# Windows PowerShell
|
|
50
|
+
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.ps1 | iex"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keep an official build current through the release manifest:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
dcc-mcp-cli update check
|
|
57
|
+
dcc-mcp-cli update apply
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`update apply` downloads and stages the latest CLI for the next launch. It
|
|
61
|
+
does not update a running `dcc-mcp-server`; update that server in its own
|
|
62
|
+
environment.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dcc-mcp-cli dcc-types
|
|
66
|
+
dcc-mcp-cli list
|
|
67
|
+
dcc-mcp-cli search --query "<task>" --dcc-type renderdoc
|
|
68
|
+
dcc-mcp-cli describe <tool-slug>
|
|
69
|
+
dcc-mcp-cli call <tool-slug> --json '{"key":"value"}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`dcc-types` reports release-catalog support; `list` reports live sessions. If a
|
|
73
|
+
tool belongs to an inactive progressive skill, call `dcc-mcp-cli load-skill <skill-name> --dcc-type renderdoc` before retrying. For post-task improvement,
|
|
74
|
+
attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range 24h --session-id <task-id>`, then pass the bounded evidence to the
|
|
75
|
+
`review_skill_improvement` prompt from `dcc-mcp-skills-creator`.
|
|
76
|
+
|
|
77
|
+
|
|
31
78
|
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
32
79
|
|
|
33
80
|
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# dcc-mcp-renderdoc
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/assets/dcc-mcp-renderdoc.svg" alt="DCC-MCP · RENDERDOC" width="600">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
## Agent workflow
|
|
8
|
+
|
|
9
|
+
AI agents should use the shared gateway through `dcc-mcp-cli`; IDE users may
|
|
10
|
+
continue to use the MCP endpoint. Prefer typed skills and tools over raw scripts.
|
|
11
|
+
|
|
12
|
+
### Install or update the CLI
|
|
13
|
+
|
|
14
|
+
`dcc-mcp-cli` is the preferred control path for every shell-capable agent. If
|
|
15
|
+
it is missing, ask the user before installing the latest official release:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Linux/macOS
|
|
19
|
+
curl -fsSL https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.sh | sh
|
|
20
|
+
|
|
21
|
+
# Windows PowerShell
|
|
22
|
+
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.ps1 | iex"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Keep an official build current through the release manifest:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
dcc-mcp-cli update check
|
|
29
|
+
dcc-mcp-cli update apply
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`update apply` downloads and stages the latest CLI for the next launch. It
|
|
33
|
+
does not update a running `dcc-mcp-server`; update that server in its own
|
|
34
|
+
environment.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dcc-mcp-cli dcc-types
|
|
38
|
+
dcc-mcp-cli list
|
|
39
|
+
dcc-mcp-cli search --query "<task>" --dcc-type renderdoc
|
|
40
|
+
dcc-mcp-cli describe <tool-slug>
|
|
41
|
+
dcc-mcp-cli call <tool-slug> --json '{"key":"value"}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`dcc-types` reports release-catalog support; `list` reports live sessions. If a
|
|
45
|
+
tool belongs to an inactive progressive skill, call `dcc-mcp-cli load-skill <skill-name> --dcc-type renderdoc` before retrying. For post-task improvement,
|
|
46
|
+
attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range 24h --session-id <task-id>`, then pass the bounded evidence to the
|
|
47
|
+
`review_skill_improvement` prompt from `dcc-mcp-skills-creator`.
|
|
48
|
+
|
|
49
|
+
|
|
3
50
|
RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
|
|
4
51
|
|
|
5
52
|
The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
|
|
@@ -102,6 +102,10 @@ def capture_program(
|
|
|
102
102
|
raise RenderDocError(f"Working directory does not exist: {cwd}")
|
|
103
103
|
|
|
104
104
|
before = {path.resolve() for path in output.parent.glob("*.rdc")}
|
|
105
|
+
process_name = trigger_process_name or target.name
|
|
106
|
+
visible_before_launch = (
|
|
107
|
+
set(_visible_process_ids(process_name)) if trigger_after_secs is not None else set()
|
|
108
|
+
)
|
|
105
109
|
cli_args = ["capture", "--working-dir", str(cwd), "--capture-file", str(output)]
|
|
106
110
|
effective_wait_for_exit = wait_for_exit and trigger_after_secs is None
|
|
107
111
|
if effective_wait_for_exit:
|
|
@@ -120,15 +124,38 @@ def capture_program(
|
|
|
120
124
|
target_pid: Optional[int] = None
|
|
121
125
|
if trigger_after_secs is not None:
|
|
122
126
|
time.sleep(max(0.0, trigger_after_secs))
|
|
123
|
-
target_pid =
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
target_pid, focused, captures = _wait_for_triggered_capture(
|
|
128
|
+
output.parent,
|
|
129
|
+
before,
|
|
130
|
+
process_name,
|
|
131
|
+
visible_before_launch,
|
|
132
|
+
capture_wait_secs,
|
|
126
133
|
)
|
|
127
|
-
focused = _trigger_capture_hotkey(target_pid)
|
|
128
|
-
captures = _wait_for_captures(output.parent, before, capture_wait_secs)
|
|
129
134
|
else:
|
|
130
135
|
focused = False
|
|
131
136
|
captures = _new_captures(output.parent, before)
|
|
137
|
+
injection_error: Optional[str] = None
|
|
138
|
+
if (
|
|
139
|
+
not captures
|
|
140
|
+
and target_pid is not None
|
|
141
|
+
and hook_children
|
|
142
|
+
and trigger_process_name is not None
|
|
143
|
+
):
|
|
144
|
+
try:
|
|
145
|
+
fallback = capture_process(
|
|
146
|
+
target_pid,
|
|
147
|
+
output_template,
|
|
148
|
+
working_directory=str(cwd),
|
|
149
|
+
trigger_after_secs=0.25,
|
|
150
|
+
capture_wait_secs=capture_wait_secs,
|
|
151
|
+
api_validation=api_validation,
|
|
152
|
+
timeout_secs=min(timeout_secs, 60),
|
|
153
|
+
command=command,
|
|
154
|
+
)
|
|
155
|
+
captures = [Path(path).resolve() for path in fallback["captures"]]
|
|
156
|
+
focused = bool(fallback["focused_target_window"]) or focused
|
|
157
|
+
except RenderDocError as exc:
|
|
158
|
+
injection_error = str(exc)
|
|
132
159
|
if not captures:
|
|
133
160
|
output_detail = "\n".join(
|
|
134
161
|
part.strip() for part in (result.stdout, result.stderr) if part.strip()
|
|
@@ -142,6 +169,7 @@ def capture_program(
|
|
|
142
169
|
"No new .rdc capture appeared; ensure the target presents a frame and was hooked "
|
|
143
170
|
f"before creating its graphics device. {diagnostics}. "
|
|
144
171
|
f"RenderDoc output: {output_detail}"
|
|
172
|
+
+ (f". Injection fallback: {injection_error}" if injection_error else "")
|
|
145
173
|
)
|
|
146
174
|
return {
|
|
147
175
|
"target": str(target),
|
|
@@ -246,32 +274,47 @@ def _trigger_capture_hotkey(process_id: Optional[int] = None) -> bool:
|
|
|
246
274
|
return True
|
|
247
275
|
|
|
248
276
|
user32.EnumWindows(find_window, 0)
|
|
249
|
-
if target:
|
|
250
|
-
|
|
251
|
-
|
|
277
|
+
if not target:
|
|
278
|
+
return False
|
|
279
|
+
user32.SetForegroundWindow(target)
|
|
280
|
+
time.sleep(0.15)
|
|
252
281
|
user32.keybd_event(0x7B, 0, 0, 0)
|
|
253
282
|
user32.keybd_event(0x7B, 0, 2, 0)
|
|
254
283
|
return bool(target)
|
|
255
284
|
|
|
256
285
|
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
def _wait_for_triggered_capture(
|
|
287
|
+
directory: Path,
|
|
288
|
+
before: set[Path],
|
|
289
|
+
process_name: str,
|
|
290
|
+
ignored_process_ids: set[int],
|
|
291
|
+
timeout_secs: int,
|
|
292
|
+
) -> tuple[Optional[int], bool, list[Path]]:
|
|
260
293
|
deadline = time.monotonic() + timeout_secs
|
|
294
|
+
handled_process_ids = set(ignored_process_ids)
|
|
295
|
+
target_pid: Optional[int] = None
|
|
296
|
+
focused = False
|
|
261
297
|
while time.monotonic() < deadline:
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
return
|
|
298
|
+
captures = _new_captures(directory, before)
|
|
299
|
+
if captures:
|
|
300
|
+
return target_pid, focused, captures
|
|
301
|
+
for process_id in _visible_process_ids(process_name):
|
|
302
|
+
if process_id in handled_process_ids:
|
|
303
|
+
continue
|
|
304
|
+
handled_process_ids.add(process_id)
|
|
305
|
+
target_pid = process_id
|
|
306
|
+
focused = _trigger_capture_hotkey(process_id) or focused
|
|
265
307
|
time.sleep(0.25)
|
|
266
|
-
return
|
|
308
|
+
return target_pid, focused, _new_captures(directory, before)
|
|
267
309
|
|
|
268
310
|
|
|
269
|
-
def
|
|
311
|
+
def _visible_process_ids(process_name: str) -> list[int]:
|
|
270
312
|
expected = Path(process_name).name.casefold()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
313
|
+
return [
|
|
314
|
+
int(process["process_id"])
|
|
315
|
+
for process in _visible_processes()
|
|
316
|
+
if process["name"].casefold() == expected
|
|
317
|
+
]
|
|
275
318
|
|
|
276
319
|
|
|
277
320
|
def _visible_processes(limit: int = 64) -> list[dict[str, Any]]:
|
|
@@ -29,7 +29,7 @@ tools:
|
|
|
29
29
|
wait_for_exit: {type: boolean, default: true}
|
|
30
30
|
api_validation: {type: boolean, default: false}
|
|
31
31
|
hook_children: {type: boolean, default: false}
|
|
32
|
-
trigger_after_secs: {type: number, minimum: 0, maximum:
|
|
32
|
+
trigger_after_secs: {type: number, minimum: 0, maximum: 900}
|
|
33
33
|
trigger_process_name: {type: string, minLength: 1, maxLength: 260}
|
|
34
34
|
capture_wait_secs: {type: integer, minimum: 1, maximum: 600, default: 30}
|
|
35
35
|
timeout_secs: {type: integer, minimum: 1, maximum: 3600, default: 300}
|
|
@@ -24,6 +24,22 @@ def test_bundled_skills_and_release_workflow_exist():
|
|
|
24
24
|
assert (root / ".github" / "workflows" / "release.yml").is_file()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def test_capture_program_accepts_ten_minute_boss_trigger():
|
|
28
|
+
root = Path(__file__).parents[1]
|
|
29
|
+
tools = (
|
|
30
|
+
root / "src" / "dcc_mcp_renderdoc" / "skills" / "renderdoc-capture" / "tools.yaml"
|
|
31
|
+
).read_text(encoding="utf-8")
|
|
32
|
+
capture_program = re.search(
|
|
33
|
+
r" - name: capture_program(?P<body>.*?)(?=\n - name:)", tools, re.DOTALL
|
|
34
|
+
)
|
|
35
|
+
assert capture_program is not None
|
|
36
|
+
maximum = re.search(
|
|
37
|
+
r"trigger_after_secs: \{[^}]*maximum: (?P<seconds>\d+)", capture_program.group("body")
|
|
38
|
+
)
|
|
39
|
+
assert maximum is not None
|
|
40
|
+
assert int(maximum.group("seconds")) >= 612
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
def test_runtime_version_matches_distribution_metadata():
|
|
28
44
|
root = Path(__file__).parents[1]
|
|
29
45
|
pyproject = (root / "pyproject.toml").read_text(encoding="utf-8")
|
|
@@ -105,16 +105,14 @@ def test_capture_program_focuses_requested_child_before_trigger(tmp_path, monkey
|
|
|
105
105
|
lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
|
|
106
106
|
)
|
|
107
107
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
108
|
-
monkeypatch.setattr(runtime, "_wait_for_visible_process", lambda name, timeout: 77)
|
|
109
108
|
monkeypatch.setattr(
|
|
110
109
|
runtime,
|
|
111
|
-
"
|
|
112
|
-
lambda
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
lambda directory, before, timeout: [tmp_path / "capture_frame1.rdc"],
|
|
110
|
+
"_wait_for_triggered_capture",
|
|
111
|
+
lambda directory, before, name, ignored, timeout: (
|
|
112
|
+
observed.setdefault("pid", 77),
|
|
113
|
+
True,
|
|
114
|
+
[tmp_path / "capture_frame1.rdc"],
|
|
115
|
+
),
|
|
118
116
|
)
|
|
119
117
|
|
|
120
118
|
result = runtime.capture_program(
|
|
@@ -137,9 +135,11 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
|
|
|
137
135
|
lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
|
|
138
136
|
)
|
|
139
137
|
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
140
|
-
monkeypatch.setattr(
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
monkeypatch.setattr(
|
|
139
|
+
runtime,
|
|
140
|
+
"_wait_for_triggered_capture",
|
|
141
|
+
lambda directory, before, name, ignored, timeout: (None, False, []),
|
|
142
|
+
)
|
|
143
143
|
monkeypatch.setattr(
|
|
144
144
|
runtime,
|
|
145
145
|
"_visible_processes",
|
|
@@ -161,6 +161,81 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
|
|
|
161
161
|
assert "RenderDoc output: Launched as ID 123" in message
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def test_triggered_capture_retargets_replacement_process(tmp_path, monkeypatch):
|
|
165
|
+
capture = tmp_path / "capture_frame1.rdc"
|
|
166
|
+
process_snapshots = iter([[77], [77, 88]])
|
|
167
|
+
triggered = []
|
|
168
|
+
monkeypatch.setattr(
|
|
169
|
+
runtime,
|
|
170
|
+
"_visible_process_ids",
|
|
171
|
+
lambda _name: next(process_snapshots, [88]),
|
|
172
|
+
)
|
|
173
|
+
monkeypatch.setattr(
|
|
174
|
+
runtime,
|
|
175
|
+
"_trigger_capture_hotkey",
|
|
176
|
+
lambda process_id: not triggered.append(process_id),
|
|
177
|
+
)
|
|
178
|
+
monkeypatch.setattr(
|
|
179
|
+
runtime,
|
|
180
|
+
"_new_captures",
|
|
181
|
+
lambda _directory, _before: [capture] if triggered == [77, 88] else [],
|
|
182
|
+
)
|
|
183
|
+
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
184
|
+
|
|
185
|
+
target_pid, focused, captures = runtime._wait_for_triggered_capture(
|
|
186
|
+
tmp_path,
|
|
187
|
+
set(),
|
|
188
|
+
"game.exe",
|
|
189
|
+
set(),
|
|
190
|
+
1,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
assert triggered == [77, 88]
|
|
194
|
+
assert target_pid == 88
|
|
195
|
+
assert focused is True
|
|
196
|
+
assert captures == [capture]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
|
|
200
|
+
target = tmp_path / "launcher.exe"
|
|
201
|
+
target.touch()
|
|
202
|
+
capture = tmp_path / "capture_frame1.rdc"
|
|
203
|
+
observed = {}
|
|
204
|
+
monkeypatch.setattr(
|
|
205
|
+
runtime,
|
|
206
|
+
"_run",
|
|
207
|
+
lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
|
|
208
|
+
)
|
|
209
|
+
monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
|
|
210
|
+
monkeypatch.setattr(
|
|
211
|
+
runtime,
|
|
212
|
+
"_wait_for_triggered_capture",
|
|
213
|
+
lambda directory, before, name, ignored, timeout: (88, True, []),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def fake_capture_process(process_id, output_template, **kwargs):
|
|
217
|
+
observed.update(process_id=process_id, output_template=output_template, **kwargs)
|
|
218
|
+
return {
|
|
219
|
+
"captures": [str(capture)],
|
|
220
|
+
"focused_target_window": True,
|
|
221
|
+
"stdout": "injected",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
monkeypatch.setattr(runtime, "capture_process", fake_capture_process)
|
|
225
|
+
|
|
226
|
+
result = runtime.capture_program(
|
|
227
|
+
str(target),
|
|
228
|
+
str(tmp_path / "capture"),
|
|
229
|
+
hook_children=True,
|
|
230
|
+
trigger_after_secs=1,
|
|
231
|
+
trigger_process_name="game.exe",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
assert observed["process_id"] == 88
|
|
235
|
+
assert observed["trigger_after_secs"] == 0.25
|
|
236
|
+
assert result["captures"] == [str(capture.resolve())]
|
|
237
|
+
|
|
238
|
+
|
|
164
239
|
def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeypatch):
|
|
165
240
|
monkeypatch.setattr(
|
|
166
241
|
runtime,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|