Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 133 additions & 33 deletions lib/simulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,48 @@ function getCreateSimulatorHelpText(selectedXcode) {
return '\n\nPlease open Xcode, navigate to "Window > Devices and Simulators" and create a new Simulator with your preferred configuration.';
}

/**
* Determines if the specified simulator executable is the "Device Hub" app that
* replaces the Simulator app on Xcode 27+.
*
* @param {String} exe - The path to the simulator executable.
*
* @returns {Boolean}
*/
function isDeviceHub(exe) {
return !!exe && path.basename(exe) === 'DeviceHub';
}

/**
* Opens and focuses the window of the specified simulator in the Xcode 27+
* "Device Hub" app using its `devices://` URL scheme. Device Hub ignores the
* legacy `-CurrentDeviceUDID` argument, so this is the only way to show a
* specific device. This launches Device Hub if it is not already running.
*
* Note that the simulator must already be booted (via `simctl boot`) or Device
* Hub will refuse to open the window. Failures are not fatal since the
* simulator itself keeps running headless.
*
* @param {Object} handle - The simulator handle.
* @param {EventEmitter} emitter - The emitter to send debug log events to.
* @param {Function} callback() - A function to call when finished.
*/
function openDeviceHubWindow(handle, emitter, callback) {
// resolve ".../DeviceHub.app" from ".../DeviceHub.app/Contents/MacOS/DeviceHub" so
// that the URL is opened by the Device Hub matching the selected Xcode
var appPath = path.resolve(handle.simulator, '..', '..', '..'),
url = 'devices://device/open?id=' + handle.udid;

emitter.emit('log-debug', __('Running: %s', 'open -a "' + appPath + '" "' + url + '"'));

appc.subprocess.run('open', ['-a', appPath, url], function (code, out, err) {
if (code) {
emitter.emit('log-debug', __('Failed to open simulator window in Device Hub (code %s), continuing', code));
}
callback();
});
}

/**
* Detects iOS simulators.
*
Expand Down Expand Up @@ -1264,46 +1306,84 @@ function launch(simHandleOrUDID, options, callback) {
return next();
}

function waitForBoot() {
// wait for the simulator to boot
async.whilst(
function (cb) { return cb(null, !booted); },
function (cb) {
list(options, function (err, info) {
Object.keys(info.devices).some(function (type) {
return info.devices[type].some(function (sim) {
if (sim.udid === handle.udid) {
if (/^booted$/i.test(sim.state)) {
booted = true;
}
return true;
}
});
});

if (booted) {
emitter.emit('log-debug', __('Simulator is booted'));
return cb();
}

setTimeout(function () {
cb();
}, 250);
});
},
function (err) {
if (!err) {
emitter.emit('log-debug', __('%s Simulator started', handle.name));
}
next(err);
}
);
}

if (isDeviceHub(handle.simulator)) {
// Xcode 27+: Device Hub ignores the -CurrentDeviceUDID argument and does not
// boot any simulator on launch, so boot the simulator via simctl, then open
// its window in Device Hub using the devices:// URL scheme
emitter.emit('log-debug', __('Running: %s', handle.simctl + ' boot ' + handle.udid));

simctl.boot({ simctl: handle.simctl, udid: handle.udid }, function (err) {
if (err) {
return next(err);
}

openDeviceHubWindow(handle, emitter, function () {
// because we didn't spawn Device Hub ourselves, we have no child
// process to listen for when it exits, so we monitor it ourselves
isSimulatorRunning(handle.simulator, function (err, pid) {
if (!err && pid) {
setTimeout(function check() {
appc.subprocess.run('ps', ['-p', pid], function (code, out, err) {
if (code) {
simExited();
} else {
setTimeout(check, 1000);
}
});
}, 1000);
}

waitForBoot();
});
});
});
return;
}

// not running, start the simulator
emitter.emit('log-debug', __('Running: %s', handle.simulator + ' -CurrentDeviceUDID ' + handle.udid));

var child = spawn(handle.simulator, ['-CurrentDeviceUDID', handle.udid], { detached: true, stdio: 'ignore' });
child.on('close', simExited);
child.unref();

// wait for the simulator to boot
async.whilst(
function (cb) { return cb(null, !booted); },
function (cb) {
list(options, function (err, info) {
Object.keys(info.devices).some(function (type) {
return info.devices[type].some(function (sim) {
if (sim.udid === handle.udid) {
if (/^booted$/i.test(sim.state)) {
booted = true;
}
return true;
}
});
});

if (booted) {
emitter.emit('log-debug', __('Simulator is booted'));
return cb();
}

setTimeout(function () {
cb();
}, 250);
});
},
function (err) {
if (!err) {
emitter.emit('log-debug', __('%s Simulator started', handle.name));
}
next(err);
}
);
waitForBoot();
}
], function (err) {
simEmitter.emit('start', err);
Expand Down Expand Up @@ -1538,6 +1618,12 @@ function launch(simHandleOrUDID, options, callback) {

// focus or hide the iOS Simulator
if (options.focus !== false && !options.hide && !options.autoExit) {
if (isDeviceHub(handle.simulator)) {
// Xcode 27+: the sim_focus script can only bring the Device Hub app itself
// to the foreground, so focus the simulator's window via the URL scheme
return openDeviceHubWindow(handle, emitter, next);
}

action = ['focus', 'focused'];
args = [
path.join(__dirname, 'sim_focus.scpt'),
Expand Down Expand Up @@ -1838,6 +1924,10 @@ function launch(simHandleOrUDID, options, callback) {
/**
* Determines if the iOS Simulator is running by scanning the output of the `ps` command.
*
* Note that on Xcode 27+ the Device Hub app hosts all simulators in a single app
* instance and is not necessarily launched with a `-CurrentDeviceUDID` argument, so
* the process is matched by its executable path and the returned udid is `null`.
*
* @param {String} proc - The path of the executable to find the pid for.
* @param {Function} callback - A function to call with the err, pid, and udid.
*/
Expand All @@ -1853,6 +1943,16 @@ function isSimulatorRunning(proc, callback) {
m,
procRE = /^\s*\d+\s+(\d+).* \-CurrentDeviceUDID (.+)/;

if (isDeviceHub(proc)) {
procRE = /^\s*\d+\s+(\d+)/;
for (; i < l; i++) {
if (lines[i].indexOf(proc) !== -1 && (m = lines[i].match(procRE))) {
return callback(null, parseInt(m[1]), null);
}
}
return callback(null, false);
}

for (; i < l; i++) {
if (m = lines[i].match(procRE)) {
return callback(null, parseInt(m[1]), m[2]);
Expand Down
Loading