Skip to content

Commit e2011ff

Browse files
Handle multiple device options also speed ios simulator session start (#14)
* Select device when multiple device is located for android Co-authored-by: SrinivasanTarget <[email protected]> * Initial support for ios simulator suport Co-authored-by: SrinivasanTarget <[email protected]> * Added tool to boot simulator Co-authored-by: SrinivasanTarget <[email protected]> * added support to download wda.app from appium releases for simulators Co-authored-by: SrinivasanTarget <[email protected]> --------- Co-authored-by: SrinivasanTarget <[email protected]>
1 parent a276565 commit e2011ff

File tree

13 files changed

+11246
-7143
lines changed

13 files changed

+11246
-7143
lines changed

package-lock.json

Lines changed: 10032 additions & 7131 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@
3333
"@modelcontextprotocol/sdk": "^1.11.0",
3434
"@xenova/transformers": "^2.17.2",
3535
"@xmldom/xmldom": "^0.9.8",
36-
"appium-uiautomator2-driver": "^4.2.3",
37-
"appium-xcuitest-driver": "^9.6.0",
36+
"appium-adb": "^12.12.1",
37+
"appium-ios-device": "^3.1.0",
38+
"appium-uiautomator2-driver": "^5.0.5",
39+
"appium-xcuitest-driver": "^10.2.1",
3840
"fast-xml-parser": "^5.2.3",
3941
"fastmcp": "^1.23.2",
4042
"form-data": "^4.0.3",
4143
"langchain": "^0.3.27",
4244
"lodash": "^4.17.21",
45+
"node-simctl": "^8.0.4",
4346
"rimraf": "^6.0.1",
4447
"xpath": "^0.0.34",
4548
"zod": "^3.24.3"
@@ -62,4 +65,3 @@
6265
"typescript": "^5.8.3"
6366
}
6467
}
65-

