start
The start command spawns flutter run as a detached background process, scrapes the VM Service URI from its output, and records the session so that every downstream tool (MCP server, stop, restart, tapir:snap, tinker, etc.) knows where the running app lives.
On this page
The session lives in a directory of its own, per project: ~/.dowel/sessions/<hash>/state.json, with the log and the FIFO beside it. It used to be one global ~/.dowel/state.json, so a second project's start silently took the slot and every command from the first project then drove the second app, succeeding each time. That file is still written, as a pointer to whichever session started last, because hand-writing it is the documented recovery recipe when start cannot boot an app.
Two projects at once still need distinct --port, --vm-service-port and --cdp-port values; start fails fast when one is taken. Override the session path with --state=<path> or DOWEL_STATE_FILE when you need to name it explicitly.
Basic Usage#
dart run dowel start
Launches a Chrome web dev session on port 3100 with the VM Service listener on port 8181. The command exits as soon as the URI is confirmed; the Flutter process continues running in the background.
Synopsis#
dart run dowel start [--device=<target>] [--port=<n>] [--vm-service-port=<n>]
[--[no-]dds] [--[no-]profile-static] [--cdp-port=<n>]
[--timeout=<n>] [--flutter-arg=<arg>]...
start accepts no positional arguments. All configuration is done via named options and flags declared in configure(ArgParser) in lib/src/commands/start_command.dart.
Options#
| Option | Type | Default | Description |
|---|---|---|---|
--device |
string | chrome |
Flutter device target. Accepts chrome, macos, linux, windows, an iOS UDID, or an Android serial number. |
--port |
int | 3100 |
Web server port passed as --web-port to flutter run. Ignored for non-Chrome targets. |
--vm-service-port |
int | 8181 |
VM Service listener port. Recorded in state.json; used by mcp:serve and connected-mode tools to open the WebSocket. |
--dds |
flag | false |
Enable Dart Development Service. When absent (default), --no-dds is forwarded to flutter run so tapir and tinker connect directly to the VM Service. |
--profile-static |
flag | false |
Start a static-profile run. Sets profile: "static" in state.json; otherwise "debug". On a device (Android, iOS, desktop) it also forwards --profile to flutter run, once even when --flutter-arg=--profile is passed too; there is no hot reload in that mode. On Chrome it stays a label, because a web profile build has no VM Service. restart replays it from state.json; restart --no-profile-static drops it. |
--cdp-port |
int | (none) | Chrome DevTools Protocol port. When set, start pre-launches Chrome with --remote-debugging-port=<n> and runs Flutter on -d web-server, recording chromePid / tmpProfileDir / cdpPort in state.json. Required for tapir:resize / tapir:device. Only valid with --device=chrome or --device=web-server, and requires Flutter SDK 3.30.0 or newer. A subsequent restart preserves this port. The port is probed before Chrome launches; if it is already in use you receive a clear error with the --cdp-port hint instead of a misleading "Is Chrome installed?" message. |
--timeout |
int | 90 |
Seconds to wait for the VM Service URI to appear in the flutter run log. Increase on cold starts where build and DartDev initialisation takes longer than the default (common on first run after a clean Flutter SDK install or on low-powered CI). On the plain branch a timeout while flutter run is still alive exits 75 and leaves the session recording (see Troubleshooting). Applies to the --cdp-port branch and to the plain one alike; on the --cdp-port branch it also bounds the wait for the web server's is being served at line, which used to be a fixed 60 seconds and failed a web compile that ran long under load. |
--flutter-arg |
string | (none) | Extra argument forwarded verbatim to flutter run, repeatable. Use it for anything start has no flag of its own for: --flutter-arg=--dart-define=KEY=VALUE, --flutter-arg=--flavor=dev, --flutter-arg=--web-renderer=html. Forwarded AFTER the arguments start builds, so a repeated flag overrides the default it chose. A value containing commas is kept whole (splitCommas is off), so --flutter-arg=--dart-define=TAGS=a,b reaches Flutter as one argument. Recorded in state.json as flutterArgs and replayed by restart; restart --flutter-arg=<arg> replaces the whole carried set, which is also how to clear one. |
Behaviour#
FIFO stdin channel. Before launching Flutter, start calls mkfifo to create a named pipe at ~/.dowel/sessions/<hash>/flutter-dev.fifo. A background tail -f /dev/null process holds the write end of the FIFO open permanently, preventing flutter run's stdin from receiving EOF when no keystroke sender is active. flutter run is then launched with the FIFO as its stdin, so the reload and hot-restart commands can send r or R into the pipe and trigger flutter_tools' own handler. This approach works on every device target (web, desktop, mobile) because it routes through flutter_tools rather than raw VM Service RPCs, which the Chrome dwds bridge rejects.
Detached spawn and URI scrape. The shell one-liner run by start launches both background processes (tail holder and flutter run) and echoes their PIDs in HOLDER=<n> / FLUTTER=<n> format. start captures those PIDs from the wrapper's stdout, then polls the log file at ~/.dowel/sessions/<hash>/flutter-dev.log every 250 ms until it finds a line matching either the web format (Debug service listening on ws://...) or the desktop/mobile format (Dart VM Service on <Platform> is available at: http://...). http:// and https:// URIs are normalised to their ws:// and wss:// equivalents and a /ws suffix is appended when missing.
State file write. After the URI is confirmed, start writes this project's session state atomically (.tmp + rename) with the full process inventory: PIDs, FIFO path, VM Service URI, web port, device target, profile mode, project root, and a UTC startedAt timestamp. The MCP server (mcp:serve) reads this file at startup to discover the running app; stop reads it to find the app to stop; status reads it to report the live process.
One process group per app. The wrapper shell is spawned in Dart's detached mode, which calls setsid(), so the tail holder, the flutter tool and every child the tool starts (frontend_server, the web server, adb or iproxy helpers) share a process group of their own, separate from the caller's. Chrome, launched detached by --cdp-port, gets its own group the same way. stop, restart and a failed --cdp-port start stop an app through that group:
- Check that the recorded pid is still the process
startspawned:stopcompares its age (ps -o etime=) against the session'sstartedAt, and a pid whose process started later is a reused number, left alone. - Read the group id from the live pid (
ps -o pgid= -p <pid>), or from the FIFO holder when the tool is gone and a child of it may not be. It is not stored, because once every member exits the number is free for reuse. A pid that shares the caller's own group is signalled alone, never its group. - Send
SIGTERMto the group and wait up to 5 seconds for every member to exit. A zombie does not count as a member; a process listing that fails counts as a live one. - Send
SIGKILLto the group if any member is left, and wait up to 5 seconds again. - Wait up to 5 seconds more for the ports the app held to come free: the web port of a browser session, the CDP port for Chrome. A port that stays bound is reported, never answered with
SIGKILL: once the group is gone, whatever holds it is not the app.
The command returns as soon as all of it is gone (about 15 seconds at worst per group, 30 with Chrome), or reports what is left when the budget runs out. On a host where ps cannot run (a slim container image) or prints an elapsed time in an unknown shape, the group cannot be found: the pid alone gets SIGTERM, as before process groups, nothing is waited on, and a warning says its children may outlive it. A SIGTERM to the tool pid alone, which is what these paths used to send, ended the tool and left frontend_server running under pid 1, still compiling; returning straight after the signal let the next start race a port the old app still held.
State File#
start writes the session atomically, once as soon as the child PIDs are known and again when the VM Service URI lands. The schema (from lib/src/state/state_file.dart:13-24):
{
"pid": 12345,
"stdinPipe": "/Users/you/.dowel/sessions/3f9a1c5e7b20/flutter-dev.fifo",
"stdinHolderPid": 12344,
"vmServiceUri": "ws://127.0.0.1:8181/AbCdEfGhIjK=/ws",
"webPort": 3100,
"vmServicePort": 8181,
"startedAt": "2026-05-19T10:00:00.000Z",
"profile": "debug",
"projectRoot": "/Users/you/Code/my-app",
"device": "chrome",
"chromePid": null,
"tmpProfileDir": null,
"cdpPort": null
}
Field reference:
| Field | Type | Notes |
|---|---|---|
pid |
int | PID of the flutter run process. stop reads its process group from it and stops the whole group. |
stdinPipe |
string | Absolute path to the FIFO. reload and hot-restart write r\n / R\n here. |
stdinHolderPid |
int | PID of the tail -f /dev/null holder that keeps the FIFO write-end open. It shares the tool's process group, so it stops with it; stop also signals it directly. |
vmServiceUri |
string | Canonical ws://host:port/<token>/ws URI. All connected-mode tools open this WebSocket. |
webPort |
int | --web-port value forwarded to Flutter. Chrome only; ignored for other targets. |
flutterArgs |
list | The --flutter-arg values this session was started with. Absent when there were none. restart reads it so the relaunched app keeps the build configuration the running one had; a dropped define compiles clean and behaves differently, where a dropped port at least refuses to bind. |
vmServicePort |
int | Informational; the port embedded in vmServiceUri. |
startedAt |
string | ISO 8601 UTC timestamp of the start invocation. |
profile |
string | "debug" or "static" (set by --profile-static). restart reads it so a profile session comes back as a profile build. |
projectRoot |
string | Absolute path to the working directory at invocation time. |
device |
string | The --device value passed to this run. |
chromePid |
int or null | PID of the Chrome process pre-launched by --cdp-port; null on non-CDP runs. stop reaps it. |
tmpProfileDir |
string or null | Chrome temporary profile directory created by --cdp-port; null otherwise. stop deletes it. |
cdpPort |
int or null | The --cdp-port value when CDP is enabled; null otherwise. Preserved across restart (forwarded into the next start before stop deletes this file). |
Examples#
Chrome web session (default):
dart run dowel start
Equivalent to dart run dowel start --device=chrome --port=3100 --vm-service-port=8181.
macOS desktop session:
dart run dowel start --device=macos
--port is ignored for non-Chrome targets. Flutter receives --no-dds (the default) so the Tapir and Tinker tools connect directly to the VM Service socket.
Custom web port with DDS enabled:
dart run dowel start --device=chrome --port=4000 --dds
Starts the Chrome session on port 4000 and omits --no-dds, letting the Dart Development Service run. Use this when your tooling requires DDS (for example, a secondary IDE debugger).
CDP session for tapir:resize / tapir:device:
dart run dowel start --device=chrome --cdp-port=9222
Pre-launches Chrome with --remote-debugging-port=9222 and runs Flutter on -d web-server, recording chromePid / tmpProfileDir / cdpPort in state.json so tapir:resize and tapir:device can drive the Chrome DevTools Protocol. Requires Flutter SDK 3.30.0 or newer. A subsequent restart preserves the CDP port.
CDP session with extended VM Service timeout (cold CI or slow machine):
dart run dowel start --device=chrome --cdp-port=9222 --timeout=180
Allows up to 180 seconds for the VM Service URI to appear in the log. Use this when the default 90 seconds is not enough for a cold Flutter SDK install or a slow CI host.
Troubleshooting#
Port already in use (--port collision). If another process holds the web port, flutter run exits immediately and the log file shows Error: Address already in use. Pick a free port with --port=<n> or stop the occupying process with lsof -ti:<port> | xargs kill.
mkfifo permission denied. start creates the FIFO at ~/.dowel/sessions/<hash>/flutter-dev.fifo. If ~/.dowel/ was created by a previous run with different ownership or mode bits, mkfifo fails. Fix the owner or mode of that session directory (chmod -R u+rwx ~/.dowel/sessions/<hash>), or remove only that project's directory (rm -rf ~/.dowel/sessions/<hash>) and run dart run dowel start again; the sessions of your other projects stay untouched. Note: start is POSIX-only (macOS/Linux). It will not work on Windows.
VM Service URI never appears (timeout). When --timeout runs out before the URI shows up in the log, start checks whether flutter run is still alive.
- Still running (exit 75). A first native build (CocoaPods, SwiftPM, Gradle) routinely takes longer than 90 seconds, and the app keeps building after
startreturns.startprintsflutter run (pid <n>) is still building after <n>s ...and exits75(EX_TEMPFAIL,DowelCommand.exitTryAgain), so a script can tell "not ready yet" from a failure. The session stays recorded withbooting: true;statusshows it, and the next connected command reads the URI from the log once the build finishes. A connected command run before that point also exits75while the recordedflutter runis alive, so a script can retry on the one code until it succeeds. Pass--timeout=600to makestartitself wait for a cold build. - Exited (exit 1).
flutter rundied before announcing a URI, almost always a build or launch error.startprintsflutter run (pid <n>) exited before announcing a VM Service URIwith the log path, stops the FIFO holder and removes the session (the log stays), sostatusdoes not report a dead app as booting. Common causes in~/.dowel/sessions/<hash>/flutter-dev.log: a wrong--devicevalue, a missingCHROME_EXECUTABLEenv var in headless environments, or a compile error in the app's entry point.
On the --cdp-port branch a timeout still fails the start with exit 1 and reaps Chrome and the flutter tool, since a web build has no long native phase to wait out.
Connected command reports an unregistered extension (exit 75). A connected command that calls ctx.callExtension('ext.<name>') before the app has registered that extension (the app is still booting, it is a release build, or the plugin is not installed in it) prints one line, ext.<name> is not registered in the running app yet (...). Retry in a few seconds., and exits 75. The JSON-RPC -32601 error reaches the command as ExtensionNotRegisteredException, so a command that wants to wait for the extension can catch it without importing package:vm_service.
Web server not ready in time (--cdp-port). On the --cdp-port branch start waits for the is being served at line before it navigates Chrome, and fails with Timed out after <n>s waiting for "is being served at". The deadline is --timeout; raise it when the web compile is slow. The failed start stops the flutter tool's and Chrome's process groups and waits for their ports, so a retry straight after it does not collide with the leftovers.
CDP port already in use (--cdp-port collision). When the configured CDP port is held by another process, start exits immediately before launching Chrome and emits: CDP port <n> is already in use; pass --cdp-port <free-port> or free it before running start. Choose a free port (for example --cdp-port=9224) or kill the occupying process with lsof -ti:<port> | xargs kill.
Related Documentation#
- stop: stop the running Flutter process group and wait until it and its ports are gone (see One process group per app), force-stop the app on an Android device (
adb -s <serial> shell am force-stop <applicationId>, the id read fromandroid/app/build.gradle*), and deletestate.json. An app that outlivesSIGKILLkeeps its session andstopexits 1. - restart: full stop + start cycle; preserves the prior session's
--cdp-port(read fromstate.jsonbeforestopdeletes it, then forwarded intostart). An explicit--cdp-porton therestartinvocation wins. The start begins only afterstophas seen the old app and its ports go, and astopthat fails aborts the restart. - reload: send
r\nto the FIFO for a hot reload without a full restart. - hot-restart: send
R\nto the FIFO for a hot restart that resets app state. - mcp:serve: start the stdio JSON-RPC MCP server; reads this project's session to discover the running app.