Skip to content

Derive mobile host port from dev tag - #8145

Closed
azooz2003-bit wants to merge 5 commits into
mainfrom
issue-7691-mobile-host-port
Closed

azooz2003-bit wants to merge 5 commits into
mainfrom
issue-7691-mobile-host-port

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #7691

Summary

  • derive the debug mobile-host default port from CMUX_TAG when no explicit port is configured
  • keep production, untagged debug, and explicit configured ports unchanged
  • cover tag-derived allocation and attach-ticket endpoint propagation with focused regression tests

Verification

  • ./scripts/lint-pbxproj-test-wiring.sh
  • git diff --check
  • swift test --package-path Packages/macOS/CmuxSettings --filter MobileHostPortPolicyTests
  • ./scripts/reload.sh --tag mh7691
  • launched mh7691, verified /tmp/cmux-debug-mh7691.sock, identify, window display "LG HDR 4K", and rpc mobile.host.status {}
  • relaunched mh7691 and verified configured_port == port == 59315 again

Dogfood status

  • macOS tagged build is ready at http://127.0.0.1:17320/mh7691
  • iOS simulator dogfood is blocked locally because the tagged web dev server cannot start without Stack web app keys in local secrets; only dogfood email/password are configured
  • CI failed once on unrelated HostSettingsShortcutNotificationTests.changedSettingsFilePostsOneShortcutNotification and CmuxCanvasUI signal 5; failed jobs were rerun and are pending

@vercel

vercel Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 15, 2026 2:40pm
cmux-staging Building Building Preview, Comment Jul 15, 2026 2:40pm

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Mobile host port selection is centralized in a launch-aware policy, with deterministic DEBUG ports for launch tags, explicit invalid-port handling, service delegation, environment-isolated tests, and physical-device attach endpoint validation.

Changes

Mobile host port and routing behavior

Layer / File(s) Summary
Launch-aware mobile host port policy
Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/MobileHostPortPolicy.swift
Centralizes stored-port validation, launch-tag-based DEBUG defaults, catalog fallback behavior, and nil resolution for invalid stored ports.
Service integration and port behavior tests
Sources/Mobile/MobileHostService.swift, cmuxTests/MobileHostServiceSettingsTests.swift, Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/SocketControl/MobileHostPortPolicyTests.swift
MobileHostService forwards environment-aware port resolution to the policy; tests verify tagged defaults, explicit overrides, invalid values, and environment control.
Physical-device attach endpoint validation
cmuxTests/MobileHostAuthorizationTests.swift
A regression test decodes the physical-device attach_url and verifies the selected Tailscale endpoint host and port.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileHostService
  participant MobileHostPortPolicy
  participant UserDefaults
  MobileHostService->>MobileHostPortPolicy: request configured port with environment
  MobileHostPortPolicy->>UserDefaults: read stored iOS pairing port
  UserDefaults-->>MobileHostPortPolicy: stored port or missing value
  MobileHostPortPolicy-->>MobileHostService: valid port or launch-aware default
Loading

