lambda-watcher 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.
Files changed (38) hide show
  1. lambda_watcher/__init__.py +4 -0
  2. lambda_watcher/__main__.py +4 -0
  3. lambda_watcher/analysis/__init__.py +115 -0
  4. lambda_watcher/analysis/deps.py +291 -0
  5. lambda_watcher/analysis/envvars.py +80 -0
  6. lambda_watcher/analysis/handler.py +111 -0
  7. lambda_watcher/analysis/inventory.py +118 -0
  8. lambda_watcher/analysis/runtime.py +117 -0
  9. lambda_watcher/analysis/secrets.py +178 -0
  10. lambda_watcher/analysis/services.py +76 -0
  11. lambda_watcher/cli.py +1406 -0
  12. lambda_watcher/config.py +324 -0
  13. lambda_watcher/db.py +466 -0
  14. lambda_watcher/diffing/__init__.py +14 -0
  15. lambda_watcher/diffing/build.py +51 -0
  16. lambda_watcher/diffing/compare.py +525 -0
  17. lambda_watcher/diffing/highlight.py +312 -0
  18. lambda_watcher/diffing/icons.py +132 -0
  19. lambda_watcher/diffing/intraline.py +162 -0
  20. lambda_watcher/diffing/render_html.py +697 -0
  21. lambda_watcher/diffing/render_text.py +198 -0
  22. lambda_watcher/extract.py +227 -0
  23. lambda_watcher/gitmirror.py +151 -0
  24. lambda_watcher/identify.py +201 -0
  25. lambda_watcher/ingest.py +480 -0
  26. lambda_watcher/notify.py +59 -0
  27. lambda_watcher/reindex.py +158 -0
  28. lambda_watcher/service.py +553 -0
  29. lambda_watcher/store.py +209 -0
  30. lambda_watcher/templates.py +124 -0
  31. lambda_watcher/utils.py +314 -0
  32. lambda_watcher/watcher.py +241 -0
  33. lambda_watcher-0.1.0.dist-info/METADATA +409 -0
  34. lambda_watcher-0.1.0.dist-info/RECORD +38 -0
  35. lambda_watcher-0.1.0.dist-info/WHEEL +5 -0
  36. lambda_watcher-0.1.0.dist-info/entry_points.txt +3 -0
  37. lambda_watcher-0.1.0.dist-info/licenses/LICENSE +201 -0
  38. lambda_watcher-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,553 @@
