Skip to content

feat(dev): start Caddy on Windows, and never touch a Caddy lt dev did not start (AP-7) - #120

Draft
DKoenig9 wants to merge 1 commit into
mainfrom
feat/windows-dev-caddy
Draft

DKoenig9 wants to merge 1 commit into
mainfrom
feat/windows-dev-caddy

Conversation

@DKoenig9

Copy link
Copy Markdown
Contributor

Why

Windows had no way to run Caddy. lt dev install answered "not supported", and every other command (up, test, tunnel, status, doctor) then advised running lt dev install. That was advice into the void.

Ownership rule, on every platform. Someone who runs Caddy runs it for something, possibly a client project on :443. Reloading our config into that Caddy would silently reroute or cut off its sites, and whoever is affected would look for the fault everywhere but here. So lt dev never touches a Caddy it did not start. It stops, says what it would have done, and puts the tools next to that. It never stops the other Caddy and never reloads it. The same direction as the registry fix in #116: when in doubt, pick the one that destroys nothing.

What changed

Ownership: by what is loaded, not by which process (src/lib/caddy.ts#detectCaddyOwner)

  • none: nothing answers on :2019.
  • ours: the loaded config carries an inert log lt-dev-owner { output discard } global option. Comments do not survive caddy adapt, so our # >>> lt-dev:<slug> >>> block markers never reach GET /config/. A named logger does, even in a Caddyfile without sites, and the default logger keeps logging as before (measured, caddy v2.11.3).
  • ours without a marker: the loaded config equals caddy adapt of our Caddyfile. That is how every existing installation is recognised. Its next reload carries the marker. Because of that, callers ask before they rewrite the Caddyfile, and every call site is ordered that way.
  • foreign: anything else that answers, including an error status or a non-JSON reply. Something holds the port, and it is not us.
  • I did not use the process command line: it is read differently on every platform, so on each one the branch for the others would go untested.

One gate for every command (src/lib/dev-caddy-gate.ts#ensureOwnCaddy), used by up, test, tunnel and the test session:

  • ours: ok.
  • foreign: refused. foreignCaddyLines prints the path of our Caddyfile, caddy reload --config … --adapter caddyfile (with the note that it replaces that Caddy's whole config) and caddy run --config … for once :2019 is free. It does not suggest stopping anything.
  • none: on Windows (with startIfDown) our Caddy is started, and it counts only once it identifies as ours. macOS/Linux point at lt dev install, because the service owns the lifecycle there.
  • down, test teardown, install and uninstall ask detectCaddyOwner first. down still stops our processes and removes our block from our file, but does not reload a foreign Caddy.
  • doctor and status state "not started by lt dev". In doctor that counts as a FAIL, because lt dev cannot route through it, but it comes without a call to action.

Windows start (D1) (src/lib/dev-service.ts)

  • caddyLaunchMode() returns:
    • service on macOS/Linux
    • on-demand on Windows
    • manual elsewhere
  • startCaddyOnDemand runs caddy run --config <ours> --adapter caddyfile via spawnDetached and logs to ~/.lenneTech/caddy.log. I did not use caddy start: it hands the background child the caller's stdout/stderr, and that pipe closes when the CLI exits.
  • lt dev install on Windows: ownership check, then Caddyfile, then start, then the caddy trust hint. The order is start first, trust second, because caddy trust was measured to fail against a Caddy that is not running.
  • isMachinePrepared on Windows means "our Caddyfile exists", so lt dev init chains into install exactly once.
  • lt dev uninstall on Windows stops our Caddy (caddy stop), and only when it is ours.

Also fixed: lt dev install reset the Caddyfile on every run. It wrote the empty stub unconditionally and dropped the block of every project that was up. ensureCaddyfile now creates the file only when it is missing, and otherwise only adds the marker. If ours is already running, it reloads so the marker loads. Without that reload, the config would no longer match the file and the next check would call our own Caddy foreign. That was a migration bug, and I caught it before the first run.

Measured on this Mac (read-only against the live instance)

Check Result
caddy adapt: comments in the config 0 hits, markers lost
caddy adapt: log lt-dev-owner without sites {"logging":{"logs":{"lt-dev-owner":…}}}
Throwaway instance (admin :2999) with marker default logs unchanged on stderr
Output of ensureOwnerMarker (new block, and inserted into an existing global block) adapts with exit 0, marker present, 1 route
detectCaddyOwner() against the live, pre-marker lt-dev Caddy ours
Same comparison against a second instance with a different config (:2999) foreign

The live Caddyfile was not modified (mtime 10:02, before this work). Every write-side test runs against LT_DEV_CADDYFILE in a temp dir with injected owner answers.

Mutation check (reverted from a backup)

Mutation Result
Marker ignored 1 red
Legacy comparison never ours 1 red
Error status counts as none 1 red
Gate does not refuse foreign 1 red
No re-check after the start 1 red
Marker not added 4 red
Advice says "stop" 1 red
down without an owner check 1 red (static guard)
ensureCaddyfile always resets 1 red
caddy start instead of caddy run 1 red

The first two mutations first went "red" because of a compile error (an unused function), i.e. for the wrong reason. I redid them with mutations that compile.

npm test: 79 suites, 1236 tests. lint and compile are clean. In one full npm run build, a test in fullstack-add-commands hit the 60 s timeout at a load average of 175. Run alone it is green (14/14), and it does not touch Caddy.

Not measured yet (Windows laptop)

PowerShell, in a project, with an lt built from this branch. Every step checks a return value, not an output line.

# 1. install starts Caddy, and the loaded config carries our marker
lt dev install; "install exit=$LASTEXITCODE"
[bool](Get-NetTCPConnection -LocalPort 2019 -State Listen -ErrorAction SilentlyContinue)      # True
$null -ne (Invoke-RestMethod http://127.0.0.1:2019/config/).logging.logs.'lt-dev-owner'       # True

# 2. KEY QUESTION: does the detached Caddy survive closing the terminal?
#    Close this PowerShell window, open a new one:
[bool](Get-NetTCPConnection -LocalPort 2019 -State Listen -ErrorAction SilentlyContinue)      # expected True

# 3. up uses the running Caddy (no second start)
lt dev up; "up exit=$LASTEXITCODE"                                                            # 0, no "Started Caddy."

# 4. A foreign Caddy is refused and stays untouched
lt dev down; lt dev uninstall --noConfirm; "uninstall exit=$LASTEXITCODE"
[bool](Get-NetTCPConnection -LocalPort 2019 -State Listen -ErrorAction SilentlyContinue)      # False
Set-Content "$env:TEMP\foreign.Caddyfile" "http://foreign.localhost:18080 {`n  respond `"foreign`"`n}"
Start-Process caddy -ArgumentList 'run','--config',"$env:TEMP\foreign.Caddyfile",'--adapter','caddyfile'
Start-Sleep 3
$before = (Invoke-WebRequest http://127.0.0.1:2019/config/ -UseBasicParsing).Content
lt dev up; "up exit=$LASTEXITCODE"                                                            # 1, lists caddy reload/run
$after  = (Invoke-WebRequest http://127.0.0.1:2019/config/ -UseBasicParsing).Content
"foreign config unchanged: $($before -eq $after)"                                             # True
(Invoke-WebRequest http://foreign.localhost:18080 -UseBasicParsing).Content                   # foreign
lt dev doctor; "doctor exit=$LASTEXITCODE"                                                    # 1, "not started by lt dev"

# 5. Close the foreign Caddy window, then: up starts ours again
lt dev up; "up exit=$LASTEXITCODE"                                                            # 0, "Started Caddy."

If step 2 shows False, the detached child does not survive the terminal, and on-demand then needs a different start (for example a scheduled task at logon). That is the one open question behind D1.

Not covered by this PR (AP-6): the remaining doctor messages (brew install caddy as the only install hint, /etc/hosts, Caddyfile validation before the first start).

🤖 Generated with Claude Code

… not start (AP-7)

Windows had no way to run Caddy: `lt dev install` said "not supported", and
every other command then advised running `lt dev install`. Decision D1: no
Windows service (it would run as SYSTEM with a CA the user's browser does not
trust). Instead `lt dev install` and `lt dev up` start Caddy on demand:
`caddy run --config <ours>` via `spawnDetached`, logging to
~/.lenneTech/caddy.log. Not `caddy start`, which hands the background child
the caller's stdio.

Ownership rule, on every platform: someone who runs Caddy runs it for
something. Reloading our config into it would silently reroute or cut off those
sites. `detectCaddyOwner` decides by the loaded config, not by the process:
- `ours` when an inert `log lt-dev-owner { output discard }` global option is
  loaded. Comments do not survive adaptation, so the block markers cannot serve
  as the marker.
- `ours` for pre-marker instances when the loaded config equals `caddy adapt`
  of our file. Callers therefore ask before they rewrite it.
- `foreign` for anything else answering on :2019, `none` when nothing answers.
Measured on this Mac: the live pre-marker instance reads as ours, a second
instance with another config as foreign.

`ensureOwnCaddy` is the one gate for up, test, tunnel and the test session. A
foreign Caddy gets our Caddyfile path plus `caddy reload` / `caddy run`, never
a stop or a reload. down, test teardown, install and uninstall ask first too.
doctor and status state "not started by lt dev" without advice.

Also fixed: `lt dev install` rewrote the Caddyfile stub on every run, dropping
the block of every project that was up. It now only adds the owner marker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant