Conversation
25777f8 to
d791f67
Compare
37a3d58 to
5e2a013
Compare
5e2a013 to
d4507d6
Compare
cda5b25 to
d09d1fa
Compare
661f259 to
9ef6db4
Compare
56a6098 to
bb31f75
Compare
* Run knip in CI and delete the dead code it found knip was configured but never ran in CI, so unused exports piled up. Tune the config so test files count as entry points, exports used only in their own file are not reported, and the Swift-only node-swift dependency is declared. Then delete what remains: five unused response helpers in middleware-utils, an unused capability toggle and its only caller, legacy device-frame constants, and three unused client helpers. Add a Knip job to the EAS Checks workflow and to `bun run check`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Say that check runs knip too Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Keep setCapabilityEnabled and capabilityDefinition for open PRs gwdp pointed out that #148, #102, and #53 import setCapabilityEnabled. Restore it and its only callee with a @public tag that names those PRs, so knip stays clean and the first PR to land makes them live. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Run knip in CI and delete the dead code it found knip was configured but never ran in CI, so unused exports piled up. Tune the config so test files count as entry points, exports used only in their own file are not reported, and the Swift-only node-swift dependency is declared. Then delete what remains: five unused response helpers in middleware-utils, an unused capability toggle and its only caller, legacy device-frame constants, and three unused client helpers. Add a Knip job to the EAS Checks workflow and to `bun run check`. * Say that check runs knip too * Keep setCapabilityEnabled and capabilityDefinition for open PRs gwdp pointed out that #148, #102, and #53 import setCapabilityEnabled. Restore it and its only callee with a @public tag that names those PRs, so knip stays clean and the first PR to land makes them live. --------- (cherry picked from commit df029c8) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
/devin review |
bb31f75 to
45a2030
Compare
There was a problem hiding this comment.
🔍 Validation evidence needs follow-up
The native test evidence omits the simulator and Xcode version. The CLI and UI changes also lack command output and visual evidence required by the review guide.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
🔍 Change exceeds review scope guidance
This PR combines native lifecycle, wire format, CLI, ownership, and UI changes across roughly 2,700 lines. The contribution guidance requests one behavior change under roughly 400 lines.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const reply = await fetchCameraStatus(); | ||
| if (cancelled) return; | ||
| const alive = !!reply?.alive; | ||
| const replyBundles = Array.isArray(reply?.bundleIds) ? reply.bundleIds : null; | ||
| const foregroundIsInjected = | ||
| !!bundleId && (replyBundles ? replyBundles.includes(bundleId) : injectedBundleIds.has(bundleId)); | ||
| const replySource = reply?.source ?? null; | ||
| const replyHasRealSource = replySource && replySource !== "placeholder"; | ||
| const attachedToCurrentHelper = | ||
| injected && alive && foregroundIsInjected && !!replyHasRealSource | ||
| && (attachedHelperPid == null || reply?.helperPid === attachedHelperPid); | ||
| setPillState((prev) => nextCameraPillState(prev, attachedToCurrentHelper)); | ||
| if (!alive) { | ||
| setInjected((prevInjected) => { | ||
| if (!prevInjected) return prevInjected; | ||
| setInjectedBundleIds(new Set()); | ||
| setAttachedHelperPid(null); | ||
| appliedMirrorRef.current = "off"; | ||
| return false; | ||
| }); | ||
| } else if (injected && attachedHelperPid != null && reply?.helperPid !== attachedHelperPid) { | ||
| setInjected(false); | ||
| setInjectedBundleIds(new Set()); | ||
| setAttachedHelperPid(null); | ||
| appliedMirrorRef.current = "off"; | ||
| } else if (alive && Array.isArray(reply?.bundleIds)) { | ||
| const next = reply.bundleIds; | ||
| setInjectedBundleIds((prev) => { | ||
| if (prev.size === next.length && next.every((b) => prev.has(b))) return prev; | ||
| return new Set(next); | ||
| }); | ||
| } | ||
| setPillState((prev) => nextCameraPillState(prev, alive)); | ||
| setEnabled(alive); |
45a2030 to
ccfade7
Compare
…he published feed
99bceac to
80b6a8a
Compare
…nd confirm helper exit
…ed, and fail a helper with no socket
…reject unknown camera commands
…nd park early preview layers safely
| BOOL undecided = !SimCamSessionHasFakeCamera(self); | ||
| if (undecided) SimCamPromoteOpenConfigToNative(self); | ||
| [self simcam_addInput:input]; | ||
| if (undecided) [self simcam_handOverHeldOutputs]; |
There was a problem hiding this comment.
🔴 Mixed camera sessions lose microphone samples
When a fake-camera session adds an audio output before its microphone input, undecided prevents handing the output to AVFoundation. simcam_addOutput only tracks that output, so its audio delegate receives no microphone samples.
Learn more
Outputs on a session without a native input are stored in simcam_addOutput instead of being added to AVFoundation. When a fake camera is present, the new gate leaves these outputs tracked after a real microphone input arrives. The fake output registry registers only video data delegates, so an audio data output cannot receive native microphone samples. The same gate exists in simcam_addInputWithNoConnections.
Example: An app adds a fake video input, an AVCaptureAudioDataOutput, and then an AVCaptureDeviceInput for its microphone. The audio output remains in SimCamSessionTrackedOutputs; recording receives no audio buffers, although the microphone input was added.
Recommended fix: When a real input joins a fake-camera session, move held audio outputs into AVFoundation while retaining fake video outputs in the synthetic session. Handle both real-input methods and preserve outputs added without connections.
Was this helpful? React with 👍 or 👎 to provide feedback.
| alive = [NSHashTable hashTableWithOptions: | ||
| NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; | ||
| }); | ||
| @synchronized (alive) { [alive addObject:connection]; } |
There was a problem hiding this comment.
🟡 Camera connections accumulate after output teardown
When apps repeatedly create camera outputs, SimCamKeepFakeConnectionAlive retains each connection forever. Releasing an output cannot remove its connection from alive, so memory grows throughout the app's lifetime.
Learn more
A fake connection is created once per output by SimCamFakeConnectionForOutput. The output retains it through an associated object, but this table retains it independently. After the output deallocates, the connection's weak output reference clears while the table keeps the connection for the rest of the app process. Repeated creation and disposal of capture outputs therefore grows the table even when no camera session remains.
Example: An app opens and closes a camera screen 1,000 times, creating a new video output each time. The 1,000 abandoned connections remain in alive; the expected retained count after teardown is zero.
Recommended fix: Give SimCamFakeConnection a safe teardown path that avoids the uninitialized superclass state, then remove the permanent strong table and let its associated output own it. Verify output deallocation and repeated capture-session creation on the supported simulator runtime.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (!SimCamSessionHasFakeCamera(self)) { | ||
| [self simcam_handOverHeldOutputs]; | ||
| objc_setAssociatedObject(self, &kSimCamSessionFakeRunKey, @NO, OBJC_ASSOCIATION_RETAIN); | ||
| [self simcam_startRunning]; | ||
| return; |
There was a problem hiding this comment.
🔴 Native photo captures become synthetic
After device-wide loading, simcam_capturePhotoWithSettings: intercepts photo captures without checking whether the output belongs to a fake session. Native photo outputs receive synthetic images, even while the fake camera is disconnected.
Learn more
The injector now loads into every eligible app, including apps using native AVFoundation sessions. simcam_capturePhotoWithSettings: always synthesizes a photo without checking whether its output is attached to a fake session. The new native-session branch does not protect photo capture, because capture calls go directly to the output swizzle.
Example: An app with a native photo output calls capturePhotoWithSettings: after the feed is disabled. Instead of using AVFoundation's native capture, the output delivers the synthetic black fallback photo.
Recommended fix: Gate photo-output interception on attachment to a fake session and forward other captures to the original implementation. Apply the same native-session gate to other globally swizzled output operations that manufacture fake results.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (gShmHeader && (!atomic_load_explicit(&gShmHeader->active, memory_order_acquire) || | ||
| kill((pid_t)gShmHeader->ownerPid, 0) != 0)) { |
There was a problem hiding this comment.
🟡 Zombie helper leaves camera connected
If the helper dies unreaped, SimCamRefreshDevice treats its PID as alive and never disconnects the old mapping. Apps keep the stale camera and cannot connect to a restarted helper.
Learn more
An unexpected helper exit cannot clear the shared header's active flag. A zombie process still passes kill(pid, 0) until reaped, so the monitor leaves its old shared-memory mapping attached and never checks the newly named region. hasProcessExited already handles this condition for host-side helper management.
Example: The helper is killed, remains a zombie, and a new helper opens the same shared-memory name. The injected app continues treating the old mapping as connected and misses the new feed.
Recommended fix: Detect zombie state as well as PID existence on the injected side, or use a liveness signal that does not depend on reaping; ensure the monitor closes the old mapping before retrying attachment.
Was this helpful? React with 👍 or 👎 to provide feedback.
| OBJC_ASSOCIATION_RETAIN); | ||
| SimCamSetPosition(output, SimCamPositionOf(self)); | ||
| SimCamMarkOutputAttachedToFakeSession(self, output); | ||
| SimCamRegisterFakeVideoOutput(output); |
There was a problem hiding this comment.
🟡 Unstarted camera sessions receive samples
When an output already has a delegate, SimCamRegisterFakeVideoOutput registers it before startRunning. The pump ignores session running state and sends samples to an unstarted session.
Learn more
SimCamRegisterFakeVideoOutput adds outputs immediately when they already have a delegate. startPumpingIfNeeded starts on registration, and its delivery loop checks connection generation but not whether the owning session is running. The new registration path thus delivers samples before the app starts capture.
Example: An app assigns a sample delegate, adds the output, and pauses before calling startRunning. The delegate receives synthetic samples during the pause.
Recommended fix: Track the owning fake session and gate queued delivery on its running state; also stop or unregister delivery when that session stops.
Was this helpful? React with 👍 or 👎 to provide feedback.
ENG-26998
Why
The camera is a device-wide capability. Enabling or disabling its feed should behave like connecting or disconnecting camera hardware while apps and SpringBoard continue running.
How
Test Plan