mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 04:58:41 -06:00
First Basic Implementation of Remote Script Execution Functionality
This commit is contained in:
@@ -24,4 +24,5 @@ pywinauto # Windows-based Macro Automation Library
|
||||
|
||||
# Audio Streaming Dependencies
|
||||
sounddevice
|
||||
numpy
|
||||
numpy
|
||||
pywin32; platform_system == "Windows"
|
||||
|
372
Data/Agent/agent_deployment.py
Normal file
372
Data/Agent/agent_deployment.py
Normal file
@@ -0,0 +1,372 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shlex
|
||||
import ctypes
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def project_paths():
|
||||
r"""Derive important paths relative to the venv python (sys.executable).
|
||||
|
||||
Layout assumed at runtime:
|
||||
<ProjectRoot>/Agent/Scripts/python.exe == sys.executable
|
||||
<ProjectRoot>/Agent/Borealis/*.py == deployed agent files
|
||||
"""
|
||||
venv_scripts = os.path.dirname(os.path.abspath(sys.executable))
|
||||
venv_root = os.path.abspath(os.path.join(venv_scripts, os.pardir))
|
||||
project_root = os.path.abspath(os.path.join(venv_root, os.pardir))
|
||||
borealis_dir = os.path.join(venv_root, "Borealis")
|
||||
logs_dir = os.path.join(project_root, "Logs")
|
||||
temp_dir = os.path.join(project_root, "Temp")
|
||||
return {
|
||||
"project_root": project_root,
|
||||
"venv_root": venv_root,
|
||||
"venv_python": sys.executable,
|
||||
"venv_pythonw": os.path.join(venv_scripts, "pythonw.exe"),
|
||||
"borealis_dir": borealis_dir,
|
||||
"logs_dir": logs_dir,
|
||||
"temp_dir": temp_dir,
|
||||
"service_script": os.path.join(borealis_dir, "windows_script_service.py"),
|
||||
# Use tray launcher for the scheduled task
|
||||
"agent_script": os.path.join(borealis_dir, "tray_launcher.py"),
|
||||
}
|
||||
|
||||
|
||||
def ensure_dirs(paths):
|
||||
os.makedirs(paths["logs_dir"], exist_ok=True)
|
||||
os.makedirs(paths["temp_dir"], exist_ok=True)
|
||||
|
||||
|
||||
def log_write(paths, name, text):
|
||||
try:
|
||||
p = os.path.join(paths["logs_dir"], name)
|
||||
with open(p, "a", encoding="utf-8") as f:
|
||||
f.write(f"{_now()} {text}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def is_admin():
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def run(cmd, cwd=None, capture=False):
|
||||
if isinstance(cmd, str):
|
||||
shell = True
|
||||
args = cmd
|
||||
else:
|
||||
shell = False
|
||||
args = cmd
|
||||
return subprocess.run(
|
||||
args,
|
||||
cwd=cwd,
|
||||
shell=shell,
|
||||
text=True,
|
||||
capture_output=capture,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def run_elevated_powershell(paths, ps_content, log_name):
|
||||
"""Run a short PowerShell script elevated and wait for completion.
|
||||
Writes combined output to Logs/log_name.
|
||||
"""
|
||||
ensure_dirs(paths)
|
||||
log_path = os.path.join(paths["logs_dir"], log_name)
|
||||
stub_path = os.path.join(paths["temp_dir"], f"elevate_{os.getpid()}_{log_name.replace('.', '_')}.ps1")
|
||||
with open(stub_path, "w", encoding="utf-8") as f:
|
||||
f.write(ps_content)
|
||||
|
||||
# Build powershell command
|
||||
ps = "powershell.exe"
|
||||
args = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\""
|
||||
|
||||
# ShellExecute to run as admin
|
||||
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
||||
class SHELLEXECUTEINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", ctypes.c_ulong),
|
||||
("fMask", ctypes.c_ulong),
|
||||
("hwnd", ctypes.c_void_p),
|
||||
("lpVerb", ctypes.c_wchar_p),
|
||||
("lpFile", ctypes.c_wchar_p),
|
||||
("lpParameters", ctypes.c_wchar_p),
|
||||
("lpDirectory", ctypes.c_wchar_p),
|
||||
("nShow", ctypes.c_int),
|
||||
("hInstApp", ctypes.c_void_p),
|
||||
("lpIDList", ctypes.c_void_p),
|
||||
("lpClass", ctypes.c_wchar_p),
|
||||
("hkeyClass", ctypes.c_void_p),
|
||||
("dwHotKey", ctypes.c_ulong),
|
||||
("hIcon", ctypes.c_void_p),
|
||||
("hProcess", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
sei = SHELLEXECUTEINFO()
|
||||
sei.cbSize = ctypes.sizeof(SHELLEXECUTEINFO)
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS
|
||||
sei.hwnd = None
|
||||
sei.lpVerb = "runas"
|
||||
sei.lpFile = ps
|
||||
sei.lpParameters = args
|
||||
sei.lpDirectory = paths["project_root"]
|
||||
sei.nShow = 1
|
||||
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)):
|
||||
log_write(paths, log_name, "[ERROR] UAC elevation failed (ShellExecuteExW)")
|
||||
try:
|
||||
os.remove(stub_path)
|
||||
except Exception:
|
||||
pass
|
||||
return 1
|
||||
# Wait for elevated process
|
||||
ctypes.windll.kernel32.WaitForSingleObject(sei.hProcess, 0xFFFFFFFF)
|
||||
# Capture output from stub if it appended
|
||||
try:
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write("")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
os.remove(stub_path)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def current_service_path(service_name):
|
||||
"""Return the configured BinaryPathName for a service (or None)."""
|
||||
try:
|
||||
r = run(["sc.exe", "qc", service_name], capture=True)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
for line in (r.stdout or "").splitlines():
|
||||
if "BINARY_PATH_NAME" in line:
|
||||
# Example: "BINARY_PATH_NAME : C:\\...\\python.exe C:\\...\\windows_script_service.py"
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
return parts[1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def ensure_script_service(paths):
|
||||
service_name = "BorealisScriptService"
|
||||
log_name = "Borealis_ScriptService_Install.log"
|
||||
ensure_dirs(paths)
|
||||
log_write(paths, log_name, "[INFO] Ensuring script execution service...")
|
||||
|
||||
# Decide if install/update needed
|
||||
need_install = False
|
||||
bin_path = current_service_path(service_name)
|
||||
expected_root = paths["venv_root"].lower()
|
||||
if not bin_path:
|
||||
need_install = True
|
||||
else:
|
||||
if expected_root not in bin_path.lower():
|
||||
need_install = True
|
||||
|
||||
if not is_admin():
|
||||
# Relaunch elevated to perform service installation/update
|
||||
venv = paths["venv_python"].replace("\"", "\"\"")
|
||||
srv = paths["service_script"].replace("\"", "\"\"")
|
||||
log = os.path.join(paths["logs_dir"], log_name).replace("\"", "\"\"")
|
||||
venv_dir = os.path.dirname(paths["venv_python"]).replace("\"", "\"\"")
|
||||
postinstall = os.path.join(venv_dir, "pywin32_postinstall.py").replace("\"", "\"\"")
|
||||
py_home = paths["venv_root"].replace("\"", "\"\"")
|
||||
content = f"""
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$venv = "{venv}"
|
||||
$srv = "{srv}"
|
||||
$log = "{log}"
|
||||
$post = "{postinstall}"
|
||||
$pyhome = "{py_home}"
|
||||
try {{
|
||||
try {{ New-Item -ItemType Directory -Force -Path (Split-Path $log -Parent) | Out-Null }} catch {{}}
|
||||
# Remove legacy service name if present
|
||||
try {{ sc.exe stop BorealisScriptAgent 2>$null | Out-Null }} catch {{}}
|
||||
try {{ sc.exe delete BorealisScriptAgent 2>$null | Out-Null }} catch {{}}
|
||||
if (Test-Path $post) {{ & $venv $post -install *>> "$log" }} else {{ & $venv -m pywin32_postinstall -install *>> "$log" }}
|
||||
try {{ & $venv $srv remove *>> "$log" }} catch {{}}
|
||||
& $venv $srv --startup auto install *>> "$log"
|
||||
# Ensure registry points to correct module and PY path
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonClass" /ve /t REG_SZ /d "windows_script_service.BorealisScriptAgentService" /f | Out-Null
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonPath" /ve /t REG_SZ /d "{paths['borealis_dir']}" /f | Out-Null
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonHome" /ve /t REG_SZ /d "$pyhome" /f | Out-Null
|
||||
sc.exe config {service_name} obj= LocalSystem start= auto | Out-File -FilePath "$log" -Append -Encoding UTF8
|
||||
sc.exe start {service_name} | Out-File -FilePath "$log" -Append -Encoding UTF8
|
||||
"[INFO] Completed service ensure." | Out-File -FilePath "$log" -Append -Encoding UTF8
|
||||
}} catch {{
|
||||
"[ERROR] $_" | Out-File -FilePath "$log" -Append -Encoding UTF8
|
||||
exit 1
|
||||
}}
|
||||
"""
|
||||
rc = run_elevated_powershell(paths, content, log_name)
|
||||
return rc == 0
|
||||
|
||||
# Admin path: perform directly
|
||||
try:
|
||||
# Ensure pywin32 service hooks present in this venv
|
||||
post_py = os.path.join(os.path.dirname(paths["venv_python"]), "pywin32_postinstall.py")
|
||||
if os.path.isfile(post_py):
|
||||
run([paths["venv_python"], post_py, "-install"]) # ignore rc
|
||||
else:
|
||||
run([paths["venv_python"], "-m", "pywin32_postinstall", "-install"]) # ignore rc
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# Remove legacy service if it exists
|
||||
run(["sc.exe", "stop", "BorealisScriptAgent"]) # ignore rc
|
||||
run(["sc.exe", "delete", "BorealisScriptAgent"]) # ignore rc
|
||||
if need_install:
|
||||
run([paths["venv_python"], paths["service_script"], "remove"]) # ignore rc
|
||||
r1 = run([paths["venv_python"], paths["service_script"], "--startup", "auto", "install"], capture=True)
|
||||
log_write(paths, log_name, f"[INFO] install rc={r1.returncode} out={r1.stdout}\nerr={r1.stderr}")
|
||||
# fix registry for module import and path
|
||||
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonClass", "/ve", "/t", "REG_SZ", "/d", "windows_script_service.BorealisScriptAgentService", "/f"]) # noqa
|
||||
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonPath", "/ve", "/t", "REG_SZ", "/d", paths["borealis_dir"], "/f"]) # noqa
|
||||
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonHome", "/ve", "/t", "REG_SZ", "/d", paths["venv_root"], "/f"]) # noqa
|
||||
run(["sc.exe", "config", service_name, "obj=", "LocalSystem"]) # ensure LocalSystem
|
||||
run(["sc.exe", "start", service_name])
|
||||
# quick validate
|
||||
qc = run(["sc.exe", "query", service_name], capture=True)
|
||||
ok = (qc.returncode == 0)
|
||||
log_write(paths, log_name, f"[INFO] ensure complete (ok={ok})")
|
||||
return ok
|
||||
except Exception as e:
|
||||
log_write(paths, log_name, f"[ERROR] ensure (admin) failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def ensure_user_logon_task(paths):
|
||||
"""Ensure the per-user scheduled task that launches the agent in the user's session.
|
||||
|
||||
Name: "Borealis Agent"
|
||||
Trigger: On logon (current user)
|
||||
Action: <venv_python> -W ignore::SyntaxWarning <agent_script>
|
||||
"""
|
||||
ensure_dirs(paths)
|
||||
log_name = "Borealis_CollectorTask_Install.log"
|
||||
task_name = "Borealis Agent"
|
||||
# Use pythonw.exe to avoid opening a console window in the user's session
|
||||
pyw = paths.get("venv_pythonw") or paths["venv_python"]
|
||||
cmd = f"\"{pyw}\" -W ignore::SyntaxWarning \"{paths['agent_script']}\""
|
||||
|
||||
# If task exists, try remove (non-elevated first)
|
||||
q = run(["schtasks.exe", "/Query", "/TN", task_name])
|
||||
if q.returncode == 0:
|
||||
d = run(["schtasks.exe", "/Delete", "/TN", task_name, "/F"], capture=True)
|
||||
if d.returncode != 0:
|
||||
# Attempt elevated deletion (task might have been created under admin previously)
|
||||
ps = f"""
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
try {{ schtasks.exe /Delete /TN "{task_name}" /F | Out-Null }} catch {{}}
|
||||
"""
|
||||
run_elevated_powershell(paths, ps, log_name)
|
||||
|
||||
c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"], capture=True)
|
||||
log_write(paths, log_name, f"[INFO] create rc={c.returncode} out={c.stdout} err={c.stderr}")
|
||||
if c.returncode != 0:
|
||||
# Fallback: elevate and register task for the current user SID
|
||||
user = os.environ.get("USERNAME", "")
|
||||
domain = os.environ.get("USERDOMAIN", os.environ.get("COMPUTERNAME", ""))
|
||||
py = (paths.get("venv_pythonw") or paths["venv_python"]).replace("\"", "\"\"")
|
||||
ag = paths["agent_script"].replace("\"", "\"\"")
|
||||
content = f"""
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$task = "{task_name}"
|
||||
$py = "{py}"
|
||||
$arg = "-W ignore::SyntaxWarning {ag}"
|
||||
$cmd = '"' + $py + '" ' + $arg
|
||||
$user = "{domain}\{user}"
|
||||
try {{
|
||||
# Resolve user SID
|
||||
$sid = (New-Object System.Security.Principal.NTAccount($user)).Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||
}} catch {{ $sid = $null }}
|
||||
try {{
|
||||
# Delete any existing task (any scope)
|
||||
try {{ schtasks.exe /Delete /TN $task /F | Out-Null }} catch {{}}
|
||||
$action = New-ScheduledTaskAction -Execute $py -Argument $arg
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$settings = New-ScheduledTaskSettingsSet -Hidden
|
||||
if ($sid) {{
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
|
||||
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
||||
}} else {{
|
||||
# Fallback: bind by username (use Interactive to avoid password)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "{domain}\{user}" -LogonType Interactive -RunLevel Limited
|
||||
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
||||
}}
|
||||
}} catch {{
|
||||
"[ERROR] Task register failed: $_" | Out-File -FilePath "{os.path.join(paths['logs_dir'], log_name)}" -Append -Encoding UTF8
|
||||
exit 1
|
||||
}}
|
||||
"""
|
||||
rc = run_elevated_powershell(paths, content, log_name)
|
||||
if rc != 0:
|
||||
return False
|
||||
else:
|
||||
# Created via schtasks; set Hidden=true via elevated PowerShell (schtasks lacks a /HIDDEN switch)
|
||||
ps_hide = f"""
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
try {{
|
||||
$settings = New-ScheduledTaskSettingsSet -Hidden
|
||||
Set-ScheduledTask -TaskName "{task_name}" -Settings $settings | Out-Null
|
||||
}} catch {{}}
|
||||
"""
|
||||
run_elevated_powershell(paths, ps_hide, log_name)
|
||||
# Start immediately (if a session is active)
|
||||
run(["schtasks.exe", "/Run", "/TN", task_name])
|
||||
return True
|
||||
|
||||
|
||||
def ensure_all():
|
||||
paths = project_paths()
|
||||
ensure_dirs(paths)
|
||||
ok_svc = ensure_script_service(paths)
|
||||
ok_task = ensure_user_logon_task(paths)
|
||||
return 0 if (ok_svc and ok_task) else 1
|
||||
|
||||
|
||||
def main(argv):
|
||||
# Simple CLI
|
||||
if len(argv) <= 1:
|
||||
print("Usage: agent_deployment.py [ensure-all|service-install|service-remove|task-ensure|task-remove]")
|
||||
return 2
|
||||
cmd = argv[1].lower()
|
||||
paths = project_paths()
|
||||
ensure_dirs(paths)
|
||||
|
||||
if cmd == "ensure-all":
|
||||
return ensure_all()
|
||||
if cmd == "service-install":
|
||||
return 0 if ensure_script_service(paths) else 1
|
||||
if cmd == "service-remove":
|
||||
name = "BorealisScriptService"
|
||||
if not is_admin():
|
||||
ps = f"try {{ sc.exe stop {name} }} catch {{}}; try {{ sc.exe delete {name} }} catch {{}}"
|
||||
return run_elevated_powershell(paths, ps, "Borealis_ScriptService_Remove.log")
|
||||
run(["sc.exe", "stop", name])
|
||||
r = run(["sc.exe", "delete", name])
|
||||
return r.returncode
|
||||
if cmd == "task-ensure":
|
||||
return 0 if ensure_user_logon_task(paths) else 1
|
||||
if cmd == "task-remove":
|
||||
tn = "Borealis Agent"
|
||||
r = run(["schtasks.exe", "/Delete", "/TN", tn, "/F"])
|
||||
return r.returncode
|
||||
|
||||
print(f"Unknown command: {cmd}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
@@ -362,6 +362,204 @@ sio = socketio.AsyncClient(reconnection=True, reconnection_attempts=0, reconnect
|
||||
role_tasks = {}
|
||||
background_tasks = []
|
||||
roles_ctx = None
|
||||
AGENT_LOOP = None
|
||||
|
||||
# ---------------- Local IPC Bridge (Service -> Agent) ----------------
|
||||
def start_agent_bridge_pipe(loop_ref):
|
||||
import threading
|
||||
import win32pipe, win32file, win32con, pywintypes
|
||||
|
||||
pipe_name = r"\\.\pipe\Borealis_Agent_Bridge"
|
||||
|
||||
def forward_to_server(msg: dict):
|
||||
try:
|
||||
evt = msg.get('type')
|
||||
if evt == 'screenshot':
|
||||
payload = {
|
||||
'agent_id': AGENT_ID,
|
||||
'node_id': msg.get('node_id') or 'user_session',
|
||||
'image_base64': msg.get('image_base64') or '',
|
||||
'timestamp': msg.get('timestamp') or int(time.time())
|
||||
}
|
||||
asyncio.run_coroutine_threadsafe(sio.emit('agent_screenshot_task', payload), loop_ref)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def server_thread():
|
||||
while True:
|
||||
try:
|
||||
handle = win32pipe.CreateNamedPipe(
|
||||
pipe_name,
|
||||
win32con.PIPE_ACCESS_DUPLEX,
|
||||
win32con.PIPE_TYPE_MESSAGE | win32con.PIPE_READMODE_MESSAGE | win32con.PIPE_WAIT,
|
||||
1, 65536, 65536, 0, None)
|
||||
except pywintypes.error:
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
try:
|
||||
win32pipe.ConnectNamedPipe(handle, None)
|
||||
except pywintypes.error:
|
||||
try:
|
||||
win32file.CloseHandle(handle)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
# Read loop per connection
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
hr, data = win32file.ReadFile(handle, 65536)
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
obj = json.loads(data.decode('utf-8', errors='ignore'))
|
||||
forward_to_server(obj)
|
||||
except Exception:
|
||||
pass
|
||||
except pywintypes.error:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
win32file.CloseHandle(handle)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
|
||||
t = threading.Thread(target=server_thread, daemon=True)
|
||||
t.start()
|
||||
|
||||
def send_service_control(msg: dict):
|
||||
try:
|
||||
import win32file
|
||||
pipe = r"\\.\pipe\Borealis_Service_Control"
|
||||
h = win32file.CreateFile(
|
||||
pipe,
|
||||
win32file.GENERIC_WRITE,
|
||||
0,
|
||||
None,
|
||||
win32file.OPEN_EXISTING,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
try:
|
||||
win32file.WriteFile(h, json.dumps(msg).encode('utf-8'))
|
||||
finally:
|
||||
win32file.CloseHandle(h)
|
||||
except Exception:
|
||||
pass
|
||||
IS_WINDOWS = sys.platform.startswith('win')
|
||||
|
||||
def _is_admin_windows():
|
||||
if not IS_WINDOWS:
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _write_temp_script(content: str, suffix: str):
|
||||
import tempfile
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), "Borealis", "quick_jobs")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
fd, path = tempfile.mkstemp(prefix="bj_", suffix=suffix, dir=temp_dir, text=True)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
|
||||
fh.write(content or "")
|
||||
return path
|
||||
|
||||
async def _run_powershell_local(path: str):
|
||||
"""Run powershell script as current user hidden window and capture output."""
|
||||
ps = None
|
||||
if IS_WINDOWS:
|
||||
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
||||
if not os.path.isfile(ps):
|
||||
ps = "powershell.exe"
|
||||
else:
|
||||
ps = "pwsh"
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
ps,
|
||||
"-ExecutionPolicy", "Bypass" if IS_WINDOWS else "Bypass",
|
||||
"-NoProfile",
|
||||
"-File", path,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
creationflags=(0x08000000 if IS_WINDOWS else 0) # CREATE_NO_WINDOW
|
||||
)
|
||||
out_b, err_b = await proc.communicate()
|
||||
rc = proc.returncode
|
||||
out = (out_b or b"").decode(errors='replace')
|
||||
err = (err_b or b"").decode(errors='replace')
|
||||
return rc, out, err
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
|
||||
async def _run_powershell_as_system(path: str):
|
||||
"""Attempt to run as SYSTEM using schtasks; requires admin."""
|
||||
if not IS_WINDOWS:
|
||||
return -1, "", "SYSTEM run not supported on this OS"
|
||||
# Name with timestamp to avoid collisions
|
||||
name = f"Borealis_QuickJob_{int(time.time())}_{random.randint(1000,9999)}"
|
||||
# Create scheduled task
|
||||
# Start time: 1 minute from now (HH:MM)
|
||||
t = time.localtime(time.time() + 60)
|
||||
st = f"{t.tm_hour:02d}:{t.tm_min:02d}"
|
||||
create_cmd = [
|
||||
"schtasks", "/Create", "/TN", name,
|
||||
"/TR", f"\"powershell.exe -ExecutionPolicy Bypass -NoProfile -File \"\"{path}\"\"\"",
|
||||
"/SC", "ONCE", "/ST", st, "/RL", "HIGHEST", "/RU", "SYSTEM", "/F"
|
||||
]
|
||||
try:
|
||||
p1 = await asyncio.create_subprocess_exec(*create_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
c_out, c_err = await p1.communicate()
|
||||
if p1.returncode != 0:
|
||||
return p1.returncode, "", (c_err or b"").decode(errors='replace')
|
||||
# Run immediately
|
||||
run_cmd = ["schtasks", "/Run", "/TN", name]
|
||||
p2 = await asyncio.create_subprocess_exec(*run_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
r_out, r_err = await p2.communicate()
|
||||
# Give some time for task to run and finish (best-effort)
|
||||
await asyncio.sleep(5)
|
||||
# We cannot reliably capture stdout from scheduled task directly; advise writing output to file in script if needed.
|
||||
# Return status of scheduling; actual script result unknown. We will try to check last run result.
|
||||
query_cmd = ["schtasks", "/Query", "/TN", name, "/V", "/FO", "LIST"]
|
||||
p3 = await asyncio.create_subprocess_exec(*query_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
q_out, q_err = await p3.communicate()
|
||||
status_txt = (q_out or b"").decode(errors='replace')
|
||||
# Cleanup
|
||||
await asyncio.create_subprocess_exec("schtasks", "/Delete", "/TN", name, "/F")
|
||||
# We cannot get stdout/stderr; return status text to stderr and treat success based on return codes
|
||||
status = "Success" if p2.returncode == 0 else "Failed"
|
||||
return 0 if status == "Success" else 1, "", status_txt
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
|
||||
async def _run_powershell_with_credentials(path: str, username: str, password: str):
|
||||
if not IS_WINDOWS:
|
||||
return -1, "", "Credentialed run not supported on this OS"
|
||||
# Build a one-liner to convert plaintext password to SecureString and run Start-Process -Credential
|
||||
ps_cmd = (
|
||||
f"$user=\"{username}\"; "
|
||||
f"$pass=\"{password}\"; "
|
||||
f"$sec=ConvertTo-SecureString $pass -AsPlainText -Force; "
|
||||
f"$cred=New-Object System.Management.Automation.PSCredential($user,$sec); "
|
||||
f"Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -NoProfile -File \"{path}\"' -Credential $cred -WindowStyle Hidden -PassThru | Wait-Process;"
|
||||
)
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"powershell.exe", "-NoProfile", "-Command", ps_cmd,
|
||||
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
||||
creationflags=(0x08000000 if IS_WINDOWS else 0)
|
||||
)
|
||||
out_b, err_b = await proc.communicate()
|
||||
out = (out_b or b"").decode(errors='replace')
|
||||
err = (err_b or b"").decode(errors='replace')
|
||||
return proc.returncode, out, err
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
|
||||
async def stop_all_roles():
|
||||
print("[DEBUG] Stopping all roles.")
|
||||
@@ -393,6 +591,14 @@ async def send_heartbeat():
|
||||
"last_seen": int(time.time())
|
||||
}
|
||||
await sio.emit("agent_heartbeat", payload)
|
||||
# Also report collector status alive ping with last_user
|
||||
import getpass
|
||||
await sio.emit('collector_status', {
|
||||
'agent_id': AGENT_ID,
|
||||
'hostname': socket.gethostname(),
|
||||
'active': True,
|
||||
'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}"
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[WARN] heartbeat emit failed: {e}")
|
||||
# Send periodic heartbeats every 60 seconds
|
||||
@@ -706,6 +912,18 @@ async def connect():
|
||||
except Exception as e:
|
||||
print(f"[WARN] initial heartbeat failed: {e}")
|
||||
|
||||
# Let server know collector is active and who the user is
|
||||
try:
|
||||
import getpass
|
||||
await sio.emit('collector_status', {
|
||||
'agent_id': AGENT_ID,
|
||||
'hostname': socket.gethostname(),
|
||||
'active': True,
|
||||
'last_user': f"{os.environ.get('USERDOMAIN') or socket.gethostname()}\\{getpass.getuser()}"
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await sio.emit('request_config', {"agent_id": AGENT_ID})
|
||||
|
||||
@sio.event
|
||||
@@ -747,6 +965,15 @@ async def on_agent_config(cfg):
|
||||
task.cancel()
|
||||
role_tasks.clear()
|
||||
|
||||
# Forward screenshot config to service helper (interval only)
|
||||
try:
|
||||
for role_cfg in roles:
|
||||
if role_cfg.get('role') == 'screenshot':
|
||||
interval_ms = int(role_cfg.get('interval', 1000))
|
||||
send_service_control({ 'type': 'screenshot_config', 'interval_ms': interval_ms })
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for role_cfg in roles:
|
||||
nid = role_cfg.get('node_id')
|
||||
role = role_cfg.get('role')
|
||||
@@ -759,6 +986,49 @@ async def on_agent_config(cfg):
|
||||
task = asyncio.create_task(agent_roles.macro_task(roles_ctx, role_cfg))
|
||||
role_tasks[nid] = task
|
||||
|
||||
@sio.on('quick_job_run')
|
||||
async def on_quick_job_run(payload):
|
||||
try:
|
||||
target = (payload.get('target_hostname') or '').strip().lower()
|
||||
if not target or target != socket.gethostname().lower():
|
||||
return
|
||||
job_id = payload.get('job_id')
|
||||
script_type = (payload.get('script_type') or '').lower()
|
||||
run_mode = (payload.get('run_mode') or 'current_user').lower()
|
||||
content = payload.get('script_content') or ''
|
||||
if script_type != 'powershell':
|
||||
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
||||
return
|
||||
path = _write_temp_script(content, '.ps1')
|
||||
rc = 0; out = ''; err = ''
|
||||
if run_mode == 'system':
|
||||
if not _is_admin_windows():
|
||||
rc, out, err = -1, '', 'Agent is not elevated. SYSTEM execution requires running the agent as Administrator or service.'
|
||||
else:
|
||||
rc, out, err = await _run_powershell_as_system(path)
|
||||
elif run_mode == 'admin':
|
||||
# Admin credentialed runs are disabled in current design
|
||||
rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM (service) or Current User.'
|
||||
else:
|
||||
rc, out, err = await _run_powershell_local(path)
|
||||
status = 'Success' if rc == 0 else 'Failed'
|
||||
await sio.emit('quick_job_result', {
|
||||
'job_id': job_id,
|
||||
'status': status,
|
||||
'stdout': out,
|
||||
'stderr': err,
|
||||
})
|
||||
except Exception as e:
|
||||
try:
|
||||
await sio.emit('quick_job_result', {
|
||||
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
|
||||
'status': 'Failed',
|
||||
'stdout': '',
|
||||
'stderr': str(e),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@sio.on('list_agent_windows')
|
||||
async def handle_list_agent_windows(data):
|
||||
windows = agent_roles.get_window_list()
|
||||
@@ -811,6 +1081,11 @@ async def connect_loop():
|
||||
if __name__=='__main__':
|
||||
app=QtWidgets.QApplication(sys.argv)
|
||||
loop=QEventLoop(app); asyncio.set_event_loop(loop)
|
||||
AGENT_LOOP = loop
|
||||
try:
|
||||
start_agent_bridge_pipe(loop)
|
||||
except Exception:
|
||||
pass
|
||||
dummy_window=PersistentWindow(); dummy_window.show()
|
||||
# Initialize roles context for role tasks
|
||||
roles_ctx = agent_roles.RolesContext(sio=sio, agent_id=AGENT_ID, config=CONFIG)
|
||||
|
120
Data/Agent/script_agent.py
Normal file
120
Data/Agent/script_agent.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import socketio
|
||||
|
||||
|
||||
def get_project_root():
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
|
||||
def get_server_url():
|
||||
# Try to reuse the agent config if present
|
||||
cfg_path = os.path.join(get_project_root(), "agent_settings.json")
|
||||
try:
|
||||
if os.path.isfile(cfg_path):
|
||||
with open(cfg_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
url = data.get("borealis_server_url")
|
||||
if isinstance(url, str) and url.strip():
|
||||
return url.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return "http://localhost:5000"
|
||||
|
||||
|
||||
def run_powershell_script_content(content: str):
|
||||
# Store ephemeral script under <ProjectRoot>/Temp
|
||||
temp_dir = os.path.join(get_project_root(), "Temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
fd, path = tempfile.mkstemp(prefix="sj_", suffix=".ps1", dir=temp_dir, text=True)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
|
||||
fh.write(content or "")
|
||||
|
||||
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
||||
if not os.path.isfile(ps):
|
||||
ps = "powershell.exe"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[ps, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60*60,
|
||||
)
|
||||
return proc.returncode, proc.stdout or "", proc.stderr or ""
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
|
||||
|
||||
async def main():
|
||||
sio = socketio.AsyncClient(reconnection=True)
|
||||
hostname = socket.gethostname()
|
||||
|
||||
@sio.event
|
||||
async def connect():
|
||||
print("[ScriptAgent] Connected to server")
|
||||
# Identify as script agent (no heartbeat to avoid UI duplication)
|
||||
try:
|
||||
await sio.emit("connect_agent", {"agent_id": f"{hostname}-script"})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@sio.on("quick_job_run")
|
||||
async def on_quick_job_run(payload):
|
||||
# Treat as generic script_run internally
|
||||
try:
|
||||
target = (payload.get('target_hostname') or '').strip().lower()
|
||||
if target and target != hostname.lower():
|
||||
return
|
||||
job_id = payload.get('job_id')
|
||||
script_type = (payload.get('script_type') or '').lower()
|
||||
content = payload.get('script_content') or ''
|
||||
if script_type != 'powershell':
|
||||
await sio.emit('quick_job_result', {
|
||||
'job_id': job_id,
|
||||
'status': 'Failed',
|
||||
'stdout': '',
|
||||
'stderr': f"Unsupported type: {script_type}"
|
||||
})
|
||||
return
|
||||
rc, out, err = run_powershell_script_content(content)
|
||||
status = 'Success' if rc == 0 else 'Failed'
|
||||
await sio.emit('quick_job_result', {
|
||||
'job_id': job_id,
|
||||
'status': status,
|
||||
'stdout': out,
|
||||
'stderr': err,
|
||||
})
|
||||
except Exception as e:
|
||||
try:
|
||||
await sio.emit('quick_job_result', {
|
||||
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
|
||||
'status': 'Failed',
|
||||
'stdout': '',
|
||||
'stderr': str(e),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@sio.event
|
||||
async def disconnect():
|
||||
print("[ScriptAgent] Disconnected")
|
||||
|
||||
url = get_server_url()
|
||||
while True:
|
||||
try:
|
||||
await sio.connect(url, transports=['websocket'])
|
||||
await sio.wait()
|
||||
except Exception as e:
|
||||
print(f"[ScriptAgent] reconnect in 5s: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
147
Data/Agent/tray_launcher.py
Normal file
147
Data/Agent/tray_launcher.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
|
||||
def project_paths():
|
||||
# Expected layout when running from venv: <Root>\Agent\Borealis
|
||||
borealis_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
agent_dir = os.path.abspath(os.path.join(borealis_dir, os.pardir))
|
||||
venv_scripts = os.path.join(agent_dir, 'Scripts')
|
||||
pyw = os.path.join(venv_scripts, 'pythonw.exe')
|
||||
py = os.path.join(venv_scripts, 'python.exe')
|
||||
icon_path = os.path.join(borealis_dir, 'Borealis.ico')
|
||||
agent_script = os.path.join(borealis_dir, 'borealis-agent.py')
|
||||
return {
|
||||
'borealis_dir': borealis_dir,
|
||||
'venv_scripts': venv_scripts,
|
||||
'pythonw': pyw if os.path.isfile(pyw) else sys.executable,
|
||||
'python': py if os.path.isfile(py) else sys.executable,
|
||||
'agent_script': agent_script,
|
||||
'icon': icon_path if os.path.isfile(icon_path) else None,
|
||||
}
|
||||
|
||||
|
||||
class TrayApp(QtWidgets.QSystemTrayIcon):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
paths = project_paths()
|
||||
self.paths = paths
|
||||
icon = QtGui.QIcon(paths['icon']) if paths['icon'] else app.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
|
||||
super().__init__(icon)
|
||||
self.setToolTip('Borealis Agent')
|
||||
self.menu = QtWidgets.QMenu()
|
||||
|
||||
self.action_show_console = self.menu.addAction('Switch to Foreground Mode')
|
||||
self.action_hide_console = self.menu.addAction('Switch to Background Mode')
|
||||
self.action_restart = self.menu.addAction('Restart Agent')
|
||||
self.action_restart_service = self.menu.addAction('Restart Script Execution Service')
|
||||
self.menu.addSeparator()
|
||||
self.action_quit = self.menu.addAction('Quit Agent and Tray')
|
||||
|
||||
self.action_show_console.triggered.connect(self.switch_to_console)
|
||||
self.action_hide_console.triggered.connect(self.switch_to_background)
|
||||
self.action_restart.triggered.connect(self.restart_agent)
|
||||
self.action_restart_service.triggered.connect(self.restart_script_service)
|
||||
self.action_quit.triggered.connect(self.quit_all)
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
self.proc = None
|
||||
self.console_mode = False
|
||||
# Start in background mode by default
|
||||
self.switch_to_background()
|
||||
self.show()
|
||||
|
||||
def _start_agent(self, console=False):
|
||||
self._stop_agent()
|
||||
exe = self.paths['python'] if console else self.paths['pythonw']
|
||||
args = [exe, '-W', 'ignore::SyntaxWarning', self.paths['agent_script']]
|
||||
creationflags = 0
|
||||
if not console and os.name == 'nt':
|
||||
# CREATE_NO_WINDOW
|
||||
creationflags = 0x08000000
|
||||
try:
|
||||
self.proc = subprocess.Popen(args, cwd=self.paths['borealis_dir'], creationflags=creationflags)
|
||||
self.console_mode = console
|
||||
self._update_actions(console)
|
||||
except Exception:
|
||||
self.proc = None
|
||||
|
||||
def _stop_agent(self):
|
||||
if self.proc is not None:
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
self.proc.send_signal(signal.SIGTERM)
|
||||
else:
|
||||
self.proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except Exception:
|
||||
try:
|
||||
self.proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self.proc = None
|
||||
|
||||
def _update_actions(self, console):
|
||||
self.action_show_console.setEnabled(not console)
|
||||
self.action_hide_console.setEnabled(console)
|
||||
|
||||
def switch_to_console(self):
|
||||
self._start_agent(console=True)
|
||||
|
||||
def switch_to_background(self):
|
||||
self._start_agent(console=False)
|
||||
|
||||
def restart_agent(self):
|
||||
# Restart using current mode
|
||||
self._start_agent(console=self.console_mode)
|
||||
|
||||
def restart_script_service(self):
|
||||
# Try direct stop/start; if fails (likely due to permissions), attempt elevation via PowerShell
|
||||
service_name = 'BorealisScriptService'
|
||||
try:
|
||||
# Stop service
|
||||
subprocess.run(["sc.exe", "stop", service_name], check=False, capture_output=True)
|
||||
# Start service
|
||||
subprocess.run(["sc.exe", "start", service_name], check=False, capture_output=True)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: elevate via PowerShell
|
||||
try:
|
||||
script = (
|
||||
f"$ErrorActionPreference='Continue'; "
|
||||
f"try {{ Stop-Service -Name '{service_name}' -Force -ErrorAction SilentlyContinue }} catch {{}}; "
|
||||
f"Start-Sleep -Seconds 1; "
|
||||
f"try {{ Start-Service -Name '{service_name}' }} catch {{}};"
|
||||
)
|
||||
# Start-Process PowerShell -Verb RunAs to elevate
|
||||
ps_cmd = [
|
||||
'powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
"Start-Process PowerShell -Verb RunAs -ArgumentList '-NoProfile -ExecutionPolicy Bypass -Command \"" + script.replace("\"", "\\\"") + "\"'"
|
||||
]
|
||||
subprocess.Popen(ps_cmd)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def quit_all(self):
|
||||
self._stop_agent()
|
||||
self.hide()
|
||||
self.app.quit()
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
tray = TrayApp(app)
|
||||
return app.exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
101
Data/Agent/windows_script_service.py
Normal file
101
Data/Agent/windows_script_service.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import win32serviceutil
|
||||
import win32service
|
||||
import win32event
|
||||
import servicemanager
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
|
||||
class BorealisScriptAgentService(win32serviceutil.ServiceFramework):
|
||||
_svc_name_ = "BorealisScriptService"
|
||||
_svc_display_name_ = "Borealis Script Execution Service"
|
||||
_svc_description_ = "Executes automation scripts (PowerShell, etc.) as LocalSystem and bridges to Borealis Server."
|
||||
|
||||
def __init__(self, args):
|
||||
win32serviceutil.ServiceFramework.__init__(self, args)
|
||||
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
|
||||
self.proc = None
|
||||
try:
|
||||
self._log("Service initialized")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def SvcStop(self):
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
try:
|
||||
self._log("Stop requested")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self.proc and self.proc.poll() is None:
|
||||
try:
|
||||
self.proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
win32event.SetEvent(self.hWaitStop)
|
||||
|
||||
def SvcDoRun(self):
|
||||
try:
|
||||
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
|
||||
servicemanager.PYS_SERVICE_STARTED,
|
||||
(self._svc_name_, ''))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._log("SvcDoRun entered")
|
||||
except Exception:
|
||||
pass
|
||||
# Mark the service as running once initialized
|
||||
try:
|
||||
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
|
||||
except Exception:
|
||||
pass
|
||||
self.main()
|
||||
|
||||
def main(self):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
agent_py = os.path.join(script_dir, 'script_agent.py')
|
||||
python = sys.executable
|
||||
try:
|
||||
self._log(f"Launching script_agent via {python}")
|
||||
self.proc = subprocess.Popen(
|
||||
[python, '-W', 'ignore::SyntaxWarning', agent_py],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=0x08000000 if os.name == 'nt' else 0
|
||||
)
|
||||
self._log("script_agent started")
|
||||
except Exception:
|
||||
self.proc = None
|
||||
try:
|
||||
self._log("Failed to start script_agent")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Wait until stop or child exits
|
||||
while True:
|
||||
rc = win32event.WaitForSingleObject(self.hWaitStop, 1000)
|
||||
if rc == win32event.WAIT_OBJECT_0:
|
||||
break
|
||||
if self.proc and self.proc.poll() is not None:
|
||||
break
|
||||
|
||||
def _log(self, msg: str):
|
||||
try:
|
||||
root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
||||
logs = os.path.join(root, 'Logs')
|
||||
os.makedirs(logs, exist_ok=True)
|
||||
p = os.path.join(logs, 'ScriptService_Runtime.log')
|
||||
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
with open(p, 'a', encoding='utf-8') as f:
|
||||
f.write(f"{ts} {msg}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
win32serviceutil.HandleCommandLine(BorealisScriptAgentService)
|
Reference in New Issue
Block a user