agentlink-cli 0.1.0__py3-none-any.whl
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.
- agentlink_cli-0.1.0.dist-info/METADATA +136 -0
- agentlink_cli-0.1.0.dist-info/RECORD +55 -0
- agentlink_cli-0.1.0.dist-info/WHEEL +4 -0
- agentlink_cli-0.1.0.dist-info/entry_points.txt +3 -0
- connector/__init__.py +3 -0
- connector/acp/__init__.py +6 -0
- connector/acp/adapter.py +1221 -0
- connector/acp/config_options.py +175 -0
- connector/acp/discovery.py +385 -0
- connector/acp/manifest.py +110 -0
- connector/acp/manifests/__init__.py +1 -0
- connector/acp/manifests/codebuddy.json +37 -0
- connector/acp/manifests/cursor.json +39 -0
- connector/acp/manifests/gemini.json +33 -0
- connector/acp/manifests/grok_build.json +31 -0
- connector/acp/reducer.py +615 -0
- connector/acp/rpc.py +308 -0
- connector/adapter.py +39 -0
- connector/attachments.py +36 -0
- connector/capabilities.py +603 -0
- connector/claude/__init__.py +8 -0
- connector/claude/history_adapter.py +642 -0
- connector/claude/normalized.py +23 -0
- connector/claude/normalizers.py +97 -0
- connector/claude/path_utils.py +13 -0
- connector/claude/preferences.py +38 -0
- connector/claude/sdk_adapter.py +1376 -0
- connector/claude/timeline_identity.py +47 -0
- connector/claude/timeline_reducer.py +379 -0
- connector/claude/trust.py +69 -0
- connector/cli.py +280 -0
- connector/codex/__init__.py +3 -0
- connector/codex/adapter.py +1150 -0
- connector/codex/history.py +199 -0
- connector/codex/reducer.py +1309 -0
- connector/codex/rpc.py +261 -0
- connector/control.py +298 -0
- connector/json_rpc.py +143 -0
- connector/launch.py +310 -0
- connector/local/__init__.py +6 -0
- connector/local/common.py +118 -0
- connector/local/file_ops.py +144 -0
- connector/local/ops.py +92 -0
- connector/local/shell.py +225 -0
- connector/local/terminal.py +658 -0
- connector/local_ops.py +5 -0
- connector/local_runtime.py +139 -0
- connector/logging.py +50 -0
- connector/perf.py +89 -0
- connector/protocol.py +26 -0
- connector/registry.py +49 -0
- connector/runtime.py +1309 -0
- connector/sync_state.py +155 -0
- connector/time.py +7 -0
- connector/version.py +13 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import hashlib
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from connector.local.common import (
|
|
9
|
+
MAX_DIR_ENTRIES,
|
|
10
|
+
MAX_READ_TEXT_BYTES,
|
|
11
|
+
StaleFileError,
|
|
12
|
+
encoding,
|
|
13
|
+
nearest_existing_dir,
|
|
14
|
+
required_string,
|
|
15
|
+
required_text,
|
|
16
|
+
resolve_path,
|
|
17
|
+
workspace_root,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileOps:
|
|
22
|
+
def _windows_drive_entries(self) -> list[dict[str, Any]]:
|
|
23
|
+
if sys.platform != "win32":
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
entries: list[dict[str, Any]] = []
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
for code in range(ord("A"), ord("Z") + 1):
|
|
30
|
+
letter = chr(code)
|
|
31
|
+
path = Path(f"{letter}:\\")
|
|
32
|
+
try:
|
|
33
|
+
exists = path.exists()
|
|
34
|
+
except OSError:
|
|
35
|
+
exists = False
|
|
36
|
+
if not exists:
|
|
37
|
+
continue
|
|
38
|
+
entries.append({"name": f"{letter}:", "path": str(path), "type": "directory", "size": None})
|
|
39
|
+
return entries
|
|
40
|
+
|
|
41
|
+
async def prepare_download(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
root = workspace_root(params)
|
|
43
|
+
path = resolve_path(root, required_string(params, "path"))
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
raise FileNotFoundError(f"file not found: {path}")
|
|
46
|
+
data = path.read_bytes()
|
|
47
|
+
return {
|
|
48
|
+
"path": str(path),
|
|
49
|
+
"name": path.name,
|
|
50
|
+
"size": len(data),
|
|
51
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
52
|
+
"mediaType": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def prepared_download_path(self, params: dict[str, Any]) -> str:
|
|
56
|
+
root = workspace_root(params)
|
|
57
|
+
return str(resolve_path(root, required_string(params, "path")))
|
|
58
|
+
|
|
59
|
+
async def write_file(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
root = workspace_root(params)
|
|
61
|
+
path = resolve_path(root, required_string(params, "path"))
|
|
62
|
+
content_encoding = encoding(params)
|
|
63
|
+
content = required_text(params, "content")
|
|
64
|
+
if_match = params.get("ifMatch")
|
|
65
|
+
if not path.parent.is_dir():
|
|
66
|
+
raise FileNotFoundError(f"parent directory not found: {path.parent}")
|
|
67
|
+
if if_match is not None:
|
|
68
|
+
if not isinstance(if_match, str):
|
|
69
|
+
raise ValueError("ifMatch must be a sha256 hex string")
|
|
70
|
+
current_hash = ""
|
|
71
|
+
if path.is_file():
|
|
72
|
+
current_hash = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
73
|
+
elif if_match != "":
|
|
74
|
+
raise StaleFileError(
|
|
75
|
+
f"file disappeared (expected sha256={if_match})"
|
|
76
|
+
)
|
|
77
|
+
if if_match and current_hash != if_match:
|
|
78
|
+
raise StaleFileError(
|
|
79
|
+
f"file changed on disk (expected sha256={if_match}, found sha256={current_hash or 'none'})"
|
|
80
|
+
)
|
|
81
|
+
data = content.encode(content_encoding)
|
|
82
|
+
path.write_bytes(data)
|
|
83
|
+
return {
|
|
84
|
+
"path": str(path),
|
|
85
|
+
"encoding": "utf8",
|
|
86
|
+
"bytesWritten": len(data),
|
|
87
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async def read_text(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
root = workspace_root(params)
|
|
92
|
+
path = resolve_path(root, required_string(params, "path"))
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
raise FileNotFoundError(f"file not found: {path}")
|
|
95
|
+
raw_max = params.get("maxBytes", 1_048_576)
|
|
96
|
+
if not isinstance(raw_max, int):
|
|
97
|
+
raise ValueError("maxBytes must be an integer")
|
|
98
|
+
max_bytes = min(max(raw_max, 1), MAX_READ_TEXT_BYTES)
|
|
99
|
+
full = path.read_bytes()
|
|
100
|
+
clipped = full[:max_bytes]
|
|
101
|
+
truncated = len(full) > max_bytes
|
|
102
|
+
binary = b"\x00" in clipped
|
|
103
|
+
content = "" if binary else clipped.decode("utf-8", errors="replace")
|
|
104
|
+
return {
|
|
105
|
+
"path": str(path),
|
|
106
|
+
"name": path.name,
|
|
107
|
+
"size": len(full),
|
|
108
|
+
"sha256": hashlib.sha256(full).hexdigest(),
|
|
109
|
+
"encoding": "utf8",
|
|
110
|
+
"content": content,
|
|
111
|
+
"truncated": truncated,
|
|
112
|
+
"binary": binary,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async def read_dir(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
root = workspace_root(params)
|
|
117
|
+
raw_path = params.get("path")
|
|
118
|
+
if sys.platform == "win32" and raw_path == "":
|
|
119
|
+
return {"path": "", "entries": self._windows_drive_entries(), "truncated": False}
|
|
120
|
+
path = root if raw_path is None else resolve_path(root, required_string(params, "path"))
|
|
121
|
+
path = nearest_existing_dir(path, fallback=root)
|
|
122
|
+
|
|
123
|
+
entries: list[dict[str, Any]] = []
|
|
124
|
+
for child in sorted(path.iterdir(), key=lambda item: item.name):
|
|
125
|
+
if len(entries) >= MAX_DIR_ENTRIES:
|
|
126
|
+
break
|
|
127
|
+
try:
|
|
128
|
+
stat = child.stat()
|
|
129
|
+
except OSError:
|
|
130
|
+
stat = None
|
|
131
|
+
entries.append(
|
|
132
|
+
{
|
|
133
|
+
"name": child.name,
|
|
134
|
+
"path": str(child),
|
|
135
|
+
"type": "directory" if child.is_dir() else "file" if child.is_file() else "other",
|
|
136
|
+
"size": stat.st_size if stat is not None and child.is_file() else None,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"path": str(path),
|
|
142
|
+
"entries": entries,
|
|
143
|
+
"truncated": len(entries) >= MAX_DIR_ENTRIES,
|
|
144
|
+
}
|
connector/local/ops.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from connector.local.common import Notify
|
|
7
|
+
from connector.local.file_ops import FileOps
|
|
8
|
+
from connector.local.shell import ShellBackend, UnixShellBackend, WindowsShellBackend
|
|
9
|
+
from connector.local.terminal import TerminalBackend, default_terminal_backend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalOps:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
files: FileOps,
|
|
17
|
+
shell: ShellBackend,
|
|
18
|
+
terminal: TerminalBackend,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.files = files
|
|
21
|
+
self.shell = shell
|
|
22
|
+
self.terminal = terminal
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def notify(self) -> Notify | None:
|
|
26
|
+
return self.shell.notify
|
|
27
|
+
|
|
28
|
+
@notify.setter
|
|
29
|
+
def notify(self, value: Notify | None) -> None:
|
|
30
|
+
self.shell.notify = value
|
|
31
|
+
self.terminal.notify = value
|
|
32
|
+
|
|
33
|
+
async def prepare_download(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
return await self.files.prepare_download(params)
|
|
35
|
+
|
|
36
|
+
def prepared_download_path(self, params: dict[str, Any]) -> str:
|
|
37
|
+
return self.files.prepared_download_path(params)
|
|
38
|
+
|
|
39
|
+
async def write_file(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
40
|
+
return await self.files.write_file(params)
|
|
41
|
+
|
|
42
|
+
async def read_text(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
return await self.files.read_text(params)
|
|
44
|
+
|
|
45
|
+
async def read_dir(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
return await self.files.read_dir(params)
|
|
47
|
+
|
|
48
|
+
async def shell_exec(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
return await self.shell.exec(params)
|
|
50
|
+
|
|
51
|
+
async def shell_task_start(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
52
|
+
return await self.shell.task_start(params)
|
|
53
|
+
|
|
54
|
+
async def shell_task_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
return await self.shell.task_cancel(params)
|
|
56
|
+
|
|
57
|
+
async def terminal_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
return await self.terminal.create(params)
|
|
59
|
+
|
|
60
|
+
async def terminal_write(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
return await self.terminal.write(params)
|
|
62
|
+
|
|
63
|
+
async def terminal_resize(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
return await self.terminal.resize(params)
|
|
65
|
+
|
|
66
|
+
async def terminal_close(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
67
|
+
return await self.terminal.close(params)
|
|
68
|
+
|
|
69
|
+
async def terminal_rename(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
return await self.terminal.rename(params)
|
|
71
|
+
|
|
72
|
+
async def terminal_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
return await self.terminal.list(params)
|
|
74
|
+
|
|
75
|
+
async def terminal_release(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
return await self.terminal.release(params)
|
|
77
|
+
|
|
78
|
+
async def terminal_snapshot(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
79
|
+
return await self.terminal.snapshot(params)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def create_local_ops(
|
|
83
|
+
notify: Notify | None = None,
|
|
84
|
+
) -> LocalOps:
|
|
85
|
+
files = FileOps()
|
|
86
|
+
shell: ShellBackend
|
|
87
|
+
if sys.platform == "win32":
|
|
88
|
+
shell = WindowsShellBackend(notify=notify)
|
|
89
|
+
else:
|
|
90
|
+
shell = UnixShellBackend(notify=notify)
|
|
91
|
+
terminal = default_terminal_backend(notify=notify)
|
|
92
|
+
return LocalOps(files=files, shell=shell, terminal=terminal)
|
connector/local/shell.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from connector.local.common import (
|
|
12
|
+
Notify,
|
|
13
|
+
required_int,
|
|
14
|
+
required_string,
|
|
15
|
+
resolve_path,
|
|
16
|
+
shell_result,
|
|
17
|
+
workspace_root,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShellBackend:
|
|
22
|
+
def __init__(self, notify: Notify | None = None) -> None:
|
|
23
|
+
self.notify = notify
|
|
24
|
+
self._shell_tasks: dict[str, dict[str, Any]] = {}
|
|
25
|
+
|
|
26
|
+
async def exec(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
root = workspace_root(params)
|
|
28
|
+
cwd = resolve_path(root, required_string(params, "cwd"))
|
|
29
|
+
if not cwd.is_dir():
|
|
30
|
+
raise NotADirectoryError(f"cwd not found: {cwd}")
|
|
31
|
+
command = required_string(params, "command")
|
|
32
|
+
timeout_ms = required_int(params, "timeoutMs")
|
|
33
|
+
if timeout_ms <= 0:
|
|
34
|
+
raise ValueError("timeoutMs must be positive")
|
|
35
|
+
|
|
36
|
+
start = time.monotonic()
|
|
37
|
+
process = await self._create_process(cwd, command)
|
|
38
|
+
timed_out = False
|
|
39
|
+
try:
|
|
40
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout_ms / 1000)
|
|
41
|
+
except TimeoutError:
|
|
42
|
+
timed_out = True
|
|
43
|
+
await self._terminate_process(process)
|
|
44
|
+
stdout, stderr = await process.communicate()
|
|
45
|
+
|
|
46
|
+
return shell_result(cwd, command, process.returncode, timed_out, start, stdout, stderr)
|
|
47
|
+
|
|
48
|
+
async def task_start(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
root = workspace_root(params)
|
|
50
|
+
cwd = resolve_path(root, required_string(params, "cwd"))
|
|
51
|
+
if not cwd.is_dir():
|
|
52
|
+
raise NotADirectoryError(f"cwd not found: {cwd}")
|
|
53
|
+
task_id = required_string(params, "taskId")
|
|
54
|
+
session_id = required_string(params, "sessionId")
|
|
55
|
+
command = required_string(params, "command")
|
|
56
|
+
timeout_ms = required_int(params, "timeoutMs")
|
|
57
|
+
if timeout_ms <= 0:
|
|
58
|
+
raise ValueError("timeoutMs must be positive")
|
|
59
|
+
if task_id in self._shell_tasks:
|
|
60
|
+
raise ValueError(f"shell task already exists: {task_id}")
|
|
61
|
+
|
|
62
|
+
record: dict[str, Any] = {"process": None, "cancelled": False}
|
|
63
|
+
background = asyncio.create_task(
|
|
64
|
+
self._run_shell_task(
|
|
65
|
+
task_id=task_id,
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
cwd=cwd,
|
|
68
|
+
command=command,
|
|
69
|
+
timeout_ms=timeout_ms,
|
|
70
|
+
record=record,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
record["background"] = background
|
|
74
|
+
self._shell_tasks[task_id] = record
|
|
75
|
+
await self._notify("shell.task.started", {"taskId": task_id, "sessionId": session_id, "status": "running"})
|
|
76
|
+
return {"taskId": task_id, "sessionId": session_id, "status": "running"}
|
|
77
|
+
|
|
78
|
+
async def task_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
79
|
+
task_id = required_string(params, "taskId")
|
|
80
|
+
session_id = required_string(params, "sessionId")
|
|
81
|
+
record = self._shell_tasks.get(task_id)
|
|
82
|
+
if record is None:
|
|
83
|
+
return {"taskId": task_id, "sessionId": session_id, "cancelled": False}
|
|
84
|
+
record["cancelled"] = True
|
|
85
|
+
await self._terminate_process(record.get("process"))
|
|
86
|
+
background = record.get("background")
|
|
87
|
+
if isinstance(background, asyncio.Task):
|
|
88
|
+
background.cancel()
|
|
89
|
+
self._shell_tasks.pop(task_id, None)
|
|
90
|
+
await self._notify("shell.task.completed", {"taskId": task_id, "sessionId": session_id, "status": "cancelled"})
|
|
91
|
+
return {"taskId": task_id, "sessionId": session_id, "cancelled": True}
|
|
92
|
+
|
|
93
|
+
async def _run_shell_task(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
task_id: str,
|
|
97
|
+
session_id: str,
|
|
98
|
+
cwd: Path,
|
|
99
|
+
command: str,
|
|
100
|
+
timeout_ms: int,
|
|
101
|
+
record: dict[str, Any],
|
|
102
|
+
) -> None:
|
|
103
|
+
start = time.monotonic()
|
|
104
|
+
timed_out = False
|
|
105
|
+
stdout = b""
|
|
106
|
+
stderr = b""
|
|
107
|
+
process: asyncio.subprocess.Process | None = None
|
|
108
|
+
try:
|
|
109
|
+
process = await self._create_process(cwd, command)
|
|
110
|
+
record["process"] = process
|
|
111
|
+
try:
|
|
112
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout_ms / 1000)
|
|
113
|
+
except TimeoutError:
|
|
114
|
+
timed_out = True
|
|
115
|
+
await self._terminate_process(process)
|
|
116
|
+
stdout, stderr = await process.communicate()
|
|
117
|
+
result = shell_result(cwd, command, process.returncode, timed_out, start, stdout, stderr)
|
|
118
|
+
await self._notify(
|
|
119
|
+
"shell.task.completed",
|
|
120
|
+
{"taskId": task_id, "sessionId": session_id, "status": "completed", "result": result},
|
|
121
|
+
)
|
|
122
|
+
except asyncio.CancelledError:
|
|
123
|
+
if process is not None:
|
|
124
|
+
await self._terminate_process(process)
|
|
125
|
+
raise
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
await self._notify(
|
|
128
|
+
"shell.task.completed",
|
|
129
|
+
{
|
|
130
|
+
"taskId": task_id,
|
|
131
|
+
"sessionId": session_id,
|
|
132
|
+
"status": "failed",
|
|
133
|
+
"error": {"code": exc.__class__.__name__, "message": str(exc)},
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
finally:
|
|
137
|
+
self._shell_tasks.pop(task_id, None)
|
|
138
|
+
|
|
139
|
+
async def _create_process(self, cwd: Path, command: str) -> asyncio.subprocess.Process:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
async def _terminate_process(self, process: Any) -> None:
|
|
143
|
+
raise NotImplementedError
|
|
144
|
+
|
|
145
|
+
async def _notify(self, method: str, params: dict[str, Any]) -> None:
|
|
146
|
+
if self.notify is not None:
|
|
147
|
+
await self.notify(method, params)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class UnixShellBackend(ShellBackend):
|
|
151
|
+
async def _create_process(self, cwd: Path, command: str) -> asyncio.subprocess.Process:
|
|
152
|
+
return await asyncio.create_subprocess_shell(
|
|
153
|
+
command,
|
|
154
|
+
cwd=str(cwd),
|
|
155
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
156
|
+
stdout=asyncio.subprocess.PIPE,
|
|
157
|
+
stderr=asyncio.subprocess.PIPE,
|
|
158
|
+
start_new_session=True,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def _terminate_process(self, process: Any) -> None:
|
|
162
|
+
if process is None or getattr(process, "returncode", None) is not None:
|
|
163
|
+
return
|
|
164
|
+
pid = getattr(process, "pid", None)
|
|
165
|
+
if isinstance(pid, int):
|
|
166
|
+
try:
|
|
167
|
+
os.killpg(pid, signal.SIGTERM)
|
|
168
|
+
except ProcessLookupError:
|
|
169
|
+
return
|
|
170
|
+
except OSError:
|
|
171
|
+
process.terminate()
|
|
172
|
+
else:
|
|
173
|
+
process.terminate()
|
|
174
|
+
try:
|
|
175
|
+
await asyncio.wait_for(process.wait(), timeout=2)
|
|
176
|
+
except TimeoutError:
|
|
177
|
+
if isinstance(pid, int):
|
|
178
|
+
try:
|
|
179
|
+
os.killpg(pid, signal.SIGKILL)
|
|
180
|
+
except ProcessLookupError:
|
|
181
|
+
return
|
|
182
|
+
except OSError:
|
|
183
|
+
process.kill()
|
|
184
|
+
else:
|
|
185
|
+
process.kill()
|
|
186
|
+
await process.wait()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class WindowsShellBackend(ShellBackend):
|
|
190
|
+
async def _create_process(self, cwd: Path, command: str) -> asyncio.subprocess.Process:
|
|
191
|
+
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
192
|
+
return await asyncio.create_subprocess_shell(
|
|
193
|
+
command,
|
|
194
|
+
cwd=str(cwd),
|
|
195
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
196
|
+
stdout=asyncio.subprocess.PIPE,
|
|
197
|
+
stderr=asyncio.subprocess.PIPE,
|
|
198
|
+
creationflags=creationflags,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def _terminate_process(self, process: Any) -> None:
|
|
202
|
+
if process is None or getattr(process, "returncode", None) is not None:
|
|
203
|
+
return
|
|
204
|
+
pid = getattr(process, "pid", None)
|
|
205
|
+
if isinstance(pid, int):
|
|
206
|
+
try:
|
|
207
|
+
taskkill = await asyncio.create_subprocess_exec(
|
|
208
|
+
"taskkill",
|
|
209
|
+
"/T",
|
|
210
|
+
"/F",
|
|
211
|
+
"/PID",
|
|
212
|
+
str(pid),
|
|
213
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
214
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
215
|
+
)
|
|
216
|
+
await asyncio.wait_for(taskkill.wait(), timeout=5)
|
|
217
|
+
except Exception:
|
|
218
|
+
process.terminate()
|
|
219
|
+
else:
|
|
220
|
+
process.terminate()
|
|
221
|
+
try:
|
|
222
|
+
await asyncio.wait_for(process.wait(), timeout=5)
|
|
223
|
+
except TimeoutError:
|
|
224
|
+
process.kill()
|
|
225
|
+
await process.wait()
|