@@ -10,20 +10,46 @@ class ADBService extends EventEmitter {
1010 exec ( 'adb devices -l' , ( error , stdout , stderr ) => {
1111 if ( error ) return reject ( error ) ;
1212
13- // sample: R5CX912W25A device product:e3qxxx model:SM_S928B device:e3q transport_id:1
1413 const lines = stdout . split ( 'List of devices attached' ) . pop ( ) . trim ( )
1514 . split ( '\n' ) . map ( l => l . trim ( ) ) . filter ( l => l ) ;
1615
1716 const devices = lines . map ( line => {
18- const [ id , model ] = line . match ( / ( ^ \w + ) | ( m o d e l : \w + ) / g) ;
19- return { id, model : model . split ( ':' ) . pop ( ) , raw : line } ;
20- } ) ;
17+ const matches = line . match ( / ( ^ \S + ) / ) ;
18+ if ( ! matches ) return null ;
19+ const id = matches [ 1 ] ;
20+ const modelMatch = line . match ( / m o d e l : ( \S + ) / ) ;
21+ const model = modelMatch ? modelMatch [ 1 ] : id ;
22+ const status = line . includes ( 'unauthorized' ) ? 'unauthorized'
23+ : line . includes ( 'offline' ) ? 'offline' : 'online' ;
24+ return { id, model, status, raw : line } ;
25+ } ) . filter ( Boolean ) ;
2126
2227 resolve ( devices ) ;
2328 } ) ;
2429 } ) ;
2530 }
2631
32+ startDeviceTracking ( ) {
33+ if ( this . _trackProcess ) return ;
34+ this . _trackProcess = spawn ( 'adb' , [ 'track-devices' ] ) ;
35+ this . _trackProcess . stdout . on ( 'data' , ( ) => {
36+ this . emit ( 'adbevent' , { type : 'adb.devices-changed' } ) ;
37+ } ) ;
38+ this . _trackProcess . on ( 'close' , ( ) => {
39+ this . _trackProcess = null ;
40+ this . _trackRetry = setTimeout ( ( ) => this . startDeviceTracking ( ) , 3000 ) ;
41+ } ) ;
42+ this . _trackProcess . on ( 'error' , ( ) => {
43+ this . _trackProcess = null ;
44+ } ) ;
45+ }
46+
47+ stopDeviceTracking ( ) {
48+ clearTimeout ( this . _trackRetry ) ;
49+ this . _trackProcess ?. kill ( ) ;
50+ this . _trackProcess = null ;
51+ }
52+
2753 listPackages ( deviceId ) {
2854 return new Promise ( ( resolve , reject ) => {
2955 exec ( `adb -s ${ deviceId } shell pm list packages -3` , ( error , stdout , stderr ) => {
@@ -64,6 +90,65 @@ class ADBService extends EventEmitter {
6490 } ) ;
6591 }
6692
93+ getAppState ( deviceId , packageName ) {
94+ return new Promise ( ( resolve ) => {
95+ // Check if process is running
96+ exec ( `adb -s ${ deviceId } shell pidof ${ packageName } ` , ( err1 , pidOut ) => {
97+ const pid = pidOut ?. trim ( ) ;
98+ if ( ! pid ) {
99+ return resolve ( { packageName, state : 'not-running' , pid : null } ) ;
100+ }
101+ // Check if it's the foreground app
102+ exec ( `adb -s ${ deviceId } shell "dumpsys activity activities | grep mResumedActivity"` , ( err2 , actOut ) => {
103+ const isForeground = actOut ?. includes ( packageName ) ;
104+ resolve ( {
105+ packageName,
106+ state : isForeground ? 'foreground' : 'background' ,
107+ pid,
108+ } ) ;
109+ } ) ;
110+ } ) ;
111+ } ) ;
112+ }
113+
114+ getPackageInfo ( deviceId , packageName ) {
115+ return new Promise ( ( resolve , reject ) => {
116+ exec ( `adb -s ${ deviceId } shell dumpsys package ${ packageName } | grep -E "versionName|versionCode"` , ( error , stdout ) => {
117+ if ( error ) return reject ( error ) ;
118+ const version = stdout . match ( / v e r s i o n N a m e = ( \S + ) / ) ?. [ 1 ] || 'unknown' ;
119+ const versionCode = stdout . match ( / v e r s i o n C o d e = ( \d + ) / ) ?. [ 1 ] || '' ;
120+ resolve ( { packageName, version, versionCode } ) ;
121+ } ) ;
122+ } ) ;
123+ }
124+
125+ launchApp ( deviceId , packageName ) {
126+ return new Promise ( ( resolve , reject ) => {
127+ exec ( `adb -s ${ deviceId } shell monkey -p ${ packageName } -c android.intent.category.LAUNCHER 1` , ( error ) => {
128+ if ( error ) return reject ( error ) ;
129+ resolve ( ) ;
130+ } ) ;
131+ } ) ;
132+ }
133+
134+ forceStopApp ( deviceId , packageName ) {
135+ return new Promise ( ( resolve , reject ) => {
136+ exec ( `adb -s ${ deviceId } shell am force-stop ${ packageName } ` , ( error ) => {
137+ if ( error ) return reject ( error ) ;
138+ resolve ( ) ;
139+ } ) ;
140+ } ) ;
141+ }
142+
143+ clearAppData ( deviceId , packageName ) {
144+ return new Promise ( ( resolve , reject ) => {
145+ exec ( `adb -s ${ deviceId } shell pm clear ${ packageName } ` , ( error ) => {
146+ if ( error ) return reject ( error ) ;
147+ resolve ( ) ;
148+ } ) ;
149+ } ) ;
150+ }
151+
67152 refreshPidMap ( deviceId ) {
68153 exec ( `adb -s ${ deviceId } shell ps -A -o PID,NAME` , ( error , stdout ) => {
69154 if ( error ) return ;
@@ -88,8 +173,38 @@ class ADBService extends EventEmitter {
88173 this . refreshPidMap ( deviceId ) ;
89174 this . _pidInterval = setInterval ( ( ) => this . refreshPidMap ( deviceId ) , 30000 ) ;
90175
176+ // App lifecycle tracking for single-package mode
177+ if ( packages && packages . length === 1 ) {
178+ const pkg = packages [ 0 ] ;
179+ this . _lastAppState = null ;
180+ this . _appStateRunning = true ;
181+
182+ const pollLoop = ( ) => {
183+ if ( ! this . _appStateRunning ) return ;
184+ this . getAppState ( deviceId , pkg ) . then ( state => {
185+ if ( state . state !== this . _lastAppState ) {
186+ this . _lastAppState = state . state ;
187+ this . emit ( 'adbevent' , {
188+ type : 'adb.lifecycle' ,
189+ data : { event : state . state , pkg, detail : `${ state . state } (PID: ${ state . pid || 'none' } )` }
190+ } ) ;
191+ }
192+ // Schedule next check after a short delay (serialized, no overlap)
193+ if ( this . _appStateRunning ) {
194+ this . _appStateTimeout = setTimeout ( pollLoop , 100 ) ;
195+ }
196+ } ) . catch ( ( ) => {
197+ if ( this . _appStateRunning ) {
198+ this . _appStateTimeout = setTimeout ( pollLoop , 500 ) ;
199+ }
200+ } ) ;
201+ } ;
202+ pollLoop ( ) ;
203+ }
204+
91205 // Always stream everything at Verbose — all filtering is client-side
92- const args = [ '-s' , deviceId , 'logcat' , '*:V' ] ;
206+ // -T 1 = only new logs from now, avoids replaying old buffer on stop/start
207+ const args = [ '-s' , deviceId , 'logcat' , '-T' , '1' , '*:V' ] ;
93208
94209 this . logcatProcess = spawn ( 'adb' , args ) ;
95210
@@ -127,6 +242,81 @@ class ADBService extends EventEmitter {
127242 type : 'adb.log' ,
128243 data : batch [ i ]
129244 } ) ;
245+
246+ // Detect package install/uninstall/update events
247+ const tag = batch [ i ] . tag ?. trim ( ) ;
248+ if ( ( tag === 'PackageManager' || tag === 'PackageInstaller' ) &&
249+ / \b ( i n s t a l l | u n i n s t a l l | r e m o v e | r e p l a c e | u p d a t e ) \b / i. test ( batch [ i ] . message ) ) {
250+ this . refreshPidMap ( deviceId ) ;
251+ setTimeout ( ( ) => this . refreshPidMap ( deviceId ) , 2000 ) ;
252+ this . emit ( 'adbevent' , {
253+ type : 'adb.package-changed' ,
254+ data : { message : batch [ i ] . message , tag }
255+ } ) ;
256+ }
257+
258+ // Detect app lifecycle events for tracked packages
259+ const msg = batch [ i ] . message ;
260+ const trackedPkgs = this . lastParams ?. packages || [ ] ;
261+ if ( trackedPkgs . length > 0 ) {
262+ let lifecycle = null ;
263+ if ( tag === 'ActivityManager' ) {
264+ for ( const pkg of trackedPkgs ) {
265+ if ( msg . includes ( pkg ) ) {
266+ if ( / ^ S t a r t p r o c \b / . test ( msg ) ) {
267+ lifecycle = { event : 'started' , pkg, detail : msg } ;
268+ this . refreshPidMap ( deviceId ) ;
269+ setTimeout ( ( ) => this . refreshPidMap ( deviceId ) , 1500 ) ;
270+ } else if ( / ^ D i s p l a y e d \b / . test ( msg ) ) {
271+ lifecycle = { event : 'displayed' , pkg, detail : msg } ;
272+ } else if ( / \b K i l l i n g \b / . test ( msg ) ) {
273+ lifecycle = { event : 'killed' , pkg, detail : msg } ;
274+ this . refreshPidMap ( deviceId ) ;
275+ } else if ( / \b A N R i n \b / . test ( msg ) ) {
276+ lifecycle = { event : 'anr' , pkg, detail : msg } ;
277+ } else if ( / \b F o r c e s t o p p i n g \b / . test ( msg ) ) {
278+ lifecycle = { event : 'force-stopped' , pkg, detail : msg } ;
279+ } else if ( / \b P r o c e s s .* h a s d i e d \b / . test ( msg ) ) {
280+ lifecycle = { event : 'died' , pkg, detail : msg } ;
281+ this . refreshPidMap ( deviceId ) ;
282+ }
283+ break ;
284+ }
285+ }
286+ } else if ( tag === 'ActivityTaskManager' ) {
287+ for ( const pkg of trackedPkgs ) {
288+ if ( msg . includes ( pkg ) ) {
289+ if ( / \b m o v e d T o F r o n t \b | \b t o p R e s u m e d A c t i v i t y \b | \b R e s u m e \b .* \b A c t i v i t y \b / . test ( msg ) ) {
290+ lifecycle = { event : 'foreground' , pkg, detail : msg } ;
291+ } else if ( / \b m o v e T o B a c k \b | \b P a u s e \b .* \b A c t i v i t y \b / . test ( msg ) ) {
292+ lifecycle = { event : 'background' , pkg, detail : msg } ;
293+ }
294+ break ;
295+ }
296+ }
297+ } else if ( tag === 'AndroidRuntime' && / ^ F A T A L E X C E P T I O N / . test ( msg ) ) {
298+ const crashPkg = this . pidMap ?. [ batch [ i ] . pid ] ;
299+ if ( crashPkg && trackedPkgs . some ( p => crashPkg . includes ( p ) ) ) {
300+ lifecycle = { event : 'crashed' , pkg : crashPkg , detail : msg } ;
301+ }
302+ }
303+ // Also detect resume/pause from the app's own Activity logs
304+ if ( ! lifecycle && trackedPkgs . length === 1 ) {
305+ const appPkg = this . pidMap ?. [ batch [ i ] . pid ] ;
306+ if ( appPkg && trackedPkgs . some ( p => appPkg . includes ( p ) ) ) {
307+ if ( / \b o n R e s u m e \b / . test ( msg ) ) {
308+ lifecycle = { event : 'resumed' , pkg : appPkg , detail : `${ tag } : ${ msg } ` } ;
309+ } else if ( / \b o n P a u s e \b / . test ( msg ) ) {
310+ lifecycle = { event : 'paused' , pkg : appPkg , detail : `${ tag } : ${ msg } ` } ;
311+ } else if ( / \b o n S t o p \b / . test ( msg ) ) {
312+ lifecycle = { event : 'stopped' , pkg : appPkg , detail : `${ tag } : ${ msg } ` } ;
313+ }
314+ }
315+ }
316+ if ( lifecycle ) {
317+ this . emit ( 'adbevent' , { type : 'adb.lifecycle' , data : lifecycle } ) ;
318+ }
319+ }
130320 }
131321 } ) ;
132322
@@ -151,6 +341,9 @@ class ADBService extends EventEmitter {
151341 this . logcatProcess ?. kill ( ) ;
152342 this . logcatProcess = null ;
153343 clearInterval ( this . _pidInterval ) ;
344+ this . _appStateRunning = false ;
345+ clearTimeout ( this . _appStateTimeout ) ;
346+ this . _lastAppState = null ;
154347 }
155348
156349 async restart ( params ) {
@@ -159,8 +352,10 @@ class ADBService extends EventEmitter {
159352 await this . start ( params || this . lastParams ) ;
160353 }
161354
162- clear ( ) {
163- exec ( `adb logcat -c` ) ;
355+ clear ( deviceId ) {
356+ const device = deviceId || this . lastParams ?. deviceId ;
357+ if ( device ) exec ( `adb -s ${ device } logcat -c` ) ;
358+ else exec ( `adb logcat -c` ) ;
164359 }
165360}
166361
0 commit comments