CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
iphone-simulator.ts313 linesDownload Raw Back to src
1import { execFileSync } from 'node:child_process';2import { mkdtempSync, readdirSync, rmSync } from 'node:fs';3import { tmpdir } from 'node:os';4import { join, basename, extname } from 'node:path';5 6import { trace } from './logger';7import { WebDriverAgent } from './webdriver-agent';8import {9  ActionableError,10  Button,11  InstalledApp,12  InstallOptions,13  Robot,14  ScreenElement,15  ScreenSize,16  SwipeDirection,17  Orientation,18} from './robot';19import { validatePackageName, validateLocale } from './utils';20 21export interface Simulator {22  name: string;23  uuid: string;24  state: string;25}26 27interface AppInfo {28  ApplicationType: string;29  Bundle: string;30  CFBundleDisplayName: string;31  CFBundleExecutable: string;32  CFBundleIdentifier: string;33  CFBundleName: string;34  CFBundleVersion: string;35  DataContainer: string;36  Path: string;37}38 39const TIMEOUT = 30000;40const WDA_PORT = 8100;41const MAX_BUFFER_SIZE = 1024 * 1024 * 8;42 43export class Simctl implements Robot {44  constructor(private readonly simulatorUuid: string) {}45 46  private async isWdaInstalled(): Promise<boolean> {47    const apps = await this.listApps();48    return apps49      .map((app) => app.packageName)50      .includes('com.facebook.WebDriverAgentRunner.xctrunner');51  }52 53  private async startWda(): Promise<void> {54    if (!(await this.isWdaInstalled())) {55      // wda is not even installed, won't attempt to start it56      return;57    }58 59    trace('Starting WebDriverAgent');60    const webdriverPackageName = 'com.facebook.WebDriverAgentRunner.xctrunner';61    this.simctl('launch', this.simulatorUuid, webdriverPackageName);62 63    // now we wait for wda to have a successful status64    const wda = new WebDriverAgent('localhost', WDA_PORT);65 66    // wait up to 10 seconds for wda to start67    const timeout = +new Date() + 10 * 1000;68    while (+new Date() < timeout) {69      // cross fingers and see if wda is already running70      if (await wda.isRunning()) {71        trace('WebDriverAgent is now running');72        return;73      }74 75      // wait 100ms before trying again76      await new Promise((resolve) => setTimeout(resolve, 100));77    }78 79    trace('Could not start WebDriverAgent in time, giving up');80  }81 82  private async wda(): Promise<WebDriverAgent> {83    const wda = new WebDriverAgent('localhost', WDA_PORT);84 85    if (!(await wda.isRunning())) {86      await this.startWda();87      if (!(await wda.isRunning())) {88        throw new ActionableError(89          'WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/',90        );91      }92 93      // was successfully started94    }95 96    return wda;97  }98 99  private simctl(...args: string[]): Buffer {100    return execFileSync('xcrun', ['simctl', ...args], {101      timeout: TIMEOUT,102      maxBuffer: MAX_BUFFER_SIZE,103    });104  }105 106  public async getScreenshot(): Promise<Buffer> {107    const wda = await this.wda();108    return await wda.getScreenshot();109    // alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-");110  }111 112  public async openUrl(url: string) {113    const wda = await this.wda();114    await wda.openUrl(url);115    // alternative: this.simctl("openurl", this.simulatorUuid, url);116  }117 118  public async launchApp(packageName: string, locale?: string) {119    validatePackageName(packageName);120    const args = ['launch', this.simulatorUuid, packageName];121    if (locale) {122      validateLocale(locale);123      const locales = locale.split(',').map((l) => l.trim());124      args.push('-AppleLanguages', `(${locales.join(', ')})`);125      args.push('-AppleLocale', locales[0]);126    }127 128    this.simctl(...args);129  }130 131  public async terminateApp(packageName: string) {132    validatePackageName(packageName);133    this.simctl('terminate', this.simulatorUuid, packageName);134  }135 136  private findAppBundle(dir: string): string | null {137    const entries = readdirSync(dir, { withFileTypes: true });138 139    for (const entry of entries) {140      if (entry.isDirectory() && entry.name.endsWith('.app')) {141        return join(dir, entry.name);142      }143    }144 145    return null;146  }147 148  private validateZipPaths(zipPath: string): void {149    const output = execFileSync('/usr/bin/zipinfo', ['-1', zipPath], {150      timeout: TIMEOUT,151      maxBuffer: MAX_BUFFER_SIZE,152    }).toString();153 154    const invalidPath = output155      .split('\n')156      .map((s) => s.trim())157      .filter((s) => s)158      .find((s) => s.startsWith('/') || s.includes('..'));159 160    if (invalidPath) {161      throw new ActionableError(162        `Security violation: File path '${invalidPath}' contains invalid characters`,163      );164    }165  }166 167  public async installApp(168    path: string,169    _options?: InstallOptions,170  ): Promise<void> {171    let tempDir: string | null = null;172    let installPath = path;173 174    try {175      // zip files need to be extracted prior to installation176      if (extname(path).toLowerCase() === '.zip') {177        trace(`Detected .zip file, validating contents`);178 179        // before extracting, let's make sure there's no zip-slip bombs here180        this.validateZipPaths(path);181 182        tempDir = mkdtempSync(join(tmpdir(), 'ios-app-'));183 184        try {185          execFileSync('unzip', ['-q', path, '-d', tempDir], {186            timeout: TIMEOUT,187          });188        } catch (error: any) {189          throw new ActionableError(`Failed to unzip file: ${error.message}`);190        }191 192        const appBundle = this.findAppBundle(tempDir);193        if (!appBundle) {194          throw new ActionableError(195            'No .app bundle found in the .zip file, please visit wiki at https://github.com/mobile-next/mobile-mcp/wiki for assistance.',196          );197        }198 199        installPath = appBundle;200        trace(`Found .app bundle at: ${basename(appBundle)}`);201      }202 203      // continue with installation204      this.simctl('install', this.simulatorUuid, installPath);205    } catch (error: any) {206      const stdout = error.stdout ? error.stdout.toString() : '';207      const stderr = error.stderr ? error.stderr.toString() : '';208      const output = (stdout + stderr).trim();209      throw new ActionableError(output || error.message);210    } finally {211      // Clean up temporary directory if it was created212      if (tempDir) {213        try {214          trace(`Cleaning up temporary directory`);215          rmSync(tempDir, { recursive: true, force: true });216        } catch (cleanupError) {217          trace(218            `Warning: Failed to cleanup temporary directory: ${cleanupError}`,219          );220        }221      }222    }223  }224 225  public async uninstallApp(bundleId: string): Promise<void> {226    try {227      this.simctl('uninstall', this.simulatorUuid, bundleId);228    } catch (error: any) {229      const stdout = error.stdout ? error.stdout.toString() : '';230      const stderr = error.stderr ? error.stderr.toString() : '';231      const output = (stdout + stderr).trim();232      throw new ActionableError(output || error.message);233    }234  }235 236  public async listApps(): Promise<InstalledApp[]> {237    const text = this.simctl('listapps', this.simulatorUuid).toString();238    const result = execFileSync(239      'plutil',240      ['-convert', 'json', '-o', '-', '-r', '-'],241      {242        input: text,243      },244    );245 246    const output = JSON.parse(result.toString()) as Record<string, AppInfo>;247    return Object.values(output).map((app) => ({248      packageName: app.CFBundleIdentifier,249      appName: app.CFBundleDisplayName,250    }));251  }252 253  public async getScreenSize(): Promise<ScreenSize> {254    const wda = await this.wda();255    return wda.getScreenSize();256  }257 258  public async sendKeys(keys: string) {259    const wda = await this.wda();260    return wda.sendKeys(keys);261  }262 263  public async swipe(direction: SwipeDirection): Promise<void> {264    const wda = await this.wda();265    return wda.swipe(direction);266  }267 268  public async swipeFromCoordinate(269    x: number,270    y: number,271    direction: SwipeDirection,272    distance?: number,273  ): Promise<void> {274    const wda = await this.wda();275    return wda.swipeFromCoordinate(x, y, direction, distance);276  }277 278  public async tap(x: number, y: number) {279    const wda = await this.wda();280    return wda.tap(x, y);281  }282 283  public async doubleTap(x: number, y: number): Promise<void> {284    const wda = await this.wda();285    await wda.doubleTap(x, y);286  }287 288  public async longPress(x: number, y: number, duration: number) {289    const wda = await this.wda();290    return wda.longPress(x, y, duration);291  }292 293  public async pressButton(button: Button) {294    const wda = await this.wda();295    return wda.pressButton(button);296  }297 298  public async getElementsOnScreen(): Promise<ScreenElement[]> {299    const wda = await this.wda();300    return wda.getElementsOnScreen();301  }302 303  public async setOrientation(orientation: Orientation): Promise<void> {304    const wda = await this.wda();305    return wda.setOrientation(orientation);306  }307 308  public async getOrientation(): Promise<Orientation> {309    const wda = await this.wda();310    return wda.getOrientation();311  }312}313 
basant307/AI_Governance_Project · CoolFace