1- const { spawn, exec } = require ( 'child_process' ) ;
2- const { platform } = require ( 'os' ) ;
1+ const { spawn, exec, execSync } = require ( 'child_process' ) ;
2+ const { platform, homedir } = require ( 'os' ) ;
3+ const { existsSync, mkdirSync, createWriteStream, chmodSync } = require ( 'fs' ) ;
4+ const { join } = require ( 'path' ) ;
5+ const https = require ( 'https' ) ;
36const EventEmitter = require ( 'events' ) ;
7+ const vscode = require ( 'vscode' ) ;
8+
9+ function findAdb ( ) {
10+ // 1. User-configured path takes priority
11+ const configured = vscode . workspace . getConfiguration ( 'logcatLens' ) . get ( 'adbPath' ) ;
12+ if ( configured && existsSync ( configured ) ) return configured ;
13+
14+ // 2. ANDROID_HOME / ANDROID_SDK_ROOT env vars
15+ const home = homedir ( ) ;
16+ const envDirs = [ process . env . ANDROID_HOME , process . env . ANDROID_SDK_ROOT ] . filter ( Boolean ) ;
17+ for ( const dir of envDirs ) {
18+ const p = join ( dir , 'platform-tools' , platform ( ) === 'win32' ? 'adb.exe' : 'adb' ) ;
19+ if ( existsSync ( p ) ) return p ;
20+ }
21+
22+ // 3. Common SDK install locations per platform
23+ const candidates = platform ( ) === 'win32' ? [
24+ join ( home , 'AppData' , 'Local' , 'Android' , 'Sdk' , 'platform-tools' , 'adb.exe' ) ,
25+ 'C:\\Android\\sdk\\platform-tools\\adb.exe' ,
26+ ] : platform ( ) === 'darwin' ? [
27+ join ( home , 'Library' , 'Android' , 'sdk' , 'platform-tools' , 'adb' ) ,
28+ '/opt/homebrew/bin/adb' ,
29+ '/usr/local/bin/adb' ,
30+ ] : [
31+ join ( home , 'Android' , 'Sdk' , 'platform-tools' , 'adb' ) ,
32+ '/usr/local/bin/adb' ,
33+ '/usr/bin/adb' ,
34+ ] ;
35+
36+ for ( const p of candidates ) {
37+ if ( existsSync ( p ) ) return p ;
38+ }
39+
40+ // 4. Check if adb is on PATH (works when launched from terminal)
41+ try {
42+ const cmd = platform ( ) === 'win32' ? 'where adb' : 'which adb' ;
43+ const result = execSync ( cmd , { timeout : 5000 } ) . toString ( ) . trim ( ) . split ( '\n' ) [ 0 ] ;
44+ if ( result && existsSync ( result ) ) return result ;
45+ } catch { /* not on PATH */ }
46+
47+ // 5. Not found
48+ return null ;
49+ }
50+
51+ const PLATFORM_TOOLS_URLS = {
52+ darwin : 'https://dl.google.com/android/repository/platform-tools-latest-darwin.zip' ,
53+ linux : 'https://dl.google.com/android/repository/platform-tools-latest-linux.zip' ,
54+ win32 : 'https://dl.google.com/android/repository/platform-tools-latest-windows.zip' ,
55+ } ;
56+
57+ function isAdbAvailable ( ) {
58+ return findAdb ( ) !== null ;
59+ }
60+
61+ async function downloadAndInstallAdb ( ) {
62+ const url = PLATFORM_TOOLS_URLS [ platform ( ) ] ;
63+ if ( ! url ) {
64+ vscode . window . showErrorMessage ( 'Unsupported platform for automatic ADB install.' ) ;
65+ return false ;
66+ }
67+
68+ const home = homedir ( ) ;
69+ const installDir = platform ( ) === 'win32'
70+ ? join ( home , 'AppData' , 'Local' , 'Android' , 'Sdk' )
71+ : platform ( ) === 'darwin'
72+ ? join ( home , 'Library' , 'Android' , 'sdk' )
73+ : join ( home , 'Android' , 'Sdk' ) ;
74+ const zipPath = join ( installDir , 'platform-tools.zip' ) ;
75+ const adbBin = join ( installDir , 'platform-tools' , platform ( ) === 'win32' ? 'adb.exe' : 'adb' ) ;
76+
77+ mkdirSync ( installDir , { recursive : true } ) ;
78+
79+ return vscode . window . withProgress ( {
80+ location : vscode . ProgressLocation . Notification ,
81+ title : 'Logcat Lens: Installing ADB' ,
82+ cancellable : false ,
83+ } , async ( progress ) => {
84+ progress . report ( { message : 'Downloading platform-tools...' } ) ;
85+
86+ await new Promise ( ( resolve , reject ) => {
87+ const follow = ( url ) => {
88+ https . get ( url , ( res ) => {
89+ if ( res . statusCode >= 300 && res . statusCode < 400 && res . headers . location ) {
90+ return follow ( res . headers . location ) ;
91+ }
92+ if ( res . statusCode !== 200 ) {
93+ return reject ( new Error ( `Download failed (HTTP ${ res . statusCode } )` ) ) ;
94+ }
95+ const total = parseInt ( res . headers [ 'content-length' ] , 10 ) || 0 ;
96+ let downloaded = 0 ;
97+ const file = createWriteStream ( zipPath ) ;
98+ res . on ( 'data' , ( chunk ) => {
99+ downloaded += chunk . length ;
100+ if ( total ) progress . report ( { message : `Downloading... ${ Math . round ( downloaded / total * 100 ) } %` } ) ;
101+ } ) ;
102+ res . pipe ( file ) ;
103+ file . on ( 'finish' , ( ) => file . close ( resolve ) ) ;
104+ file . on ( 'error' , reject ) ;
105+ } ) . on ( 'error' , reject ) ;
106+ } ;
107+ follow ( url ) ;
108+ } ) ;
109+
110+ progress . report ( { message : 'Extracting...' } ) ;
111+ const unzipCmd = platform ( ) === 'win32'
112+ ? `powershell -command "Expand-Archive -Force '${ zipPath } ' '${ installDir } '"`
113+ : platform ( ) === 'darwin'
114+ ? `ditto -xk "${ zipPath } " "${ installDir } "`
115+ : `unzip -o "${ zipPath } " -d "${ installDir } "` ;
116+ try {
117+ await new Promise ( ( resolve , reject ) => {
118+ exec ( unzipCmd , ( err ) => err ? reject ( err ) : resolve ( ) ) ;
119+ } ) ;
120+ } catch ( extractErr ) {
121+ // Linux fallback: try python3 if unzip isn't installed
122+ if ( platform ( ) === 'linux' ) {
123+ await new Promise ( ( resolve , reject ) => {
124+ exec ( `python3 -c "import zipfile; zipfile.ZipFile('${ zipPath } ').extractall('${ installDir } ')"` ,
125+ ( err ) => err ? reject ( err ) : resolve ( ) ) ;
126+ } ) ;
127+ } else {
128+ throw extractErr ;
129+ }
130+ }
131+
132+ if ( platform ( ) !== 'win32' ) chmodSync ( adbBin , 0o755 ) ;
133+
134+ // Clean up zip
135+ try { require ( 'fs' ) . unlinkSync ( zipPath ) ; } catch { /* ignore */ }
136+
137+ // Auto-configure the setting and refresh cached path
138+ await vscode . workspace . getConfiguration ( 'logcatLens' ) . update ( 'adbPath' , adbBin , vscode . ConfigurationTarget . Global ) ;
139+ _adbPath = adbBin ;
140+ _adbWarningShown = false ;
141+
142+ vscode . window . showInformationMessage ( `ADB installed to ${ adbBin } ` ) ;
143+ return true ;
144+ } ) ;
145+ }
146+
147+ let _adbPath ;
148+ let _adbWarningShown = false ;
149+ function getAdb ( ) {
150+ if ( ! _adbPath ) _adbPath = findAdb ( ) ;
151+ if ( ! _adbPath && ! _adbWarningShown ) {
152+ _adbWarningShown = true ;
153+ vscode . window . showErrorMessage (
154+ 'ADB not found. Install it directly, download manually, or set the path.' ,
155+ 'Install ADB' , 'Download Page' , 'Set Path'
156+ ) . then ( choice => {
157+ if ( choice === 'Install ADB' ) {
158+ downloadAndInstallAdb ( ) ;
159+ } else if ( choice === 'Download Page' ) {
160+ vscode . env . openExternal ( vscode . Uri . parse ( 'https://developer.android.com/tools/releases/platform-tools' ) ) ;
161+ } else if ( choice === 'Set Path' ) {
162+ vscode . commands . executeCommand ( 'workbench.action.openSettings' , 'logcatLens.adbPath' ) ;
163+ }
164+ } ) ;
165+ }
166+ return _adbPath || 'adb' ;
167+ }
168+
169+ // Reset cached path when settings change
170+ vscode . workspace . onDidChangeConfiguration ( e => {
171+ if ( e . affectsConfiguration ( 'logcatLens.adbPath' ) ) _adbPath = null ;
172+ } ) ;
4173
5174class ADBService extends EventEmitter {
6175 logcatProcess ;
7176
177+ _exec ( cmd , opts , cb ) {
178+ if ( typeof opts === 'function' ) { cb = opts ; opts = { } ; }
179+ const adb = getAdb ( ) ;
180+ return exec ( cmd . replace ( / \b a d b \b / , `"${ adb } "` ) , opts , cb ) ;
181+ }
182+
183+ _spawn ( args ) {
184+ return spawn ( getAdb ( ) , args ) ;
185+ }
186+
8187 listDevices ( ) {
9188 return new Promise ( ( resolve , reject ) => {
10- exec ( 'adb devices -l' , ( error , stdout , stderr ) => {
189+ this . _exec ( 'adb devices -l' , ( error , stdout , stderr ) => {
11190 if ( error ) return reject ( error ) ;
12191
13192 const lines = stdout . split ( 'List of devices attached' ) . pop ( ) . trim ( )
@@ -31,7 +210,7 @@ class ADBService extends EventEmitter {
31210
32211 startDeviceTracking ( ) {
33212 if ( this . _trackProcess ) return ;
34- this . _trackProcess = spawn ( 'adb' , [ 'track-devices' ] ) ;
213+ this . _trackProcess = this . _spawn ( [ 'track-devices' ] ) ;
35214 this . _trackProcess . stdout . on ( 'data' , ( ) => {
36215 this . emit ( 'adbevent' , { type : 'adb.devices-changed' } ) ;
37216 } ) ;
@@ -52,7 +231,7 @@ class ADBService extends EventEmitter {
52231
53232 listPackages ( deviceId ) {
54233 return new Promise ( ( resolve , reject ) => {
55- exec ( `adb -s ${ deviceId } shell pm list packages -3` , ( error , stdout , stderr ) => {
234+ this . _exec ( `adb -s ${ deviceId } shell pm list packages -3` , ( error , stdout , stderr ) => {
56235 if ( error ) return reject ( error ) ;
57236 const packages = stdout . split ( '\n' )
58237 . map ( l => l . replace ( 'package:' , '' ) . trim ( ) )
@@ -65,7 +244,7 @@ class ADBService extends EventEmitter {
65244
66245 listTags ( deviceId ) {
67246 return new Promise ( ( resolve , reject ) => {
68- exec ( `adb -s ${ deviceId } logcat -d -v tag` , { maxBuffer : 10 * 1024 * 1024 } , ( error , stdout ) => {
247+ this . _exec ( `adb -s ${ deviceId } logcat -d -v tag` , { maxBuffer : 10 * 1024 * 1024 } , ( error , stdout ) => {
69248 if ( error ) return reject ( error ) ;
70249 const tags = new Set ( ) ;
71250 stdout . split ( '\n' ) . forEach ( line => {
@@ -80,7 +259,7 @@ class ADBService extends EventEmitter {
80259 getUID ( deviceId , packageName ) {
81260 return new Promise ( ( resolve , reject ) => {
82261 const filter = platform ( ) == 'win32' ? 'FINDSTR' : 'grep' ;
83- exec ( `adb -s ${ deviceId } shell dumpsys package ${ packageName } | ${ filter } uid` , ( error , stdout , stderr ) => {
262+ this . _exec ( `adb -s ${ deviceId } shell dumpsys package ${ packageName } | ${ filter } uid` , ( error , stdout , stderr ) => {
84263 if ( error ) return reject ( error ) ;
85264
86265 // sample: uid=10520 gids=[] type=0 prot=signature
@@ -93,13 +272,13 @@ class ADBService extends EventEmitter {
93272 getAppState ( deviceId , packageName ) {
94273 return new Promise ( ( resolve ) => {
95274 // Check if process is running
96- exec ( `adb -s ${ deviceId } shell pidof ${ packageName } ` , ( err1 , pidOut ) => {
275+ this . _exec ( `adb -s ${ deviceId } shell pidof ${ packageName } ` , ( err1 , pidOut ) => {
97276 const pid = pidOut ?. trim ( ) ;
98277 if ( ! pid ) {
99278 return resolve ( { packageName, state : 'not-running' , pid : null } ) ;
100279 }
101280 // Check if it's the foreground app
102- exec ( `adb -s ${ deviceId } shell "dumpsys activity activities | grep mResumedActivity"` , ( err2 , actOut ) => {
281+ this . _exec ( `adb -s ${ deviceId } shell "dumpsys activity activities | grep mResumedActivity"` , ( err2 , actOut ) => {
103282 const isForeground = actOut ?. includes ( packageName ) ;
104283 resolve ( {
105284 packageName,
@@ -113,7 +292,7 @@ class ADBService extends EventEmitter {
113292
114293 getPackageInfo ( deviceId , packageName ) {
115294 return new Promise ( ( resolve , reject ) => {
116- exec ( `adb -s ${ deviceId } shell dumpsys package ${ packageName } | grep -E "versionName|versionCode"` , ( error , stdout ) => {
295+ this . _exec ( `adb -s ${ deviceId } shell dumpsys package ${ packageName } | grep -E "versionName|versionCode"` , ( error , stdout ) => {
117296 if ( error ) return reject ( error ) ;
118297 const version = stdout . match ( / v e r s i o n N a m e = ( \S + ) / ) ?. [ 1 ] || 'unknown' ;
119298 const versionCode = stdout . match ( / v e r s i o n C o d e = ( \d + ) / ) ?. [ 1 ] || '' ;
@@ -124,7 +303,7 @@ class ADBService extends EventEmitter {
124303
125304 launchApp ( deviceId , packageName ) {
126305 return new Promise ( ( resolve , reject ) => {
127- exec ( `adb -s ${ deviceId } shell monkey -p ${ packageName } -c android.intent.category.LAUNCHER 1` , ( error ) => {
306+ this . _exec ( `adb -s ${ deviceId } shell monkey -p ${ packageName } -c android.intent.category.LAUNCHER 1` , ( error ) => {
128307 if ( error ) return reject ( error ) ;
129308 resolve ( ) ;
130309 } ) ;
@@ -133,7 +312,7 @@ class ADBService extends EventEmitter {
133312
134313 forceStopApp ( deviceId , packageName ) {
135314 return new Promise ( ( resolve , reject ) => {
136- exec ( `adb -s ${ deviceId } shell am force-stop ${ packageName } ` , ( error ) => {
315+ this . _exec ( `adb -s ${ deviceId } shell am force-stop ${ packageName } ` , ( error ) => {
137316 if ( error ) return reject ( error ) ;
138317 resolve ( ) ;
139318 } ) ;
@@ -142,15 +321,15 @@ class ADBService extends EventEmitter {
142321
143322 clearAppData ( deviceId , packageName ) {
144323 return new Promise ( ( resolve , reject ) => {
145- exec ( `adb -s ${ deviceId } shell pm clear ${ packageName } ` , ( error ) => {
324+ this . _exec ( `adb -s ${ deviceId } shell pm clear ${ packageName } ` , ( error ) => {
146325 if ( error ) return reject ( error ) ;
147326 resolve ( ) ;
148327 } ) ;
149328 } ) ;
150329 }
151330
152331 refreshPidMap ( deviceId ) {
153- exec ( `adb -s ${ deviceId } shell ps -A -o PID,NAME` , ( error , stdout ) => {
332+ this . _exec ( `adb -s ${ deviceId } shell ps -A -o PID,NAME` , ( error , stdout ) => {
154333 if ( error ) return ;
155334 this . pidMap = { } ;
156335 stdout . split ( '\n' ) . forEach ( line => {
@@ -206,7 +385,7 @@ class ADBService extends EventEmitter {
206385 // -T 1 = only new logs from now, avoids replaying old buffer on stop/start
207386 const args = [ '-s' , deviceId , 'logcat' , '-T' , '1' , '*:V' ] ;
208387
209- this . logcatProcess = spawn ( 'adb' , args ) ;
388+ this . logcatProcess = this . _spawn ( args ) ;
210389
211390 // Buffer partial lines across data chunks
212391 let lineBuffer = '' ;
@@ -354,9 +533,12 @@ class ADBService extends EventEmitter {
354533
355534 clear ( deviceId ) {
356535 const device = deviceId || this . lastParams ?. deviceId ;
357- if ( device ) exec ( `adb -s ${ device } logcat -c` ) ;
358- else exec ( `adb logcat -c` ) ;
536+ if ( device ) this . _exec ( `adb -s ${ device } logcat -c` ) ;
537+ else this . _exec ( `adb logcat -c` ) ;
359538 }
360539}
361540
362541module . exports = ADBService ;
542+ module . exports . isAdbAvailable = isAdbAvailable ;
543+ module . exports . downloadAndInstallAdb = downloadAndInstallAdb ;
544+ module . exports . resetAdbCache = ( ) => { _adbPath = null ; _adbWarningShown = false ; } ;
0 commit comments