claude-dev-env 7.0.0 → 7.1.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/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from pr_verification.config.constants import LOCAL_CHECKS_PASSED_LABEL
|
|
8
|
+
from pr_verification.lock import SupervisorLock
|
|
9
|
+
from pr_verification.model import PullRequestCandidate, RepositorySettings
|
|
10
|
+
|
|
11
|
+
from .checkout import read_local_checkout as _read_local_checkout
|
|
12
|
+
from .config.constants import (
|
|
13
|
+
GIT_BASE_REF_TEMPLATE,
|
|
14
|
+
STATE_REASON_BASE_FETCH_FAILED,
|
|
15
|
+
STATE_REASON_CHECKOUT_MISMATCH,
|
|
16
|
+
STATE_REASON_CHILD_INCOMPLETE,
|
|
17
|
+
STATE_REASON_DIRTY_CHECKOUT,
|
|
18
|
+
STATE_REASON_GITHUB_UNAVAILABLE,
|
|
19
|
+
STATE_REASON_PUBLISH_FAILED,
|
|
20
|
+
STATE_REASON_PULL_REQUEST_CLOSED,
|
|
21
|
+
STATE_REASON_UNCHANGED,
|
|
22
|
+
STATE_STATUS_CLOSED,
|
|
23
|
+
STATE_STATUS_ERROR,
|
|
24
|
+
STATE_STATUS_OFFLINE,
|
|
25
|
+
STATE_STATUS_PASSED,
|
|
26
|
+
STATE_STATUS_WAITING,
|
|
27
|
+
)
|
|
28
|
+
from .execution import _child_produced_report, run_verification_child
|
|
29
|
+
from .git import _fetch_base
|
|
30
|
+
from .model import (
|
|
31
|
+
AdvisoryRegistration,
|
|
32
|
+
AdvisorySettings,
|
|
33
|
+
AdvisoryState,
|
|
34
|
+
ChildOutcome,
|
|
35
|
+
candidate_identity_matches_state,
|
|
36
|
+
)
|
|
37
|
+
from .publisher import Publication, _cache_status_for_publication, _publish_report
|
|
38
|
+
from .state import _utc_timestamp
|
|
39
|
+
from .state import read_state as _read_state
|
|
40
|
+
from .state import write_state as _write_state
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AdvisoryGitHub(Protocol):
|
|
44
|
+
def list_open_candidates(
|
|
45
|
+
self,
|
|
46
|
+
repository: RepositorySettings,
|
|
47
|
+
*,
|
|
48
|
+
should_require_merge_commit: bool = True,
|
|
49
|
+
) -> tuple[PullRequestCandidate, ...]: ...
|
|
50
|
+
|
|
51
|
+
def remove_label(
|
|
52
|
+
self, repository: RepositorySettings, pull_request_number: int, label: str
|
|
53
|
+
) -> None: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
AdvisoryPublisher = Callable[
|
|
57
|
+
[object, RepositorySettings, int, Path, Path, Path], Publication
|
|
58
|
+
]
|
|
59
|
+
ChildRunner = Callable[[AdvisoryRegistration, str, float], ChildOutcome]
|
|
60
|
+
AdvisoryGitHubFactory = Callable[[RepositorySettings], AdvisoryGitHub]
|
|
61
|
+
Clock = Callable[[], str]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AutomaticAdvisoryRunner:
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
settings: AdvisorySettings,
|
|
68
|
+
github: AdvisoryGitHub | None,
|
|
69
|
+
*,
|
|
70
|
+
child_runner: ChildRunner | None = None,
|
|
71
|
+
publisher: AdvisoryPublisher | None = None,
|
|
72
|
+
github_factory: AdvisoryGitHubFactory | None = None,
|
|
73
|
+
clock: Clock | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.settings = settings
|
|
76
|
+
self.github = github
|
|
77
|
+
self.child_runner = child_runner or run_verification_child
|
|
78
|
+
self.publisher = publisher or _publish_report
|
|
79
|
+
if github_factory is not None:
|
|
80
|
+
self.github_factory = github_factory
|
|
81
|
+
elif github is not None:
|
|
82
|
+
self.github_factory = lambda _repository: github
|
|
83
|
+
else:
|
|
84
|
+
self.github_factory = _github_is_unavailable
|
|
85
|
+
self.clock = clock or _utc_timestamp
|
|
86
|
+
|
|
87
|
+
def run_once(self, should_rerun: bool = False) -> tuple[AdvisoryState, ...]:
|
|
88
|
+
"""Run each registration once and return its persisted state.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
should_rerun: True to force a fresh run for every registration.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The persisted state of each registration, in settings order.
|
|
95
|
+
"""
|
|
96
|
+
return tuple(
|
|
97
|
+
self.run_registration(each_registration, should_rerun)
|
|
98
|
+
for each_registration in self.settings.registrations
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def run_registration(
|
|
102
|
+
self,
|
|
103
|
+
registration: AdvisoryRegistration,
|
|
104
|
+
should_rerun: bool,
|
|
105
|
+
) -> AdvisoryState:
|
|
106
|
+
with SupervisorLock(registration.repository_lock_root):
|
|
107
|
+
return self._run_registration_locked(registration, should_rerun)
|
|
108
|
+
|
|
109
|
+
def _run_registration_locked(
|
|
110
|
+
self,
|
|
111
|
+
registration: AdvisoryRegistration,
|
|
112
|
+
should_rerun: bool,
|
|
113
|
+
) -> AdvisoryState:
|
|
114
|
+
github_or_state = self._get_github(registration)
|
|
115
|
+
if isinstance(github_or_state, AdvisoryState):
|
|
116
|
+
return github_or_state
|
|
117
|
+
github = github_or_state
|
|
118
|
+
candidate_or_state = self._discover_candidate(registration, github)
|
|
119
|
+
if isinstance(candidate_or_state, AdvisoryState):
|
|
120
|
+
return candidate_or_state
|
|
121
|
+
maybe_previous_state = _read_state(registration.state_path)
|
|
122
|
+
maybe_invalidation_state = self._invalidate_changed_identity(
|
|
123
|
+
registration, candidate_or_state, maybe_previous_state, github
|
|
124
|
+
)
|
|
125
|
+
if maybe_invalidation_state is not None:
|
|
126
|
+
return maybe_invalidation_state
|
|
127
|
+
maybe_blocked_state = self._prepare_candidate(registration, candidate_or_state)
|
|
128
|
+
if maybe_blocked_state is not None:
|
|
129
|
+
return maybe_blocked_state
|
|
130
|
+
return self._run_ready_candidate(
|
|
131
|
+
registration,
|
|
132
|
+
candidate_or_state,
|
|
133
|
+
maybe_previous_state,
|
|
134
|
+
should_rerun,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _get_github(
|
|
138
|
+
self, registration: AdvisoryRegistration
|
|
139
|
+
) -> AdvisoryGitHub | AdvisoryState:
|
|
140
|
+
try:
|
|
141
|
+
return self.github_factory(registration.repository)
|
|
142
|
+
except (OSError, RuntimeError):
|
|
143
|
+
return self._save_state(
|
|
144
|
+
registration,
|
|
145
|
+
STATE_STATUS_OFFLINE,
|
|
146
|
+
STATE_REASON_GITHUB_UNAVAILABLE,
|
|
147
|
+
None,
|
|
148
|
+
None,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _discover_candidate(
|
|
152
|
+
self, registration: AdvisoryRegistration, github: AdvisoryGitHub
|
|
153
|
+
) -> PullRequestCandidate | AdvisoryState:
|
|
154
|
+
try:
|
|
155
|
+
maybe_candidate = self._find_open_candidate(registration, github)
|
|
156
|
+
except (OSError, RuntimeError):
|
|
157
|
+
return self._save_state(
|
|
158
|
+
registration,
|
|
159
|
+
STATE_STATUS_OFFLINE,
|
|
160
|
+
STATE_REASON_GITHUB_UNAVAILABLE,
|
|
161
|
+
None,
|
|
162
|
+
None,
|
|
163
|
+
)
|
|
164
|
+
if maybe_candidate is None:
|
|
165
|
+
if not self._remove_pass_label_safely(registration, github):
|
|
166
|
+
return self._save_state(
|
|
167
|
+
registration,
|
|
168
|
+
STATE_STATUS_ERROR,
|
|
169
|
+
STATE_REASON_PUBLISH_FAILED,
|
|
170
|
+
None,
|
|
171
|
+
None,
|
|
172
|
+
)
|
|
173
|
+
return self._save_state(
|
|
174
|
+
registration,
|
|
175
|
+
STATE_STATUS_CLOSED,
|
|
176
|
+
STATE_REASON_PULL_REQUEST_CLOSED,
|
|
177
|
+
None,
|
|
178
|
+
None,
|
|
179
|
+
)
|
|
180
|
+
return maybe_candidate
|
|
181
|
+
|
|
182
|
+
def _run_ready_candidate(
|
|
183
|
+
self,
|
|
184
|
+
registration: AdvisoryRegistration,
|
|
185
|
+
candidate: PullRequestCandidate,
|
|
186
|
+
maybe_previous_state: AdvisoryState | None,
|
|
187
|
+
should_rerun: bool,
|
|
188
|
+
) -> AdvisoryState:
|
|
189
|
+
if (
|
|
190
|
+
not should_rerun
|
|
191
|
+
and maybe_previous_state is not None
|
|
192
|
+
and candidate_identity_matches_state(candidate, maybe_previous_state)
|
|
193
|
+
):
|
|
194
|
+
return self._save_state(
|
|
195
|
+
registration,
|
|
196
|
+
maybe_previous_state.status,
|
|
197
|
+
STATE_REASON_UNCHANGED,
|
|
198
|
+
candidate,
|
|
199
|
+
maybe_previous_state.last_run,
|
|
200
|
+
)
|
|
201
|
+
return self._execute_and_publish(registration, candidate)
|
|
202
|
+
|
|
203
|
+
def _invalidate_changed_identity(
|
|
204
|
+
self,
|
|
205
|
+
registration: AdvisoryRegistration,
|
|
206
|
+
candidate: PullRequestCandidate,
|
|
207
|
+
maybe_previous_state: AdvisoryState | None,
|
|
208
|
+
github: AdvisoryGitHub,
|
|
209
|
+
) -> AdvisoryState | None:
|
|
210
|
+
if (
|
|
211
|
+
maybe_previous_state is not None
|
|
212
|
+
and maybe_previous_state.status == STATE_STATUS_PASSED
|
|
213
|
+
and candidate_identity_matches_state(candidate, maybe_previous_state)
|
|
214
|
+
):
|
|
215
|
+
return None
|
|
216
|
+
if self._remove_pass_label_safely(registration, github):
|
|
217
|
+
return None
|
|
218
|
+
return self._save_state(
|
|
219
|
+
registration,
|
|
220
|
+
STATE_STATUS_ERROR,
|
|
221
|
+
STATE_REASON_PUBLISH_FAILED,
|
|
222
|
+
candidate,
|
|
223
|
+
None,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _execute_and_publish(
|
|
227
|
+
self,
|
|
228
|
+
registration: AdvisoryRegistration,
|
|
229
|
+
candidate: PullRequestCandidate,
|
|
230
|
+
) -> AdvisoryState:
|
|
231
|
+
maybe_child_state = self._run_child(registration, candidate)
|
|
232
|
+
if maybe_child_state is not None:
|
|
233
|
+
return maybe_child_state
|
|
234
|
+
try:
|
|
235
|
+
fresh_github = self.github_factory(registration.repository)
|
|
236
|
+
except (OSError, RuntimeError):
|
|
237
|
+
return self._save_state(
|
|
238
|
+
registration,
|
|
239
|
+
STATE_STATUS_OFFLINE,
|
|
240
|
+
STATE_REASON_GITHUB_UNAVAILABLE,
|
|
241
|
+
candidate,
|
|
242
|
+
self.clock(),
|
|
243
|
+
)
|
|
244
|
+
return self._publish_and_save(
|
|
245
|
+
registration,
|
|
246
|
+
candidate,
|
|
247
|
+
fresh_github,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _run_child(
|
|
251
|
+
self,
|
|
252
|
+
registration: AdvisoryRegistration,
|
|
253
|
+
candidate: PullRequestCandidate,
|
|
254
|
+
) -> AdvisoryState | None:
|
|
255
|
+
child_outcome = self.child_runner(
|
|
256
|
+
registration,
|
|
257
|
+
GIT_BASE_REF_TEMPLATE.format(pull_request=registration.pull_request_number),
|
|
258
|
+
self.settings.child_timeout_seconds,
|
|
259
|
+
)
|
|
260
|
+
if _child_produced_report(child_outcome, registration):
|
|
261
|
+
return None
|
|
262
|
+
return self._save_state(
|
|
263
|
+
registration,
|
|
264
|
+
STATE_STATUS_ERROR,
|
|
265
|
+
STATE_REASON_CHILD_INCOMPLETE,
|
|
266
|
+
candidate,
|
|
267
|
+
self.clock(),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _prepare_candidate(
|
|
271
|
+
self,
|
|
272
|
+
registration: AdvisoryRegistration,
|
|
273
|
+
candidate: PullRequestCandidate,
|
|
274
|
+
) -> AdvisoryState | None:
|
|
275
|
+
if not _fetch_base(registration, candidate):
|
|
276
|
+
return self._save_state(
|
|
277
|
+
registration,
|
|
278
|
+
STATE_STATUS_OFFLINE,
|
|
279
|
+
STATE_REASON_BASE_FETCH_FAILED,
|
|
280
|
+
candidate,
|
|
281
|
+
None,
|
|
282
|
+
)
|
|
283
|
+
return self._check_local_checkout(registration, candidate)
|
|
284
|
+
|
|
285
|
+
def _check_local_checkout(
|
|
286
|
+
self,
|
|
287
|
+
registration: AdvisoryRegistration,
|
|
288
|
+
candidate: PullRequestCandidate,
|
|
289
|
+
) -> AdvisoryState | None:
|
|
290
|
+
local_checkout = _read_local_checkout(registration.checkout_path)
|
|
291
|
+
if not local_checkout.is_clean:
|
|
292
|
+
return self._save_state(
|
|
293
|
+
registration,
|
|
294
|
+
STATE_STATUS_WAITING,
|
|
295
|
+
STATE_REASON_DIRTY_CHECKOUT,
|
|
296
|
+
candidate,
|
|
297
|
+
None,
|
|
298
|
+
)
|
|
299
|
+
if local_checkout.head_sha != candidate.head_sha:
|
|
300
|
+
return self._save_state(
|
|
301
|
+
registration,
|
|
302
|
+
STATE_STATUS_WAITING,
|
|
303
|
+
STATE_REASON_CHECKOUT_MISMATCH,
|
|
304
|
+
candidate,
|
|
305
|
+
None,
|
|
306
|
+
)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def _remove_pass_label_safely(
|
|
310
|
+
self, registration: AdvisoryRegistration, github: AdvisoryGitHub
|
|
311
|
+
) -> bool:
|
|
312
|
+
try:
|
|
313
|
+
self._remove_pass_label(registration, github)
|
|
314
|
+
except (OSError, RuntimeError):
|
|
315
|
+
return False
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def _find_open_candidate(
|
|
319
|
+
self, registration: AdvisoryRegistration, github: AdvisoryGitHub
|
|
320
|
+
) -> PullRequestCandidate | None:
|
|
321
|
+
all_candidates = github.list_open_candidates(
|
|
322
|
+
registration.repository,
|
|
323
|
+
should_require_merge_commit=False,
|
|
324
|
+
)
|
|
325
|
+
return next(
|
|
326
|
+
(
|
|
327
|
+
each_candidate
|
|
328
|
+
for each_candidate in all_candidates
|
|
329
|
+
if each_candidate.pull_request_number
|
|
330
|
+
== registration.pull_request_number
|
|
331
|
+
),
|
|
332
|
+
None,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def _remove_pass_label(
|
|
336
|
+
self, registration: AdvisoryRegistration, github: AdvisoryGitHub
|
|
337
|
+
) -> None:
|
|
338
|
+
github.remove_label(
|
|
339
|
+
registration.repository,
|
|
340
|
+
registration.pull_request_number,
|
|
341
|
+
LOCAL_CHECKS_PASSED_LABEL,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _publish_and_save(
|
|
345
|
+
self,
|
|
346
|
+
registration: AdvisoryRegistration,
|
|
347
|
+
candidate: PullRequestCandidate,
|
|
348
|
+
github: AdvisoryGitHub,
|
|
349
|
+
) -> AdvisoryState:
|
|
350
|
+
try:
|
|
351
|
+
publication = self.publisher(
|
|
352
|
+
github,
|
|
353
|
+
registration.repository,
|
|
354
|
+
registration.pull_request_number,
|
|
355
|
+
registration.checkout_path,
|
|
356
|
+
registration.selected_manifest_path,
|
|
357
|
+
registration.report_path,
|
|
358
|
+
)
|
|
359
|
+
except (OSError, RuntimeError):
|
|
360
|
+
return self._save_state(
|
|
361
|
+
registration,
|
|
362
|
+
STATE_STATUS_ERROR,
|
|
363
|
+
STATE_REASON_PUBLISH_FAILED,
|
|
364
|
+
candidate,
|
|
365
|
+
self.clock(),
|
|
366
|
+
)
|
|
367
|
+
return self._save_state(
|
|
368
|
+
registration,
|
|
369
|
+
_cache_status_for_publication(publication.status),
|
|
370
|
+
publication.description,
|
|
371
|
+
candidate,
|
|
372
|
+
self.clock(),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _save_state(
|
|
376
|
+
self,
|
|
377
|
+
registration: AdvisoryRegistration,
|
|
378
|
+
status: str,
|
|
379
|
+
reason: str,
|
|
380
|
+
maybe_candidate: PullRequestCandidate | None,
|
|
381
|
+
maybe_last_run: str | None,
|
|
382
|
+
) -> AdvisoryState:
|
|
383
|
+
state = AdvisoryState(
|
|
384
|
+
registration.repository.slug,
|
|
385
|
+
registration.pull_request_number,
|
|
386
|
+
status,
|
|
387
|
+
reason,
|
|
388
|
+
maybe_candidate.head_sha if maybe_candidate else None,
|
|
389
|
+
maybe_candidate.base_sha if maybe_candidate else None,
|
|
390
|
+
self.clock(),
|
|
391
|
+
maybe_last_run,
|
|
392
|
+
str(registration.report_path),
|
|
393
|
+
)
|
|
394
|
+
_write_state(registration.state_path, state)
|
|
395
|
+
return state
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _github_is_unavailable(_repository: RepositorySettings) -> AdvisoryGitHub:
|
|
399
|
+
raise RuntimeError("GitHub client factory is unavailable")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .config.constants import (
|
|
10
|
+
JSON_INDENT_SPACES,
|
|
11
|
+
REPORT_NEWLINE,
|
|
12
|
+
STATE_BASE_SHA_KEY,
|
|
13
|
+
STATE_FILE_SUFFIX,
|
|
14
|
+
STATE_HEAD_SHA_KEY,
|
|
15
|
+
STATE_LAST_POLL_KEY,
|
|
16
|
+
STATE_LAST_RUN_KEY,
|
|
17
|
+
STATE_PULL_REQUEST_KEY,
|
|
18
|
+
STATE_REASON_KEY,
|
|
19
|
+
STATE_REPORT_KEY,
|
|
20
|
+
STATE_REPOSITORY_KEY,
|
|
21
|
+
STATE_STATUS_KEY,
|
|
22
|
+
STATE_VERSION,
|
|
23
|
+
STATE_VERSION_KEY,
|
|
24
|
+
UTF8_ENCODING,
|
|
25
|
+
)
|
|
26
|
+
from .model import AdvisoryState
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_state(state_path: Path) -> AdvisoryState | None:
|
|
30
|
+
"""Read one persisted advisory state when its shape is valid.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
state_path: JSON state file to read.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Parsed advisory state, or None when the file is absent or invalid.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
parsed_state = json.loads(state_path.read_text(encoding=UTF8_ENCODING))
|
|
40
|
+
except (OSError, json.JSONDecodeError):
|
|
41
|
+
return None
|
|
42
|
+
if not isinstance(parsed_state, Mapping):
|
|
43
|
+
return None
|
|
44
|
+
return _state_from_mapping(parsed_state)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _state_from_mapping(
|
|
48
|
+
all_state_fields: Mapping[str, object],
|
|
49
|
+
) -> AdvisoryState | None:
|
|
50
|
+
if all_state_fields.get(STATE_VERSION_KEY) != STATE_VERSION:
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
repository = _require_state_text(all_state_fields.get(STATE_REPOSITORY_KEY))
|
|
54
|
+
status = _require_state_text(all_state_fields.get(STATE_STATUS_KEY))
|
|
55
|
+
reason = _require_state_text(all_state_fields.get(STATE_REASON_KEY))
|
|
56
|
+
last_poll = _require_state_text(all_state_fields.get(STATE_LAST_POLL_KEY))
|
|
57
|
+
report_path = _require_state_text(all_state_fields.get(STATE_REPORT_KEY))
|
|
58
|
+
except TypeError:
|
|
59
|
+
return None
|
|
60
|
+
pull_request_number = all_state_fields.get(STATE_PULL_REQUEST_KEY)
|
|
61
|
+
if not isinstance(pull_request_number, int):
|
|
62
|
+
return None
|
|
63
|
+
return AdvisoryState(
|
|
64
|
+
repository,
|
|
65
|
+
pull_request_number,
|
|
66
|
+
status,
|
|
67
|
+
reason,
|
|
68
|
+
_optional_text(all_state_fields.get(STATE_HEAD_SHA_KEY)),
|
|
69
|
+
_optional_text(all_state_fields.get(STATE_BASE_SHA_KEY)),
|
|
70
|
+
last_poll,
|
|
71
|
+
_optional_text(all_state_fields.get(STATE_LAST_RUN_KEY)),
|
|
72
|
+
report_path,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require_state_text(state_field: object) -> str:
|
|
77
|
+
if not isinstance(state_field, str):
|
|
78
|
+
raise TypeError("state field must be text")
|
|
79
|
+
return state_field
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _optional_text(state_field: object) -> str | None:
|
|
83
|
+
return state_field if isinstance(state_field, str) else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_state(state_path: Path, state: AdvisoryState) -> None:
|
|
87
|
+
"""Atomically persist one advisory state.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
state_path: Destination JSON state file.
|
|
91
|
+
state: State record to persist.
|
|
92
|
+
"""
|
|
93
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
state_mapping = {
|
|
95
|
+
STATE_VERSION_KEY: STATE_VERSION,
|
|
96
|
+
STATE_REPOSITORY_KEY: state.repository,
|
|
97
|
+
STATE_PULL_REQUEST_KEY: state.pull_request_number,
|
|
98
|
+
STATE_STATUS_KEY: state.status,
|
|
99
|
+
STATE_REASON_KEY: state.reason,
|
|
100
|
+
STATE_HEAD_SHA_KEY: state.head_sha,
|
|
101
|
+
STATE_BASE_SHA_KEY: state.base_sha,
|
|
102
|
+
STATE_LAST_POLL_KEY: state.last_poll,
|
|
103
|
+
STATE_LAST_RUN_KEY: state.last_run,
|
|
104
|
+
STATE_REPORT_KEY: state.report_path,
|
|
105
|
+
}
|
|
106
|
+
temporary_state_path = state_path.with_name(state_path.name + STATE_FILE_SUFFIX)
|
|
107
|
+
temporary_state_path.write_text(
|
|
108
|
+
json.dumps(state_mapping, indent=JSON_INDENT_SPACES, sort_keys=True)
|
|
109
|
+
+ REPORT_NEWLINE,
|
|
110
|
+
encoding=UTF8_ENCODING,
|
|
111
|
+
)
|
|
112
|
+
os.replace(temporary_state_path, state_path)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _utc_timestamp() -> str:
|
|
116
|
+
return datetime.now(UTC).isoformat()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Windows creation flags that hide the advisory poller and its children.
|
|
2
|
+
|
|
3
|
+
A poller with no console makes each git call and each check open a fresh
|
|
4
|
+
console window. A hidden console is inherited by descendants, so those
|
|
5
|
+
children stay off the screen.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .config.constants import (
|
|
14
|
+
CREATE_NEW_PROCESS_GROUP_ATTRIBUTE,
|
|
15
|
+
CREATE_NO_WINDOW_ATTRIBUTE,
|
|
16
|
+
WINDOWS_PLATFORM,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def hidden_window_creation_flags() -> int:
|
|
21
|
+
"""Return the flag that runs a Windows child without a visible console.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The hidden-console flag on Windows, zero elsewhere.
|
|
25
|
+
"""
|
|
26
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
27
|
+
return 0
|
|
28
|
+
return getattr(subprocess, CREATE_NO_WINDOW_ATTRIBUTE, 0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detached_poller_creation_flags() -> int:
|
|
32
|
+
"""Return the flags for the long-running poller on Windows.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A new process group plus a hidden console on Windows, zero elsewhere.
|
|
36
|
+
"""
|
|
37
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
38
|
+
return 0
|
|
39
|
+
return getattr(subprocess, CREATE_NEW_PROCESS_GROUP_ATTRIBUTE, 0) | getattr(
|
|
40
|
+
subprocess, CREATE_NO_WINDOW_ATTRIBUTE, 0
|
|
41
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
|
|
5
|
+
from .config.constants import (
|
|
6
|
+
WINDOWS_EXTENDED_LIMIT_INFORMATION_CLASS,
|
|
7
|
+
WINDOWS_JOB_KILL_ON_CLOSE_LIMIT,
|
|
8
|
+
WINDOWS_PROCESS_SET_QUOTA_ACCESS,
|
|
9
|
+
WINDOWS_PROCESS_TERMINATE_ACCESS,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _JobObjectBasicLimitInformation(ctypes.Structure):
|
|
14
|
+
_fields_ = [
|
|
15
|
+
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
|
16
|
+
("PerJobUserTimeLimit", ctypes.c_longlong),
|
|
17
|
+
("LimitFlags", ctypes.c_uint32),
|
|
18
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
19
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
20
|
+
("ActiveProcessLimit", ctypes.c_uint32),
|
|
21
|
+
("Affinity", ctypes.c_size_t),
|
|
22
|
+
("PriorityClass", ctypes.c_uint32),
|
|
23
|
+
("SchedulingClass", ctypes.c_uint32),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _IoCounters(ctypes.Structure):
|
|
28
|
+
_fields_ = [
|
|
29
|
+
("ReadOperationCount", ctypes.c_ulonglong),
|
|
30
|
+
("WriteOperationCount", ctypes.c_ulonglong),
|
|
31
|
+
("OtherOperationCount", ctypes.c_ulonglong),
|
|
32
|
+
("ReadTransferCount", ctypes.c_ulonglong),
|
|
33
|
+
("WriteTransferCount", ctypes.c_ulonglong),
|
|
34
|
+
("OtherTransferCount", ctypes.c_ulonglong),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _JobObjectExtendedLimitInformation(ctypes.Structure):
|
|
39
|
+
_fields_ = [
|
|
40
|
+
("BasicLimitInformation", _JobObjectBasicLimitInformation),
|
|
41
|
+
("IoInfo", _IoCounters),
|
|
42
|
+
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
43
|
+
("JobMemoryLimit", ctypes.c_size_t),
|
|
44
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
45
|
+
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _create_kill_on_close_job() -> int | None:
|
|
50
|
+
create_job = ctypes.windll.kernel32.CreateJobObjectW
|
|
51
|
+
create_job.argtypes = (ctypes.c_void_p, ctypes.c_wchar_p)
|
|
52
|
+
create_job.restype = ctypes.c_void_p
|
|
53
|
+
maybe_job_handle = create_job(None, None)
|
|
54
|
+
if maybe_job_handle is None:
|
|
55
|
+
return None
|
|
56
|
+
job_handle = int(maybe_job_handle)
|
|
57
|
+
if _configure_kill_on_close(job_handle):
|
|
58
|
+
return job_handle
|
|
59
|
+
_close_handle(job_handle)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _configure_kill_on_close(job_handle: int) -> bool:
|
|
64
|
+
job_limits = _JobObjectExtendedLimitInformation()
|
|
65
|
+
job_limits.BasicLimitInformation.LimitFlags = WINDOWS_JOB_KILL_ON_CLOSE_LIMIT
|
|
66
|
+
set_job_information = ctypes.windll.kernel32.SetInformationJobObject
|
|
67
|
+
set_job_information.argtypes = (
|
|
68
|
+
ctypes.c_void_p,
|
|
69
|
+
ctypes.c_int,
|
|
70
|
+
ctypes.c_void_p,
|
|
71
|
+
ctypes.c_uint32,
|
|
72
|
+
)
|
|
73
|
+
set_job_information.restype = ctypes.c_int
|
|
74
|
+
return bool(
|
|
75
|
+
set_job_information(
|
|
76
|
+
ctypes.c_void_p(job_handle),
|
|
77
|
+
WINDOWS_EXTENDED_LIMIT_INFORMATION_CLASS,
|
|
78
|
+
ctypes.byref(job_limits),
|
|
79
|
+
ctypes.sizeof(job_limits),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _assign_process(job_handle: int, process_identifier: int) -> bool:
|
|
85
|
+
process_access = WINDOWS_PROCESS_SET_QUOTA_ACCESS | WINDOWS_PROCESS_TERMINATE_ACCESS
|
|
86
|
+
open_process = ctypes.windll.kernel32.OpenProcess
|
|
87
|
+
open_process.argtypes = (ctypes.c_uint32, ctypes.c_int, ctypes.c_uint32)
|
|
88
|
+
open_process.restype = ctypes.c_void_p
|
|
89
|
+
maybe_process_handle = open_process(process_access, 0, process_identifier)
|
|
90
|
+
if maybe_process_handle is None:
|
|
91
|
+
return False
|
|
92
|
+
process_handle = int(maybe_process_handle)
|
|
93
|
+
try:
|
|
94
|
+
return _assign_process_handle(job_handle, process_handle)
|
|
95
|
+
finally:
|
|
96
|
+
_close_handle(process_handle)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _assign_process_handle(job_handle: int, process_handle: int) -> bool:
|
|
100
|
+
assign_process_to_job = ctypes.windll.kernel32.AssignProcessToJobObject
|
|
101
|
+
assign_process_to_job.argtypes = (ctypes.c_void_p, ctypes.c_void_p)
|
|
102
|
+
assign_process_to_job.restype = ctypes.c_int
|
|
103
|
+
return bool(
|
|
104
|
+
assign_process_to_job(
|
|
105
|
+
ctypes.c_void_p(job_handle),
|
|
106
|
+
ctypes.c_void_p(process_handle),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _terminate_job(job_handle: int) -> None:
|
|
112
|
+
terminate_job_object = ctypes.windll.kernel32.TerminateJobObject
|
|
113
|
+
terminate_job_object.argtypes = (ctypes.c_void_p, ctypes.c_uint32)
|
|
114
|
+
terminate_job_object.restype = ctypes.c_int
|
|
115
|
+
terminate_job_object(ctypes.c_void_p(job_handle), 1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _close_handle(job_handle: int) -> None:
|
|
119
|
+
close_windows_handle = ctypes.windll.kernel32.CloseHandle
|
|
120
|
+
close_windows_handle.argtypes = (ctypes.c_void_p,)
|
|
121
|
+
close_windows_handle.restype = ctypes.c_int
|
|
122
|
+
close_windows_handle(ctypes.c_void_p(job_handle))
|