@@ -3,9 +3,7 @@ import {exec, SubProcess} from 'teen_process';
33import path , { dirname } from 'node:path' ;
44import { fileURLToPath } from 'node:url' ;
55import { log } from './logger' ;
6- import _ from 'lodash' ;
76import { PLATFORM_NAME_TVOS } from './constants' ;
8- import B from 'bluebird' ;
97import _fs from 'node:fs' ;
108import { waitForCondition } from 'asyncbox' ;
119import { arch } from 'node:os' ;
@@ -19,13 +17,18 @@ const currentFilename =
1917
2018const currentDirname = dirname ( currentFilename ) ;
2119
20+ let moduleRootCache : string | undefined ;
21+
2222/**
2323 * Calculates the path to the current module's root folder
2424 *
2525 * @returns {string } The full path to module root
2626 * @throws {Error } If the current module root folder cannot be determined
2727 */
28- const getModuleRoot = _ . memoize ( function getModuleRoot ( ) : string {
28+ const getModuleRoot = function getModuleRoot ( ) : string {
29+ if ( moduleRootCache ) {
30+ return moduleRootCache ;
31+ }
2932 let currentDir = currentDirname ;
3033 let isAtFsRoot = false ;
3134 while ( ! isAtFsRoot ) {
@@ -35,22 +38,37 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string {
3538 _fs . existsSync ( manifestPath ) &&
3639 JSON . parse ( _fs . readFileSync ( manifestPath , 'utf8' ) ) . name === 'appium-webdriveragent'
3740 ) {
41+ moduleRootCache = currentDir ;
3842 return currentDir ;
3943 }
4044 } catch { }
4145 currentDir = path . dirname ( currentDir ) ;
4246 isAtFsRoot = currentDir . length <= path . dirname ( currentDir ) . length ;
4347 }
4448 throw new Error ( 'Cannot find the root folder of the appium-webdriveragent Node.js module' ) ;
45- } ) ;
49+ } ;
4650
4751export const BOOTSTRAP_PATH = getModuleRoot ( ) ;
4852
53+ /**
54+ * Arguments for setting xctestrun file
55+ */
56+ export interface XctestrunFileArgs {
57+ deviceInfo : DeviceInfo ;
58+ sdkVersion : string ;
59+ bootstrapPath : string ;
60+ wdaRemotePort : number | string ;
61+ wdaBindingIP ?: string ;
62+ }
63+
64+ /**
65+ * Find and terminate all processes matching the given pgrep pattern.
66+ */
4967export async function killAppUsingPattern ( pgrepPattern : string ) : Promise < void > {
5068 const signals = [ 2 , 15 , 9 ] ;
5169 for ( const signal of signals ) {
5270 const matchedPids = await getPIDsUsingPattern ( pgrepPattern ) ;
53- if ( _ . isEmpty ( matchedPids ) ) {
71+ if ( matchedPids . length === 0 ) {
5472 return ;
5573 }
5674 const args = [ `-${ signal } ` , ...matchedPids ] ;
@@ -59,21 +77,24 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
5977 } catch ( err : any ) {
6078 log . debug ( `kill ${ args . join ( ' ' ) } -> ${ err . message } ` ) ;
6179 }
62- if ( signal === _ . last ( signals ) ) {
80+ if ( signal === signals [ signals . length - 1 ] ) {
6381 // there is no need to wait after SIGKILL
6482 return ;
6583 }
6684 try {
6785 await waitForCondition (
6886 async ( ) => {
69- const pidCheckPromises = matchedPids . map ( ( pid ) =>
70- exec ( 'kill' , [ '-0' , pid ] )
87+ const pidCheckPromises = matchedPids . map ( async ( pid ) => {
88+ try {
89+ await exec ( 'kill' , [ '-0' , pid ] ) ;
7190 // the process is still alive
72- . then ( ( ) => false )
91+ return false ;
92+ } catch {
7393 // the process is dead
74- . catch ( ( ) => true ) ,
75- ) ;
76- return ( await B . all ( pidCheckPromises ) ) . every ( ( x ) => x === true ) ;
94+ return true ;
95+ }
96+ } ) ;
97+ return ( await Promise . all ( pidCheckPromises ) ) . every ( ( x ) => x === true ) ;
7798 } ,
7899 {
79100 waitMs : 1000 ,
@@ -93,9 +114,12 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
93114 * @returns Return true if the platformName is tvOS
94115 */
95116export function isTvOS ( platformName : string ) : boolean {
96- return _ . toLower ( platformName ) === _ . toLower ( PLATFORM_NAME_TVOS ) ;
117+ return platformName ?. toLowerCase ( ) === PLATFORM_NAME_TVOS . toLowerCase ( ) ;
97118}
98119
120+ /**
121+ * Configure keychain access required for real-device code signing.
122+ */
99123export async function setRealDeviceSecurity (
100124 keychainPath : string ,
101125 keychainPassword : string ,
@@ -106,17 +130,6 @@ export async function setRealDeviceSecurity(
106130 await exec ( 'security' , [ 'set-keychain-settings' , '-t' , '3600' , '-l' , keychainPath ] ) ;
107131}
108132
109- /**
110- * Arguments for setting xctestrun file
111- */
112- export interface XctestrunFileArgs {
113- deviceInfo : DeviceInfo ;
114- sdkVersion : string ;
115- bootstrapPath : string ;
116- wdaRemotePort : number | string ;
117- wdaBindingIP ?: string ;
118- }
119-
120133/**
121134 * Creates xctestrun file per device & platform version.
122135 * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
@@ -140,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise<string>
140153 wdaRemotePort ,
141154 wdaBindingIP ,
142155 ) ;
143- const newXctestRunContent = _ . merge ( xctestRunContent , updateWDAPort ) ;
156+ const newXctestRunContent = mergeObjects ( xctestRunContent , updateWDAPort ) ;
144157 await plist . updatePlistFile ( xctestrunFilePath , newXctestRunContent , true ) ;
145158
146159 return xctestrunFilePath ;
@@ -285,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise<number | null> {
285298 return mtime . getTime ( ) ;
286299}
287300
301+ /**
302+ * Escape regular expression metacharacters in a string.
303+ */
304+ export function escapeRegExp ( value : string ) : string {
305+ return value . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
306+ }
307+
308+ /**
309+ * Truncate a string to the given length and append ellipsis if needed.
310+ */
311+ export function truncateString ( value : string , length : number ) : string {
312+ if ( value . length <= length ) {
313+ return value ;
314+ }
315+ return `${ value . slice ( 0 , Math . max ( 0 , length - 1 ) ) } …` ;
316+ }
317+
288318/**
289319 * Kills running XCTest processes for the particular device.
290320 */
@@ -296,7 +326,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr
296326 processPatterns . push ( `xctest.*${ udid } ` ) ;
297327 }
298328 log . debug ( `Killing running processes '${ processPatterns . join ( ', ' ) } ' for the device ${ udid } ...` ) ;
299- await B . all ( processPatterns . map ( killAppUsingPattern ) ) ;
329+ await Promise . all ( processPatterns . map ( killAppUsingPattern ) ) ;
300330}
301331
302332/**
@@ -329,22 +359,25 @@ export async function getPIDsListeningOnPort(
329359 return result ;
330360 }
331361
332- if ( ! _ . isFunction ( filteringFunc ) ) {
362+ if ( typeof filteringFunc !== 'function' ) {
333363 return result ;
334364 }
335- return await B . filter ( result , async ( pid ) => {
336- let stdout : string ;
337- try {
338- ( { stdout} = await exec ( 'ps' , [ '-p' , pid , '-o' , 'command' ] ) ) ;
339- } catch ( e : any ) {
340- if ( e . code === 1 ) {
341- // The process does not exist anymore, there's nothing to filter
342- return false ;
365+ const filtered = await Promise . all (
366+ result . map ( async ( pid ) => {
367+ let stdout : string ;
368+ try {
369+ ( { stdout} = await exec ( 'ps' , [ '-p' , pid , '-o' , 'command' ] ) ) ;
370+ } catch ( e : any ) {
371+ if ( e . code === 1 ) {
372+ // The process does not exist anymore, there's nothing to filter
373+ return null ;
374+ }
375+ throw e ;
343376 }
344- throw e ;
345- }
346- return await filteringFunc ( stdout ) ;
347- } ) ;
377+ return ( await filteringFunc ( stdout ) ) ? pid : null ;
378+ } ) ,
379+ ) ;
380+ return filtered . filter ( ( pid ) : pid is string => Boolean ( pid ) ) ;
348381}
349382
350383// Private functions
@@ -359,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
359392 return stdout
360393 . split ( / \s + / )
361394 . map ( ( x ) => parseInt ( x , 10 ) )
362- . filter ( _ . isInteger )
395+ . filter ( Number . isInteger )
363396 . map ( ( x ) => `${ x } ` ) ;
364397 } catch ( err : any ) {
365398 log . debug (
@@ -368,3 +401,27 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
368401 return [ ] ;
369402 }
370403}
404+
405+ function mergeObjects < T extends Record < string , any > , U extends Record < string , any > > (
406+ target : T ,
407+ source : U ,
408+ ) : T & U {
409+ const output : Record < string , any > = { ...target } ;
410+ for ( const [ key , sourceValue ] of Object . entries ( source ) ) {
411+ const targetValue = output [ key ] ;
412+ if ( isPlainObject ( targetValue ) && isPlainObject ( sourceValue ) ) {
413+ output [ key ] = mergeObjects ( targetValue , sourceValue ) ;
414+ continue ;
415+ }
416+ output [ key ] = sourceValue ;
417+ }
418+ return output as T & U ;
419+ }
420+
421+ function isPlainObject ( value : unknown ) : value is Record < string , any > {
422+ if ( value == null || typeof value !== 'object' || Array . isArray ( value ) ) {
423+ return false ;
424+ }
425+ const prototype = Object . getPrototypeOf ( value ) ;
426+ return prototype === Object . prototype || prototype === null ;
427+ }
0 commit comments