sing-box-cli 0.0.3__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.
- sing_box_cli-0.0.3.dist-info/METADATA +75 -0
- sing_box_cli-0.0.3.dist-info/RECORD +10 -0
- sing_box_cli-0.0.3.dist-info/WHEEL +4 -0
- sing_box_cli-0.0.3.dist-info/entry_points.txt +2 -0
- sing_box_cli-0.0.3.dist-info/licenses/LICENSE +21 -0
- sing_box_service/__init__.py +5 -0
- sing_box_service/config.py +94 -0
- sing_box_service/main.py +144 -0
- sing_box_service/py.typed +0 -0
- sing_box_service/service.py +172 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sing-box-cli
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: cross-platform sing-box-service manager.
|
|
5
|
+
Project-URL: Homepage, https://github.com/aittcuszz/sing-box-service
|
|
6
|
+
Project-URL: Issues, https://github.com/atticuszz/sing-box-service/issues
|
|
7
|
+
Project-URL: documentation, https://atticuszz.github.io/sing-box-service/
|
|
8
|
+
Project-URL: Changelog, https://github.com/atticuszz/sing-box-service/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: "A.J.Zeller" <zhouge1831@gmail.com>
|
|
10
|
+
Maintainer-email: "A.J.Zeller" <zhouge1831@gmail.com>
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2024 A.J.Zeller
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python
|
|
35
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Requires-Dist: psutil>=6.1.1
|
|
42
|
+
Requires-Dist: requests>=2.32.3
|
|
43
|
+
Requires-Dist: typer>=0.15.1
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# Sing-Box CLI
|
|
47
|
+
|
|
48
|
+
cross-platform sing-box service manager
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
### init UV
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# root
|
|
58
|
+
sudo su
|
|
59
|
+
# install as root
|
|
60
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
61
|
+
# create link
|
|
62
|
+
ln -s $(which uv) /usr/local/bin/uv
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### add sing-box-service
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
sudo uv tool install sing-box-cli
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Run
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
sudo uvx sing-box --help
|
|
75
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sing_box_service/__init__.py,sha256=eYKke1kgc4teyR5wOBo7lsh1KjHpliVV1XpEbnK9nYE,66
|
|
2
|
+
sing_box_service/config.py,sha256=oTEzYRHQvRC3kPvxtaaVm-QgEzJUnPcaoBWJyTQqve8,3340
|
|
3
|
+
sing_box_service/main.py,sha256=c0KBPnd4dYVkyolZ9rUivOCwIRMQzM1lct3wST4R9C8,3636
|
|
4
|
+
sing_box_service/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
sing_box_service/service.py,sha256=SXB6lz0oNMgVkRNXWeo-zqRoxAefqLbcX-ZuspR5DvQ,5236
|
|
6
|
+
sing_box_cli-0.0.3.dist-info/METADATA,sha256=Gfu9arQeZ9LUg5msbKsbGNJBKOqIU1l7QGxIhspUUJA,2654
|
|
7
|
+
sing_box_cli-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
sing_box_cli-0.0.3.dist-info/entry_points.txt,sha256=30dsIhKb2r4vXsdW9j6F4qyDZF0YL-5wV74iKYyYhv0,55
|
|
9
|
+
sing_box_cli-0.0.3.dist-info/licenses/LICENSE,sha256=QeGTPA1ps6IxVztxQ62u7-x-oEe1S_fvleYlm5VlBc8,1067
|
|
10
|
+
sing_box_cli-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 A.J.Zeller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from rich import print
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Config:
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.is_windows = platform.system() == "Windows"
|
|
13
|
+
user = (
|
|
14
|
+
os.environ.get("SUDO_USER")
|
|
15
|
+
or os.environ.get("USER")
|
|
16
|
+
or os.environ.get("USERNAME")
|
|
17
|
+
)
|
|
18
|
+
if not user:
|
|
19
|
+
raise ValueError("❌ Unable to detect user name")
|
|
20
|
+
|
|
21
|
+
self.user = user
|
|
22
|
+
bin_filename = "sing-box.exe" if self.is_windows else "sing-box"
|
|
23
|
+
bin_path = shutil.which(bin_filename)
|
|
24
|
+
if not bin_path:
|
|
25
|
+
raise FileNotFoundError(f"❌ {bin_filename} not found in PATH")
|
|
26
|
+
|
|
27
|
+
self.bin_path = Path(bin_path)
|
|
28
|
+
|
|
29
|
+
if self.is_windows:
|
|
30
|
+
self.install_dir = Path(os.environ["ProgramFiles"]) / "sing-box"
|
|
31
|
+
else:
|
|
32
|
+
self.user_home = Path(os.path.expanduser(f"~{self.user}"))
|
|
33
|
+
self.install_dir = self.user_home / "proxy/sing-box"
|
|
34
|
+
|
|
35
|
+
self.config_file = self.install_dir / "config.json"
|
|
36
|
+
self.subscription_file = self.install_dir / "subscription.txt"
|
|
37
|
+
|
|
38
|
+
def init_directories(self) -> None:
|
|
39
|
+
self.install_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
if not self.config_file.exists():
|
|
41
|
+
self.config_file.write_text("{}")
|
|
42
|
+
print(f"📁 Created empty config file: {self.config_file}")
|
|
43
|
+
|
|
44
|
+
if not self.subscription_file.exists():
|
|
45
|
+
self.subscription_file.touch()
|
|
46
|
+
print(f"📁 Created subscription file: {self.subscription_file}")
|
|
47
|
+
|
|
48
|
+
if not self.is_windows:
|
|
49
|
+
shutil.chown(self.install_dir, user=self.user, group=self.user)
|
|
50
|
+
shutil.chown(self.config_file, user=self.user, group=self.user)
|
|
51
|
+
shutil.chown(self.subscription_file, user=self.user, group=self.user)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def sub_url(self) -> str:
|
|
55
|
+
if not self.subscription_file.exists():
|
|
56
|
+
return ""
|
|
57
|
+
return self.subscription_file.read_text().strip()
|
|
58
|
+
|
|
59
|
+
def update_config(self) -> bool:
|
|
60
|
+
if not self.sub_url:
|
|
61
|
+
print("❌ No valid subscription URL found.")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
print(f"⌛ Updating configuration from {self.sub_url}")
|
|
65
|
+
try:
|
|
66
|
+
response = requests.get(self.sub_url)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
self.config_file.write_text(response.text)
|
|
69
|
+
if not self.is_windows:
|
|
70
|
+
shutil.chown(self.config_file, user=self.user, group=self.user)
|
|
71
|
+
print("✅ Configuration updated successfully.")
|
|
72
|
+
return True
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"❌ Failed to update configuration: {e}")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def add_subscription(self, url: str) -> bool:
|
|
78
|
+
if not url.startswith(("http://", "https://")):
|
|
79
|
+
print("❌ Invalid URL format.")
|
|
80
|
+
return False
|
|
81
|
+
self.subscription_file.write_text(url)
|
|
82
|
+
print("📁 Subscription added successfully.")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def show_config(self) -> None:
|
|
86
|
+
print(f"📄 Configuration file: {self.config_file}")
|
|
87
|
+
print(self.config_file.read_text())
|
|
88
|
+
|
|
89
|
+
def show_subscription(self) -> None:
|
|
90
|
+
if self.sub_url:
|
|
91
|
+
print("🔗 Current subscription URL:")
|
|
92
|
+
print(self.sub_url)
|
|
93
|
+
else:
|
|
94
|
+
print("❌ No subscription URL found.")
|
sing_box_service/main.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .service import LinuxServiceManager, WindowsServiceManager
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="sing-box service manager.")
|
|
12
|
+
service = typer.Typer(help="Service management commands")
|
|
13
|
+
config = typer.Typer(help="Configuration management commands")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app.add_typer(service, name="service")
|
|
17
|
+
app.add_typer(config, name="config")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SingBoxCLI:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.config = Config()
|
|
23
|
+
self.service = (
|
|
24
|
+
WindowsServiceManager(self.config)
|
|
25
|
+
if self.config.is_windows
|
|
26
|
+
else LinuxServiceManager(self.config)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def ensure_root(self) -> None:
|
|
30
|
+
"""https://gist.github.com/RDCH106/fdd419ef7dd803932b16056aab1d2300"""
|
|
31
|
+
try:
|
|
32
|
+
if os.geteuid() != 0:
|
|
33
|
+
print("⚠️ This script must be run as root.")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
except AttributeError:
|
|
36
|
+
import ctypes
|
|
37
|
+
|
|
38
|
+
if not ctypes.windll.shell32.IsUserAnAdmin(): # type: ignore
|
|
39
|
+
print("⚠️ This script must be run as Administrator.")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@service.command("start")
|
|
44
|
+
def service_start() -> None:
|
|
45
|
+
"""Start sing-box service"""
|
|
46
|
+
cli = SingBoxCLI()
|
|
47
|
+
cli.ensure_root()
|
|
48
|
+
cli.config.init_directories()
|
|
49
|
+
cli.service.init_service()
|
|
50
|
+
cli.service.start()
|
|
51
|
+
print("✅ Service started.")
|
|
52
|
+
print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
|
|
53
|
+
print("🔌 Default API: http://127.0.0.1:9090")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@service.command("stop")
|
|
57
|
+
def service_stop() -> None:
|
|
58
|
+
"""Stop sing-box service"""
|
|
59
|
+
cli = SingBoxCLI()
|
|
60
|
+
cli.ensure_root()
|
|
61
|
+
cli.service.stop()
|
|
62
|
+
print("✋ Service stopped.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@service.command("restart")
|
|
66
|
+
def service_restart() -> None:
|
|
67
|
+
"""Restart sing-box service"""
|
|
68
|
+
cli = SingBoxCLI()
|
|
69
|
+
cli.ensure_root()
|
|
70
|
+
cli.service.init_service()
|
|
71
|
+
cli.service.restart()
|
|
72
|
+
print("✅ Service restarted.")
|
|
73
|
+
print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
|
|
74
|
+
print("🔌 Default API: http://127.0.0.1:9090")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@service.command("status")
|
|
78
|
+
def service_status() -> None:
|
|
79
|
+
"""Check service status"""
|
|
80
|
+
cli = SingBoxCLI()
|
|
81
|
+
cli.ensure_root()
|
|
82
|
+
status = cli.service.status()
|
|
83
|
+
print(f"🏃 Service status: {status}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@service.command("disable")
|
|
87
|
+
def service_disable() -> None:
|
|
88
|
+
"""Disable sing-box service autostart"""
|
|
89
|
+
cli = SingBoxCLI()
|
|
90
|
+
cli.ensure_root()
|
|
91
|
+
cli.service.stop()
|
|
92
|
+
cli.service.disable()
|
|
93
|
+
print("✋ Autostart disabled.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@config.command("add-sub")
|
|
97
|
+
def config_add_sub(url: str) -> None:
|
|
98
|
+
"""Add subscription URL"""
|
|
99
|
+
cli = SingBoxCLI()
|
|
100
|
+
cli.ensure_root()
|
|
101
|
+
if cli.config.add_subscription(url):
|
|
102
|
+
if cli.config.update_config():
|
|
103
|
+
cli.service.restart()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@config.command("update")
|
|
107
|
+
def config_update() -> None:
|
|
108
|
+
"""Update configuration from subscription URL"""
|
|
109
|
+
cli = SingBoxCLI()
|
|
110
|
+
cli.ensure_root()
|
|
111
|
+
if cli.config.update_config():
|
|
112
|
+
cli.service.restart()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@config.command("show-sub")
|
|
116
|
+
def config_show_sub() -> None:
|
|
117
|
+
"""Show subscription URL"""
|
|
118
|
+
cli = SingBoxCLI()
|
|
119
|
+
cli.config.show_subscription()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@config.command("show")
|
|
123
|
+
def config_show() -> None:
|
|
124
|
+
"""Show configuration"""
|
|
125
|
+
cli = SingBoxCLI()
|
|
126
|
+
cli.config.show_config()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def logs() -> None:
|
|
131
|
+
"""Show sing-box service logs"""
|
|
132
|
+
cli = SingBoxCLI()
|
|
133
|
+
if cli.config.is_windows:
|
|
134
|
+
print("⚠️ Log viewing is not supported on Windows")
|
|
135
|
+
else:
|
|
136
|
+
subprocess.run(["journalctl", "-u", "sing-box", "-o", "cat", "-f"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main() -> None:
|
|
140
|
+
app()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import psutil
|
|
5
|
+
from rich import print
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServiceManager:
|
|
11
|
+
def __init__(self, config: Config):
|
|
12
|
+
self.config = config
|
|
13
|
+
|
|
14
|
+
def create_service(self) -> None:
|
|
15
|
+
raise NotImplementedError()
|
|
16
|
+
|
|
17
|
+
def check_service(self) -> bool:
|
|
18
|
+
raise NotImplementedError()
|
|
19
|
+
|
|
20
|
+
def init_service(self) -> None:
|
|
21
|
+
if not self.check_service():
|
|
22
|
+
print("⌛ Creating service...")
|
|
23
|
+
self.create_service()
|
|
24
|
+
|
|
25
|
+
def start(self) -> None:
|
|
26
|
+
raise NotImplementedError()
|
|
27
|
+
|
|
28
|
+
def stop(self) -> None:
|
|
29
|
+
raise NotImplementedError()
|
|
30
|
+
|
|
31
|
+
def restart(self) -> None:
|
|
32
|
+
raise NotImplementedError()
|
|
33
|
+
|
|
34
|
+
def status(self) -> str:
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
37
|
+
def disable(self) -> None:
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WindowsServiceManager(ServiceManager):
|
|
42
|
+
def __init__(self, config: Config):
|
|
43
|
+
super().__init__(config)
|
|
44
|
+
self.task_name = "sing-box"
|
|
45
|
+
|
|
46
|
+
def create_service(self) -> None:
|
|
47
|
+
start_script = self.config.install_dir / "start-singbox.ps1"
|
|
48
|
+
script_content = f"""
|
|
49
|
+
Add-Type -Name Window -Namespace Console -MemberDefinition '
|
|
50
|
+
[DllImport("Kernel32.dll")]
|
|
51
|
+
public static extern IntPtr GetConsoleWindow();
|
|
52
|
+
[DllImport("user32.dll")]
|
|
53
|
+
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
|
|
54
|
+
'
|
|
55
|
+
$console = [Console.Window]::GetConsoleWindow()
|
|
56
|
+
[Console.Window]::ShowWindow($console, 0)
|
|
57
|
+
|
|
58
|
+
Set-Location "{self.config.install_dir}"
|
|
59
|
+
& "{self.config.bin_path}" tools synctime -w -C "{self.config.install_dir}"
|
|
60
|
+
& "{self.config.bin_path}" run -C "{self.config.install_dir}"
|
|
61
|
+
"""
|
|
62
|
+
start_script.write_text(script_content)
|
|
63
|
+
|
|
64
|
+
subprocess.run(
|
|
65
|
+
[
|
|
66
|
+
"schtasks",
|
|
67
|
+
"/create",
|
|
68
|
+
"/tn",
|
|
69
|
+
self.task_name,
|
|
70
|
+
"/tr",
|
|
71
|
+
f"powershell -ExecutionPolicy Bypass -File {start_script}",
|
|
72
|
+
"/sc",
|
|
73
|
+
"onlogon",
|
|
74
|
+
"/ru",
|
|
75
|
+
self.config.user,
|
|
76
|
+
"/rl",
|
|
77
|
+
"HIGHEST",
|
|
78
|
+
"/f",
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def check_service(self) -> bool:
|
|
83
|
+
try:
|
|
84
|
+
subprocess.run(
|
|
85
|
+
["schtasks", "/query", "/tn", self.task_name],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
check=True,
|
|
88
|
+
)
|
|
89
|
+
return True
|
|
90
|
+
except subprocess.CalledProcessError:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def start(self) -> None:
|
|
94
|
+
subprocess.run(["schtasks", "/run", "/tn", self.task_name])
|
|
95
|
+
|
|
96
|
+
def stop(self) -> None:
|
|
97
|
+
subprocess.run(["schtasks", "/end", "/tn", self.task_name])
|
|
98
|
+
for proc in psutil.process_iter(["name"]):
|
|
99
|
+
if proc.info["name"] == "sing-box.exe":
|
|
100
|
+
proc.kill()
|
|
101
|
+
|
|
102
|
+
def restart(self) -> None:
|
|
103
|
+
self.stop()
|
|
104
|
+
self.start()
|
|
105
|
+
|
|
106
|
+
def status(self) -> str:
|
|
107
|
+
try:
|
|
108
|
+
output = subprocess.check_output(
|
|
109
|
+
["schtasks", "/query", "/tn", self.task_name]
|
|
110
|
+
)
|
|
111
|
+
return "Running" if "Running" in output.decode() else "Stopped"
|
|
112
|
+
except Exception:
|
|
113
|
+
return "Not installed"
|
|
114
|
+
|
|
115
|
+
def disable(self) -> None:
|
|
116
|
+
self.stop()
|
|
117
|
+
subprocess.run(["schtasks", "/delete", "/tn", self.task_name, "/f"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LinuxServiceManager(ServiceManager):
|
|
121
|
+
def __init__(self, config: Config):
|
|
122
|
+
super().__init__(config)
|
|
123
|
+
self.service_name = "sing-box"
|
|
124
|
+
self.service_file = Path("/etc/systemd/system/sing-box.service")
|
|
125
|
+
|
|
126
|
+
def create_service(self) -> None:
|
|
127
|
+
service_content = f"""
|
|
128
|
+
[Unit]
|
|
129
|
+
Description=sing-box service
|
|
130
|
+
Documentation=https://sing-box.sagernet.org
|
|
131
|
+
After=network.target nss-lookup.target
|
|
132
|
+
|
|
133
|
+
[Service]
|
|
134
|
+
Type=simple
|
|
135
|
+
LimitNPROC=500
|
|
136
|
+
LimitNOFILE=1000000
|
|
137
|
+
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
|
138
|
+
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
|
139
|
+
Restart=always
|
|
140
|
+
ExecStartPre={self.config.bin_path} tools synctime -w -C {self.config.install_dir}
|
|
141
|
+
ExecStart={self.config.bin_path} run -C {self.config.install_dir}
|
|
142
|
+
ExecReload=/bin/kill -HUP $MAINPID
|
|
143
|
+
|
|
144
|
+
[Install]
|
|
145
|
+
WantedBy=multi-user.target
|
|
146
|
+
"""
|
|
147
|
+
self.service_file.write_text(service_content)
|
|
148
|
+
subprocess.run(["systemctl", "daemon-reload"])
|
|
149
|
+
subprocess.run(["systemctl", "enable", self.service_name])
|
|
150
|
+
|
|
151
|
+
def check_service(self) -> bool:
|
|
152
|
+
return self.service_file.exists()
|
|
153
|
+
|
|
154
|
+
def start(self) -> None:
|
|
155
|
+
subprocess.run(["systemctl", "start", self.service_name])
|
|
156
|
+
|
|
157
|
+
def stop(self) -> None:
|
|
158
|
+
subprocess.run(["systemctl", "stop", self.service_name])
|
|
159
|
+
|
|
160
|
+
def restart(self) -> None:
|
|
161
|
+
subprocess.run(["systemctl", "restart", self.service_name])
|
|
162
|
+
|
|
163
|
+
def status(self) -> str:
|
|
164
|
+
try:
|
|
165
|
+
subprocess.check_call(["systemctl", "is-active", self.service_name])
|
|
166
|
+
return "Running"
|
|
167
|
+
except Exception:
|
|
168
|
+
return "Stopped"
|
|
169
|
+
|
|
170
|
+
def disable(self) -> None:
|
|
171
|
+
self.stop()
|
|
172
|
+
subprocess.run(["systemctl", "disable", self.service_name])
|