command-gate 0.2.4__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.
- cgate/__init__.py +26 -0
- cgate/__main__.py +8 -0
- cgate/_version.py +24 -0
- cgate/cli/__init__.py +1 -0
- cgate/cli/_console.py +17 -0
- cgate/cli/connections.py +212 -0
- cgate/cli/history.py +191 -0
- cgate/cli/install.py +182 -0
- cgate/cli/main.py +115 -0
- cgate/cli/mcp.py +197 -0
- cgate/cli/uninstall.py +403 -0
- cgate/cli/update.py +538 -0
- cgate/cli/watch.py +20 -0
- cgate/connections/__init__.py +1 -0
- cgate/connections/auth.py +93 -0
- cgate/connections/detect.py +78 -0
- cgate/connections/store.py +88 -0
- cgate/core/__init__.py +1 -0
- cgate/core/path_env.py +218 -0
- cgate/core/paths.py +35 -0
- cgate/core/update_log.py +36 -0
- cgate/db/__init__.py +1 -0
- cgate/db/batches.py +111 -0
- cgate/db/commands.py +191 -0
- cgate/db/connection.py +104 -0
- cgate/db/mode.py +74 -0
- cgate/db/rows.py +99 -0
- cgate/db/schema.py +54 -0
- cgate/db/server_settings.py +105 -0
- cgate/db/types.py +77 -0
- cgate/executor/__init__.py +7 -0
- cgate/executor/base.py +71 -0
- cgate/executor/selector.py +61 -0
- cgate/executor/ssh.py +157 -0
- cgate/executor/winrm.py +129 -0
- cgate/helper/__init__.py +10 -0
- cgate/helper/__main__.py +112 -0
- cgate/helper/waiter.py +123 -0
- cgate/mcp_installer.py +161 -0
- cgate/mcp_server/__init__.py +6 -0
- cgate/mcp_server/__main__.py +6 -0
- cgate/mcp_server/auto_resolution.py +80 -0
- cgate/mcp_server/server.py +271 -0
- cgate/mcp_server/tools.py +351 -0
- cgate/risk.py +129 -0
- cgate/update.py +713 -0
- cgate/watch/__init__.py +7 -0
- cgate/watch/app.py +560 -0
- cgate/watch/approval.py +237 -0
- cgate/watch/command_detail_modal.py +68 -0
- cgate/watch/history_modal.py +242 -0
- cgate/watch/mode_modal.py +110 -0
- cgate/watch/queue.py +106 -0
- cgate/watch/render.py +156 -0
- cgate/watch/server_settings_modal.py +179 -0
- cgate/watch/session.py +40 -0
- cgate/watch/theme.py +32 -0
- cgate/watch/widgets.py +35 -0
- command_gate-0.2.4.dist-info/METADATA +204 -0
- command_gate-0.2.4.dist-info/RECORD +63 -0
- command_gate-0.2.4.dist-info/WHEEL +4 -0
- command_gate-0.2.4.dist-info/entry_points.txt +2 -0
- command_gate-0.2.4.dist-info/licenses/LICENSE +21 -0
cgate/cli/uninstall.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Remove cgate from this machine: binary, data, MCP registrations, or all."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from cgate.connections.auth import remove_credential
|
|
17
|
+
from cgate.connections.store import ConnectionsRepo
|
|
18
|
+
from cgate.core.paths import data_dir, db_path
|
|
19
|
+
from cgate.core.update_log import append_log
|
|
20
|
+
from cgate.db.connection import Database, init_database
|
|
21
|
+
from cgate.db.types import Connection
|
|
22
|
+
from cgate.mcp_installer import ClientInstall, detect_clients, is_registered, unregister
|
|
23
|
+
from cgate.update import (
|
|
24
|
+
current_binary_path,
|
|
25
|
+
ensure_helper_binary,
|
|
26
|
+
find_blocking_processes,
|
|
27
|
+
find_mcp_serving_pids,
|
|
28
|
+
helper_binary_path,
|
|
29
|
+
kill_process,
|
|
30
|
+
spawn_helper,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Brief delay after killing a blocking process so Windows releases the file
|
|
34
|
+
# lock before we retry the delete. Same value `update apply` uses for the
|
|
35
|
+
# analogous kill-and-retry swap (cli/update.py).
|
|
36
|
+
_KILL_SETTLE_SECONDS: float = 1.0
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def uninstall_cmd(
|
|
42
|
+
*,
|
|
43
|
+
binary: Annotated[
|
|
44
|
+
bool,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--binary",
|
|
47
|
+
help="Only remove the cgate binary (skips data and MCP cleanup).",
|
|
48
|
+
),
|
|
49
|
+
] = False,
|
|
50
|
+
data: Annotated[
|
|
51
|
+
bool,
|
|
52
|
+
typer.Option(
|
|
53
|
+
"--data",
|
|
54
|
+
help="Only remove the data directory (DB + OS keyring entries).",
|
|
55
|
+
),
|
|
56
|
+
] = False,
|
|
57
|
+
mcp: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
typer.Option(
|
|
60
|
+
"--mcp",
|
|
61
|
+
help="Only unregister cgate from detected IA clients.",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
64
|
+
yes: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--yes",
|
|
68
|
+
"-y",
|
|
69
|
+
help="Skip every confirmation prompt.",
|
|
70
|
+
),
|
|
71
|
+
] = False,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Remove cgate from this machine.
|
|
74
|
+
|
|
75
|
+
Without any scope flag, removes everything. Combine flags to limit
|
|
76
|
+
scope (e.g. `--data --yes` for an unattended reset). Without --yes,
|
|
77
|
+
each destructive step prompts individually so partial failures leave
|
|
78
|
+
the rest of the install recoverable.
|
|
79
|
+
"""
|
|
80
|
+
do_all = not (binary or data or mcp)
|
|
81
|
+
targets = {
|
|
82
|
+
"binary": binary or do_all,
|
|
83
|
+
"data": data or do_all,
|
|
84
|
+
"mcp": mcp or do_all,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
binary_path = current_binary_path()
|
|
88
|
+
data_path = data_dir()
|
|
89
|
+
mcp_clients: list[ClientInstall] = (
|
|
90
|
+
[c for c in detect_clients() if is_registered(c)] if targets["mcp"] else []
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Each target is only actionable if the user asked for it AND there is
|
|
94
|
+
# something to act on. Without this distinction `cgate uninstall` in a
|
|
95
|
+
# dev environment would silently print three "nothing here" lines
|
|
96
|
+
# instead of one concise "Nothing to do".
|
|
97
|
+
binary_actionable = (
|
|
98
|
+
targets["binary"] and binary_path is not None and binary_path.exists()
|
|
99
|
+
)
|
|
100
|
+
data_actionable = targets["data"] and data_path.exists()
|
|
101
|
+
mcp_actionable = targets["mcp"] and bool(mcp_clients)
|
|
102
|
+
|
|
103
|
+
console.print("[bold]Will remove:[/bold]")
|
|
104
|
+
if targets["mcp"]:
|
|
105
|
+
if mcp_actionable:
|
|
106
|
+
console.print(f" MCP: unregister from {len(mcp_clients)} client(s):")
|
|
107
|
+
for c in mcp_clients:
|
|
108
|
+
console.print(f" - {c.label}")
|
|
109
|
+
else:
|
|
110
|
+
console.print(" MCP: [dim](no clients currently registered)[/dim]")
|
|
111
|
+
if targets["data"]:
|
|
112
|
+
if data_actionable:
|
|
113
|
+
console.print(f" Data: [bold]{data_path}[/bold]")
|
|
114
|
+
else:
|
|
115
|
+
console.print(f" Data: [dim]{data_path} (does not exist)[/dim]")
|
|
116
|
+
if targets["binary"]:
|
|
117
|
+
if binary_path and binary_path.exists():
|
|
118
|
+
console.print(f" Binary: [bold]{binary_path}[/bold]")
|
|
119
|
+
elif binary_path:
|
|
120
|
+
console.print(f" Binary: [dim]{binary_path} (already gone)[/dim]")
|
|
121
|
+
else:
|
|
122
|
+
console.print(
|
|
123
|
+
" Binary: [dim]not running from a frozen PyInstaller binary[/dim]"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Order: MCP first (external state, easy to redo), then data (local state),
|
|
127
|
+
# then binary last (the running program itself). Each helper handles its
|
|
128
|
+
# own "not actionable" state (no clients, missing dir, dev env) so the
|
|
129
|
+
# output stays informative even when one or more scopes are no-ops.
|
|
130
|
+
mcp_ok = True
|
|
131
|
+
data_ok = True
|
|
132
|
+
binary_status = "not_applicable"
|
|
133
|
+
if targets["mcp"]:
|
|
134
|
+
mcp_ok = _uninstall_mcp(mcp_clients, yes)
|
|
135
|
+
if targets["data"]:
|
|
136
|
+
data_ok = _uninstall_data(data_path, yes)
|
|
137
|
+
if targets["binary"]:
|
|
138
|
+
binary_status = _uninstall_binary(binary_path, yes)
|
|
139
|
+
|
|
140
|
+
# Only summarise as "Nothing to do" when the user did not request any
|
|
141
|
+
# specific scope (do_all path) AND none of the scopes were actionable.
|
|
142
|
+
# Otherwise the summary must reflect what each helper actually
|
|
143
|
+
# accomplished -- printing "Uninstall complete" regardless of per-step
|
|
144
|
+
# outcome previously hid real failures (e.g. a locked binary on
|
|
145
|
+
# Windows) behind a misleading green line.
|
|
146
|
+
explicit_flags = binary or data or mcp
|
|
147
|
+
if (
|
|
148
|
+
not explicit_flags
|
|
149
|
+
and not binary_actionable
|
|
150
|
+
and not data_actionable
|
|
151
|
+
and not mcp_actionable
|
|
152
|
+
):
|
|
153
|
+
console.print("[yellow]Nothing to do.[/yellow]")
|
|
154
|
+
elif binary_status == "deferred":
|
|
155
|
+
console.print(
|
|
156
|
+
"[green]Uninstall complete[/green] [dim](the binary is locked by "
|
|
157
|
+
"this running process and will be deleted automatically a few "
|
|
158
|
+
"seconds after it exits -- no further action needed).[/dim]"
|
|
159
|
+
)
|
|
160
|
+
elif not mcp_ok or not data_ok or binary_status in ("failed", "skipped"):
|
|
161
|
+
console.print(
|
|
162
|
+
"[yellow]Uninstall finished with unresolved steps -- see warnings above.[/yellow]"
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
console.print("[green]Uninstall complete.[/green]")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _uninstall_mcp(clients: list[ClientInstall], yes: bool) -> bool:
|
|
169
|
+
"""Unregister cgate from each detected IA client.
|
|
170
|
+
|
|
171
|
+
Returns False if any client was skipped or failed to unregister, so
|
|
172
|
+
the final summary can report unresolved steps instead of a blanket
|
|
173
|
+
"Uninstall complete.".
|
|
174
|
+
"""
|
|
175
|
+
if not clients:
|
|
176
|
+
console.print(" [dim]No MCP clients to unregister.[/dim]")
|
|
177
|
+
return True
|
|
178
|
+
all_ok = True
|
|
179
|
+
for client in clients:
|
|
180
|
+
if not yes and not typer.confirm(
|
|
181
|
+
f"Unregister from {client.label}?", default=True
|
|
182
|
+
):
|
|
183
|
+
console.print(f" [dim]Skipped {client.label}.[/dim]")
|
|
184
|
+
all_ok = False
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
_ = unregister(client)
|
|
188
|
+
console.print(f" Unregistered [bold]{client.label}[/bold].")
|
|
189
|
+
except OSError as exc:
|
|
190
|
+
console.print(
|
|
191
|
+
f" [red]Failed to unregister {client.label}:[/red] {exc}"
|
|
192
|
+
)
|
|
193
|
+
all_ok = False
|
|
194
|
+
return all_ok
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _uninstall_data(data_path: Path, yes: bool) -> bool:
|
|
198
|
+
"""Remove the data directory, including keyring entries for its connections.
|
|
199
|
+
|
|
200
|
+
Returns False on skip or any failure, so the final summary can report
|
|
201
|
+
unresolved steps instead of a blanket "Uninstall complete.".
|
|
202
|
+
"""
|
|
203
|
+
if not data_path.exists():
|
|
204
|
+
console.print(f" [dim]{data_path} already gone.[/dim]")
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
if not yes and not typer.confirm(
|
|
208
|
+
f"Delete data directory {data_path}?", default=False
|
|
209
|
+
):
|
|
210
|
+
console.print(" [dim]Skipped.[/dim]")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
ok = True
|
|
214
|
+
|
|
215
|
+
# Enumerate connections first so we can clean each connection's
|
|
216
|
+
# OS keyring entry before the DB row that names it disappears.
|
|
217
|
+
connections: list[Connection] = []
|
|
218
|
+
try:
|
|
219
|
+
db = Database(path=db_path())
|
|
220
|
+
init_database(db)
|
|
221
|
+
connections = ConnectionsRepo(db).list_all()
|
|
222
|
+
except Exception as exc: # noqa: BLE001 - listing must not block cleanup
|
|
223
|
+
console.print(
|
|
224
|
+
f" [yellow]Could not enumerate connections:[/yellow] {exc}"
|
|
225
|
+
)
|
|
226
|
+
ok = False
|
|
227
|
+
|
|
228
|
+
for conn in connections:
|
|
229
|
+
try:
|
|
230
|
+
remove_credential(conn.alias)
|
|
231
|
+
console.print(f" Removed keyring entry for [bold]{conn.alias}[/bold].")
|
|
232
|
+
except Exception as exc: # noqa: BLE001 - per-credential failures are non-fatal
|
|
233
|
+
console.print(
|
|
234
|
+
f" [yellow]Could not remove keyring for {conn.alias}:[/yellow] {exc}"
|
|
235
|
+
)
|
|
236
|
+
ok = False
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
shutil.rmtree(data_path)
|
|
240
|
+
console.print(f" Removed [bold]{data_path}[/bold].")
|
|
241
|
+
except OSError as exc:
|
|
242
|
+
console.print(f" [red]Failed to remove {data_path}:[/red] {exc}")
|
|
243
|
+
ok = False
|
|
244
|
+
|
|
245
|
+
return ok
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _uninstall_binary(binary_path: Path | None, yes: bool) -> str:
|
|
249
|
+
"""Delete the cgate binary.
|
|
250
|
+
|
|
251
|
+
Returns one of "removed", "deferred", "skipped", "already_gone",
|
|
252
|
+
"not_applicable", or "failed" so the caller can print an honest final
|
|
253
|
+
summary instead of a blanket "Uninstall complete" regardless of outcome.
|
|
254
|
+
"""
|
|
255
|
+
if binary_path is None:
|
|
256
|
+
console.print(
|
|
257
|
+
" [dim]Not running from a PyInstaller binary; nothing to remove.[/dim]"
|
|
258
|
+
)
|
|
259
|
+
return "not_applicable"
|
|
260
|
+
if not binary_path.exists():
|
|
261
|
+
console.print(f" [dim]{binary_path} already gone.[/dim]")
|
|
262
|
+
return "already_gone"
|
|
263
|
+
|
|
264
|
+
if not yes and not typer.confirm(
|
|
265
|
+
f"Delete binary at {binary_path}?", default=False
|
|
266
|
+
):
|
|
267
|
+
console.print(" [dim]Skipped.[/dim]")
|
|
268
|
+
return "skipped"
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
binary_path.unlink()
|
|
272
|
+
console.print(f" Removed [bold]{binary_path}[/bold].")
|
|
273
|
+
_cleanup_helper_binary(binary_path)
|
|
274
|
+
return "removed"
|
|
275
|
+
except OSError as exc:
|
|
276
|
+
if sys.platform != "win32":
|
|
277
|
+
console.print(f" [red]Could not delete binary:[/red] {exc}")
|
|
278
|
+
return "failed"
|
|
279
|
+
|
|
280
|
+
# Windows only, direct unlink failed above. The running cgate.exe always
|
|
281
|
+
# holds a lock on its own image file while executing, so this is the
|
|
282
|
+
# expected case, not a genuine error -- the exact self-lock problem
|
|
283
|
+
# `update apply` already solves in cli/update.py. Reuse its blocker
|
|
284
|
+
# detection instead of guessing whether the lock is us or someone else.
|
|
285
|
+
self_pid = os.getpid()
|
|
286
|
+
other_blockers = find_blocking_processes(binary_path, exclude_pid=self_pid)
|
|
287
|
+
|
|
288
|
+
if other_blockers:
|
|
289
|
+
pid_list = ", ".join(str(pid) for pid in other_blockers)
|
|
290
|
+
console.print(
|
|
291
|
+
f" [yellow]Other running cgate processes are blocking the "
|
|
292
|
+
f"delete:[/yellow] PID(s) {pid_list}"
|
|
293
|
+
)
|
|
294
|
+
mcp_pids = find_mcp_serving_pids(other_blockers)
|
|
295
|
+
if mcp_pids:
|
|
296
|
+
console.print(
|
|
297
|
+
" [red]One of them appears to be serving a live MCP "
|
|
298
|
+
"session for an IA client (Claude Code / opencode / "
|
|
299
|
+
"Cursor). Killing it disconnects that session "
|
|
300
|
+
"immediately.[/red]"
|
|
301
|
+
)
|
|
302
|
+
proceed = yes or typer.confirm(
|
|
303
|
+
" Kill blocking process(es) and retry?", default=False
|
|
304
|
+
)
|
|
305
|
+
if not proceed:
|
|
306
|
+
console.print(
|
|
307
|
+
" [dim]Skipped. Close those processes and re-run "
|
|
308
|
+
"`cgate uninstall --binary` to finish.[/dim]"
|
|
309
|
+
)
|
|
310
|
+
return "skipped"
|
|
311
|
+
if any(kill_process(pid) for pid in other_blockers):
|
|
312
|
+
time.sleep(_KILL_SETTLE_SECONDS)
|
|
313
|
+
try:
|
|
314
|
+
binary_path.unlink()
|
|
315
|
+
console.print(f" Removed [bold]{binary_path}[/bold].")
|
|
316
|
+
_cleanup_helper_binary(binary_path)
|
|
317
|
+
return "removed"
|
|
318
|
+
except OSError:
|
|
319
|
+
pass # still locked (likely by us) -- fall through below
|
|
320
|
+
|
|
321
|
+
if _delete_via_helper_or_fallback(binary_path, wait_pids=[self_pid]):
|
|
322
|
+
console.print(
|
|
323
|
+
f" [yellow]{binary_path}[/yellow] is locked by this running "
|
|
324
|
+
"process. It will be deleted automatically a few seconds "
|
|
325
|
+
"after this command exits -- no further action needed."
|
|
326
|
+
)
|
|
327
|
+
_cleanup_helper_binary(binary_path)
|
|
328
|
+
return "deferred"
|
|
329
|
+
|
|
330
|
+
console.print(
|
|
331
|
+
" [red]Could not delete binary:[/red] locked by this running "
|
|
332
|
+
"process, and the background delete helper failed to start."
|
|
333
|
+
)
|
|
334
|
+
console.print(f' [dim]Close cgate and delete manually: del "{binary_path}"[/dim]')
|
|
335
|
+
return "failed"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _cleanup_helper_binary(binary_path: Path) -> None:
|
|
339
|
+
"""Best-effort removal of the sibling ``cgate-helper.exe``.
|
|
340
|
+
|
|
341
|
+
Called once the main binary is gone or on its way out. Never raises
|
|
342
|
+
and never affects the overall uninstall outcome -- this is
|
|
343
|
+
disk-clutter cleanup (the helper is an implementation detail the
|
|
344
|
+
user never installed by hand), not core functionality. If the helper
|
|
345
|
+
is still in use -- most likely because it's the very process
|
|
346
|
+
performing a deferred delete of ``binary_path`` right now -- fall
|
|
347
|
+
back to the ``cmd.exe`` shell-chain instead of another compiled
|
|
348
|
+
helper: ``cmd.exe`` isn't a PyInstaller ``--onefile`` binary, so it
|
|
349
|
+
has no self-lock problem to solve for a second time.
|
|
350
|
+
"""
|
|
351
|
+
helper = helper_binary_path(binary_path)
|
|
352
|
+
if not helper.exists():
|
|
353
|
+
return
|
|
354
|
+
try:
|
|
355
|
+
helper.unlink()
|
|
356
|
+
except OSError:
|
|
357
|
+
_spawn_delayed_delete(helper)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _delete_via_helper_or_fallback(target: Path, *, wait_pids: list[int]) -> bool:
|
|
361
|
+
"""Prefer the compiled ``cgate-helper.exe`` to delete ``target``.
|
|
362
|
+
|
|
363
|
+
Falls back to the ``cmd.exe`` shell-chain when it isn't available or
|
|
364
|
+
fails to spawn. There's no in-flight ``Release`` here (unlike
|
|
365
|
+
``update apply``, this isn't installing anything), so
|
|
366
|
+
``ensure_helper_binary`` pairs the helper with the *currently
|
|
367
|
+
installed* version's own release tag instead of "latest".
|
|
368
|
+
"""
|
|
369
|
+
helper = ensure_helper_binary(target)
|
|
370
|
+
if helper is not None:
|
|
371
|
+
wait_args = [arg for pid in wait_pids for arg in ("--wait-pid", str(pid))]
|
|
372
|
+
if spawn_helper(helper, "delete", "--target", str(target), *wait_args):
|
|
373
|
+
append_log(f"uninstall --binary: dispatched compiled helper {helper} for {target}")
|
|
374
|
+
return True
|
|
375
|
+
append_log("uninstall --binary: compiled helper spawn failed, falling back to shell-chain")
|
|
376
|
+
return _spawn_delayed_delete(target)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _spawn_delayed_delete(target: Path) -> bool:
|
|
380
|
+
"""Spawn a detached helper that deletes ``target`` a few seconds after this process exits.
|
|
381
|
+
|
|
382
|
+
Windows only -- mirrors ``_spawn_delayed_swap`` in cli/update.py, which
|
|
383
|
+
solves the identical self-lock problem for ``update apply``. ``cmd.exe``
|
|
384
|
+
is not ``cgate.exe`` so it never holds the lock our own process does;
|
|
385
|
+
the ``ping`` burns ~4s so our handle on the file is guaranteed closed
|
|
386
|
+
(process exited) by the time ``del`` runs.
|
|
387
|
+
"""
|
|
388
|
+
try:
|
|
389
|
+
cmd_str = f'ping -n 5 127.0.0.1 > nul & del /F /Q "{target}"'
|
|
390
|
+
subprocess.Popen(
|
|
391
|
+
f'cmd.exe /c "{cmd_str}"',
|
|
392
|
+
# DETACHED_PROCESS | CREATE_NO_WINDOW, same combination
|
|
393
|
+
# cli/update.py uses and for the same reason: detach from our
|
|
394
|
+
# console AND suppress the window Windows would otherwise
|
|
395
|
+
# flash for cmd.exe/ping.exe.
|
|
396
|
+
creationflags=0x00000008 | 0x08000000,
|
|
397
|
+
stdout=subprocess.DEVNULL,
|
|
398
|
+
stderr=subprocess.DEVNULL,
|
|
399
|
+
close_fds=True,
|
|
400
|
+
)
|
|
401
|
+
except (subprocess.SubprocessError, OSError):
|
|
402
|
+
return False
|
|
403
|
+
return True
|