Added run as service
This commit is contained in:
parent
fb012de76b
commit
fe3b5e72d1
91
README.md
91
README.md
|
|
@ -71,7 +71,97 @@ Combos are configurable in `config.json → hotkeys` using pynput syntax (`<ctrl
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Run as a Windows Service (no interactive session required)
|
||||||
|
|
||||||
|
ICEYOU can run as a **Windows service** so it starts at boot, has no console
|
||||||
|
window, and keeps running across logoff/logon — i.e. *without* you having to
|
||||||
|
launch it from an interactive session.
|
||||||
|
|
||||||
|
### How it works (important)
|
||||||
|
|
||||||
|
ICEYOU draws a full-screen lock-out, a tray icon, and installs low-level
|
||||||
|
keyboard hooks. Windows runs services in **Session 0**, which is *isolated from
|
||||||
|
the interactive desktop* — anything a Session-0 process draws is invisible to
|
||||||
|
the logged-in user. So a plain "run the app as a service" approach would lock
|
||||||
|
nothing the user could see.
|
||||||
|
|
||||||
|
`iceyou_service.py` installs a **supervisor service** that runs as `LocalSystem`
|
||||||
|
and **launches the ICEYOU GUI agent into the active interactive session** (via
|
||||||
|
`CreateProcessAsUser`). The supervisor:
|
||||||
|
|
||||||
|
- starts the agent the moment a user logs in,
|
||||||
|
- relaunches it if the active session changes (fast user switching) or the
|
||||||
|
agent exits,
|
||||||
|
- terminates the agent and idles when no one is logged in (nothing to protect),
|
||||||
|
- writes diagnostics to `iceyou_service.log`.
|
||||||
|
|
||||||
|
The service binary is registered as the venv's `python.exe` running
|
||||||
|
`iceyou_service.py` directly (no `pythonservice.exe` host is used).
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Run these commands in an **elevated (Administrator) PowerShell**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd F:\ICEYOU
|
||||||
|
|
||||||
|
# Install the service (starts automatically at boot)
|
||||||
|
python iceyou_service.py install
|
||||||
|
|
||||||
|
# Start it now
|
||||||
|
python iceyou_service.py start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python iceyou_service.py start # start the service
|
||||||
|
python iceyou_service.py stop # stop the service (and its agent)
|
||||||
|
python iceyou_service.py restart # restart
|
||||||
|
python iceyou_service.py remove # uninstall the service
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also control it from `services.msc` (look for **ICEYOU Anti-Intrusion
|
||||||
|
Monitor**) or with `sc.exe`.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
After installation, check the registration and status:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sc.exe qc ICEYOUMonitor
|
||||||
|
Get-Service ICEYOUMonitor
|
||||||
|
Get-Content F:\ICEYOU\iceyou_service.log -Tail 30
|
||||||
|
```
|
||||||
|
|
||||||
|
The `BINARY_PATH_NAME` should show the venv `python.exe` running
|
||||||
|
`iceyou_service.py`.
|
||||||
|
|
||||||
|
### Notes & limitations
|
||||||
|
|
||||||
|
- Must be installed from an **elevated** prompt (Administrator).
|
||||||
|
- With **no user logged in** there is no desktop, so the white screen cannot
|
||||||
|
appear — the service waits and starts the agent at the next logon.
|
||||||
|
- The GUI agent runs as the **logged-in user** and uses that user's
|
||||||
|
`config.json` and camera.
|
||||||
|
- Antivirus may flag the service (keyboard hooks + session switching). Whitelist
|
||||||
|
the project folder if needed.
|
||||||
|
- This replaces the lighter **Add to Windows Startup** (`startup.py`) option.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
- **Error 1053** ("did not respond in a timely fashion"): Usually means the
|
||||||
|
registered binary path is wrong or the module name is missing. Re-run
|
||||||
|
`python iceyou_service.py remove` then `install`.
|
||||||
|
- No log file appears: Check `iceyou_service_startup_error.log` for import-time
|
||||||
|
crashes.
|
||||||
|
- Service starts then immediately stops: The most common cause is an incorrect
|
||||||
|
`binPath`. Use `sc.exe qc ICEYOUMonitor` to verify it points to the venv
|
||||||
|
`python.exe`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tray Menu
|
## Tray Menu
|
||||||
|
|
||||||
|
|
@ -311,6 +401,7 @@ To harden against CTRL+ALT+DEL → Task Manager, run as Administrator and set th
|
||||||
```
|
```
|
||||||
ICEYOU/
|
ICEYOU/
|
||||||
├── main.py Tray app entrypoint
|
├── main.py Tray app entrypoint
|
||||||
|
├── iceyou_service.py Windows service supervisor (launches agent into active session)
|
||||||
├── face_enrollment.py Standalone face capture + LBPH training
|
├── face_enrollment.py Standalone face capture + LBPH training
|
||||||
├── test_email.py CLI SMTP sanity check
|
├── test_email.py CLI SMTP sanity check
|
||||||
├── config.json Runtime settings (gitignored)
|
├── config.json Runtime settings (gitignored)
|
||||||
|
|
|
||||||
481
iceyou_service.py
Normal file
481
iceyou_service.py
Normal file
|
|
@ -0,0 +1,481 @@
|
||||||
|
"""ICEYOU Windows Service (supervisor / session launcher).
|
||||||
|
|
||||||
|
Why a *supervisor* service instead of running the app directly as a service?
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
ICEYOU is a GUI lock-down application (full-screen white-out, password/face
|
||||||
|
prompt, tray icon, low-level keyboard hooks). A normal Windows service runs in
|
||||||
|
**Session 0**, which—since Windows Vista—is isolated from the interactive
|
||||||
|
desktop. Any window a Session-0 process creates is invisible to the logged-in
|
||||||
|
user, so the white-screen lockout would never appear.
|
||||||
|
|
||||||
|
This service therefore does NOT draw any UI itself. Instead it:
|
||||||
|
|
||||||
|
1. Runs as LocalSystem, auto-starts at boot, has no console window, and keeps
|
||||||
|
running across logoff/logon (i.e. "without an interactive session").
|
||||||
|
2. Detects the currently active interactive console session.
|
||||||
|
3. Launches the ICEYOU GUI agent (``pythonw main.py``) *inside that user's
|
||||||
|
session* using CreateProcessAsUser, so the white screen / tray render
|
||||||
|
where the user can actually see them.
|
||||||
|
4. Supervises the child: if the user logs off and another logs on, or the
|
||||||
|
agent dies, it relaunches the agent in the new active session.
|
||||||
|
|
||||||
|
When no user is logged in there is nothing to protect and no desktop to draw
|
||||||
|
on, so the service simply waits and starts the agent the moment someone logs
|
||||||
|
in.
|
||||||
|
|
||||||
|
Usage (run an *elevated / Administrator* PowerShell):
|
||||||
|
|
||||||
|
# one-time: register the pywin32 service host (if not already done)
|
||||||
|
python .venv\\Scripts\\pywin32_postinstall.py -install
|
||||||
|
|
||||||
|
# install + set to start automatically at boot
|
||||||
|
python iceyou_service.py --startup auto install
|
||||||
|
|
||||||
|
# control
|
||||||
|
python iceyou_service.py start
|
||||||
|
python iceyou_service.py stop
|
||||||
|
python iceyou_service.py restart
|
||||||
|
python iceyou_service.py remove
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Paths (resolved relative to this file so the service is install-location
|
||||||
|
# independent). This file is expected to live at the project root next to
|
||||||
|
# main.py.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _bootstrap_pywin32_path() -> None:
|
||||||
|
"""Ensure the pywin32 sub-packages are importable.
|
||||||
|
|
||||||
|
When Windows starts the service it runs ``pythonservice.exe`` with a
|
||||||
|
minimal ``sys.path`` that may NOT include the pywin32 directories
|
||||||
|
(``site-packages\\win32``, ``win32\\lib``, ``Pythonwin``) where
|
||||||
|
``servicemanager``/``win32event``/... actually live. Importing them would
|
||||||
|
then fail with ``ModuleNotFoundError: No module named 'servicemanager'``
|
||||||
|
and the service times out (error 1053). We add those directories (and the
|
||||||
|
DLL directory) up front so the host can import them regardless of how it
|
||||||
|
was launched.
|
||||||
|
"""
|
||||||
|
# Always put the project root first so "import iceyou_service" and any
|
||||||
|
# "from iceyou.xxx" imports succeed even if the service host's cwd or
|
||||||
|
# initial sys.path does not contain the directory where this file lives.
|
||||||
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
site_dirs = []
|
||||||
|
# The project's virtualenv is the primary, reliable location.
|
||||||
|
site_dirs.append(PROJECT_ROOT / ".venv" / "Lib" / "site-packages")
|
||||||
|
try:
|
||||||
|
import site
|
||||||
|
for p in site.getsitepackages():
|
||||||
|
site_dirs.append(Path(p))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback: site-packages relative to the running interpreter.
|
||||||
|
site_dirs.append(Path(sys.prefix) / "Lib" / "site-packages")
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for sp in site_dirs:
|
||||||
|
if not sp or sp in seen or not sp.is_dir():
|
||||||
|
continue
|
||||||
|
seen.add(sp)
|
||||||
|
for sub in ("win32", os.path.join("win32", "lib"), "Pythonwin"):
|
||||||
|
d = sp / sub
|
||||||
|
if d.is_dir() and str(d) not in sys.path:
|
||||||
|
sys.path.append(str(d))
|
||||||
|
dll_dir = sp / "pywin32_system32"
|
||||||
|
if dll_dir.is_dir():
|
||||||
|
os.environ["PATH"] = str(dll_dir) + os.pathsep + os.environ.get("PATH", "")
|
||||||
|
try:
|
||||||
|
os.add_dll_directory(str(dll_dir))
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _early_crash_dump(exc: BaseException) -> None:
|
||||||
|
"""Write any import-time exception to a file so we can see it even if
|
||||||
|
the service host never reaches our normal logger."""
|
||||||
|
try:
|
||||||
|
with open(PROJECT_ROOT / "iceyou_service_startup_error.log", "a", encoding="utf-8") as f:
|
||||||
|
import traceback
|
||||||
|
f.write("\n" + "=" * 80 + "\n")
|
||||||
|
f.write("ICEYOU service startup crash at " + __import__("datetime").datetime.now().isoformat() + "\n")
|
||||||
|
f.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
||||||
|
f.write("=" * 80 + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
_bootstrap_pywin32_path()
|
||||||
|
|
||||||
|
import servicemanager
|
||||||
|
import win32event
|
||||||
|
import win32service
|
||||||
|
import win32serviceutil
|
||||||
|
import win32ts
|
||||||
|
import win32con
|
||||||
|
import win32process
|
||||||
|
import win32profile
|
||||||
|
import win32api
|
||||||
|
except Exception as _bootstrap_exc: # pragma: no cover - only triggered on host failure
|
||||||
|
_early_crash_dump(_bootstrap_exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
MAIN_SCRIPT = PROJECT_ROOT / "main.py"
|
||||||
|
LOG_FILE = PROJECT_ROOT / "iceyou_service.log"
|
||||||
|
|
||||||
|
# How often (seconds) the supervisor re-checks the active session / child.
|
||||||
|
POLL_INTERVAL = 3.0
|
||||||
|
|
||||||
|
# Sentinel returned by WTSGetActiveConsoleSessionId when no one is at the
|
||||||
|
# physical console (e.g. nobody logged in, or a fast-user-switch transition).
|
||||||
|
NO_ACTIVE_SESSION = 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def _find_pythonw() -> str:
|
||||||
|
"""Locate the windowed Python interpreter used to run the GUI agent.
|
||||||
|
|
||||||
|
Prefer the project's virtualenv (so the agent gets the right packages),
|
||||||
|
then a pythonw next to the current interpreter, then plain python.
|
||||||
|
"""
|
||||||
|
candidates = [
|
||||||
|
PROJECT_ROOT / ".venv" / "Scripts" / "pythonw.exe",
|
||||||
|
Path(sys.executable).with_name("pythonw.exe"),
|
||||||
|
PROJECT_ROOT / ".venv" / "Scripts" / "python.exe",
|
||||||
|
Path(sys.executable),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c.exists():
|
||||||
|
return str(c)
|
||||||
|
# Last resort: hope pythonw is on PATH.
|
||||||
|
return "pythonw.exe"
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_logging() -> logging.Logger:
|
||||||
|
logger = logging.getLogger("iceyou.service")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
if not logger.handlers:
|
||||||
|
try:
|
||||||
|
handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
class ICEYOUService(win32serviceutil.ServiceFramework):
|
||||||
|
_svc_name_ = "ICEYOUMonitor"
|
||||||
|
_svc_display_name_ = "ICEYOU Anti-Intrusion Monitor"
|
||||||
|
_svc_description_ = (
|
||||||
|
"Supervises the ICEYOU anti-intrusion GUI agent, launching it into the "
|
||||||
|
"active interactive desktop session so the screen lock-out and tray "
|
||||||
|
"icon are visible to the logged-in user. Runs without an interactive "
|
||||||
|
"session and survives logoff/logon."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tell the SCM we want to be notified of console session changes so we can
|
||||||
|
# react immediately to logon/unlock rather than only via polling.
|
||||||
|
def __init__(self, args):
|
||||||
|
super().__init__(args)
|
||||||
|
# Both auto-reset, initially non-signalled.
|
||||||
|
self._stop_event = win32event.CreateEvent(None, 0, 0, None)
|
||||||
|
self._wake_event = win32event.CreateEvent(None, 0, 0, None)
|
||||||
|
self._stopping = False
|
||||||
|
self._child_handle = None # PyHANDLE of the running agent process
|
||||||
|
self._child_session = None # session id the agent was launched in
|
||||||
|
self.log = _setup_logging()
|
||||||
|
|
||||||
|
# ---- SCM control handlers ------------------------------------------
|
||||||
|
def SvcStop(self):
|
||||||
|
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||||
|
self.log.info("Stop requested; terminating agent and shutting down.")
|
||||||
|
self._stopping = True
|
||||||
|
self._terminate_child()
|
||||||
|
win32event.SetEvent(self._stop_event)
|
||||||
|
|
||||||
|
def SvcDoRun(self):
|
||||||
|
servicemanager.LogMsg(
|
||||||
|
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
||||||
|
servicemanager.PYS_SERVICE_STARTED,
|
||||||
|
(self._svc_name_, ""),
|
||||||
|
)
|
||||||
|
# MUST report RUNNING promptly (< ~30 s) or SCM times out with error 1053.
|
||||||
|
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
|
||||||
|
self.log.info("ICEYOU service starting. project_root=%s", PROJECT_ROOT)
|
||||||
|
try:
|
||||||
|
self._run_supervisor()
|
||||||
|
except Exception: # never let the service crash silently
|
||||||
|
self.log.exception("Supervisor loop crashed.")
|
||||||
|
self.log.info("ICEYOU service stopped.")
|
||||||
|
|
||||||
|
def SvcOtherEx(self, control, event_type, data):
|
||||||
|
# React promptly to logon / unlock by nudging the supervisor loop.
|
||||||
|
if control == win32service.SERVICE_CONTROL_SESSIONCHANGE:
|
||||||
|
self.log.info("Session change event: type=%s", event_type)
|
||||||
|
win32event.SetEvent(self._wake_event)
|
||||||
|
|
||||||
|
# ---- Supervisor loop ------------------------------------------------
|
||||||
|
def _run_supervisor(self):
|
||||||
|
pythonw = _find_pythonw()
|
||||||
|
self.log.info("GUI agent interpreter: %s", pythonw)
|
||||||
|
self.log.info("GUI agent script: %s", MAIN_SCRIPT)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Wake on: stop request, session change, or poll timeout.
|
||||||
|
rc = win32event.WaitForMultipleObjects(
|
||||||
|
(self._stop_event, self._wake_event),
|
||||||
|
False,
|
||||||
|
int(POLL_INTERVAL * 1000),
|
||||||
|
)
|
||||||
|
if rc == win32event.WAIT_OBJECT_0 or self._stopping:
|
||||||
|
break # stop event signalled
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ensure_agent(pythonw)
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Error while ensuring agent is running.")
|
||||||
|
|
||||||
|
def _ensure_agent(self, pythonw: str):
|
||||||
|
"""Make sure exactly one agent is running in the active session."""
|
||||||
|
session_id = win32ts.WTSGetActiveConsoleSessionId()
|
||||||
|
|
||||||
|
if session_id == NO_ACTIVE_SESSION:
|
||||||
|
# No one at the console: nothing to protect. Make sure no stale
|
||||||
|
# agent lingers and wait for a logon.
|
||||||
|
if self._child_handle is not None:
|
||||||
|
self.log.info("No active session; terminating stale agent.")
|
||||||
|
self._terminate_child()
|
||||||
|
return
|
||||||
|
|
||||||
|
child_alive = self._child_alive()
|
||||||
|
|
||||||
|
if child_alive and self._child_session == session_id:
|
||||||
|
return # already running in the right session
|
||||||
|
|
||||||
|
if child_alive and self._child_session != session_id:
|
||||||
|
# Active session changed (e.g. fast user switch). Move the agent.
|
||||||
|
self.log.info(
|
||||||
|
"Active session changed %s -> %s; relaunching agent.",
|
||||||
|
self._child_session, session_id,
|
||||||
|
)
|
||||||
|
self._terminate_child()
|
||||||
|
|
||||||
|
self._launch_agent(pythonw, session_id)
|
||||||
|
|
||||||
|
def _child_alive(self) -> bool:
|
||||||
|
if self._child_handle is None:
|
||||||
|
return False
|
||||||
|
rc = win32event.WaitForSingleObject(self._child_handle, 0)
|
||||||
|
return rc == win32event.WAIT_TIMEOUT # still running
|
||||||
|
|
||||||
|
def _terminate_child(self):
|
||||||
|
if self._child_handle is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if self._child_alive():
|
||||||
|
win32process.TerminateProcess(self._child_handle, 0)
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Failed to terminate agent process.")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
win32api.CloseHandle(self._child_handle)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._child_handle = None
|
||||||
|
self._child_session = None
|
||||||
|
|
||||||
|
def _launch_agent(self, pythonw: str, session_id: int):
|
||||||
|
"""Spawn the GUI agent inside the given interactive session."""
|
||||||
|
user_token = None
|
||||||
|
env = None
|
||||||
|
try:
|
||||||
|
# Requires SE_TCB_NAME, which LocalSystem holds.
|
||||||
|
user_token = win32ts.WTSQueryUserToken(session_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
env = win32profile.CreateEnvironmentBlock(user_token, False)
|
||||||
|
creation_flags = (
|
||||||
|
win32con.CREATE_NO_WINDOW
|
||||||
|
| win32con.CREATE_UNICODE_ENVIRONMENT
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.log.warning(
|
||||||
|
"CreateEnvironmentBlock failed; launching without env block."
|
||||||
|
)
|
||||||
|
env = None
|
||||||
|
creation_flags = win32con.CREATE_NO_WINDOW
|
||||||
|
|
||||||
|
startup = win32process.STARTUPINFO()
|
||||||
|
# Target the interactive default desktop so windows are visible.
|
||||||
|
startup.lpDesktop = "winsta0\\default"
|
||||||
|
|
||||||
|
cmdline = f'"{pythonw}" "{MAIN_SCRIPT}"'
|
||||||
|
|
||||||
|
proc_info = win32process.CreateProcessAsUser(
|
||||||
|
user_token, # hToken
|
||||||
|
None, # appName (use cmdline)
|
||||||
|
cmdline, # commandLine
|
||||||
|
None, # processAttributes
|
||||||
|
None, # threadAttributes
|
||||||
|
False, # inheritHandles
|
||||||
|
creation_flags,
|
||||||
|
env, # environment
|
||||||
|
str(PROJECT_ROOT), # currentDirectory
|
||||||
|
startup,
|
||||||
|
)
|
||||||
|
h_process, h_thread, pid, tid = proc_info
|
||||||
|
try:
|
||||||
|
win32api.CloseHandle(h_thread)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._child_handle = h_process
|
||||||
|
self._child_session = session_id
|
||||||
|
self.log.info(
|
||||||
|
"Launched ICEYOU agent in session %s (pid=%s).", session_id, pid
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.log.exception(
|
||||||
|
"Failed to launch agent in session %s.", session_id
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if user_token is not None:
|
||||||
|
try:
|
||||||
|
win32api.CloseHandle(user_token)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _service_command_line() -> str:
|
||||||
|
"""Full command the SCM should launch to run this service.
|
||||||
|
|
||||||
|
We bypass pywin32's ``pythonservice.exe`` host entirely and register the
|
||||||
|
venv's ``python.exe`` running THIS script directly. When the SCM starts it
|
||||||
|
with no extra arguments, ``main()`` calls ``StartServiceCtrlDispatcher`` and
|
||||||
|
connects to the SCM. This avoids all the host-exe path/module problems that
|
||||||
|
caused the silent error 1053.
|
||||||
|
"""
|
||||||
|
py = PROJECT_ROOT / ".venv" / "Scripts" / "python.exe"
|
||||||
|
if not py.exists():
|
||||||
|
py = Path(sys.executable)
|
||||||
|
script = Path(__file__).resolve()
|
||||||
|
return f'"{py}" "{script}"'
|
||||||
|
|
||||||
|
|
||||||
|
def _install_service(startup_type: int = win32service.SERVICE_AUTO_START) -> None:
|
||||||
|
cmdline = _service_command_line()
|
||||||
|
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
|
||||||
|
try:
|
||||||
|
# Remove a stale registration first so we always get a clean binPath.
|
||||||
|
try:
|
||||||
|
old = win32service.OpenService(
|
||||||
|
hscm, ICEYOUService._svc_name_, win32service.SERVICE_ALL_ACCESS
|
||||||
|
)
|
||||||
|
win32service.DeleteService(old)
|
||||||
|
win32service.CloseServiceHandle(old)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
hs = win32service.CreateService(
|
||||||
|
hscm,
|
||||||
|
ICEYOUService._svc_name_,
|
||||||
|
ICEYOUService._svc_display_name_,
|
||||||
|
win32service.SERVICE_ALL_ACCESS,
|
||||||
|
win32service.SERVICE_WIN32_OWN_PROCESS,
|
||||||
|
startup_type,
|
||||||
|
win32service.SERVICE_ERROR_NORMAL,
|
||||||
|
cmdline,
|
||||||
|
None, # load order group
|
||||||
|
0, # tag id
|
||||||
|
None, # dependencies
|
||||||
|
None, # account (LocalSystem)
|
||||||
|
None, # password
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
win32service.ChangeServiceConfig2(
|
||||||
|
hs,
|
||||||
|
win32service.SERVICE_CONFIG_DESCRIPTION,
|
||||||
|
ICEYOUService._svc_description_,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
win32service.CloseServiceHandle(hs)
|
||||||
|
print(f"Service {ICEYOUService._svc_name_} installed (auto-start).")
|
||||||
|
print(f" binPath = {cmdline}")
|
||||||
|
finally:
|
||||||
|
win32service.CloseServiceHandle(hscm)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_service() -> None:
|
||||||
|
try:
|
||||||
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
win32serviceutil.RemoveService(ICEYOUService._svc_name_)
|
||||||
|
print(f"Service {ICEYOUService._svc_name_} removed.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Remove failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_as_service() -> None:
|
||||||
|
"""Entry point used when the SCM launches us as a service process."""
|
||||||
|
try:
|
||||||
|
servicemanager.Initialize()
|
||||||
|
servicemanager.PrepareToHostSingle(ICEYOUService)
|
||||||
|
servicemanager.StartServiceCtrlDispatcher()
|
||||||
|
except Exception as exc:
|
||||||
|
_early_crash_dump(exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
# No args => launched by the Service Control Manager.
|
||||||
|
_run_as_service()
|
||||||
|
return
|
||||||
|
|
||||||
|
args_lower = [a.lower() for a in sys.argv[1:]]
|
||||||
|
|
||||||
|
if "install" in args_lower:
|
||||||
|
startup = (
|
||||||
|
win32service.SERVICE_DISABLED
|
||||||
|
if "disabled" in args_lower
|
||||||
|
else win32service.SERVICE_AUTO_START
|
||||||
|
)
|
||||||
|
_install_service(startup)
|
||||||
|
elif "remove" in args_lower or "uninstall" in args_lower:
|
||||||
|
_remove_service()
|
||||||
|
elif "start" in args_lower:
|
||||||
|
win32serviceutil.StartService(ICEYOUService._svc_name_)
|
||||||
|
print("Service start requested.")
|
||||||
|
elif "stop" in args_lower:
|
||||||
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
||||||
|
print("Service stop requested.")
|
||||||
|
elif "restart" in args_lower:
|
||||||
|
try:
|
||||||
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
win32serviceutil.StartService(ICEYOUService._svc_name_)
|
||||||
|
print("Service restarted.")
|
||||||
|
else:
|
||||||
|
# Anything else: defer to pywin32's standard handler.
|
||||||
|
win32serviceutil.HandleCommandLine(ICEYOUService)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user