Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/resign #504

Merged
merged 24 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion packages/typescript-private/device-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"author": "Dogu Technologies",
"main": "./build/src/index.js",
"scripts": {
"build": "tsc -b",
"build": "tsc -b && shx cp -r res build",
"clean": "shx rm -rf build generated dist",
"dev": "nodemon",
"rebuild": "yarn run clean && yarn run build",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Instance } from '@dogu-tech/common';
import { DeviceHost, DeviceHostEnsureBrowserAndDriverRequestBody, GetFreePortQuery } from '@dogu-tech/device-client-common';
import { DeviceHost, DeviceHostEnsureBrowserAndDriverRequestBody, GetFreePortQuery, ResignAppFileRequestBody } from '@dogu-tech/device-client-common';
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { BrowserManagerService } from '../browser-manager/browser-manager.service';
import { getFreePort } from '../internal/util/net';
import { pathMap } from '../path-map';
import { DeviceHostResignAppFileService } from './device-host.resign-app-file';

@Controller(DeviceHost.controller)
export class DeviceHostController {
constructor(private readonly browserManagerService: BrowserManagerService) {}
constructor(private readonly browserManagerService: BrowserManagerService, private readonly appFileSerivce: DeviceHostResignAppFileService) {}

@Get(DeviceHost.getFreePort.path)
async getFreePort(@Query() query: GetFreePortQuery): Promise<Instance<typeof DeviceHost.getFreePort.responseBody>> {
Expand Down Expand Up @@ -45,4 +46,15 @@ export class DeviceHostController {
},
};
}

