Added run as service

This commit is contained in:
Subinacls 2026-06-05 18:57:21 -04:00
parent fb012de76b
commit fe3b5e72d1
2 changed files with 572 additions and 0 deletions

View File

@ -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
@ -311,6 +401,7 @@ To harden against CTRL+ALT+DEL → Task Manager, run as Administrator and set th
```
ICEYOU/
├── main.py Tray app entrypoint
├── iceyou_service.py Windows service supervisor (launches agent into active session)
├── face_enrollment.py Standalone face capture + LBPH training
├── test_email.py CLI SMTP sanity check
├── config.json Runtime settings (gitignored)

481
iceyou_service.py Normal file
View 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**, whichsince Windows Vistais 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()