src/devicemanager/adb-manager.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { ADB } from 'appium-adb';
2+
import { log } from '../locators/logger.js';
3+
/**
4+
* Singleton ADB Manager to prevent multiple ADB instances
5+
* This ensures only one ADB instance per host machine
6+
*/
7+
export class ADBManager {
8+
private static instance: ADBManager;
9+
private adbInstance: ADB | null = null;
10+
private isInitialized = false;
11+
private initializationPromise: Promise<ADB> | null = null;
12+
13+
private constructor() {}
14+
15+
/**
16+
* Get the singleton instance of ADBManager
17+
*/
18+
public static getInstance(): ADBManager {
19+
if (!ADBManager.instance) {
20+
ADBManager.instance = new ADBManager();
21+
}
22+
return ADBManager.instance;
23+
}
24+
25+
/**
26+
* Initialize ADB instance with configuration
27+
* @param options ADB configuration options
28+
* @returns Promise<ADB> The initialized ADB instance
29+
*/
30+
public async initialize(
31+
options: { adbExecTimeout?: number; udid?: string } = {}
32+
): Promise<ADB> {
33+
// If already initialized, return existing instance
34+
if (this.isInitialized && this.adbInstance) {
35+
log.debug(
36+
'ADB instance already initialized, returning existing instance'
37+
);
38+
return this.adbInstance;
39+
}
40+
41+
// If initialization is in progress, wait for it
42+
if (this.initializationPromise) {
43+
log.debug('ADB initialization in progress, waiting for completion');
44+
return await this.initializationPromise;
45+
}
46+
47+
// Start initialization
48+
this.initializationPromise = this._createADBInstance(options);
49+
50+
try {
51+
this.adbInstance = await this.initializationPromise;
52+
this.isInitialized = true;
53+
log.info('ADB instance initialized successfully');
54+
return this.adbInstance;
55+
} catch (error) {
56+
log.error(`Failed to initialize ADB instance: ${error}`);
57+
this.initializationPromise = null;
58+
throw error;
59+
}
60+
}
61+
62+
/**
63+
* Get the current ADB instance
64+
* @returns ADB instance or null if not initialized
65+
*/
66+
public getADBInstance(): ADB | null {
67+
return this.adbInstance;
68+
}
69+
70+
/**
71+
* Check if ADB is initialized
72+
* @returns boolean indicating initialization status
73+
*/
74+
public isADBInitialized(): boolean {
75+
return this.isInitialized && this.adbInstance !== null;
76+
}
77+
78+
/**
79+
* Reset the ADB instance (for testing or cleanup)
80+
*/
81+
public async reset(): Promise<void> {
82+
if (this.adbInstance) {
83+
try {
84+
// Cleanup any existing ADB instance
85+
log.info('Resetting ADB instance');
86+
this.adbInstance = null;
87+
this.isInitialized = false;
88+
this.initializationPromise = null;
89+
} catch (error) {
90+
log.error(`Error resetting ADB instance: ${error}`);
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Create ADB instance with proper error handling
97+
* @param options ADB configuration options
98+
* @returns Promise<ADB> The created ADB instance
99+
*/
100+
private async _createADBInstance(
101+
options: { adbExecTimeout?: number; udid?: string } = {}
102+
): Promise<ADB> {
103+
const defaultOptions = {
104+
adbExecTimeout: 60000,
105+
...options,
106+
};
107+
108+
log.info(
109+
`Creating ADB instance with options: ${JSON.stringify(defaultOptions)}`
110+
);
111+
112+
try {
113+
const adb = await ADB.createADB(defaultOptions);
114+
log.info('ADB instance created successfully');
115+
return adb;
116+
} catch (error) {
117+
log.error(`Failed to create ADB instance: ${error}`);
118+
throw new Error(`ADB initialization failed: ${error}`);
119+
}
120+
}
121+
122+
/**
123+
* Get ADB instance for specific device operations
124+
* This method ensures we reuse the singleton instance
125+
* @param udid Optional device UDID for device-specific operations
126+
* @returns Promise<ADB> The ADB instance
127+
*/
128+
public async getADBForDevice(udid?: string): Promise<ADB> {
129+
if (!this.isADBInitialized()) {
130+
await this.initialize({ udid });
131+
}
132+
133+
if (!this.adbInstance) {
134+
throw new Error('ADB instance not available');
135+
}
136+
137+
return this.adbInstance;
138+
}
139+
}
140+
141+
/**
142+
* Global ADB Manager instance
143+
* Use this throughout the application to access ADB functionality
144+
*/
145+
export const adbManager = ADBManager.getInstance();
146+
147+
/**
148+
* Convenience function to get ADB instance
149+
* @param options ADB configuration options
150+
* @returns Promise<ADB> The ADB instance
151+
*/
152+
export async function getADBInstance(
153+
options: { adbExecTimeout?: number; udid?: string } = {}
154+
): Promise<ADB> {
155+
return await adbManager.getADBForDevice(options.udid);
156+
}
157+
158+
/**
159+
* Convenience function to get existing ADB instance (without initialization)
160+
* @returns ADB instance or null if not initialized
161+
*/
162+
export function getExistingADBInstance(): ADB | null {
163+
return adbManager.getADBInstance();
164+
}

src/devicemanager/ios-manager.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Simctl } from 'node-simctl';
2+
import { utilities } from 'appium-ios-device';
3+
import { log } from '../locators/logger.js';
4+
5+
export interface IOSDevice {
6+
name: string;
7+
udid: string;
8+
state?: string;
9+
type: 'simulator' | 'real';
10+
platform?: string;
11+
}
12+
13+
/**
14+
* iOS Device Manager to list and manage iOS devices and simulators
15+
*/
16+
export class IOSManager {
17+
private static instance: IOSManager;
18+
private simctl: Simctl;
19+
20+
private constructor() {
21+
this.simctl = new Simctl();
22+
}
23+
24+
/**
25+
* Get the singleton instance of IOSManager
26+
*/
27+
public static getInstance(): IOSManager {
28+
if (!IOSManager.instance) {
29+
IOSManager.instance = new IOSManager();
30+
}
31+
return IOSManager.instance;
32+
}
33+
34+
/**
35+
* Check if running on macOS (required for iOS development)
36+
*/
37+
public isMac(): boolean {
38+
return process.platform === 'darwin';
39+
}
40+
41+
/**
42+
* List all iOS simulators
43+
* @returns Array of iOS simulators
44+
*/
45+
public async listSimulators(): Promise<IOSDevice[]> {
46+
if (!this.isMac()) {
47+
log.warn('iOS simulators are only available on macOS');
48+
return [];
49+
}
50+
51+
try {
52+
const devices = await this.simctl.getDevices();
53+
const simulators: IOSDevice[] = [];
54+
55+
// devices is an object with runtime as key (e.g., "18.2") and array of devices as value
56+
// node-simctl returns simplified runtime keys that are already iOS versions
57+
for (const [runtime, deviceList] of Object.entries(devices)) {
58+
if (Array.isArray(deviceList)) {
59+
for (const device of deviceList) {
60+
simulators.push({
61+
name: device.name,
62+
udid: device.udid,
63+
state: device.state,
64+
type: 'simulator',
65+
platform: runtime, // Runtime is already the iOS version (e.g., "18.2")
66+
});
67+
}
68+
}
69+
}
70+
71+
return simulators;
72+
} catch (error) {
73+
log.error(`Error listing iOS simulators: ${error}`);
74+
return [];
75+
}
76+
}
77+
78+
/**
79+
* List only booted (running) iOS simulators
80+
* @returns Array of booted simulators
81+
*/
82+
public async listBootedSimulators(): Promise<IOSDevice[]> {
83+
const allSimulators = await this.listSimulators();
84+
return allSimulators.filter(simulator => simulator.state === 'Booted');
85+
}
86+
87+
/**
88+
* List all connected real iOS devices
89+
* @returns Array of real iOS devices
90+
*/
91+
public async listRealDevices(): Promise<IOSDevice[]> {
92+
if (!this.isMac()) {
93+
log.warn('iOS real devices are only available on macOS');
94+
return [];
95+
}
96+
97+
try {
98+
const devices = await utilities.getConnectedDevices();
99+
return devices.map((udid: string) => ({
100+
name: udid, // We'll use UDID as name for now
101+
udid: udid,
102+
type: 'real' as const,
103+
}));
104+
} catch (error) {
105+
log.error(`Error listing iOS real devices: ${error}`);
106+
return [];
107+
}
108+
}
109+
110+
/**
111+
* Get all available iOS simulators
112+
* @returns Array of all iOS simulators (both booted and shutdown)
113+
*/
114+
public async getAvailableSimulators(): Promise<IOSDevice[]> {
115+
return await this.listSimulators();
116+
}
117+
118+
/**
119+
* Get all available real devices
120+
* @returns Array of real iOS devices
121+
*/
122+
public async getAvailableRealDevices(): Promise<IOSDevice[]> {
123+
return await this.listRealDevices();
124+
}
125+
126+
/**
127+
* Get devices based on device type
128+
* @param deviceType 'simulator' or 'real'
129+
* @returns Array of iOS devices
130+
*/
131+
public async getDevicesByType(
132+
deviceType: 'simulator' | 'real'
133+
): Promise<IOSDevice[]> {
134+
if (deviceType === 'simulator') {
135+
return await this.getAvailableSimulators();
136+
} else {
137+
return await this.getAvailableRealDevices();
138+
}
139+
}
140+
}
141+
142+
/**
143+
* Global iOS Manager instance
144+
*/
145+
export const iosManager = IOSManager.getInstance();

src/locators/logger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class Logger {
1010
error(...args: any[]): void {
1111
console.error(...args);
1212
}
13+
14+
debug(...args: any[]): void {
15+
console.debug(...args);
16+
}
1317
}
1418

1519
export const log = new Logger();

0 commit comments

Comments
 (0)