# Services and reboots

> Keeping apps alive with native launchd jobs, systemd user units or Windows scheduled tasks, the PATH trap, log rotation, and "lost at last reboot".

Hallway keeps the apps you care about running with the OS's own service manager: **launchd jobs** on macOS, the
mechanism macOS uses for its own background services, **systemd user units** on Linux, and, best effort,
**per-user scheduled tasks** on Windows ([below](#windows)). There is no PM2 and no second process manager.

## Hallway itself

<span class="ms ms--v0">v0</span> Hallway runs itself the same way, as a user LaunchAgent on macOS, a systemd user
unit on Linux, or a scheduled task on Windows:

```sh
hallway service install      # loaded now and at every login
hallway service status
hallway service restart
hallway service uninstall
```

On macOS the job is `~/Library/LaunchAgents/hallway.daemon.plist`; on Linux it is
`~/.config/systemd/user/hallway-daemon.service`; on Windows it is the scheduled task `\hallway.daemon` (see
[Windows](#windows)). All start at **login**, not at boot, which is the right behaviour
for a laptop. On Linux, enabling lingering once (`loginctl enable-linger $USER`) keeps Hallway's units running
without a login session and starts them at boot; Hallway never enables it itself, and `hallway doctor` warns while it
is off.

Installing, starting, stopping and restarting these jobs have run for real against launchd, systemd and Windows' Task
Scheduler in CI. Coming back after a logout or a reboot has not been tested yet, on any OS.

## Managed services

<span class="ms ms--shipped">v1</span> An app with a `service:` block is a **managed service**: Hallway generates a job for it,
`~/Library/LaunchAgents/hallway.app.<slug>.plist` on macOS, `~/.config/systemd/user/hallway-app-<slug>.service` on
Linux, or the scheduled task `\hallway.app.<slug>` on Windows, and keeps it alive.

```yaml
# apps/slides.yaml
name: Slides
port: 4321
expected: running
service:
  command: ["npx", "astro", "dev", "--port", "4321"]
  cwd: ~/Code/slides
```

```sh
hallway service add slides     # generate and load the job: starts at login, restarted if it exits
hallway stop slides            # stop and disable it, so it stays stopped across logins; expected → stopped
hallway start slides
hallway restart slides
hallway logs slides --follow
hallway service rm slides      # stop managing it; the app stays declared
```

Every Hallway job has a `hallway.` label (`hallway.daemon`, `hallway.app.<slug>`, and in
[fronted mode](/networking/fronted-mode/) `hallway.caddy` and, for a custom domain, `hallway.dnsmasq`); on Linux the
units are named the same way with hyphens (`hallway-daemon.service`, `hallway-app-<slug>.service`). So they are easy
to find, audit and remove, and can't be confused with jobs you made yourself.

Starting a server for the first time is usually done with [`hallway run`](/agents/hallway-run/), which declares the
app and creates its job in one step.

### Ports and environment

Each managed service gets `PORT` set to its port and `HOST=127.0.0.1` (plus `HOSTNAME`, for frameworks that read
that). `$PORT` and `$HOST` in the command's arguments are replaced with the app's values when the job is generated.
Frameworks that ignore `PORT` need a flag:

| Framework | Flags |
| --- | --- |
| Vite | `--port $PORT --strictPort` |
| Astro | `--port $PORT` |
| Next.js | `-p $PORT` |

In a YAML `command:` list, `$PORT` needs no quoting; on the `hallway run` command line, quote it (`'$PORT'`) so your
shell doesn't expand it first.

`--strictPort` matters for Vite: without it, Vite quietly picks another port when its port is busy, and the app
"moves" away from the address everything else expects.

### Windows

<span class="ms ms--shipped">v1</span> Since 0.5.0, best effort. Windows has no user-level launchd or systemd, so the
daemon and each managed app are a **scheduled task** in Task Scheduler's root folder, named by its label
(`\hallway.daemon`, `\hallway.app.<slug>`). Each is registered as you, with no administrator rights and no stored
password, and starts when you **log on**.

Task Scheduler restarts a task only when it fails to *start*, not when its program exits, so the task runs a small
supervisor, `hallway supervise` (internal, not in `--help`), inside a headless console so no window opens. The
supervisor runs the command in its directory, appends its output to the usual log files, starts it again 10 seconds
after it exits, and records its state for `hallway service status`. The task's files are in
`%LOCALAPPDATA%\hallway\tasks\`.

`hallway stop` disables the task as well as ending it, so it stays stopped at the next logon, as on the other OSes.

- **Commands are looked up as cmd.exe does** (`PATHEXT`), so `vite` finds npm's `vite.cmd`, never the `#!/bin/sh`
  script npm puts beside it.
- **Nothing runs while you're logged off.** That would need your password stored with the task, or a service account
  and administrator rights; Hallway asks for neither, so lingering has no Windows equivalent.
- **Checked on GitHub's Windows runners** (install, `hallway run`, stop, start, restart, `--persist` surviving a killed
  process, log rotation, doctor and removal), **not yet on a real Windows desktop**: a real logon starting the tasks,
  a logoff stopping them, no console window flashing, and a standard (non-administrator) user registering them are
  all still unverified.

### The PATH trap

launchd jobs and systemd units inherit no shell environment, so a job's `PATH` has to be written into it. Copying your current shell's
`PATH` is a latent bug: some Node version managers put **per-shell directories** on it. fnm, for example, adds a
`fnm_multishells/<pid>_<timestamp>/bin` symlink created for one terminal session. Copy that into a job and the
service's only route to `node`, `npm` and `npx` is a leftover from a shell that closed long ago. Clean it up and every
`#!/usr/bin/env node` tool the service runs breaks, while the service itself keeps running.

So Hallway's job generator:

- resolves the command and the Node binary to **absolute, real paths** in the version manager's stable install
  directory, never a shim;
- puts **that Node's own `bin` directory first** on `PATH`, so `node`, `npm` and `npx` always resolve, to the same
  Node that runs the service;
- builds the rest of `PATH` from stable directories only, dropping per-shell entries and anything that doesn't exist;
- records the Node version, so `hallway doctor` can warn when it has been uninstalled, and
  `hallway service regenerate` can fix it.

### Logs

Each service writes to `~/.hallway/logs/<slug>.out.log` and `.err.log`. None of launchd, systemd or Task Scheduler rotates those
files, and because they keep them open, renaming doesn't work either, so the daemon copies and truncates them in place once they
pass a size limit (10 MB by default), keeping three generations.

## Lost at last reboot

<span class="ms ms--shipped">v1</span> When Hallway starts, it compares the machine's boot time with the last one it saw. If the machine has rebooted, every
app or listener that was alive before the reboot and is not listening now goes into a notice at the top of the
dashboard: **the doors that closed while you were away**.

```text
⚠ Lost at last reboot (Mon 28 Sep 20:05): 6 apps        [Review]  [Dismiss]
```

**Review** lists each one: what it was, where it ran from (`~/Code/my-app @ main`) and the command that started it,
with three actions:

- **Restart once** starts it again the way [`hallway run`](/agents/hallway-run/) does: the operating system's service
  manager owns it, on the port it had. It won't come back after another reboot.
- **Make it a service** does the same, and also starts it at login and restarts it if it exits.
- **Dismiss** takes it off the list (the banner's own Dismiss clears the whole notice).

Something that wasn't an app yet becomes one when you start it (`apps/<slug>.yaml`, on its own port, with the command
and directory Hallway recorded). Anything that comes back, by one of these actions or any other way, is marked
**back** until you dismiss it. "Alive before the reboot" means seen in Hallway's last scans before it: a server you
stopped a day earlier isn't listed. The notice survives Hallway restarting, until you dismiss it.

From a terminal:

```sh
hallway lost                      # what was lost, with ids, directories and commands
hallway lost restart <id|port>    # start one again, once
hallway lost service <id|port>    # start it again, as a service
hallway lost dismiss <id|port>    # or --all
```

Services that start at login get their start-up grace period (`services.startGraceSeconds`, 30 seconds by default)
before anything is called lost, so a slow starter isn't listed by mistake. A server started by one of these actions
shows as <span class="state state--starting">◐ starting</span> while it comes up. The notice is kept in
`state/lost.json` and is also in `/api/state` and [`GET /api/lost`](/reference/http-api/), in `llms.txt`, and in the
[MCP server](/agents/mcp/)'s `get_status`; MCP can read it but can't restart anything.

:::note[Checked by simulation]
"Lost at last reboot" has been checked on Linux with a simulated reboot (the recorded boot time moved back, the servers
killed, the daemon restarted). It has not yet been seen after a real reboot, on Linux or on a Mac.
:::

This only works because Hallway [records what it observes](/concepts/apps/#observed-listeners-the-scanner) while the
processes are alive. Once a process is gone, its working directory can't be recovered by any means, so a tool that
starts looking after the reboot is already too late.
