@@ -71,39 +71,111 @@ const batchCalls = <A extends any[], R>(
7171 } ;
7272}
7373
74- export const getConnectedDevices = batchCalls ( async ( adbClient : Adb . Client ) => {
75- try {
76- const devices = await ( adbClient . listDevices ( ) as Promise < Adb . Device [ ] > ) ;
77- return devices
78- . filter ( ( d ) =>
79- d . type !== 'offline' &&
80- d . type !== 'unauthorized' &&
81- ! d . type . startsWith ( "no permissions" )
82- ) . map ( d => d . id ) ;
83- } catch ( e ) {
84- if ( isErrorLike ( e ) && (
85- e . code === 'ENOENT' || // No ADB available
86- e . code === 'EACCES' || // ADB available, but we aren't allowed to run it
87- e . code === 'EPERM' || // Permissions error launching ADB
88- e . code === 'ECONNREFUSED' || // Tried to start ADB, but still couldn't connect
89- e . code === 'ENOTDIR' || // ADB path contains something that's not a directory
90- e . signal === 'SIGKILL' || // In some envs 'adb start-server' is always killed (why?)
91- ( e . cmd && e . code ) // ADB available, but "adb start-server" failed
92- )
93- ) {
94- if ( e . code !== 'ENOENT' ) {
95- console . log ( `ADB unavailable, ${ e . cmd
96- ? `${ e . cmd } exited with ${ e . code } `
97- : `due to ${ e . code } `
98- } `) ;
74+ export const getConnectedDevices = batchCalls (
75+ async ( adbClient : Adb . Client ) : Promise < Record < string , Record < string , string > > > => {
76+ try {
77+ const devices = await ( adbClient . listDevices ( ) as Promise < Adb . Device [ ] > ) ;
78+ const deviceIds = devices
79+ . filter ( ( d ) =>
80+ d . type !== 'offline' &&
81+ d . type !== 'unauthorized' &&
82+ ! d . type . startsWith ( "no permissions" )
83+ ) . map ( d => d . id ) ;
84+
85+ const deviceDetails = Object . fromEntries ( await Promise . all (
86+ deviceIds . map ( async ( id ) : Promise < [ string , Record < string , string > ] > => {
87+ const name = await getDeviceName ( adbClient , id ) ;
88+ return [ id , { id, name } ] ;
89+ } )
90+ ) ) ;
91+
92+ // Clear any non-present device names from the cache
93+ filterDeviceNameCache ( deviceIds ) ;
94+ return deviceDetails ;
95+ } catch ( e ) {
96+ if ( isErrorLike ( e ) && (
97+ e . code === 'ENOENT' || // No ADB available
98+ e . code === 'EACCES' || // ADB available, but we aren't allowed to run it
99+ e . code === 'EPERM' || // Permissions error launching ADB
100+ e . code === 'ECONNREFUSED' || // Tried to start ADB, but still couldn't connect
101+ e . code === 'ENOTDIR' || // ADB path contains something that's not a directory
102+ e . signal === 'SIGKILL' || // In some envs 'adb start-server' is always killed (why?)
103+ ( e . cmd && e . code ) // ADB available, but "adb start-server" failed
104+ )
105+ ) {
106+ if ( e . code !== 'ENOENT' ) {
107+ console . log ( `ADB unavailable, ${ e . cmd
108+ ? `${ e . cmd } exited with ${ e . code } `
109+ : `due to ${ e . code } `
110+ } `) ;
111+ }
112+ return { } ;
113+ } else {
114+ logError ( e ) ;
115+ throw e ;
99116 }
100- return [ ] ;
117+ }
118+ }
119+ ) ;
120+
121+
122+ const cachedDeviceNames : { [ deviceId : string ] : string | undefined } = { } ;
123+
124+ const getDeviceName = async ( adbClient : Adb . Client , deviceId : string ) => {
125+ if ( cachedDeviceNames [ deviceId ] ) {
126+ return cachedDeviceNames [ deviceId ] ! ;
127+ }
128+
129+ let deviceName : string ;
130+ try {
131+ const device = adbClient . getDevice ( deviceId ) ;
132+
133+ if ( deviceId . startsWith ( 'emulator-' ) ) {
134+ const props = await device . getProperties ( ) ;
135+
136+ const avdName = (
137+ props [ 'ro.boot.qemu.avd_name' ] || // New emulators
138+ props [ 'ro.kernel.qemu.avd_name' ] // Old emulators
139+ ) ?. replace ( / _ / g, ' ' ) ;
140+
141+ const osVersion = props [ 'ro.build.version.release' ] ;
142+
143+ deviceName = avdName || `Android ${ osVersion } emulator` ;
101144 } else {
102- logError ( e ) ;
103- throw e ;
145+ const name = (
146+ await run ( device , [ 'settings' , 'get' , 'global' , 'device_name' ] )
147+ . catch ( ( ) => { } )
148+ ) ?. trim ( ) ;
149+
150+ if ( name ) {
151+ deviceName = name ;
152+ } else {
153+ const props = await device . getProperties ( ) ;
154+
155+ deviceName = props [ 'ro.product.model' ] ||
156+ deviceId ;
157+ }
104158 }
159+ } catch ( e : any ) {
160+ console . log ( `Error getting device name for ${ deviceId } ` , e . message ) ;
161+ deviceName = deviceId ;
162+ // N.b. we do cache despite the error - many errors could be persistent, and it's
163+ // no huge problem (and more consistent) to stick with the raw id instead.
105164 }
106- } )
165+
166+ cachedDeviceNames [ deviceId ] = deviceName ;
167+ return deviceName ;
168+ } ;
169+
170+ // Clear any non-connected device names from the cache (to avoid leaks, and
171+ // so that we do update the name if they reconnect later.)
172+ const filterDeviceNameCache = ( connectedIds : string [ ] ) => {
173+ Object . keys ( cachedDeviceNames ) . forEach ( ( id ) => {
174+ if ( ! connectedIds . includes ( id ) ) {
175+ delete cachedDeviceNames [ id ] ;
176+ }
177+ } ) ;
178+ } ;
107179
108180export function stringAsStream ( input : string ) {
109181 const contentStream = new stream . Readable ( ) ;
0 commit comments