1
+ """Register the watcher with the platform's service manager.
2
+
3
+ ``lambda-watcher watch`` is a foreground process, and a tool whose whole promise
4
+ is *"you keep downloading zips, it does the rest"* cannot also ask you to
5
+ remember to start it. This module turns the recipes that used to live only in
6
+ `docs/autostart.md` into something ``lw start`` can carry out itself: render the
7
+ unit file, hand it to launchd / systemd / Task Scheduler, and answer whether the
8
+ thing is actually running.
9
+
10
+ Everything here installs a *user* service — no sudo, no system-wide daemon. The
11
+ watcher reads one person's Downloads folder and writes one person's archive, so
12
+ it has no business running as root.
13
+
14
+ Managers are picked by platform, and the choice can fail over: Linux prefers a
15
+ systemd user unit but falls back to a plain detached process with a pidfile,
16
+ because WSL and minimal containers frequently have no systemd user session and
17
+ that is exactly where a filesystem watcher gets used.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import shutil
24
+ import signal
25
+ import subprocess
26
+ import sys
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from .config import Config
31
+ from .utils import LOG
32
+
33
+ #: Identifiers registered with the OS. Changing either of these orphans the
34
+ #: service someone already installed, so they are constants, not config.
35
+ LAUNCHD_LABEL = "com.lambdawatcher"
36
+ SYSTEMD_UNIT = "lambda-watcher"
37
+ SCHEDULED_TASK = "lambda-watcher"
38
+
39
+ #: How long any service-manager subprocess gets before we give up on it.
40
+ _TIMEOUT = 30
41
+
42
+
43
+ class ServiceError(RuntimeError):
44
+ """A service manager refused to do something, with a reason worth showing."""
45
+
46
+
47
+ @dataclass
48
+ class ServiceStatus:
49
+ """What the platform says about our service right now."""
50
+
51
+ manager: str # launchd | systemd | schtasks | pidfile | none
52
+ installed: bool = False
53
+ running: bool = False
54
+ unit_path: Path | None = None
55
+ log_path: Path | None = None
56
+ pid: int | None = None
57
+ detail: str = ""
58
+
59
+ @property
60
+ def summary(self) -> str:
61
+ if self.running:
62
+ return "running"
63
+ if self.installed:
64
+ return "installed, not running"
65
+ return "not installed"
66
+
67
+
68
+ # --------------------------------------------------------------- command
69
+ def watch_argv(config_path: Path | None = None) -> list[str]:
70
+ """The command a service manager should run, as absolute argv.
71
+
72
+ A unit file is read by a daemon with no shell, no virtualenv activation and
73
+ frequently no PATH worth the name, so every element has to be absolute.
74
+ ``python -m lambda_watcher`` is the dependable form — it works from a venv,
75
+ a pipx install, a uv tool install and a plain ``pip install --user`` alike —
76
+ but the console script reads far better in a file a human may open, so it
77
+ wins whenever we can find it next to the running interpreter.
78
+ """
79
+ argv: list[str] = []
80
+ script_dir = Path(sys.executable).parent
81
+ suffixes = (".exe", "") if sys.platform.startswith("win") else ("",)
82
+ for name in ("lambda-watcher", "lw"):
83
+ for suffix in suffixes:
84
+ candidate = script_dir / f"{name}{suffix}"
85
+ if candidate.exists():
86
+ argv = [str(candidate)]
87
+ break
88
+ if argv:
89
+ break
90
+ if not argv:
91
+ # No console script beside the interpreter: an editable checkout run
92
+ # through `python -m`, or a layout that puts scripts elsewhere.
93
+ argv = [_python_for_service(), "-m", "lambda_watcher"]
94
+
95
+ if config_path is not None:
96
+ argv += ["--config", str(Path(config_path).expanduser().resolve())]
97
+ argv.append("watch")
98
+ return argv
99
+
100
+
101
+ def _python_for_service() -> str:
102
+ """``sys.executable``, preferring the console-free build on Windows.
103
+
104
+ A scheduled task pointed at ``python.exe`` flashes a console window at every
105
+ logon; ``pythonw.exe`` is the same interpreter without one.
106
+ """
107
+ executable = Path(sys.executable)
108
+ if sys.platform.startswith("win"):
109
+ windowless = executable.with_name("pythonw.exe")
110
+ if windowless.exists():
111
+ return str(windowless)
112
+ return str(executable)
113
+
114
+
115
+ def service_environment(cfg: Config) -> dict[str, str]:
116
+ """Environment the service needs that a login session would have supplied.
117
+
118
+ Service managers start with an environment stripped almost bare, so anything
119
+ the user set in a shell profile is gone by the time the watcher runs. Only
120
+ the variables that would silently point the service at a *different archive*
121
+ than the one the user is looking at are worth baking in.
122
+ """
123
+ env: dict[str, str] = {}
124
+ for name in ("LAMBDA_WATCHER_HOME", "LAMBDA_WATCHER_CONFIG", "LAMBDA_WATCHER_LOG_LEVEL"):
125
+ value = os.environ.get(name)
126
+ if value:
127
+ env[name] = value
128
+ return env
129
+
130
+
131
+ def _run(argv: list[str], check: bool = False) -> subprocess.CompletedProcess[str]:
132
+ LOG.debug("service: %s", " ".join(argv))
133
+ try:
134
+ proc = subprocess.run(
135
+ argv, capture_output=True, text=True, timeout=_TIMEOUT, check=False
136
+ )
137
+ except FileNotFoundError as exc:
138
+ raise ServiceError(f"{argv[0]} is not installed") from exc
139
+ except subprocess.SubprocessError as exc:
140
+ raise ServiceError(f"{argv[0]} did not finish: {exc}") from exc
141
+ if check and proc.returncode != 0:
142
+ detail = (proc.stderr or proc.stdout or "").strip()
143
+ raise ServiceError(f"{' '.join(argv[:2])} failed: {detail or proc.returncode}")
144
+ return proc
145
+
146
+
147
+ # ---------------------------------------------------------------- managers
148
+ class Manager:
149
+ """One platform's way of running something at login and keeping it up."""
150
+
151
+ name = "none"
152
+
153
+ def __init__(self, cfg: Config, config_path: Path | None = None) -> None:
154
+ self.cfg = cfg
155
+ self.config_path = config_path
156
+
157
+ # Subclasses implement these four; `restart` is derived from them.
158
+ def install(self) -> ServiceStatus: raise NotImplementedError
159
+ def uninstall(self) -> None: raise NotImplementedError
160
+ def stop(self) -> None: raise NotImplementedError
161
+ def status(self) -> ServiceStatus: raise NotImplementedError
162
+
163
+ def restart(self) -> ServiceStatus:
164
+ self.stop()
165
+ return self.install()
166
+
167
+ # -- shared helpers --------------------------------------------------
168
+ @property
169
+ def argv(self) -> list[str]:
170
+ return watch_argv(self.config_path)
171
+
172
+ @property
173
+ def log_path(self) -> Path:
174
+ return self.cfg.log_dir / "service.log"
175
+
176
+ def _prepare_logs(self) -> Path:
177
+ self.cfg.log_dir.mkdir(parents=True, exist_ok=True)
178
+ return self.log_path
179
+
180
+
181
+ class LaunchdManager(Manager):
182
+ """macOS user agent. Loaded per-user, started at login, restarted on crash."""
183
+
184
+ name = "launchd"
185
+
186
+ @property
187
+ def unit_path(self) -> Path:
188
+ return Path("~/Library/LaunchAgents").expanduser() / f"{LAUNCHD_LABEL}.plist"
189
+
190
+ def install(self) -> ServiceStatus:
191
+ log = self._prepare_logs()
192
+ self.unit_path.parent.mkdir(parents=True, exist_ok=True)
193
+ self.unit_path.write_text(self._plist(log), encoding="utf-8")
194
+ # An already-loaded agent has to come out before the new definition goes
195
+ # in; launchd will not re-read a plist for a label it already knows.
196
+ _run(["launchctl", "unload", str(self.unit_path)])
197
+ _run(["launchctl", "load", "-w", str(self.unit_path)], check=True)
198
+ return self.status()
199
+
200
+ def uninstall(self) -> None:
201
+ if self.unit_path.exists():
202
+ _run(["launchctl", "unload", "-w", str(self.unit_path)])
203
+ self.unit_path.unlink()
204
+
205
+ def stop(self) -> None:
206
+ if self.unit_path.exists():
207
+ # `unload` rather than `stop`: KeepAlive would restart it instantly.
208
+ _run(["launchctl", "unload", str(self.unit_path)])
209
+
210
+ def status(self) -> ServiceStatus:
211
+ state = ServiceStatus(self.name, unit_path=self.unit_path, log_path=self.log_path)
212
+ state.installed = self.unit_path.exists()
213
+ if not state.installed:
214
+ return state
215
+ proc = _run(["launchctl", "list", LAUNCHD_LABEL])
216
+ if proc.returncode != 0:
217
+ state.detail = "registered but not loaded"
218
+ return state
219
+ for line in proc.stdout.splitlines():
220
+ if '"PID"' in line:
221
+ digits = "".join(c for c in line.split("=")[-1] if c.isdigit())
222
+ if digits:
223
+ state.pid = int(digits)
224
+ state.running = state.pid is not None
225
+ return state
226
+
227
+ def _plist(self, log: Path) -> str:
228
+ args = "\n".join(f" <string>{_xml(a)}</string>" for a in self.argv)
229
+ env = service_environment(self.cfg)
230
+ env_block = ""
231
+ if env:
232
+ pairs = "\n".join(
233
+ f" <key>{_xml(k)}</key>\n <string>{_xml(v)}</string>"
234
+ for k, v in env.items()
235
+ )
236
+ env_block = f"\n <key>EnvironmentVariables</key>\n <dict>\n{pairs}\n </dict>\n"
237
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
238
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
239
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
240
+ <plist version="1.0">
241
+ <dict>
242
+ <key>Label</key>
243
+ <string>{LAUNCHD_LABEL}</string>
244
+
245
+ <key>ProgramArguments</key>
246
+ <array>
247
+ {args}
248
+ </array>
249
+
250
+ <key>RunAtLoad</key>
251
+ <true/>
252
+ <key>KeepAlive</key>
253
+ <true/>
254
+ {env_block}
255
+ <key>StandardOutPath</key>
256
+ <string>{_xml(str(log))}</string>
257
+ <key>StandardErrorPath</key>
258
+ <string>{_xml(str(log))}</string>
259
+ </dict>
260
+ </plist>
261
+ """
262
+
263
+
264
+ class SystemdManager(Manager):
265
+ """Linux systemd *user* unit — no root, enabled for the login session."""
266
+
267
+ name = "systemd"
268
+
269
+ @property
270
+ def unit_path(self) -> Path:
271
+ return Path("~/.config/systemd/user").expanduser() / f"{SYSTEMD_UNIT}.service"
272
+
273
+ @staticmethod
274
+ def available() -> bool:
275
+ """True when there is a systemd user session to talk to.
276
+
277
+ `systemctl` being on PATH is not enough: WSL images ship it while
278
+ running no user manager at all, and every call there fails with
279
+ "Failed to connect to bus".
280
+ """
281
+ if not shutil.which("systemctl"):
282
+ return False
283
+ try:
284
+ proc = subprocess.run(
285
+ ["systemctl", "--user", "show-environment"],
286
+ capture_output=True, text=True, timeout=10, check=False,
287
+ )
288
+ except (OSError, subprocess.SubprocessError):
289
+ return False
290
+ return proc.returncode == 0
291
+
292
+ def install(self) -> ServiceStatus:
293
+ self._prepare_logs()
294
+ self.unit_path.parent.mkdir(parents=True, exist_ok=True)
295
+ self.unit_path.write_text(self._unit(), encoding="utf-8")
296
+ _run(["systemctl", "--user", "daemon-reload"], check=True)
297
+ _run(["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT], check=True)
298
+ return self.status()
299
+
300
+ def uninstall(self) -> None:
301
+ if self.unit_path.exists():
302
+ _run(["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT])
303
+ self.unit_path.unlink()
304
+ _run(["systemctl", "--user", "daemon-reload"])
305
+
306
+ def stop(self) -> None:
307
+ _run(["systemctl", "--user", "stop", SYSTEMD_UNIT])
308
+
309
+ def status(self) -> ServiceStatus:
310
+ state = ServiceStatus(self.name, unit_path=self.unit_path)
311
+ state.installed = self.unit_path.exists()
312
+ if not state.installed:
313
+ return state
314
+ state.running = _run(["systemctl", "--user", "is-active", SYSTEMD_UNIT]).stdout.strip() == "active"
315
+ proc = _run(["systemctl", "--user", "show", SYSTEMD_UNIT, "--property=MainPID", "--value"])
316
+ pid = proc.stdout.strip()
317
+ if pid.isdigit() and int(pid) > 0:
318
+ state.pid = int(pid)
319
+ state.detail = "journalctl --user -u lambda-watcher -f"
320
+ return state
321
+
322
+ def _unit(self) -> str:
323
+ exec_start = " ".join(_sh_quote(a) for a in self.argv)
324
+ env_lines = "".join(
325
+ f'Environment="{k}={v}"\n' for k, v in service_environment(self.cfg).items()
326
+ )
327
+ return f"""[Unit]
328
+ Description=Watch Downloads for AWS Lambda deployment packages
329
+ After=default.target
330
+
331
+ [Service]
332
+ Type=simple
333
+ ExecStart={exec_start}
334
+ {env_lines}Restart=on-failure
335
+ RestartSec=10
336
+
337
+ [Install]
338
+ WantedBy=default.target
339
+ """
340
+
341
+
342
+ class SchtasksManager(Manager):
343
+ """Windows Task Scheduler entry, triggered at logon."""
344
+
345
+ name = "schtasks"
346
+
347
+ def install(self) -> ServiceStatus:
348
+ self._prepare_logs()
349
+ command = " ".join(_win_quote(a) for a in self.argv)
350
+ _run([
351
+ "schtasks", "/Create", "/TN", SCHEDULED_TASK, "/TR", command,
352
+ "/SC", "ONLOGON", "/F", "/RL", "LIMITED",
353
+ ], check=True)
354
+ # ONLOGON only fires at the *next* logon, so start it once by hand to
355
+ # make `lw start` mean what it says.
356
+ _run(["schtasks", "/Run", "/TN", SCHEDULED_TASK])
357
+ return self.status()
358
+
359
+ def uninstall(self) -> None:
360
+ _run(["schtasks", "/End", "/TN", SCHEDULED_TASK])
361
+ _run(["schtasks", "/Delete", "/TN", SCHEDULED_TASK, "/F"])
362
+
363
+ def stop(self) -> None:
364
+ _run(["schtasks", "/End", "/TN", SCHEDULED_TASK])
365
+
366
+ def status(self) -> ServiceStatus:
367
+ state = ServiceStatus(self.name, log_path=self.log_path)
368
+ proc = _run(["schtasks", "/Query", "/TN", SCHEDULED_TASK, "/FO", "LIST", "/V"])
369
+ if proc.returncode != 0:
370
+ return state
371
+ state.installed = True
372
+ for line in proc.stdout.splitlines():
373
+ if line.lower().startswith("status:"):
374
+ state.running = line.split(":", 1)[1].strip().lower() == "running"
375
+ return state
376
+
377
+
378
+ class PidfileManager(Manager):
379
+ """Last-resort manager: a detached process tracked by a pidfile.
380
+
381
+ This is what WSL and systemd-less Linux get. It genuinely keeps the watcher
382
+ running in the background and survives the terminal closing, but nothing
383
+ restarts it after a reboot — callers are expected to say so.
384
+ """
385
+
386
+ name = "pidfile"
387
+
388
+ @property
389
+ def pid_path(self) -> Path:
390
+ return self.cfg.root / "watcher.pid"
391
+
392
+ def install(self) -> ServiceStatus:
393
+ existing = self.status()
394
+ if existing.running:
395
+ return existing
396
+ log = self._prepare_logs()
397
+ self.cfg.root.mkdir(parents=True, exist_ok=True)
398
+ handle = open(log, "a", encoding="utf-8") # noqa: SIM115 - owned by the child
399
+ try:
400
+ proc = subprocess.Popen(
401
+ self.argv,
402
+ stdout=handle, stderr=handle, stdin=subprocess.DEVNULL,
403
+ start_new_session=True,
404
+ env={**os.environ, **service_environment(self.cfg)},
405
+ )
406
+ except OSError as exc:
407
+ handle.close()
408
+ raise ServiceError(f"could not start the watcher: {exc}") from exc
409
+ finally:
410
+ handle.close()
411
+ self.pid_path.write_text(str(proc.pid), encoding="utf-8")
412
+ return self.status()
413
+
414
+ def uninstall(self) -> None:
415
+ self.stop()
416
+ self.pid_path.unlink(missing_ok=True)
417
+
418
+ def stop(self) -> None:
419
+ pid = self._recorded_pid()
420
+ if pid is None:
421
+ return
422
+ try:
423
+ os.kill(pid, signal.SIGTERM)
424
+ except ProcessLookupError:
425
+ pass
426
+ except OSError as exc:
427
+ raise ServiceError(f"could not stop pid {pid}: {exc}") from exc
428
+ self.pid_path.unlink(missing_ok=True)
429
+
430
+ def status(self) -> ServiceStatus:
431
+ state = ServiceStatus(self.name, log_path=self.log_path)
432
+ pid = self._recorded_pid()
433
+ if pid is None:
434
+ return state
435
+ state.installed = True
436
+ state.pid = pid
437
+ state.running = _pid_alive(pid)
438
+ if not state.running:
439
+ state.detail = "the recorded process is gone"
440
+ else:
441
+ state.detail = "started in the background; will not survive a reboot"
442
+ return state
443
+
444
+ def _recorded_pid(self) -> int | None:
445
+ try:
446
+ text = self.pid_path.read_text(encoding="utf-8").strip()
447
+ except OSError:
448
+ return None
449
+ return int(text) if text.isdigit() else None
450
+
451
+
452
+ def _pid_alive(pid: int) -> bool:
453
+ """Is that process still running?
454
+
455
+ A process this one started and then signalled stays visible to ``kill(0)``
456
+ as a zombie until somebody collects its exit status, and normally nobody
457
+ does — ``lw stop`` exits immediately afterwards. Reaping first keeps a
458
+ caller that outlives the watcher (the test suite, a `lw status` in the same
459
+ session) from reporting a stopped watcher as running.
460
+ """
461
+ try:
462
+ reaped, _status = os.waitpid(pid, os.WNOHANG)
463
+ if reaped == pid:
464
+ return False
465
+ except ChildProcessError:
466
+ pass # not ours; nothing to reap
467
+ except (AttributeError, OSError):
468
+ pass # no usable waitpid here
469
+ if sys.platform.startswith("win"):
470
+ # `os.kill(pid, 0)` on Windows is TerminateProcess with an exit code of
471
+ # zero, not a probe. Nothing selects PidfileManager there, and this
472
+ # would be a spectacular way to find out otherwise.
473
+ return False
474
+ try:
475
+ os.kill(pid, 0)
476
+ except ProcessLookupError:
477
+ return False
478
+ except PermissionError:
479
+ return True # alive, just not ours to signal
480
+ except OSError:
481
+ return False
482
+ return True
483
+
484
+
485
+ # ---------------------------------------------------------------- factory
486
+ def get_manager(cfg: Config, config_path: Path | None = None) -> Manager:
487
+ """The right manager for this machine.
488
+
489
+ Order is per-platform preference with a working fallback, never an error:
490
+ somebody on WSL should still get a background watcher out of ``lw start``,
491
+ just one that is honest about not surviving a reboot.
492
+ """
493
+ if sys.platform == "darwin" and shutil.which("launchctl"):
494
+ return LaunchdManager(cfg, config_path)
495
+ if sys.platform.startswith("win"):
496
+ return SchtasksManager(cfg, config_path)
497
+ if sys.platform.startswith(("linux", "freebsd")) and SystemdManager.available():
498
+ return SystemdManager(cfg, config_path)
499
+ return PidfileManager(cfg, config_path)
500
+
501
+
502
+ def current_status(cfg: Config, config_path: Path | None = None) -> ServiceStatus:
503
+ """Best-effort status that never raises — safe for a dashboard to call.
504
+
505
+ A manager whose own unit is absent may still be shadowed by one installed
506
+ under a different manager (a systemd unit written before the user moved to a
507
+ machine without a user bus, say), so every manager that could plausibly know
508
+ something gets asked before we report "not installed".
509
+ """
510
+ manager = get_manager(cfg, config_path)
511
+ candidates: list[Manager] = [manager]
512
+ if isinstance(manager, PidfileManager) and sys.platform.startswith("linux"):
513
+ candidates.append(SystemdManager(cfg, config_path))
514
+ for candidate in candidates:
515
+ try:
516
+ state = candidate.status()
517
+ except ServiceError as exc:
518
+ LOG.debug("status via %s failed: %s", candidate.name, exc)
519
+ continue
520
+ if state.installed:
521
+ return state
522
+ return ServiceStatus(manager.name, log_path=manager.log_path)
523
+
524
+
525
+ # ------------------------------------------------------------------ quoting
526
+ def _xml(value: str) -> str:
527
+ return (
528
+ value.replace("&", "&amp;").replace("<", "&lt;")
529
+ .replace(">", "&gt;").replace('"', "&quot;")
530
+ )
531
+
532
+
533
+ def _sh_quote(value: str) -> str:
534
+ """Quote one systemd ExecStart argument.
535
+
536
+ systemd does its own unquoting rather than handing the line to a shell, so
537
+ this is deliberately not `shlex.quote`: only double quotes and backslashes
538
+ need escaping, and a path with a space has to come out as one quoted token.
539
+ """
540
+ if value and not any(c in value for c in ' \t"\\\''):
541
+ return value
542
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
543
+ return f'"{escaped}"'
544
+
545
+
546
+ def _win_quote(value: str) -> str:
547
+ """Quote one argument inside a schtasks /TR command string.
548
+
549
+ NTFS forbids ``"`` in a path, so wrapping anything containing a space is the
550
+ whole job. The escape is ``\\"`` because /TR's value is itself a quoted
551
+ argument by the time the task scheduler reads it.
552
+ """
553
+ return f'\\"{value}\\"' if " " in value else value