tunnelmate-cli 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/PKG-INFO +1 -1
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/pyproject.toml +1 -1
- tunnelmate_cli-1.0.4/tests/test_cross_platform.py +84 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/__init__.py +1 -1
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/bot/telegram_bot.py +11 -1
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/cli/formatting.py +5 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/cli/interactive.py +69 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/cli/main.py +58 -2
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/config.py +1 -0
- tunnelmate_cli-1.0.4/tunnelmate/core/autostart.py +283 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/daemon/service.py +31 -3
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/PKG-INFO +1 -1
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/SOURCES.txt +1 -0
- tunnelmate_cli-1.0.2/tests/test_cross_platform.py +0 -36
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/README.md +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/setup.cfg +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_cli.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_cloudflared.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_config.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_db.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_health.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tests/test_ipc.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/bot/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/cli/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/cli/ssh_client.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/client/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/client/ipc_client.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/cloudflared.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/health.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/models.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/process.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/core/supervisor.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/daemon/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/daemon/protocol.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/daemon/server.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/db/__init__.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/db/repository.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/db/schema.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate/exceptions.py +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/dependency_links.txt +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/entry_points.txt +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/requires.txt +0 -0
- {tunnelmate_cli-1.0.2 → tunnelmate_cli-1.0.4}/tunnelmate_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tunnelmate-cli"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Cloudflare Quick Tunnel manager with CLI, Interactive TUI, Daemon IPC, and Telegram Bot"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for cross-platform process management and Windows/Linux compatibility."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tunnelmate.core.process import is_pid_alive, spawn_background_process, terminate_process_by_pid
|
|
7
|
+
from tunnelmate.core.cloudflared import find_cloudflared
|
|
8
|
+
from tunnelmate.cli.ssh_client import clean_ssh_target
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_is_pid_alive():
|
|
12
|
+
# Current process must be alive
|
|
13
|
+
assert is_pid_alive(os.getpid()) is True
|
|
14
|
+
# Non-existent PID must not be alive
|
|
15
|
+
assert is_pid_alive(99999999) is False
|
|
16
|
+
assert is_pid_alive(-1) is False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_spawn_and_terminate_process():
|
|
20
|
+
# Launch a short-lived python background sleep process
|
|
21
|
+
cmd = [sys.executable, "-c", "import time; time.sleep(10)"]
|
|
22
|
+
proc = spawn_background_process(cmd)
|
|
23
|
+
pid = proc.pid
|
|
24
|
+
assert pid is not None
|
|
25
|
+
assert is_pid_alive(pid) is True
|
|
26
|
+
|
|
27
|
+
# Terminate process safely using cross-platform helper
|
|
28
|
+
terminated = terminate_process_by_pid(pid, timeout=3.0)
|
|
29
|
+
assert terminated is True
|
|
30
|
+
assert is_pid_alive(pid) is False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_clean_ssh_target_windows_formats():
|
|
34
|
+
assert clean_ssh_target("shashan@subjective.trycloudflare.com") == "shashan@subjective.trycloudflare.com"
|
|
35
|
+
assert clean_ssh_target("shashan@https://subjective.trycloudflare.com/") == "shashan@subjective.trycloudflare.com"
|
|
36
|
+
assert clean_ssh_target("https://subjective.trycloudflare.com:22") == "subjective.trycloudflare.com"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_bot_auto_start_persistence(tmp_path: Path):
|
|
40
|
+
from tunnelmate.config import Settings, load_settings, save_settings
|
|
41
|
+
|
|
42
|
+
st = Settings(home_dir=tmp_path, bot_auto_start=False)
|
|
43
|
+
assert st.bot_auto_start is False
|
|
44
|
+
|
|
45
|
+
st.bot_auto_start = True
|
|
46
|
+
save_settings(st)
|
|
47
|
+
|
|
48
|
+
loaded = load_settings(custom_home=tmp_path)
|
|
49
|
+
assert loaded.bot_auto_start is True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_autostart_status_query():
|
|
53
|
+
from tunnelmate.core.autostart import is_autostart_enabled
|
|
54
|
+
|
|
55
|
+
# Should return a tuple of (bool, str) without throwing exceptions
|
|
56
|
+
enabled, details = is_autostart_enabled()
|
|
57
|
+
assert isinstance(enabled, bool)
|
|
58
|
+
assert isinstance(details, str)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_daemon_startup_auto_restoration(tmp_path: Path):
|
|
62
|
+
from tunnelmate.config import Settings
|
|
63
|
+
from tunnelmate.db.repository import Repository
|
|
64
|
+
from tunnelmate.core.models import Tunnel, TunnelStatus, TunnelProtocol
|
|
65
|
+
|
|
66
|
+
st = Settings(home_dir=tmp_path, bot_auto_start=True, telegram_bot_token="test_token")
|
|
67
|
+
repo = Repository(st.resolved_db_path)
|
|
68
|
+
|
|
69
|
+
# Create one running tunnel and one stopped tunnel
|
|
70
|
+
t1 = Tunnel(id="t1", name="running-web", target_port=8080, protocol=TunnelProtocol.HTTP, status=TunnelStatus.RUNNING)
|
|
71
|
+
t2 = Tunnel(id="t2", name="stopped-ssh", target_port=22, protocol=TunnelProtocol.TCP, status=TunnelStatus.STOPPED)
|
|
72
|
+
repo.create_tunnel(t1)
|
|
73
|
+
repo.create_tunnel(t2)
|
|
74
|
+
|
|
75
|
+
# Check repository tunnels
|
|
76
|
+
tunnels = repo.list_tunnels()
|
|
77
|
+
to_restore = [t for t in tunnels if t.status.value in ("running", "starting", "restarting")]
|
|
78
|
+
assert len(to_restore) == 1
|
|
79
|
+
assert to_restore[0].name == "running-web"
|
|
80
|
+
|
|
81
|
+
# Verify bot_auto_start setting is preserved
|
|
82
|
+
assert st.bot_auto_start is True
|
|
83
|
+
|
|
84
|
+
|
|
@@ -26,7 +26,7 @@ from telegram.ext import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from tunnelmate.client.ipc_client import TunnelMateClient
|
|
29
|
-
from tunnelmate.config import Settings, load_settings
|
|
29
|
+
from tunnelmate.config import Settings, load_settings, save_settings
|
|
30
30
|
from tunnelmate.core.models import HealthStatus, Tunnel, TunnelProtocol, TunnelStatus
|
|
31
31
|
from tunnelmate.core.process import is_pid_alive
|
|
32
32
|
from tunnelmate.daemon.protocol import IPCRequest, IPCResponse
|
|
@@ -704,6 +704,16 @@ class TunnelMateBot:
|
|
|
704
704
|
|
|
705
705
|
self._write_pid()
|
|
706
706
|
|
|
707
|
+
# Mark bot last status as started so after system reboot it automatically starts
|
|
708
|
+
try:
|
|
709
|
+
curr = load_settings()
|
|
710
|
+
if not curr.bot_auto_start:
|
|
711
|
+
curr.bot_auto_start = True
|
|
712
|
+
save_settings(curr)
|
|
713
|
+
self.settings = curr
|
|
714
|
+
except Exception as e:
|
|
715
|
+
logger.debug("Failed to persist bot_auto_start state: %s", e)
|
|
716
|
+
|
|
707
717
|
request_config = HTTPXRequest(
|
|
708
718
|
connection_pool_size=8,
|
|
709
719
|
read_timeout=30.0,
|
|
@@ -172,6 +172,7 @@ def print_bot_status_panel(
|
|
|
172
172
|
has_token: bool,
|
|
173
173
|
has_pwd: bool,
|
|
174
174
|
admin_chat_ids: List[int],
|
|
175
|
+
bot_auto_start: bool = False,
|
|
175
176
|
) -> None:
|
|
176
177
|
"""Display Telegram bot operational status panel."""
|
|
177
178
|
status_text = f"🟢 RUNNING (PID: {pid})" if is_running else "🔴 STOPPED"
|
|
@@ -189,6 +190,10 @@ def print_bot_status_panel(
|
|
|
189
190
|
if admin_chat_ids:
|
|
190
191
|
admin_info += f" ({', '.join(str(cid) for cid in admin_chat_ids)})"
|
|
191
192
|
table.add_row("Admin Whitelist:", admin_info)
|
|
193
|
+
table.add_row(
|
|
194
|
+
"Auto-Restore on Boot:",
|
|
195
|
+
"✅ Enabled (Starts automatically after restart)" if bot_auto_start else "❌ Disabled",
|
|
196
|
+
)
|
|
192
197
|
|
|
193
198
|
console.print(Panel(table, title="🤖 Telegram Bot Controller", border_style="cyan"))
|
|
194
199
|
|
|
@@ -87,6 +87,7 @@ def run_interactive_menu(client: Optional[TunnelMateClient] = None) -> None:
|
|
|
87
87
|
"🗑️ Delete a Tunnel",
|
|
88
88
|
"📊 Daemon Statistics",
|
|
89
89
|
"🤖 Telegram Bot Management",
|
|
90
|
+
"⚙️ Auto-Start on Boot Settings",
|
|
90
91
|
"🚪 Exit",
|
|
91
92
|
],
|
|
92
93
|
).ask()
|
|
@@ -99,6 +100,10 @@ def run_interactive_menu(client: Optional[TunnelMateClient] = None) -> None:
|
|
|
99
100
|
_handle_telegram_bot_menu()
|
|
100
101
|
continue
|
|
101
102
|
|
|
103
|
+
if choice == "⚙️ Auto-Start on Boot Settings":
|
|
104
|
+
_handle_autostart_menu()
|
|
105
|
+
continue
|
|
106
|
+
|
|
102
107
|
try:
|
|
103
108
|
if choice == "📋 List All Tunnels & URLs":
|
|
104
109
|
tunnels = client.list_tunnels()
|
|
@@ -248,6 +253,7 @@ def _handle_telegram_bot_menu() -> None:
|
|
|
248
253
|
has_token=has_token,
|
|
249
254
|
has_pwd=has_pwd,
|
|
250
255
|
admin_chat_ids=settings.admin_chat_ids,
|
|
256
|
+
bot_auto_start=settings.bot_auto_start,
|
|
251
257
|
)
|
|
252
258
|
|
|
253
259
|
choices = []
|
|
@@ -285,20 +291,27 @@ def _handle_telegram_bot_menu() -> None:
|
|
|
285
291
|
|
|
286
292
|
cmd = [sys.executable, "-m", "tunnelmate.bot.telegram_bot"]
|
|
287
293
|
spawn_background_process(cmd)
|
|
294
|
+
settings.bot_auto_start = True
|
|
295
|
+
save_settings(settings)
|
|
296
|
+
|
|
288
297
|
with console.status("[bold cyan]Starting Telegram bot in background...[/bold cyan]"):
|
|
289
298
|
time.sleep(1.0)
|
|
290
299
|
new_pid = TunnelMateBot.get_running_pid(settings)
|
|
291
300
|
if new_pid:
|
|
292
301
|
console.print(f"[bold green]✔ Telegram Bot started in background (PID: {new_pid}).[/bold green]")
|
|
302
|
+
console.print("[dim]Auto-restore on reboot: Enabled[/dim]")
|
|
293
303
|
else:
|
|
294
304
|
console.print("[bold red]Failed to start bot in background. Verify your token or logs.[/bold red]")
|
|
295
305
|
|
|
296
306
|
elif bot_action == "🛑 Stop Telegram Bot":
|
|
307
|
+
settings.bot_auto_start = False
|
|
308
|
+
save_settings(settings)
|
|
297
309
|
if pid:
|
|
298
310
|
with console.status(f"[bold yellow]Stopping Telegram Bot (PID: {pid})...[/bold yellow]"):
|
|
299
311
|
terminated = terminate_process_by_pid(pid)
|
|
300
312
|
if terminated:
|
|
301
313
|
console.print("[bold green]✔ Telegram Bot stopped successfully.[/bold green]")
|
|
314
|
+
console.print("[dim]Auto-restore on reboot: Disabled[/dim]")
|
|
302
315
|
else:
|
|
303
316
|
console.print("[bold red]Failed to stop Telegram Bot process.[/bold red]")
|
|
304
317
|
else:
|
|
@@ -447,3 +460,59 @@ def _handle_telegram_bot_menu() -> None:
|
|
|
447
460
|
console.print()
|
|
448
461
|
questionary.press_any_key_to_continue().ask()
|
|
449
462
|
|
|
463
|
+
|
|
464
|
+
def _handle_autostart_menu() -> None:
|
|
465
|
+
"""Dedicated interactive submenu for managing system boot autostart."""
|
|
466
|
+
from rich.panel import Panel
|
|
467
|
+
from tunnelmate.core.autostart import disable_autostart, enable_autostart, is_autostart_enabled
|
|
468
|
+
|
|
469
|
+
while True:
|
|
470
|
+
console.clear()
|
|
471
|
+
print_banner()
|
|
472
|
+
|
|
473
|
+
enabled, details = is_autostart_enabled()
|
|
474
|
+
status_text = "[bold green]🟢 ENABLED[/bold green]" if enabled else "[bold red]🔴 DISABLED[/bold red]"
|
|
475
|
+
|
|
476
|
+
console.print(
|
|
477
|
+
Panel(
|
|
478
|
+
f"Auto-Start on Boot: {status_text}\n"
|
|
479
|
+
f"Configuration: [white]{details}[/white]\n\n"
|
|
480
|
+
"[dim]When enabled, the TunnelMate daemon launches automatically on system restart.\n"
|
|
481
|
+
"All tunnels and Telegram bot active before reboot will automatically resume.[/dim]",
|
|
482
|
+
title="⚙️ [bold cyan]System Boot Auto-Start[/bold cyan]",
|
|
483
|
+
border_style="cyan",
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
choices = []
|
|
488
|
+
if enabled:
|
|
489
|
+
choices.append("❌ Disable Auto-Start on Boot")
|
|
490
|
+
else:
|
|
491
|
+
choices.append("✔ Enable Auto-Start on Boot")
|
|
492
|
+
choices.append("🔙 Back to Main Menu")
|
|
493
|
+
|
|
494
|
+
action = questionary.select("Select action:", choices=choices).ask()
|
|
495
|
+
if not action or action == "🔙 Back to Main Menu":
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
if action == "✔ Enable Auto-Start on Boot":
|
|
499
|
+
with console.status("[bold cyan]Configuring system auto-start for TunnelMate daemon...[/bold cyan]"):
|
|
500
|
+
ok, msg = enable_autostart()
|
|
501
|
+
if ok:
|
|
502
|
+
console.print(f"[bold green]✔ {msg}[/bold green]")
|
|
503
|
+
else:
|
|
504
|
+
console.print(f"[bold red]❌ {msg}[/bold red]")
|
|
505
|
+
console.print()
|
|
506
|
+
questionary.press_any_key_to_continue().ask()
|
|
507
|
+
|
|
508
|
+
elif action == "❌ Disable Auto-Start on Boot":
|
|
509
|
+
with console.status("[bold yellow]Removing auto-start configuration...[/bold yellow]"):
|
|
510
|
+
ok, msg = disable_autostart()
|
|
511
|
+
if ok:
|
|
512
|
+
console.print(f"[bold yellow]✔ {msg}[/bold yellow]")
|
|
513
|
+
else:
|
|
514
|
+
console.print(f"[bold red]❌ {msg}[/bold red]")
|
|
515
|
+
console.print()
|
|
516
|
+
questionary.press_any_key_to_continue().ask()
|
|
517
|
+
|
|
518
|
+
|
|
@@ -42,9 +42,11 @@ app = typer.Typer(
|
|
|
42
42
|
)
|
|
43
43
|
daemon_app = typer.Typer(name="daemon", help="Manage background daemon service.")
|
|
44
44
|
bot_app = typer.Typer(name="bot", help="Manage Telegram remote management bot.")
|
|
45
|
+
autostart_app = typer.Typer(name="autostart", help="Configure system boot / restart autostart for TunnelMate.")
|
|
45
46
|
|
|
46
47
|
app.add_typer(daemon_app, name="daemon")
|
|
47
48
|
app.add_typer(bot_app, name="bot")
|
|
49
|
+
app.add_typer(autostart_app, name="autostart")
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def _get_client() -> TunnelMateClient:
|
|
@@ -382,6 +384,46 @@ def cmd_daemon_status():
|
|
|
382
384
|
console.print(f"[bold yellow]Daemon process exists (PID: {pid}) but IPC is unresponsive.[/bold yellow]")
|
|
383
385
|
|
|
384
386
|
|
|
387
|
+
@autostart_app.command(name="enable")
|
|
388
|
+
@daemon_app.command(name="enable")
|
|
389
|
+
def cmd_autostart_enable():
|
|
390
|
+
"""Enable TunnelMate daemon to start automatically on system reboot / boot."""
|
|
391
|
+
from tunnelmate.core.autostart import enable_autostart
|
|
392
|
+
|
|
393
|
+
with console.status("[bold cyan]Configuring system auto-start for TunnelMate daemon...[/bold cyan]"):
|
|
394
|
+
success, msg = enable_autostart()
|
|
395
|
+
if success:
|
|
396
|
+
console.print(f"[bold green]✔ {msg}[/bold green]")
|
|
397
|
+
else:
|
|
398
|
+
console.print(f"[bold red]❌ {msg}[/bold red]")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@autostart_app.command(name="disable")
|
|
402
|
+
@daemon_app.command(name="disable")
|
|
403
|
+
def cmd_autostart_disable():
|
|
404
|
+
"""Disable TunnelMate daemon from starting automatically on system boot."""
|
|
405
|
+
from tunnelmate.core.autostart import disable_autostart
|
|
406
|
+
|
|
407
|
+
with console.status("[bold yellow]Removing auto-start configuration...[/bold yellow]"):
|
|
408
|
+
success, msg = disable_autostart()
|
|
409
|
+
if success:
|
|
410
|
+
console.print(f"[bold yellow]✔ {msg}[/bold yellow]")
|
|
411
|
+
else:
|
|
412
|
+
console.print(f"[bold red]❌ {msg}[/bold red]")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@autostart_app.command(name="status")
|
|
416
|
+
def cmd_autostart_status():
|
|
417
|
+
"""Check if TunnelMate daemon is configured to start on boot / restart."""
|
|
418
|
+
from tunnelmate.core.autostart import is_autostart_enabled
|
|
419
|
+
|
|
420
|
+
enabled, msg = is_autostart_enabled()
|
|
421
|
+
if enabled:
|
|
422
|
+
console.print(f"[bold green]✔ Auto-Start is ENABLED on boot:[/bold green] {msg}")
|
|
423
|
+
else:
|
|
424
|
+
console.print(f"[bold red]❌ Auto-Start is DISABLED on boot:[/bold red] {msg}")
|
|
425
|
+
|
|
426
|
+
|
|
385
427
|
# --- Telegram Bot Subcommands ---
|
|
386
428
|
|
|
387
429
|
|
|
@@ -540,11 +582,12 @@ def cmd_bot_status():
|
|
|
540
582
|
console.print(f"Telegram Token Configured: {'[bold green]Yes[/bold green]' if has_token else '[bold red]No[/bold red]'}")
|
|
541
583
|
console.print(f"Admin Password Configured: {'[bold green]Yes[/bold green]' if has_pwd else '[bold yellow]No[/bold yellow]'}")
|
|
542
584
|
console.print(f"Admin Whitelisted Chat IDs: {admins_count}")
|
|
585
|
+
console.print(f"Auto-Restore on Reboot: {'[bold green]Enabled[/bold green]' if settings.bot_auto_start else '[dim]Disabled[/dim]'}")
|
|
543
586
|
|
|
544
587
|
|
|
545
588
|
@bot_app.command(name="start")
|
|
546
589
|
def cmd_bot_start():
|
|
547
|
-
"""Launch Telegram Bot in background."""
|
|
590
|
+
"""Launch Telegram Bot in background and enable auto-restore."""
|
|
548
591
|
from tunnelmate.bot.telegram_bot import TunnelMateBot
|
|
549
592
|
settings = load_settings()
|
|
550
593
|
if not settings.telegram_bot_token:
|
|
@@ -555,26 +598,39 @@ def cmd_bot_start():
|
|
|
555
598
|
existing_pid = TunnelMateBot.get_running_pid(settings)
|
|
556
599
|
if existing_pid:
|
|
557
600
|
console.print(f"[bold yellow]Telegram Bot is already running in background (PID: {existing_pid}).[/bold yellow]")
|
|
601
|
+
if not settings.bot_auto_start:
|
|
602
|
+
settings.bot_auto_start = True
|
|
603
|
+
save_settings(settings)
|
|
558
604
|
return
|
|
559
605
|
|
|
560
606
|
# Spawn bot process in background
|
|
561
607
|
cmd = [sys.executable, "-m", "tunnelmate.bot.telegram_bot"]
|
|
562
608
|
spawn_background_process(cmd)
|
|
563
609
|
|
|
610
|
+
# Persist autostart state
|
|
611
|
+
settings.bot_auto_start = True
|
|
612
|
+
save_settings(settings)
|
|
613
|
+
|
|
564
614
|
time.sleep(1.0)
|
|
565
615
|
new_pid = TunnelMateBot.get_running_pid(settings)
|
|
566
616
|
if new_pid:
|
|
567
617
|
console.print(f"[bold green]✔ TunnelMate Telegram bot started in background (PID: {new_pid}).[/bold green]")
|
|
618
|
+
console.print("[dim]Auto-restore on system restart: Enabled[/dim]")
|
|
568
619
|
else:
|
|
569
620
|
console.print("[bold red]Failed to start bot in background. Try 'tunnelmate bot run' to view logs.[/bold red]")
|
|
570
621
|
|
|
571
622
|
|
|
572
623
|
@bot_app.command(name="stop")
|
|
573
624
|
def cmd_bot_stop():
|
|
574
|
-
"""Stop the background Telegram Bot process."""
|
|
625
|
+
"""Stop the background Telegram Bot process and disable auto-restore."""
|
|
575
626
|
from tunnelmate.bot.telegram_bot import TunnelMateBot
|
|
576
627
|
settings = load_settings()
|
|
577
628
|
pid = TunnelMateBot.get_running_pid(settings)
|
|
629
|
+
|
|
630
|
+
# Set bot_auto_start to False
|
|
631
|
+
settings.bot_auto_start = False
|
|
632
|
+
save_settings(settings)
|
|
633
|
+
|
|
578
634
|
if not pid:
|
|
579
635
|
console.print("[dim]Telegram Bot is not currently running.[/dim]")
|
|
580
636
|
return
|
|
@@ -34,6 +34,7 @@ class Settings(BaseModel):
|
|
|
34
34
|
admin_password_salt: Optional[str] = None
|
|
35
35
|
admin_chat_ids: List[int] = Field(default_factory=list)
|
|
36
36
|
allow_public_subscribers: bool = True
|
|
37
|
+
bot_auto_start: bool = False
|
|
37
38
|
|
|
38
39
|
# Supervision & Automation
|
|
39
40
|
health_check_interval: int = 60 # seconds
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Cross-platform system boot and reboot autostart management for TunnelMate daemon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_daemon_exec_cmd() -> str:
|
|
15
|
+
"""Return the optimal command string to execute the daemon."""
|
|
16
|
+
which_tm = shutil.which("tunnelmate")
|
|
17
|
+
if which_tm:
|
|
18
|
+
return f"{which_tm} daemon run"
|
|
19
|
+
return f"{sys.executable} -m tunnelmate.daemon.service"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_autostart_enabled() -> Tuple[bool, str]:
|
|
23
|
+
"""Check if TunnelMate daemon is configured to start on system boot."""
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
try:
|
|
26
|
+
import winreg
|
|
27
|
+
|
|
28
|
+
key = winreg.OpenKey(
|
|
29
|
+
winreg.HKEY_CURRENT_USER,
|
|
30
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
31
|
+
0,
|
|
32
|
+
winreg.KEY_READ,
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
val, _ = winreg.QueryValueEx(key, "TunnelMateDaemon")
|
|
36
|
+
return True, f"Windows Registry Run key configured: {val}"
|
|
37
|
+
finally:
|
|
38
|
+
winreg.CloseKey(key)
|
|
39
|
+
except OSError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# Check schtasks
|
|
43
|
+
try:
|
|
44
|
+
res = subprocess.run(
|
|
45
|
+
["schtasks", "/Query", "/TN", "TunnelMateDaemon"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
if res.returncode == 0:
|
|
50
|
+
return True, "Windows Scheduled Task 'TunnelMateDaemon' active"
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
return False, "Autostart is not configured on Windows."
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
# Linux / Unix
|
|
58
|
+
if shutil.which("systemctl"):
|
|
59
|
+
try:
|
|
60
|
+
res = subprocess.run(
|
|
61
|
+
["systemctl", "--user", "is-enabled", "tunnelmate-daemon.service"],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
)
|
|
65
|
+
if res.returncode == 0 and "enabled" in res.stdout.lower():
|
|
66
|
+
return True, "systemd user service 'tunnelmate-daemon' is enabled"
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# Check crontab
|
|
71
|
+
if shutil.which("crontab"):
|
|
72
|
+
try:
|
|
73
|
+
res = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
|
|
74
|
+
if res.returncode == 0 and "@reboot" in res.stdout and "tunnelmate" in res.stdout:
|
|
75
|
+
return True, "Cron @reboot entry configured"
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
return False, "Autostart is not enabled on Linux."
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def enable_autostart() -> Tuple[bool, str]:
|
|
83
|
+
"""Configure system to start TunnelMate daemon automatically on boot/restart."""
|
|
84
|
+
exec_cmd = _get_daemon_exec_cmd()
|
|
85
|
+
|
|
86
|
+
if sys.platform == "win32":
|
|
87
|
+
details = []
|
|
88
|
+
# 1. Register in HKCU Run Key
|
|
89
|
+
try:
|
|
90
|
+
import winreg
|
|
91
|
+
|
|
92
|
+
key = winreg.OpenKey(
|
|
93
|
+
winreg.HKEY_CURRENT_USER,
|
|
94
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
95
|
+
0,
|
|
96
|
+
winreg.KEY_SET_VALUE,
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
# Use pythonw if available for windowless execution on Windows
|
|
100
|
+
pythonw = Path(sys.executable).parent / "pythonw.exe"
|
|
101
|
+
run_target = (
|
|
102
|
+
f'"{pythonw}" -m tunnelmate.daemon.service'
|
|
103
|
+
if pythonw.exists()
|
|
104
|
+
else f'"{sys.executable}" -m tunnelmate.daemon.service'
|
|
105
|
+
)
|
|
106
|
+
winreg.SetValueEx(key, "TunnelMateDaemon", 0, winreg.REG_SZ, run_target)
|
|
107
|
+
details.append("Windows Registry Run key added")
|
|
108
|
+
finally:
|
|
109
|
+
winreg.CloseKey(key)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
details.append(f"Registry error: {e}")
|
|
112
|
+
|
|
113
|
+
# 2. Add Task Scheduler task
|
|
114
|
+
try:
|
|
115
|
+
cmd = [
|
|
116
|
+
"schtasks",
|
|
117
|
+
"/Create",
|
|
118
|
+
"/TN",
|
|
119
|
+
"TunnelMateDaemon",
|
|
120
|
+
"/TR",
|
|
121
|
+
f'"{sys.executable}" -m tunnelmate.daemon.service',
|
|
122
|
+
"/SC",
|
|
123
|
+
"ONLOGON",
|
|
124
|
+
"/F",
|
|
125
|
+
]
|
|
126
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
127
|
+
if res.returncode == 0:
|
|
128
|
+
details.append("Scheduled Task 'TunnelMateDaemon' created")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
return True, f"Auto-start successfully enabled on Windows ({'; '.join(details)})."
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
# Linux / Unix
|
|
136
|
+
systemd_configured = False
|
|
137
|
+
linger_configured = False
|
|
138
|
+
cron_configured = False
|
|
139
|
+
|
|
140
|
+
if shutil.which("systemctl"):
|
|
141
|
+
try:
|
|
142
|
+
user_unit_dir = Path.home() / ".config" / "systemd" / "user"
|
|
143
|
+
user_unit_dir.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
service_file = user_unit_dir / "tunnelmate-daemon.service"
|
|
145
|
+
|
|
146
|
+
unit_content = f"""[Unit]
|
|
147
|
+
Description=TunnelMate Cloudflare Quick Tunnel Daemon
|
|
148
|
+
Documentation=https://github.com/tunnelmate/tunnelmate
|
|
149
|
+
After=network-online.target
|
|
150
|
+
Wants=network-online.target
|
|
151
|
+
|
|
152
|
+
[Service]
|
|
153
|
+
Type=simple
|
|
154
|
+
ExecStart={exec_cmd}
|
|
155
|
+
Restart=always
|
|
156
|
+
RestartSec=5s
|
|
157
|
+
Environment="PYTHONUNBUFFERED=1"
|
|
158
|
+
|
|
159
|
+
[Install]
|
|
160
|
+
WantedBy=default.target
|
|
161
|
+
"""
|
|
162
|
+
service_file.write_text(unit_content, encoding="utf-8")
|
|
163
|
+
|
|
164
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
165
|
+
res_enable = subprocess.run(
|
|
166
|
+
["systemctl", "--user", "enable", "tunnelmate-daemon.service"],
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
)
|
|
170
|
+
if res_enable.returncode == 0:
|
|
171
|
+
systemd_configured = True
|
|
172
|
+
# Also start now if stopped
|
|
173
|
+
subprocess.run(
|
|
174
|
+
["systemctl", "--user", "start", "tunnelmate-daemon.service"],
|
|
175
|
+
capture_output=True,
|
|
176
|
+
)
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
# Configure user lingering so user service boots even before manual login
|
|
181
|
+
if shutil.which("loginctl"):
|
|
182
|
+
try:
|
|
183
|
+
username = getpass.getuser()
|
|
184
|
+
res_linger = subprocess.run(
|
|
185
|
+
["loginctl", "enable-linger", username],
|
|
186
|
+
capture_output=True,
|
|
187
|
+
text=True,
|
|
188
|
+
)
|
|
189
|
+
if res_linger.returncode == 0:
|
|
190
|
+
linger_configured = True
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
# Fallback / supplementary crontab @reboot entry
|
|
195
|
+
if shutil.which("crontab"):
|
|
196
|
+
try:
|
|
197
|
+
res_cron = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
|
|
198
|
+
existing = res_cron.stdout if res_cron.returncode == 0 else ""
|
|
199
|
+
cron_line = f"@reboot {exec_cmd} >/dev/null 2>&1"
|
|
200
|
+
if cron_line not in existing:
|
|
201
|
+
new_cron = (existing.strip() + f"\n{cron_line}\n").strip() + "\n"
|
|
202
|
+
proc = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE)
|
|
203
|
+
proc.communicate(new_cron.encode("utf-8"))
|
|
204
|
+
if proc.returncode == 0:
|
|
205
|
+
cron_configured = True
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
msgs = []
|
|
210
|
+
if systemd_configured:
|
|
211
|
+
msgs.append("systemd user service enabled")
|
|
212
|
+
if linger_configured:
|
|
213
|
+
msgs.append("loginctl lingering enabled for boot persistence")
|
|
214
|
+
if cron_configured:
|
|
215
|
+
msgs.append("@reboot cron schedule registered")
|
|
216
|
+
|
|
217
|
+
if msgs:
|
|
218
|
+
return True, f"Auto-start successfully enabled: {', '.join(msgs)}."
|
|
219
|
+
return False, "Failed to register autostart via systemd or crontab."
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def disable_autostart() -> Tuple[bool, str]:
|
|
223
|
+
"""Remove TunnelMate daemon from system boot / restart autostart."""
|
|
224
|
+
if sys.platform == "win32":
|
|
225
|
+
details = []
|
|
226
|
+
try:
|
|
227
|
+
import winreg
|
|
228
|
+
|
|
229
|
+
key = winreg.OpenKey(
|
|
230
|
+
winreg.HKEY_CURRENT_USER,
|
|
231
|
+
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
232
|
+
0,
|
|
233
|
+
winreg.KEY_SET_VALUE,
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
winreg.DeleteValue(key, "TunnelMateDaemon")
|
|
237
|
+
details.append("Registry Run key removed")
|
|
238
|
+
finally:
|
|
239
|
+
winreg.CloseKey(key)
|
|
240
|
+
except OSError:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
subprocess.run(
|
|
245
|
+
["schtasks", "/Delete", "/TN", "TunnelMateDaemon", "/F"],
|
|
246
|
+
capture_output=True,
|
|
247
|
+
)
|
|
248
|
+
details.append("Scheduled task removed")
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
return True, "Auto-start disabled on Windows."
|
|
253
|
+
|
|
254
|
+
else:
|
|
255
|
+
# Linux
|
|
256
|
+
details = []
|
|
257
|
+
if shutil.which("systemctl"):
|
|
258
|
+
try:
|
|
259
|
+
subprocess.run(
|
|
260
|
+
["systemctl", "--user", "disable", "tunnelmate-daemon.service"],
|
|
261
|
+
capture_output=True,
|
|
262
|
+
)
|
|
263
|
+
subprocess.run(
|
|
264
|
+
["systemctl", "--user", "stop", "tunnelmate-daemon.service"],
|
|
265
|
+
capture_output=True,
|
|
266
|
+
)
|
|
267
|
+
details.append("systemd user service disabled")
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
if shutil.which("crontab"):
|
|
272
|
+
try:
|
|
273
|
+
res_cron = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
|
|
274
|
+
if res_cron.returncode == 0 and "tunnelmate" in res_cron.stdout:
|
|
275
|
+
lines = [l for l in res_cron.stdout.splitlines() if "tunnelmate" not in l]
|
|
276
|
+
new_cron = "\n".join(lines).strip() + "\n" if lines else ""
|
|
277
|
+
proc = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE)
|
|
278
|
+
proc.communicate(new_cron.encode("utf-8"))
|
|
279
|
+
details.append("crontab entry removed")
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
return True, f"Auto-start disabled ({', '.join(details) if details else 'None'})."
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
14
|
from tunnelmate.config import Settings, load_settings
|
|
15
|
-
from tunnelmate.core.process import is_pid_alive
|
|
15
|
+
from tunnelmate.core.process import is_pid_alive, spawn_background_process
|
|
16
16
|
from tunnelmate.core.supervisor import TunnelSupervisor
|
|
17
17
|
from tunnelmate.daemon.server import DaemonIPCServer
|
|
18
18
|
from tunnelmate.db.repository import Repository
|
|
@@ -66,6 +66,11 @@ class DaemonService:
|
|
|
66
66
|
while not self._stop_event.is_set():
|
|
67
67
|
try:
|
|
68
68
|
await asyncio.sleep(interval)
|
|
69
|
+
# Keep settings synced with disk (e.g., bot_auto_start changes)
|
|
70
|
+
try:
|
|
71
|
+
self.settings = load_settings()
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
69
74
|
tunnels = await asyncio.to_thread(self.supervisor.list_tunnels)
|
|
70
75
|
for t in tunnels:
|
|
71
76
|
if t.status.value == "running":
|
|
@@ -73,6 +78,17 @@ class DaemonService:
|
|
|
73
78
|
await asyncio.to_thread(self.supervisor.check_health, t.id)
|
|
74
79
|
except Exception as e:
|
|
75
80
|
logger.debug("Health check failed for %s: %s", t.name, e)
|
|
81
|
+
|
|
82
|
+
# Auto-recover Telegram Bot if bot_auto_start is True and bot process exited
|
|
83
|
+
if self.settings.bot_auto_start and self.settings.telegram_bot_token:
|
|
84
|
+
from tunnelmate.bot.telegram_bot import TunnelMateBot
|
|
85
|
+
if not TunnelMateBot.get_running_pid(self.settings):
|
|
86
|
+
logger.info("Telegram Bot was active but process exited. Auto-recovering...")
|
|
87
|
+
try:
|
|
88
|
+
cmd = [sys.executable, "-m", "tunnelmate.bot.telegram_bot"]
|
|
89
|
+
spawn_background_process(cmd)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug("Could not recover Telegram Bot: %s", e)
|
|
76
92
|
except asyncio.CancelledError:
|
|
77
93
|
break
|
|
78
94
|
except Exception as e:
|
|
@@ -102,16 +118,28 @@ class DaemonService:
|
|
|
102
118
|
# Start periodic health task
|
|
103
119
|
self._health_task = asyncio.create_task(self._health_loop())
|
|
104
120
|
|
|
105
|
-
# Auto-restart tunnels marked as running previously
|
|
121
|
+
# Auto-restart tunnels marked as running previously
|
|
106
122
|
try:
|
|
107
123
|
tunnels = await asyncio.to_thread(self.supervisor.list_tunnels)
|
|
108
124
|
for t in tunnels:
|
|
109
|
-
if t.
|
|
125
|
+
if t.status.value in ("running", "starting", "restarting"):
|
|
110
126
|
logger.info("Restoring previously active tunnel: %s", t.name)
|
|
111
127
|
asyncio.create_task(asyncio.to_thread(self.supervisor.start_tunnel, t.id))
|
|
112
128
|
except Exception as e:
|
|
113
129
|
logger.warning("Could not restore previous tunnels: %s", e)
|
|
114
130
|
|
|
131
|
+
# Auto-restore Telegram Bot if it was previously active
|
|
132
|
+
if self.settings.bot_auto_start and self.settings.telegram_bot_token:
|
|
133
|
+
from tunnelmate.bot.telegram_bot import TunnelMateBot
|
|
134
|
+
existing_bot_pid = TunnelMateBot.get_running_pid(self.settings)
|
|
135
|
+
if not existing_bot_pid:
|
|
136
|
+
logger.info("Restoring previously active Telegram Bot in background...")
|
|
137
|
+
try:
|
|
138
|
+
cmd = [sys.executable, "-m", "tunnelmate.bot.telegram_bot"]
|
|
139
|
+
spawn_background_process(cmd)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning("Could not auto-restore Telegram Bot: %s", e)
|
|
142
|
+
|
|
115
143
|
# Wait until stopped
|
|
116
144
|
await self._stop_event.wait()
|
|
117
145
|
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""Tests for cross-platform process management and Windows/Linux compatibility."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from tunnelmate.core.process import is_pid_alive, spawn_background_process, terminate_process_by_pid
|
|
7
|
-
from tunnelmate.core.cloudflared import find_cloudflared
|
|
8
|
-
from tunnelmate.cli.ssh_client import clean_ssh_target
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_is_pid_alive():
|
|
12
|
-
# Current process must be alive
|
|
13
|
-
assert is_pid_alive(os.getpid()) is True
|
|
14
|
-
# Non-existent PID must not be alive
|
|
15
|
-
assert is_pid_alive(99999999) is False
|
|
16
|
-
assert is_pid_alive(-1) is False
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_spawn_and_terminate_process():
|
|
20
|
-
# Launch a short-lived python background sleep process
|
|
21
|
-
cmd = [sys.executable, "-c", "import time; time.sleep(10)"]
|
|
22
|
-
proc = spawn_background_process(cmd)
|
|
23
|
-
pid = proc.pid
|
|
24
|
-
assert pid is not None
|
|
25
|
-
assert is_pid_alive(pid) is True
|
|
26
|
-
|
|
27
|
-
# Terminate process safely using cross-platform helper
|
|
28
|
-
terminated = terminate_process_by_pid(pid, timeout=3.0)
|
|
29
|
-
assert terminated is True
|
|
30
|
-
assert is_pid_alive(pid) is False
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_clean_ssh_target_windows_formats():
|
|
34
|
-
assert clean_ssh_target("shashan@subjective.trycloudflare.com") == "shashan@subjective.trycloudflare.com"
|
|
35
|
-
assert clean_ssh_target("shashan@https://subjective.trycloudflare.com/") == "shashan@subjective.trycloudflare.com"
|
|
36
|
-
assert clean_ssh_target("https://subjective.trycloudflare.com:22") == "subjective.trycloudflare.com"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|