Suggested reviewers: lawrencecchen

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change derives stable per-tag ports and adds attach-ticket propagation coverage, matching the issue's reconnection requirements.
Out of Scope Changes check ✅ Passed The changed files stay focused on port selection and related regression tests, with no obvious unrelated additions.
Cmux Swift Actor Isolation ✅ Passed New policy is a pure Sendable value type; service shims are nonisolated, and the added tests only exercise env/default logic—no new actor-isolation debt.
Cmux Swift Blocking Runtime ✅ Passed The production diff only moves port selection into a pure MobileHostPortPolicy and wires it from MobileHostService; no new waits, sleeps, syncs, or locks were added outside tests.
Cmux Browser Automation Off-Main ✅ Passed PR only changes mobile-host port policy/tests; it doesn't touch TerminalController or ControlCommandExecutionPolicy, and no browser.* automation diffs are present.
Cmux Expensive Synchronous Load ✅ Passed The diff only refactors mobile-host port selection and tests; it adds no agent-history loader, JSON/JSONL scan, or other expensive sync load on main/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No fresh authoritative read was replaced by a stale cache; the live listener uses applied state, and attach-ticket payloads still encode selected routes directly.
Cmux No Hacky Sleeps ✅ Passed PASS: The diff is Swift-only and only refactors port-policy selection/tests; no new sleeps, timers, polling, or delayed dispatch were added.
Cmux Algorithmic Complexity ✅ Passed PASS: the new port policy uses O(1) lookups plus a linear scan over CMUX_TAG; no nested scans or scalable collection rescans were introduced.
Cmux Swift Concurrency ✅ Passed The diff only refactors synchronous port selection and adds tests; no new DispatchQueue/Task/Combine/completion-handler patterns were added.
Cmux Swift @Concurrent ✅ Passed The commit only refactors sync port-selection helpers into a pure Sendable policy and updates tests; no new @concurrent or nonisolated async misuse appears.
Cmux Swift Package Boundaries ✅ Passed Core port-selection logic was extracted to CmuxSettings.MobileHostPortPolicy; Sources/Mobile now only delegates, which fits the app-glue allowance.
Cmux Swiftpm Lockfiles ✅ Passed Diff only touches Swift source/tests; no Package.swift, Package.resolved, .gitignore, or Xcode project/package-reference files changed.
Cmux Swift Logging ✅ Passed PASS: The patch adds port-policy refactors and tests only; no new print/debugPrint/dump/NSLog, ad hoc stdout logging, or Logger changes appear in the diff.
Cmux User-Facing Error Privacy ✅ Passed Diff only refactors port selection and adds tests; no production user-facing errors, alerts, command output, or API error text changed.
Cmux Full Internationalization ✅ Passed The diff only changes port-selection logic and tests; no user-facing Swift text, string catalogs, web messages, or Info.plist locale data were added or changed.
Cmux Swiftui State Layout ✅ Passed PASS: The diff only changes non-UI port-policy/service/test code; it adds no SwiftUI views, state objects, GeometryReader, or render-time mutations.
Cmux Architecture Rethink ✅ Passed Pure MobileHostPortPolicy centralizes port selection with clear ownership; no production sleeps, polling, locks, observers, or split lifecycle wiring were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes mobile-host port policy and tests; no NSWindow/NSPanel/WindowGroup/NSWindowController or cmuxAuxiliaryWindowIdentifiers changes appear.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test files; no temp, cache, build-output, or other artifact paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The touched production files only refactor port selection and add a DEBUG tagged-build policy; no new test/debug observability accessor or seam was added.
Cmux No Ambient Global State ✅ Passed No new ambient global state: MobileHostPortPolicy is constructable/injectable, and production changes add no file-scope funcs, mutable globals, or new singletons.
Title check ✅ Passed The title clearly and concisely captures the main change: deriving the mobile host port from the development tag.
Description check ✅ Passed The description covers the summary and verification well; only non-critical template sections like demo video and checklist are omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7691-mobile-host-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/MobileHostServiceSettingsTests.swift`:
- Around line 168-187: The tests currently mutate global process environment
through withEnvironmentValue, causing unsafe concurrent behavior. In
cmuxTests/MobileHostServiceSettingsTests.swift:168-187, remove
withEnvironmentValue, update withLaunchTag and withoutLaunchTag to provide
environment dictionaries to their bodies, and update all call sites to pass
those dictionaries into MobileHostService.configuredPort. In
Sources/Mobile/MobileHostService.swift:480-510, add an environment parameter
defaulting to ProcessInfo.processInfo.environment to configuredPort,
resolvedDesiredPort, and defaultConfiguredPort, then propagate it to
taggedDevelopmentDefaultPort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80493906-9619-4654-b2be-96944fd50160

📥 Commits

Reviewing files that changed from the base of the PR and between f008967 and 654a5f0.

📒 Files selected for processing (3)
  • Sources/Mobile/MobileHostService.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/MobileHostServiceSettingsTests.swift

Comment thread cmuxTests/MobileHostServiceSettingsTests.swift Outdated
@greptile-apps

greptile-apps Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR centralizes mobile host port selection into a new MobileHostPortPolicy value type in the CmuxSettings package, which derives a stable per-tag port from CMUX_TAG (via FNV-1a hash into the 49152–65535 range) for DEBUG builds when no explicit port is configured. MobileHostService.configuredPort and resolvedDesiredPort now delegate entirely to this policy.

  • Port policy extracted to MobileHostPortPolicy: explicit override → tag-derived DEBUG default → catalog default, with resolvedDesiredPort returning nil for out-of-range stored values to avoid disturbing a running listener.
  • Previous review concerns addressed: the environment parameter is no longer dead code — it is now threaded top-to-bottom through the public API, and tests inject explicit [String: String] dictionaries via withLaunchTag/withoutLaunchTag helpers rather than relying on setenv, correctly bypassing the ProcessInfo.processInfo.environment cache.
  • New regression tests: tagged port stability and override precedence are covered in both MobileHostPortPolicyTests and MobileHostServiceSettingsTests; a new attach-ticket test asserts the physical-device payload uses the actual selected route endpoint rather than the service's configured port.

Confidence Score: 5/5

Safe to merge: the port selection refactor is self-contained, production behavior is backward-compatible for release and untagged builds, and both previous review concerns have been resolved in this iteration.

The new MobileHostPortPolicy correctly threads the environment dictionary from the public API down through the private hash helper, eliminating the ProcessInfo cache problem flagged in earlier review threads. The FNV-1a hash arithmetic is correct, collision avoidance handles boundary cases, and the resolvedDesiredPort nil-on-invalid contract is preserved. Tests now inject environment dictionaries directly rather than via setenv, making them reliable. No correctness or safety issues were found in this pass.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/MobileHostPortPolicy.swift New pure value-type policy encapsulating port selection with FNV-1a tag hashing; logic and range arithmetic are correct, hash variant is proper FNV-1a, collision avoidance for catalog default works correctly for all boundary cases.
Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/SocketControl/MobileHostPortPolicyTests.swift New unit tests for MobileHostPortPolicy; environment injection via explicit dictionaries avoids the ProcessInfo cache issue flagged in previous threads; isolated UserDefaults suites prevent cross-test contamination.
Sources/Mobile/MobileHostService.swift configuredPort and resolvedDesiredPort now delegate to MobileHostPortPolicy; production callers using the default environment parameter correctly read the tag from ProcessInfo at call time; behavior is backward-compatible for release and untagged builds.
cmuxTests/MobileHostAuthorizationTests.swift New regression test verifies attach ticket payload uses the actual route endpoint (host/port) for physical devices, not the service's configured port; test is well-scoped and correctly decodes the QR code components for assertion.
cmuxTests/MobileHostServiceSettingsTests.swift Existing tests updated to pass explicit environment dictionaries via withLaunchTag/withoutLaunchTag helpers, correctly bypassing the ProcessInfo cache; new tests cover tagged default derivation and explicit override precedence.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["MobileHostService.configuredPort(defaults:environment:)"] --> B["MobileHostPortPolicy().configuredPort(defaults:environment:)"]
    B --> C{Explicit port in UserDefaults?}
    C -- "yes, valid (1-65535)" --> D["Return stored port"]
    C -- "no / invalid" --> E["defaultConfiguredPort(environment:catalogDefaultPort:)"]
    E --> F{DEBUG build?}
    F -- "no" --> G["Return catalog default"]
    F -- "yes" --> H{CMUX_TAG set and non-default?}
    H -- "no" --> G
    H -- "yes" --> I["FNV-1a hash of tag → port in 49152–65535"]
    I --> J{Port == catalog default?}
    J -- "yes" --> K["Shift +1 mod portCount"]
    J -- "no" --> L["Return tag-derived port"]
    K --> L
    M["MobileHostService.resolvedDesiredPort(defaults:environment:)"] --> N["MobileHostPortPolicy().resolvedDesiredPort(defaults:environment:)"]
    N --> O{Explicit port in UserDefaults?}
    O -- "no" --> E
    O -- "yes, valid" --> P["Return stored port"]
    O -- "yes, invalid" --> Q["Return nil"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["MobileHostService.configuredPort(defaults:environment:)"] --> B["MobileHostPortPolicy().configuredPort(defaults:environment:)"]
    B --> C{Explicit port in UserDefaults?}
    C -- "yes, valid (1-65535)" --> D["Return stored port"]
    C -- "no / invalid" --> E["defaultConfiguredPort(environment:catalogDefaultPort:)"]
    E --> F{DEBUG build?}
    F -- "no" --> G["Return catalog default"]
    F -- "yes" --> H{CMUX_TAG set and non-default?}
    H -- "no" --> G
    H -- "yes" --> I["FNV-1a hash of tag → port in 49152–65535"]
    I --> J{Port == catalog default?}
    J -- "yes" --> K["Shift +1 mod portCount"]
    J -- "no" --> L["Return tag-derived port"]
    K --> L
    M["MobileHostService.resolvedDesiredPort(defaults:environment:)"] --> N["MobileHostPortPolicy().resolvedDesiredPort(defaults:environment:)"]
    N --> O{Explicit port in UserDefaults?}
    O -- "no" --> E
    O -- "yes, valid" --> P["Return stored port"]
    O -- "yes, invalid" --> Q["Return nil"]
Loading

Reviews (4): Last reviewed commit: "Use value policy for mobile host ports" | Re-trigger Greptile

Comment on lines +160 to +187
private static func withLaunchTag<T>(_ tag: String, _ body: () throws -> T) rethrows -> T {
try withEnvironmentValue(SocketControlSettings.launchTagEnvKey, value: tag, body)
}

private static func withoutLaunchTag<T>(_ body: () throws -> T) rethrows -> T {
try withEnvironmentValue(SocketControlSettings.launchTagEnvKey, value: nil, body)
}

private static func withEnvironmentValue<T>(
_ key: String,
value: String?,
_ body: () throws -> T
) rethrows -> T {
let previous = getenv(key).map { String(cString: $0) }
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
defer {
if let previous {
setenv(key, previous, 1)
} else {
unsetenv(key)
}
}
return try body()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 setenv not visible through ProcessInfo.processInfo.environment cache

withLaunchTag / withoutLaunchTag mutate the C-level environ via setenv/unsetenv, but taggedDevelopmentDefaultPort reads the tag through SocketControlSettings.launchTag(environment:) where the default argument is ProcessInfo.processInfo.environment. On macOS, NSProcessInfo.environment is populated from environ at first access and then cached as an immutable NSDictionary — subsequent setenv calls are not reflected. Because ProcessInfo.processInfo.environment is almost certainly first accessed during test-framework initialization (XCTest reads several env-key indicators during setup), the cache is warm before any test body runs, so withLaunchTag("nodivs") never causes the tag to appear in the dictionary the production code reads.

The established pattern for this codebase is to pass a custom [String: String] directly to the function under test (see every SocketControlSettings test that injects environment:). The environment: parameter on taggedDevelopmentDefaultPort was evidently added with this intent, but because the function is private, the test target cannot reach it, so the setenv workaround was used instead. To correctly cover tag-derived port allocation, configuredPort and resolvedDesiredPort would need to propagate an environment parameter (even if @testable-only-visible as internal) so tests can inject a known dictionary without touching the process environment.

Comment thread Sources/Mobile/MobileHostService.swift Outdated
Comment on lines +512 to +537
#if DEBUG
nonisolated private static func taggedDevelopmentDefaultPort(
environment: [String: String] = ProcessInfo.processInfo.environment
) -> Int? {
guard let tag = SocketControlSettings.launchTag(environment: environment) else {
return nil
}
let normalizedTag = tag.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !normalizedTag.isEmpty, normalizedTag != "default" else { return nil }

var hash: UInt32 = 2_166_136_261
for byte in normalizedTag.utf8 {
hash ^= UInt32(byte)
hash &*= 16_777_619
}

let lowerBound = 49_152
let upperBound = 65_535
let portCount = upperBound - lowerBound + 1
var port = lowerBound + Int(hash % UInt32(portCount))
if port == CmxMobileDefaults.defaultHostPort {
port = lowerBound + ((port - lowerBound + 1) % portCount)
}
return port
}
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dead environment parameter on private production function

taggedDevelopmentDefaultPort accepts an environment: [String: String] parameter that was presumably added to allow dictionary-injection testing. However, no production caller passes a custom environment (both call sites inside defaultConfiguredPort use the default), and the test target cannot reach this private function at all — tests ended up using setenv instead. The parameter is dead code in the production binary and does not enable the testability it implies. It should either be promoted to an internal entry point that threads the environment from configuredPort/resolvedDesiredPort downward, or be removed if the setenv approach is confirmed to work reliably on the target platform.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxTests/MobileHostServiceSettingsTests.swift (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unnecessary @Suite(.serialized).

You successfully isolated the test state by using unique UserDefaults suites and passing the environment dictionary via dependency injection. Since the tests no longer mutate the global process environment via setenv, they are fully thread-safe and can run concurrently. Removing .serialized will improve test execution speed.

♻️ Proposed refactor
-@Suite(.serialized)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/MobileHostServiceSettingsTests.swift` at line 13, Remove the
unnecessary .serialized configuration from the test suite annotation in
MobileHostServiceSettingsTests, leaving the suite otherwise unchanged so its
isolated UserDefaults and injected environment continue to support concurrent
execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cmuxTests/MobileHostServiceSettingsTests.swift`:
- Line 13: Remove the unnecessary .serialized configuration from the test suite
annotation in MobileHostServiceSettingsTests, leaving the suite otherwise
unchanged so its isolated UserDefaults and injected environment continue to
support concurrent execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68a633a6-a5eb-4a15-bfc3-afe346232085

📥 Commits

Reviewing files that changed from the base of the PR and between 654a5f0 and f47c3ab.

📒 Files selected for processing (3)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/MobileHostPortPolicy.swift
  • Sources/Mobile/MobileHostService.swift
  • cmuxTests/MobileHostServiceSettingsTests.swift

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — b0868c27 Deployed Jul 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tagged dev builds fight over the mobile-host port, breaking iPhone reconnection

3 participants