LocalRun

Service kinds

A .test name can point at very different things: a process LocalRun started, a dev server you started in a terminal, a database managed by launchd, or a Docker port forward. What you are allowed to do differs in each case, and doing the wrong thing has real consequences: signalling a launchd-supervised process, for example, just makes it restart and look unkillable.

So LocalRun classifies every listening service and the daemon decides which controls are offered. The tray renders what it is told. A greyed-out button always has a reason behind it, and hovering it shows you that reason.

The four kinds

KindWhat it isStart / stop
projectLocalRun started itFull control, logs captured
adoptedSomething you started; LocalRun only proxies to itStop kills the process tree; start needs a run command
systemlaunchd-supervised, or owned by another userRouted through the supervisor
proxyPort-only entry, or a Docker port forwardNeither

project

The normal case. You gave LocalRun a folder and a run command, so it spawned the process and is its parent. It has the output, so the Logs tab is live. Start, stop and restart all do exactly what they say.

adopted

You started something yourself (npm run dev in a terminal, say), and LocalRun is proxying a .test name to it. It can stop that process, because stopping is just a matter of killing the tree. It cannot show you logs, because the output went to your terminal, not to LocalRun.

Starting an adopted project again requires a run command. If LocalRun cannot work out how the process was launched, Start stays disabled and says so.

Killing a tree, not a group

A shell-started dev server is usually not its own process-group leader, so signalling the group would either miss it or take out your shell. LocalRun walks the descendants by parent pid instead.

system

The port belongs to something launchd supervises, or to another user. LocalRun never signals these directly; launchd would simply restart the process and you would see a service that refuses to die.

Instead, stop and start are routed through the supervisor, using launchctl. A user-scoped service needs no password. A root LaunchDaemon does, and macOS shows its own native authorisation prompt; the button tells you in advance that it will ask.

proxy

Either a port-only entry with nothing else known about it, or a docker-proxy process. Neither can be started or stopped meaningfully; killing docker-proxy does not stop the container behind it, so LocalRun refuses rather than pretending.

How the classification is made

None of the detection needs elevated privileges:

  • If the process's parent is the daemon, it is a project.
  • launchctl list maps a pid to a label for user services.
  • A root LaunchDaemon is found by matching the executable path against the plists in /Library/LaunchDaemons/ and reading the label with plutil.
  • A docker-proxy executable marks the entry as a proxy.

What the daemon reports

Along with the kind, the daemon returns a capability set for each project: whether it can be stopped, started, restarted, whether logs exist, whether a terminal can be opened, whether the action will need a password, and a plain-English reason.

The practical upshot: if a button is disabled, it is because the daemon knows the action would not work: not because the UI is being cautious. Hover it and the tooltip tells you which case you are in.

Next: Keep running covers who supervises a project once it is started.