@Post(DeviceHost.resignAppFile.path)
async resignAppFile(@Body() body: ResignAppFileRequestBody): Promise<Instance<typeof DeviceHost.resignAppFile.responseBody>> {
const result = await this.appFileSerivce.queueResign(body);
return {
value: {
$case: 'data',
data: result,
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { BrowserManagerModule } from '../browser-manager/browser-manager.module';
import { DeviceHostController } from './device-host.controller';
import { DeviceHostDownloadSharedResourceService } from './device-host.download-shared-resource';
import { DeviceHostResignAppFileService } from './device-host.resign-app-file';

@Module({
imports: [BrowserManagerModule],
controllers: [DeviceHostController],
providers: [DeviceHostDownloadSharedResourceService],
providers: [DeviceHostDownloadSharedResourceService, DeviceHostResignAppFileService],
exports: [DeviceHostDownloadSharedResourceService],
})
export class DeviceHostModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Milisecond, setAxiosErrorFilterToIntercepter } from '@dogu-tech/common';
import { ResignAppFileRequestBody, ResignAppFileResponseBodyData } from '@dogu-tech/device-client-common';
import { ChildProcess, HostPaths } from '@dogu-tech/node';
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { env } from '../env';
import { DoguLogger } from '../logger/logger';
import { ResignShPath } from '../res-map';

@Injectable()
export class DeviceHostResignAppFileService {
private queueLock = new AsyncLock();
private readonly client = axios.create();

constructor(private readonly logger: DoguLogger) {
setAxiosErrorFilterToIntercepter(this.client);
}

public async queueResign(message: ResignAppFileRequestBody): Promise<ResignAppFileResponseBodyData> {
const result = await this.queueLock.acquire(
message.filePath,
async () => {
return await this.resign(message);
},
{
timeout: Milisecond.t15Minutes,
maxOccupationTime: Milisecond.t15Minutes,
},
);
if (result.result !== 'success') {
this.logger.error('DeviceHostResignAppFileService.queueResign error', { result });
}
return result;
}

private async resign(message: ResignAppFileRequestBody): Promise<ResignAppFileResponseBodyData> {
const appPath = message.filePath;
const doguHome = HostPaths.doguHomePath;
const configsPath = HostPaths.configsPath(doguHome);
const identityName = env.APPLE_RESIGN_IDENTITY_NAME;
const provisioningProfilePath = HostPaths.resignProvisoningProfilePath(configsPath);

if (process.platform !== 'darwin') {
return { result: 'not-macos' };
}

if (!appPath.endsWith('.ipa')) {
return { result: 'not-ipa' };
}

if (0 == identityName.length) {
return { result: 'no-identity-specified' };
}
if (!fs.existsSync(provisioningProfilePath)) {
return { result: 'no-provisioning' };
}

if (!fs.existsSync(HostPaths.doguTempPath())) {
await fs.promises.mkdir(HostPaths.doguTempPath(), { recursive: true });
}

const identityResult = await ChildProcess.execIgnoreError('security find-identity', {}, this.logger);
if (!identityResult.stderr.includes(identityName) && identityResult.stderr.includes(identityName)) {
return { result: 'no-identity-exists' };
}

const contents = await fs.promises.readFile(ResignShPath, { encoding: 'utf-8' });
const shellPath = path.resolve(HostPaths.doguTempPath(), `resign-${uuidv4()}.sh`);
await fs.promises.writeFile(shellPath, contents, { encoding: 'utf-8' });
oneofthezombies marked this conversation as resolved.
Show resolved Hide resolved
await fs.promises.chmod(shellPath, 0o755);

const resignedAppPath = path.resolve(HostPaths.doguTempPath(), `resign-${uuidv4()}.ipa`);
const args = [shellPath, appPath, `"${identityName}"`, '-p', provisioningProfilePath, resignedAppPath].join(' ');

this.logger.info('DeviceHostResignAppFileService.resign', { args });
const result = await ChildProcess.exec(args, {}, this.logger);
this.logger.info('DeviceHostResignAppFileService.resign done', { result });
await fs.promises.rm(appPath, { recursive: true, force: true });
await fs.promises.cp(resignedAppPath, appPath, { recursive: true, force: true });

await fs.promises.rm(shellPath, { recursive: true, force: true });
oneofthezombies marked this conversation as resolved.
Show resolved Hide resolved
await fs.promises.rm(resignedAppPath, { recursive: true, force: true });
oneofthezombies marked this conversation as resolved.
Show resolved Hide resolved
return { result: 'success' };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@dogu-private/types';
import { Closable, errorify, loopTime, Milisecond, Printable, PromiseOrValue, stringify } from '@dogu-tech/common';
import { AppiumCapabilities, BrowserInstallation, StreamingOfferDto } from '@dogu-tech/device-client-common';
import { killChildProcess } from '@dogu-tech/node';
import { ChildProcessError, killChildProcess } from '@dogu-tech/node';
import { ChildProcess } from 'child_process';
import compressing from 'compressing';
import fs from 'fs';
Expand All @@ -29,6 +29,7 @@ import { env } from '../../env';
import { GamiumContext } from '../../gamium/gamium.context';
import { createIdaLogger } from '../../logger/logger.instance';
import { IdeviceDiagnostics, IdeviceSyslog, MobileDevice, Xctrace } from '../externals';
import { IdeviceInstaller } from '../externals/cli/ideviceinstaller';
import { IosDeviceAgentProcess } from '../externals/cli/ios-device-agent';
import { ZombieTunnel } from '../externals/cli/mobiledevice-tunnel';
import { WebdriverAgentProcess } from '../externals/cli/webdriver-agent-process';
Expand Down Expand Up @@ -410,11 +411,24 @@ export class IosChannel implements DeviceChannel {
async uninstallApp(appPath: string, printable?: Printable): Promise<void> {
const dotAppPath = await this.findDotAppPath(appPath);
const appName = await MobileDevice.getBundleId(dotAppPath);
await MobileDevice.uninstallApp(this.serial, appName, printable);
await IdeviceInstaller.uninstallApp(this.serial, appName, printable);
}

async installApp(appPath: string, printable?: Printable): Promise<void> {
await MobileDevice.installApp(this.serial, appPath, printable);
const result = await IdeviceInstaller.installApp(this.serial, appPath, printable).catch((error) => {
if (!(error instanceof ChildProcessError)) {
throw error;
}
const logInfos = error.bufferLogger?.sortedLogInfos() ?? [];
if (logInfos.find((log) => stringify(log.message).includes('MismatchedApplicationIdentifierEntitlement'))) {
return { errorType: 'MismatchedApplicationIdentifierEntitlement' };
}
throw error;
});
if ('errorType' in result && result.errorType === 'MismatchedApplicationIdentifierEntitlement') {
await this.uninstallApp(appPath, printable);
await IdeviceInstaller.installApp(this.serial, appPath, printable);
}
}

async runApp(appPath: string, printable?: Printable): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { PlatformAbility } from '@dogu-private/dost-children';
import { Printable, stringify } from '@dogu-tech/common';
import { ChildProcess, HostPaths } from '@dogu-tech/node';
import child_process from 'child_process';
import fs from 'fs';
import { registerBootstrapHandler } from '../../../bootstrap/bootstrap.service';
import { idcLogger } from '../../../logger/logger.instance';

class IdeviceInstallerImpl {
async uninstallApp(udid: string, appName: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
this.tryAccessAndFix();
const exe = HostPaths.external.libimobiledevice.ideviceinstaller();
const args = ['-u', udid, '--uninstall', appName];
printable.info(`IdeviceInstallerImpl.uninstallApp ${exe} ${stringify(args)}`);
return ChildProcess.spawnAndWait(
exe,
args,
{
env: {
...process.env,
DYLD_LIBRARY_PATH: this.libPath(),
},
},
printable,
);
}

async installApp(udid: string, appPath: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
this.tryAccessAndFix();
const exe = HostPaths.external.libimobiledevice.ideviceinstaller();
const args = ['-u', udid, '--install', appPath];
printable.info(`IdeviceInstallerImpl.installApp ${exe} ${stringify(args)}`);
return ChildProcess.spawnAndWait(
exe,
args,
{
env: {
...process.env,
DYLD_LIBRARY_PATH: this.libPath(),
},
},
printable,
);
}

private tryAccessAndFix = (): void => {
const bin = HostPaths.external.libimobiledevice.ideviceinstaller();
try {
fs.accessSync(bin, fs.constants.X_OK);
} catch (error) {
this.makeAccessableSync();
}
};

private makeAccessableSync = (): void => {
try {
fs.chmodSync(HostPaths.external.libimobiledevice.ideviceinstaller(), 0o777);
} catch (error) {
const cause = error instanceof Error ? error : new Error(stringify(error));
oneofthezombies marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Failed to chmod ideviceinstaller`, { cause });
}
};
private libPath(): string {
return [HostPaths.external.libimobiledevice.libimobiledeviceLibPath(), process.env.DYLD_LIBRARY_PATH].join(':');
}
}

registerBootstrapHandler(
__filename,
async () => {
if (process.platform !== 'darwin') {
return;
}
try {
await fs.promises.chmod(HostPaths.external.libimobiledevice.ideviceinstaller(), 0o777);
} catch (error) {
const cause = error instanceof Error ? error : new Error(stringify(error));
oneofthezombies marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Failed to chmod ideviceinstaller`, { cause });
}
},
() => new PlatformAbility().isIosEnabled,
);

export const IdeviceInstaller = new IdeviceInstallerImpl();
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { IosDeviceAgentService } from '../../services/device-agent/ios-device-ag
import { StreamingService } from '../../services/streaming/streaming-service';
import { Zombieable, ZombieProps, ZombieQueriable } from '../../services/zombie/zombie-component';
import { ZombieServiceInstance } from '../../services/zombie/zombie-service';
import { MobileDevice, XcodeBuild } from '../index';
import { XcodeBuild } from '../index';
import { DerivedData } from '../xcode/deriveddata';
import { Xctestrun as XctestrunFile } from '../xcode/xctestrun';
import { IdeviceInstaller } from './ideviceinstaller';
import { ZombieTunnel } from './mobiledevice-tunnel';
import { XCTestRunContext } from './xcodebuild';

Expand Down Expand Up @@ -216,10 +217,10 @@ class ZombieIdaXCTest implements Zombieable {

const xctestrunPath = this.xctestrunfile.filePath;
await this.dissmissAlert(sessionId);
await MobileDevice.uninstallApp(this.serial, 'com.dogu.IOSDeviceAgentRunner', this.logger).catch(() => {
await IdeviceInstaller.uninstallApp(this.serial, 'com.dogu.IOSDeviceAgentRunner', this.logger).catch(() => {
this.logger.warn?.('uninstallApp com.dogu.IOSDeviceAgentRunner failed');
});
await MobileDevice.uninstallApp(this.serial, 'com.dogu.IOSDeviceAgentRunner.xctrunner', this.logger).catch(() => {
await IdeviceInstaller.uninstallApp(this.serial, 'com.dogu.IOSDeviceAgentRunner.xctrunner', this.logger).catch(() => {
this.logger.warn?.('uninstallApp com.dogu.IOSDeviceAgentRunner.xctrunner failed');
});
await this.dissmissAlert(sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ class MobileDeviceImpl {
if (!dotAppPath.endsWith('.app')) {
throw Error(`appPath must be end with .app: ${dotAppPath}`);
}
const { stdout } = await ChildProcess.execIgnoreError(`${pathMap().macos.mobiledevice} get_bundle_id ${dotAppPath}`, {}, idcLogger);
const { stdout } = await ChildProcess.execIgnoreError(`${pathMap().macos.mobiledevice} get_bundle_id "${dotAppPath}"`, {}, idcLogger);
return stdout.trim();
}

uninstallApp(udid: string, appName: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
return ChildProcess.spawnAndWait(pathMap().macos.mobiledevice, ['uninstall_app', '-u', udid, appName], {}, printable);
}
// uninstallApp(udid: string, appName: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
// return ChildProcess.spawnAndWait(pathMap().macos.mobiledevice, ['uninstall_app', '-u', udid, appName], {}, printable);
// }

installApp(udid: string, appPath: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
return ChildProcess.spawnAndWait(pathMap().macos.mobiledevice, ['install_app', '-u', udid, appPath], {}, printable);
}
// installApp(udid: string, appPath: string, printable: Printable = idcLogger): Promise<child_process.ChildProcess> {
// return ChildProcess.spawnAndWait(pathMap().macos.mobiledevice, ['install_app', '-u', udid, appPath], {}, printable);
// }

@Retry()
async listApps(udid: string): Promise<string[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Zombieable, ZombieProps, ZombieQueriable } from '../../services/zombie/
import { ZombieServiceInstance } from '../../services/zombie/zombie-service';
import { XcodeBuild } from '../index';
import { DerivedData } from '../xcode/deriveddata';
import { MobileDevice } from './mobiledevice';
import { IdeviceInstaller } from './ideviceinstaller';
import { ZombieTunnel } from './mobiledevice-tunnel';
import { XCTestRunContext } from './xcodebuild';

Expand Down Expand Up @@ -136,7 +136,7 @@ class ZombieWdaXCTest implements Zombieable {
this.logger.debug?.(`ZombieWdaXCTest.revive`);
await delay(1000);

await MobileDevice.uninstallApp(this.serial, 'com.facebook.WebDriverAgentRunner', this.logger).catch(() => {
await IdeviceInstaller.uninstallApp(this.serial, 'com.facebook.WebDriverAgentRunner', this.logger).catch(() => {
this.logger.warn?.('uninstallApp com.facebook.WebDriverAgentRunner failed');
});
await XcodeBuild.killPreviousXcodebuild(this.serial, `webdriveragent.*${this.serial}`, this.logger).catch(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,13 @@ export class XCTestRunContext {
}

get history(): string {
let logInfos = Array.from(this.buffer.buffers).flatMap((buffer) =>
buffer[1].flatMap((log) => {
return {
level: buffer[0],
time: log.time,
message: log.message,
details: log.details,
};
}),
);
logInfos.sort((a, b) => a.time - b.time);
const bufferLogs =
'>>> LOG DUMP start\n' + logInfos.map((log) => `${log.level}|${new Date(log.time).toISOString()}|${log.message} ${stringify(log.details)}`).join('\n') + '\n>>> LOG DUMP end';
'>>> LOG DUMP start\n' +
this.buffer
.sortedLogInfos()
.map((log) => `${log.level}|${new Date(log.time).toISOString()}|${stringify(log.message)} ${stringify(log.details)}`)
.join('\n') +
'\n>>> LOG DUMP end';
return bufferLogs;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/typescript-private/device-server/src/res-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import path from 'path';

export const ResPath = path.resolve(__dirname, '../res');

export const ResignShPath = path.resolve(ResPath, 'resign.sh');
Loading