Skip to content

feat(run): switch a running Flutter app between checkouts without rebuilding - #1174

Open
Senzaiken wants to merge 2 commits into
eneskirca:mainfrom
Senzaiken:feat/run-node-hot-switch
Open

Senzaiken wants to merge 2 commits into
eneskirca:mainfrom
Senzaiken:feat/run-node-hot-switch

Conversation

@Senzaiken

Copy link
Copy Markdown
Contributor

Pointing a running Flutter run node at another checkout (one worktree to the next) no longer means a full native rebuild. When the running app is the one the new folder would build — same device, same flavor, a Flutter run on both sides — the toolbar offers ⇄ Switch (keep app) beside Rebuild: it detaches the Flutter tool, flutter attaches from the new folder to the app still running on the device, and hot restarts. Measured on an iOS simulator: about 25 s (mostly attach's file sync) instead of a full Xcode build, the app's state resets, and nothing native is rebuilt.

Why

The run node (#1118) made two checkouts of one app easy to run side by side, but switching a node from one worktree to another meant stop → flutter run again: pods, Xcode build, install — minutes, for what between two worktrees of the same app is almost always a Dart-only difference. Flutter can already do better: a debug app keeps running when its tool detaches, and another tool can attach to it.

What it does

  1. Detach — types d, flutter's own detach key: the tool exits, the app keeps running.
  2. Attach — flutter attach from the new folder to the same device. Only the arguments attach accepts survive (attachArgs, checked against flutter attach --help): the target and dart-defines are kept; --flavor, --web-port and other run-only flags are dropped, because attach rejects unknown options.
  3. Wait until attach has connected (attachConnected: its key-command help printed after its own ▶ flutter attach launch line, so an earlier run's help on screen does not count).
  4. Hot restart (SIGUSR2 to attach's --pid-file, the same signal path the node already uses).

Every failure leaves the app running and says so, with Rebuild one click away. canHotSwitch is the one rule for when the button appears.

On an iOS simulator, attach is given --debug-url. Attach finds an already-running app by mDNS, which the simulator barely supports, and the URL flutter run printed belongs to its DDS proxy, which closes when that tool detaches. simulatorVmServiceUrl reads the app's own "The Dart VM service is listening on …" line from xcrun simctl spawn <udid> log show and takes the newest whose process is still alive. If none is found, the switch is refused and Rebuild is offered.

Also in this PR:

  • Dart "request": "attach" configurations in launch.json now run as flutter attach (every other attach is still refused, as before).
  • Booting a simulator falls back to the active Xcode's Simulator.app when open -a Simulator cannot find one; with neither, simulators run headless, as before.

Why a restart and not a reload

A freshly attached tool only pushes files that change after it connected. Measured: after attaching from the other checkout, a hot reload printed "Reloaded 0 libraries" and the app kept running the old checkout's code. A restart pushes the whole program. The cost is the app's in-memory state.

Limits, stated in the UI

  • Dart-only. Checkouts that differ in native code (plugins, ios/, Info.plist) need Rebuild; the node cannot know that, which is why Rebuild is always beside Switch.
  • appFlavor. Attach has no --flavor, and Flutter refuses a hand-passed FLUTTER_APP_FLAVOR define (_ensureReservedDartDefineIsUnset), so after an attach appFlavor is the pubspec's default. An app whose lib/ reads appFlavor is refused the switch with that reason.
  • Same device and same flavor only; debug mode only.

How it was checked

  • Live, on an iOS 27.1 simulator with Flutter 3.47, two checkouts of the same app (whose lib/config/environment_config.dart differ). The checkout the app was actually running was read back from the app itself over the Dart VM service (getScripts / getObject) at each step:
    • flutter run from checkout A → the app runs A's code.
    • d → the tool exited in 2 s with code 0; the app kept running (same pid).
    • Plain flutter attach (no URL) → never connected ("not discovered after 30 seconds").
    • Through the node's own startRun({attach}) → launcher → attachConnected → signalRun('restart'): attach (with the discovered --debug-url) connected after a 20.8 s file sync; the hot restart took 2.4 s; the app — same pid, nothing reinstalled — then ran B's code.
    • Detach, attach back from A, plain hot reload → "Reloaded 0 libraries", still B's code.
  • Unit tests: attachArgs, flavorOf, canHotSwitch, attachConnected, parseVmServiceLog, isVmServiceUrl, the attach plans (including --debug-url precedence), Dart attach configurations, and on the host the attach launcher, the non-Flutter refusal and the appFlavor guard.
  • npm run typecheck clean; renderer + shared + run-node suites: 10,532 passed.

Not verified

  • The toolbar's button flow clicked through in the app — the steps it drives were exercised live through the same host functions, but not via the UI.
  • Android emulators and physical devices (no --debug-url is passed there; attach discovers apps on those normally).
  • Linux / Server Edition.

Surfaces

Desktop: full. Server Edition: the same core handlers; simulator discovery is macOS-only and simply finds nothing elsewhere. SSH / relay / Windows: unchanged from #1118 (not offered). Mobile: N/A.

…uilding

Changing a run node's folder used to stop the app and `flutter run` again
from the new checkout — a full native build for what is usually a Dart-only
difference between worktrees. When the running app is the one the new folder
would build (same device, same flavor, a Flutter run on both sides —
canHotSwitch), the toolbar now offers "Switch (keep app)" beside "Rebuild":

  1. type `d` — flutter's own detach: the tool exits, the app keeps running;
  2. `flutter attach` from the new folder to the same device;
  3. once it has connected (its key help printed after its own launch line),
     a hot RESTART by SIGUSR2. Not a reload: a freshly attached tool only
     pushes files changed after it connected.

The app's state resets; nothing native is rebuilt. Every failure leaves the
app running and says so, with Rebuild one click away.

attachArgs keeps only the flags `flutter attach --help` lists (target,
dart-defines, app id, VM service URL, device options) and drops run-only
ones — attach has no --flavor and rejects unknown options. Flutter refuses a
hand-passed FLUTTER_APP_FLAVOR, so after an attach appFlavor is the pubspec
default; an app whose lib/ reads appFlavor is refused the switch with that
reason. Dart "request": "attach" configurations now run as flutter attach;
every other attach is still refused.
…vice URL

Live check (Flutter 3.47, iOS 27.1 simulator, two doc-flutter checkouts)
found the switch's attach step could never connect on a simulator:
`flutter attach` discovers an already-running app by mDNS, which the
simulator barely supports ("The Dart VM Service was not discovered after
30 seconds"), and the URL `flutter run` printed is its DDS proxy, which
closes with the detached tool.

simulatorVmServiceUrl reads the app's own "The Dart VM service is listening
on …" announcement from `simctl spawn <udid> log show` (about 3 s) and takes
the newest whose process is still alive; the switch passes it as
`--debug-url`. Not found ⇒ the switch is refused with Rebuild offered.

Measured with this in place, through the node's own startRun / launcher /
attachConnected / signalRun: attach connected after a 20.8 s file sync, the
SIGUSR2 hot restart took 2.4 s, the app (same pid, no rebuild) then ran the
second checkout's code — read back from the VM. A plain hot reload after
attaching reloaded 0 libraries and kept the old code, confirming restart
(not reload) is required.

Also: booting a simulator falls back to the active Xcode's Simulator.app
when `open -a Simulator` cannot find one (a machine where LaunchServices
has none runs its simulators headless).
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