Repository navigation
Expand file tree
/
Copy pathcloud_service.py
More file actions
378 lines (335 loc) · 15.1 KB
/
Copy pathcloud_service.py
File metadata and controls
378 lines (335 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""Windows Service wrapper for the cloud antivirus server.
This allows the server to run 24/7 as a Windows service that:
- Auto-starts on boot
- Survives logoffs
- Restarts on crash
- Works even when no one is logged in
Install:
python cloud_service.py install
Uninstall:
python cloud_service.py remove
Start:
python cloud_service.py start
Stop:
python cloud_service.py stop
Or use the Windows Services Manager (services.msc) to manage it.
"""
import os
import sys
import logging
from pathlib import Path
# Set up paths
BASE_DIR = Path(__file__).resolve().parent
os.chdir(str(BASE_DIR))
# Add to path
if str(BASE_DIR) not in sys.path:
sys.path.insert(0, str(BASE_DIR))
if str(BASE_DIR / 'cloud') not in sys.path:
sys.path.insert(0, str(BASE_DIR / 'cloud'))
from utils.subprocess_safe import safe_popen
try:
import win32serviceutil
import win32service
import win32event
import servicemanager
HAS_WIN32 = True
except ImportError:
HAS_WIN32 = False
print("pywin32 is required. Install it with: pip install pywin32")
def _find_executable(name, candidates):
"""Return the first existing candidate path for an executable, else None."""
for cand in candidates:
if cand and os.path.isfile(cand):
return cand
# Also check PATH as a fallback
try:
import shutil as _sh
on_path = _sh.which(name)
if on_path:
return on_path
except Exception:
pass
return None
# User/profile paths (portable across PCs)
_USERPROFILE = os.environ.get('USERPROFILE', os.path.expanduser('~'))
_LOCALAPPDATA = os.environ.get('LOCALAPPDATA', os.path.join(_USERPROFILE, 'AppData', 'Local'))
_PROGRAMFILES = os.environ.get('ProgramFiles', r'C:\Program Files')
_PROGRAMFILES_X86 = os.environ.get('ProgramFiles(x86)', r'C:\Program Files (x86)')
# Known locations for cloudflared.exe (order = priority)
CLOUDFLARED_CANDIDATES = [
r'C:\caddy\cloudflared.exe',
os.path.join(_LOCALAPPDATA, 'IsolationBytes', 'cloudflared', 'cloudflared.exe'),
os.path.join(_LOCALAPPDATA, 'Programs', 'cloudflared', 'cloudflared.exe'),
os.path.join(_PROGRAMFILES, 'cloudflared', 'cloudflared.exe'),
os.path.join(_PROGRAMFILES_X86, 'cloudflared', 'cloudflared.exe'),
os.path.join(_USERPROFILE, '.cloudflared', 'cloudflared.exe'),
str(BASE_DIR / 'cloud' / 'cloudflared.exe'),
]
# Known locations for caddy.exe
CADDY_CANDIDATES = [
r'C:\caddy\caddy.exe',
os.path.join(_LOCALAPPDATA, 'IsolationBytes', 'caddy', 'caddy.exe'),
os.path.join(_PROGRAMFILES, 'Caddy', 'caddy.exe'),
os.path.join(_PROGRAMFILES_X86, 'Caddy', 'caddy.exe'),
str(BASE_DIR / 'cloud' / 'caddy.exe'),
]
CADDYFILE_CANDIDATES = [
r'C:\caddy\Caddyfile',
os.path.join(_LOCALAPPDATA, 'IsolationBytes', 'caddy', 'Caddyfile'),
str(BASE_DIR / 'cloud' / 'Caddyfile'),
]
CLOUDFLARED_CONFIG_CANDIDATES = [
os.path.join(_USERPROFILE, '.cloudflared', 'config.yml'),
str(BASE_DIR / 'cloud' / 'cloudflared.yml'),
]
class CloudServerService(win32serviceutil.ServiceFramework if HAS_WIN32 else object):
"""Windows service that runs the cloud antivirus server 24/7."""
_svc_name_ = "AntivirusCloudServer"
_svc_display_name_ = "Antivirus Cloud Server"
_svc_description_ = "Runs the antivirus cloud dashboard, local agent, and AI assistant 24/7. Auto-starts on boot and survives logoffs."
def __init__(self, args):
if not HAS_WIN32:
raise RuntimeError("pywin32 is not installed")
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self._server_process = None
self._caddy_process = None
self._cloudflared_process = None
self._stop_requested = False
logging.basicConfig(
filename=str(BASE_DIR / 'cloud' / 'service.log'),
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
self.log = logging.getLogger('CloudServerService')
def SvcStop(self):
"""Called when the service is stopped."""
self.log.info("Service stop requested")
self._stop_requested = True
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Kill the cloudflared tunnel
if self._cloudflared_process:
try:
self._cloudflared_process.terminate()
except Exception:
pass
# Kill Caddy
if self._caddy_process:
try:
self._caddy_process.terminate()
except Exception:
pass
# Kill the server process
if self._server_process:
try:
self._server_process.terminate()
except Exception:
pass
win32event.SetEvent(self.hWaitStop)
def SvcDoRun(self):
"""Called when the service starts."""
self.log.info("Antivirus Cloud Server service starting")
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, '')
)
self._run_server()
def _wait_for_port(self, port, timeout=120):
"""Wait for a port to be listening. Returns True if ready, False on timeout."""
import socket
import time
deadline = time.time() + timeout
while time.time() < deadline and not self._stop_requested:
try:
with socket.create_connection(("127.0.0.1", port), timeout=2):
return True
except (OSError, socket.error):
time.sleep(2)
return False
def _run_server(self):
"""Run the cloud server, Caddy, and Cloudflare tunnel as subprocesses."""
import subprocess
import threading
import time
import shutil
from dotenv import dotenv_values
child_env = os.environ.copy()
for env_path in (BASE_DIR / '.env', BASE_DIR / 'cloud' / '.env', BASE_DIR / '.env.server'):
if env_path.exists():
for key, value in dotenv_values(env_path).items():
if value is not None:
child_env[key] = value
# Use a stable local directory outside OneDrive to avoid sync/permission issues
# when running as SYSTEM. Copy the EXE there on each start.
local_dir = r'C:\AntivirusServer'
os.makedirs(local_dir, exist_ok=True)
# Source EXE locations (check both)
source_exe = str(BASE_DIR / 'dist' / 'cloud_server.exe')
server_script = str(BASE_DIR / 'cloud' / 'cloud_server.py')
python_exe = sys.executable
# Copy the EXE to the stable local directory
local_exe = os.path.join(local_dir, 'cloud_server.exe')
if os.path.exists(source_exe):
try:
# Always remove old copy first, then copy fresh
if os.path.exists(local_exe):
os.remove(local_exe)
shutil.copy2(source_exe, local_exe)
self.log.info(f"Copied cloud_server.exe to {local_exe}")
except Exception as e:
self.log.warning(f"Failed to copy EXE to local dir: {e}, using source path")
local_exe = source_exe
# Always prefer the source EXE (latest build) over the local copy
server_exe_to_use = source_exe if os.path.exists(source_exe) else local_exe
if os.path.exists(server_exe_to_use):
# Use the built EXE — it has everything bundled
self._server_process = safe_popen(
[server_exe_to_use],
cwd=local_dir if os.path.exists(local_exe) else str(BASE_DIR),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
env=child_env,
)
self.log.info(f"Server EXE started with PID {self._server_process.pid}")
else:
# Fallback: run with Python
self._server_process = safe_popen(
[python_exe, server_script],
cwd=str(BASE_DIR),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
env=child_env,
)
self.log.info(f"Server (Python) started with PID {self._server_process.pid}")
# Wait for the server to be ready (up to 120 seconds for large EXE extraction)
self.log.info("Waiting for server to be ready (up to 120s)...")
if self._wait_for_port(8000, timeout=120):
self.log.info("Server is ready on port 8000")
else:
self.log.warning("Server did not become ready in 120 seconds")
# Start Caddy reverse proxy (if installed and Caddyfile exists)
caddy_exe = _find_executable('caddy', CADDY_CANDIDATES)
caddyfile = next((p for p in CADDYFILE_CANDIDATES if p and os.path.exists(p)), None)
if caddy_exe and caddyfile:
try:
self._caddy_process = safe_popen(
[caddy_exe, 'run', '--config', caddyfile],
cwd=os.path.dirname(caddy_exe),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.log.info(f"Caddy started with PID {self._caddy_process.pid} ({caddy_exe})")
except Exception as e:
self.log.warning(f"Failed to start Caddy: {e}")
else:
self.log.info(f"Caddy not found (searched {CADDY_CANDIDATES}) — skipping")
# Start Cloudflare tunnel (if installed) — provides access without port forwarding
cloudflared_exe = _find_executable('cloudflared', CLOUDFLARED_CANDIDATES)
cloudflared_config = next((p for p in CLOUDFLARED_CONFIG_CANDIDATES if p and os.path.exists(p)), None)
if cloudflared_exe:
try:
cmd = [cloudflared_exe, 'tunnel']
if cloudflared_config:
cmd += ['--config', cloudflared_config]
cmd += ['run', 'isolation-bytes']
self._cloudflared_process = safe_popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.log.info(f"Cloudflare tunnel started with PID {self._cloudflared_process.pid} ({cloudflared_exe})")
except Exception as e:
self.log.warning(f"Failed to start Cloudflare tunnel: {e}")
else:
self.log.info(f"cloudflared not found (searched {CLOUDFLARED_CANDIDATES}) — skipping")
# Monitor all processes — restart if they crash
while not self._stop_requested:
# Check server
ret = self._server_process.poll()
if ret is not None:
self.log.warning(f"Server process exited with code {ret}. Restarting in 10 seconds...")
time.sleep(10)
if not self._stop_requested:
self.log.info("Restarting server...")
# Re-copy the EXE in case it was updated
if os.path.exists(source_exe):
try:
if os.path.exists(local_exe):
os.remove(local_exe)
shutil.copy2(source_exe, local_exe)
except Exception:
pass
# Always prefer the source EXE (latest build)
restart_exe = source_exe if os.path.exists(source_exe) else local_exe
if os.path.exists(restart_exe):
self._server_process = safe_popen(
[restart_exe],
cwd=str(BASE_DIR),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
env=child_env,
)
else:
self._server_process = safe_popen(
[python_exe, server_script],
cwd=str(BASE_DIR),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
env=child_env,
)
self.log.info(f"Server restarted with PID {self._server_process.pid}")
# Wait for port to be ready after restart
self._wait_for_port(8000, timeout=120)
# Check Caddy
if self._caddy_process and self._caddy_process.poll() is not None:
if caddy_exe and caddyfile and not self._stop_requested:
self.log.warning("Caddy exited. Restarting in 5 seconds...")
time.sleep(5)
if not self._stop_requested:
self._caddy_process = safe_popen(
[caddy_exe, 'run', '--config', caddyfile],
cwd=os.path.dirname(caddy_exe),
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.log.info(f"Caddy restarted with PID {self._caddy_process.pid}")
# Check cloudflared
if self._cloudflared_process and self._cloudflared_process.poll() is not None:
if cloudflared_exe and not self._stop_requested:
self.log.warning("Cloudflare tunnel exited. Restarting in 10 seconds...")
time.sleep(10)
if not self._stop_requested:
cmd = [cloudflared_exe, 'tunnel']
if cloudflared_config:
cmd += ['--config', cloudflared_config]
cmd += ['run', 'isolation-bytes']
self._cloudflared_process = safe_popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.log.info(f"Cloudflare tunnel restarted with PID {self._cloudflared_process.pid}")
# Wait a bit before checking again
win32event.WaitForSingleObject(self.hWaitStop, 5000)
self.log.info("Service stopped")
if __name__ == '__main__':
if not HAS_WIN32:
print("pywin32 is required. Install it with: pip install pywin32")
sys.exit(1)
if len(sys.argv) == 1:
# Running as a service
servicemanager.Initialize()
servicemanager.PrepareToHostSingle(CloudServerService)
servicemanager.StartServiceCtrlDispatcher()
else:
# Command line usage
win32serviceutil.HandleCommandLine(CloudServerService)