1+ import * as _ from 'lodash' ;
2+ import { spawn } from 'child_process' ;
3+ import * as util from 'util' ;
4+ import * as fs from 'fs' ;
5+
6+ import { getPortPromise as getPort } from 'portfinder' ;
7+ import { generateSPKIFingerprint } from 'mockttp' ;
8+ import ChromeRemoteInterface = require( 'chrome-remote-interface' ) ;
9+
10+ import { Interceptor } from '.' ;
11+
12+ import { HtkConfig } from '../config' ;
13+ import { delay } from '../util' ;
14+ import { getTerminalEnvVars } from './terminal/terminal-env-overrides' ;
15+
16+ const readFile = util . promisify ( fs . readFile ) ;
17+
18+ export class ElectronInterceptor implements Interceptor {
19+ readonly id = 'electron' ;
20+ readonly version = '1.0.0' ;
21+
22+ private debugClients : {
23+ [ port : string ] : Array < ChromeRemoteInterface . CdpClient >
24+ } = { } ;
25+
26+ constructor ( private config : HtkConfig ) { }
27+
28+ private certData = readFile ( this . config . https . certPath , 'utf8' )
29+
30+ async isActivable ( ) : Promise < boolean > {
31+ return true ;
32+ }
33+
34+ isActive ( proxyPort : number | string ) {
35+ return ! ! this . debugClients [ proxyPort ] &&
36+ ! ! this . debugClients [ proxyPort ] . length ;
37+ }
38+
39+ async activate ( proxyPort : number , options : {
40+ pathToApplication : string
41+ } ) : Promise < void | { } > {
42+ const debugPort = await getPort ( { port : proxyPort } ) ;
43+
44+ spawn ( options . pathToApplication , [ `--inspect-brk=${ debugPort } ` ] , {
45+ stdio : 'inherit' ,
46+ env : Object . assign ( { } ,
47+ process . env ,
48+ getTerminalEnvVars ( proxyPort , this . config . https , process . env )
49+ )
50+ } ) ;
51+
52+ let debugClient : ChromeRemoteInterface . CdpClient | undefined ;
53+ let retries = 10 ;
54+ while ( ! debugClient && retries >= 0 ) {
55+ try {
56+ debugClient = await ChromeRemoteInterface ( { port : debugPort } ) ;
57+ } catch ( error ) {
58+ if ( error . code !== 'ECONNREFUSED' || retries === 0 ) {
59+ throw error ;
60+ }
61+
62+ retries = retries - 1 ;
63+ await delay ( 500 ) ;
64+ }
65+ }
66+ if ( ! debugClient ) throw new Error ( 'Could not initialize CDP client' ) ;
67+
68+ this . debugClients [ proxyPort ] = this . debugClients [ proxyPort ] || [ ] ;
69+ this . debugClients [ proxyPort ] . push ( debugClient ) ;
70+
71+ const callFramePromise = new Promise < string > ( ( resolve ) => {
72+ debugClient ! . Debugger . paused ( ( stack ) => {
73+ resolve ( stack . callFrames [ 0 ] . callFrameId ) ;
74+ } ) ;
75+ } ) ;
76+
77+ debugClient . Runtime . runIfWaitingForDebugger ( ) ;
78+ await debugClient . Runtime . enable ( ) ;
79+ await debugClient . Debugger . enable ( ) ;
80+
81+ const callFrameId = await callFramePromise ;
82+
83+ // Patch in our various module overrides:
84+ await debugClient . Debugger . evaluateOnCallFrame ( {
85+ expression : `require("${
86+ // Inside the Electron process, load our electron-intercepting JS
87+ require . resolve ( '../../overrides/js/prepend-electron.js' )
88+ } ")({
89+ newlineEncodedCertData: "${ ( await this . certData ) . replace ( / \r \n | \r | \n / g, '\\n' ) } ",
90+ spkiFingerprint: "${ generateSPKIFingerprint ( await this . certData ) } "
91+ })` ,
92+ callFrameId
93+ } ) ;
94+
95+ debugClient . Debugger . resume ( ) ;
96+ debugClient . once ( 'disconnect' , ( ) => {
97+ _ . remove ( this . debugClients [ proxyPort ] , c => c === debugClient ) ;
98+ } ) ;
99+ }
100+
101+ async deactivate ( proxyPort : number | string ) : Promise < void > {
102+ if ( ! this . isActive ( proxyPort ) ) return ;
103+
104+ await Promise . all (
105+ this . debugClients [ proxyPort ] . map ( async ( debugClient ) => {
106+ // Politely signal self to shutdown cleanly
107+ await debugClient . Runtime . evaluate ( {
108+ expression : 'process.kill(process.pid, "SIGTERM")'
109+ } ) ;
110+
111+ // Wait up to 1s for a clean shutdown & disconnect
112+ const cleanShutdown = await Promise . race ( [
113+ new Promise ( ( resolve ) =>
114+ debugClient . once ( 'disconnect' , ( ) => resolve ( true ) )
115+ ) ,
116+ delay ( 1000 ) . then ( ( ) => false )
117+ ] ) ;
118+
119+ if ( ! cleanShutdown ) {
120+ // Didn't shutdown? Inject a hard exit.
121+ await debugClient . Runtime . evaluate ( {
122+ expression : 'process.exit(0)'
123+ } ) . catch ( ( ) => { } ) // Ignore errors (there's an inherent race here)
124+ } ;
125+ } )
126+ ) ;
127+ }
128+
129+ async deactivateAll ( ) : Promise < void > {
130+ await Promise . all < void > (
131+ Object . keys ( this . debugClients ) . map ( port => this . deactivate ( port ) )
132+ ) ;
133+ }
134+
135+ }
0 commit comments