@@ -104,28 +104,19 @@ export async function acquireXcodebuildSimulatorSetRedirect(
104104 } ,
105105 } ) ;
106106
107- const giveLockBack = async ( ) : Promise < unknown > => {
108- try {
109- await releaseLock ( ) ;
110- return null ;
111- } catch ( error ) {
112- return error ;
113- }
114- } ;
107+ const paths = { xctestDeviceSetPath, backupPath } ;
115108
116- try {
117- reconcileXcodebuildSimulatorSetRedirect ( {
118- xctestDeviceSetPath,
119- backupPath,
120- } ) ;
121- if ( sameResolvedPath ( requestedSetPath , xctestDeviceSetPath ) ) {
122- // Nothing was redirected, so nothing was displaced and this simulator needs no handle: the
123- // caller has no redirect to report, and a lock it never held must not arrive as one. The
124- // give-back records its own failure where the claim lives.
125- recordHandBackFailure ( await giveLockBack ( ) , lockDirPath ) ;
126- return null ;
127- }
109+ if ( sameResolvedPath ( requestedSetPath , xctestDeviceSetPath ) ) {
110+ // Nothing is displaced and the caller gets no handle, so this hand-back has no failure of its own
111+ // to report: a lock this simulator never needed must not arrive as a redirect problem. Whatever it
112+ // could not verify is recorded where the claim lives.
113+ recordReleaseFailure ( ( await handBackDeviceSet ( paths , releaseLock ) ) . releaseFailure , lockDirPath ) ;
114+ return null ;
115+ }
128116
117+ try {
118+ // Clean up whatever an earlier run left, out of the way of the rename below.
119+ reconcileXcodebuildSimulatorSetRedirect ( paths ) ;
129120 fs . mkdirSync ( requestedSetPath , { recursive : true } ) ;
130121 if ( fs . existsSync ( xctestDeviceSetPath ) ) {
131122 fs . renameSync ( xctestDeviceSetPath , backupPath ) ;
@@ -135,56 +126,37 @@ export async function acquireXcodebuildSimulatorSetRedirect(
135126 xctestDeviceSetPath,
136127 } ) ;
137128 } catch ( error ) {
138- // The redirect failure outranks everything the hand-back does, including a restore that could not
139- // run on the way out. Both are recorded; the caller hears why the redirect failed.
140- try {
141- reconcileXcodebuildSimulatorSetRedirect ( {
142- xctestDeviceSetPath,
143- backupPath,
144- } ) ;
145- } catch ( restoreError ) {
146- emitDiagnostic ( {
147- level : 'warn' ,
148- phase : 'ios_runner_xctest_device_set_restore_failed' ,
149- data : { xctestDeviceSetPath, backupPath, error : String ( restoreError ) } ,
150- } ) ;
151- }
152- recordHandBackFailure ( await giveLockBack ( ) , lockDirPath ) ;
129+ const handBack = await handBackDeviceSet ( paths , releaseLock ) ;
130+ recordReleaseFailure ( handBack . releaseFailure , lockDirPath ) ;
153131 throw new AppError ( 'COMMAND_FAILED' , 'Failed to redirect XCTest device set path' , {
154132 requestedSetPath,
155133 xctestDeviceSetPath,
156134 backupPath,
157135 error : String ( error ) ,
136+ ...( handBack . restoreFailure === null
137+ ? { }
138+ : {
139+ restoreError : String ( handBack . restoreFailure ) ,
140+ hint :
141+ `The host's own device set is still renamed aside at ${ backupPath } : restore it, ` +
142+ 'or remove that path, before another runner build redirects it.' ,
143+ } ) ,
158144 } ) ;
159145 }
160146
161147 let givenBack = false ;
162- // One ordered give-back: restore the host's own device set, then hand the lock back. A restore that
163- // could not run is a fact about this machine — the symlink stays pointed at this simulator's set and
164- // every later `simctl` run sees the wrong devices — so its failure always outranks the release, and
165- // the lock goes back anyway so the next acquire does not wait on it. Whatever the release could not
166- // do is recorded either way; the two doors differ only in whether the caller also throws it.
167148 const giveBack = async ( reportUnverifiedRelease : boolean ) : Promise < void > => {
168149 if ( givenBack ) {
169150 return ;
170151 }
171152 givenBack = true ;
172- let restoreFailure : unknown = null ;
173- try {
174- reconcileXcodebuildSimulatorSetRedirect ( {
175- xctestDeviceSetPath,
176- backupPath,
177- } ) ;
178- } catch ( error ) {
179- restoreFailure = error ;
153+ const handBack = await handBackDeviceSet ( paths , releaseLock ) ;
154+ recordReleaseFailure ( handBack . releaseFailure , lockDirPath ) ;
155+ if ( handBack . restoreFailure !== null ) {
156+ throw handBack . restoreFailure ;
180157 }
181- const releaseFailure = await giveLockBack ( ) ;
182- recordHandBackFailure ( releaseFailure , lockDirPath ) ;
183- if ( restoreFailure !== null ) {
184- throw restoreFailure ;
185- }
186- if ( releaseFailure !== null && reportUnverifiedRelease ) {
187- throw releaseFailure ;
158+ if ( handBack . releaseFailure !== null && reportUnverifiedRelease ) {
159+ throw handBack . releaseFailure ;
188160 }
189161 } ;
190162 return {
@@ -193,11 +165,45 @@ export async function acquireXcodebuildSimulatorSetRedirect(
193165 } ;
194166}
195167
168+ type DeviceSetHandBack = {
169+ /** The host's own `XCTestDevices` could not be put back, so the symlink is still in its place. */
170+ restoreFailure : unknown ;
171+ /** The lock could not be given back, or could not be verified as ours when it was. */
172+ releaseFailure : unknown ;
173+ } ;
174+
175+ /**
176+ * The one ordered hand-back, used by every path that leaves this redirect behind: restore the host's
177+ * own device set, then release the lock. Neither step can hide the other, because a restore that could
178+ * not run is a fact about this machine that outlives the request — every later `simctl` run sees the
179+ * wrong devices — and the lock going back is what keeps the next acquire from waiting on a claim nobody
180+ * is acting on. Deciding which of the two the caller hears is left to the caller that has a report to
181+ * make: the redirect failure, the restore failure, or neither.
182+ */
183+ async function handBackDeviceSet (
184+ paths : { xctestDeviceSetPath : string ; backupPath : string } ,
185+ releaseLock : ( ) => Promise < void > ,
186+ ) : Promise < DeviceSetHandBack > {
187+ let restoreFailure : unknown = null ;
188+ let releaseFailure : unknown = null ;
189+ try {
190+ reconcileXcodebuildSimulatorSetRedirect ( paths ) ;
191+ } catch ( error ) {
192+ restoreFailure = error ;
193+ }
194+ try {
195+ await releaseLock ( ) ;
196+ } catch ( error ) {
197+ releaseFailure = error ;
198+ }
199+ return { restoreFailure, releaseFailure } ;
200+ }
201+
196202/**
197- * Records a hand-back failure that no caller is being made to throw. A release whose ownership could
198- * not be verified is already recorded where the claim lives, so only the rest reaches the log.
203+ * Records a lock release that no caller is being made to throw. A release whose ownership could not be
204+ * verified is already recorded where the claim lives, so only the rest reaches the log.
199205 */
200- function recordHandBackFailure ( error : unknown , lockDirPath : string ) : void {
206+ function recordReleaseFailure ( error : unknown , lockDirPath : string ) : void {
201207 if ( error === null || isOwnerReleaseUnverified ( error ) ) {
202208 return ;
203209 }
0 commit comments