11import * as vscode from 'vscode' ;
22import * as path from 'path' ;
33import * as fs from 'fs' ;
4- import { RTTConsoleDecoderOpts , TerminalInputMode } from '../common' ;
4+ import * as net from 'net' ;
5+ import { getAnyFreePort , RTTConsoleDecoderOpts , TerminalInputMode } from '../common' ;
56import { SWORTTSource } from './swo/sources/common' ;
67import EventEmitter = require( 'events' ) ;
8+ import { getNonce } from './swo/common' ;
9+
10+ export interface IRTTTerminalOptions
11+ {
12+ port : string ;
13+ prompt : string ;
14+ noprompt : boolean ;
15+ clear : boolean ;
16+ logfile : string ;
17+ inputmode : TerminalInputMode ;
18+ binary : boolean ;
19+ scale : number ;
20+ encoding : string ;
21+ nonce : string ;
22+ }
23+
24+ export class RTTTerminalOptions implements IRTTTerminalOptions {
25+ constructor ( options : RTTConsoleDecoderOpts ,
26+ public nonce ,
27+ public port = options . tcpPort ,
28+ public prompt = options . prompt || `RTT-${ options . port } > ` ,
29+ public noprompt = ! ! options . noprompt ,
30+ public clear = ! ! options . clear ,
31+ public logfile = options . logfile || '' ,
32+ public inputmode = options . inputmode ,
33+ public binary = options . type === 'binary' ,
34+ public scale = options . scale || 1 ,
35+ public encoding = options . encoding || ( options . type === 'binary' ? 'unsigned' : 'utf8' ) ,
36+ ) { }
37+ }
38+
739export class RTTTerminal extends EventEmitter implements SWORTTSource {
40+ protected termOptions : RTTTerminalOptions ;
41+ protected nonce = getNonce ( ) ;
842 connected : boolean ;
943 protected _rttTerminal : vscode . Terminal = null ;
1044 public get rttTerminal ( ) : vscode . Terminal {
@@ -26,8 +60,10 @@ export class RTTTerminal extends EventEmitter implements SWORTTSource {
2660
2761 constructor (
2862 protected context : vscode . ExtensionContext ,
29- public options : RTTConsoleDecoderOpts ) {
63+ public options : RTTConsoleDecoderOpts ,
64+ protected rttTermServer : TerminalServer ) {
3065 super ( ) ;
66+ this . termOptions = new RTTTerminalOptions ( options , this . nonce ) ;
3167 }
3268 dispose ( ) {
3369 // process.kill(this.rttTerminal.processId)
@@ -40,47 +76,26 @@ export class RTTTerminal extends EventEmitter implements SWORTTSource {
4076 }
4177
4278 public startTerminal ( ) : boolean {
79+ if ( this . connected ) {
80+ return true ;
81+ }
4382 const script = path . join ( this . context . extensionPath , 'dist' , 'tcp_cat.bundle.js' ) ;
44- this . _name = RTTTerminal . createTermName ( this . options ) ;
83+ this . _name = RTTTerminal . createTermName ( this . options , null ) ;
4584 const args = {
4685 name : this . name ,
4786 shellPath : 'node' ,
4887 shellArgs : [ script ,
49- "--port " , this . options . tcpPort , // Can be [host:]port
50- "--encoding " , this . options . encoding || 'utf8'
88+ "--useserver " , this . rttTermServer . portNumber ( ) . toString ( ) ,
89+ "--nonce " , this . termOptions . nonce
5190 ]
5291 } ;
5392
54- if ( this . options . noprompt ) {
55- args . shellArgs . push ( '--noprompt' ) ;
56- } else {
57- args . shellArgs . push ( '--prompt' , this . options . prompt || `RTT-${ this . options . port } > ` ) ;
58- }
59-
60- if ( this . options . clear ) {
61- args . shellArgs . push ( '--clear' ) ;
62- }
63- if ( this . options . inputmode === TerminalInputMode . RAW ) {
64- args . shellArgs . push ( '--raw' ) ;
65- } else if ( this . options . inputmode === TerminalInputMode . RAWECHO ) {
66- args . shellArgs . push ( '--rawecho' ) ;
67- }
68-
69- if ( this . options . type === 'binary' ) {
70- args . shellArgs . push ( '--binary' ) ;
71- if ( this . options . scale ) {
72- args . shellArgs . push ( '--scale' , `${ this . options . scale } ` ) ;
73- }
74- }
75-
7693 if ( this . options . logfile ) {
7794 try {
7895 fs . writeFileSync ( this . options . logfile , "" ) ;
79- args . shellArgs . push ( '--logfile' , this . options . logfile ) ;
8096 }
8197 catch ( e ) {
82- vscode . window . showErrorMessage (
83- `RTT logging failed ${ this . options . logfile } : ${ e . toString ( ) } ` ) ;
98+ vscode . window . showErrorMessage ( `RTT logging failed ${ this . options . logfile } : ${ e . toString ( ) } ` ) ;
8499 }
85100 }
86101
@@ -89,6 +104,8 @@ export class RTTTerminal extends EventEmitter implements SWORTTSource {
89104 setTimeout ( ( ) => {
90105 this . _rttTerminal . show ( ) ;
91106 } , 100 ) ;
107+ this . rttTermServer . addClient ( this . nonce ) ;
108+ this . sendOptions ( this . termOptions ) ;
92109 this . connected = true ;
93110 this . inUse = true ;
94111 return true ;
@@ -99,24 +116,144 @@ export class RTTTerminal extends EventEmitter implements SWORTTSource {
99116 }
100117 }
101118
102- static createTermName ( options : RTTConsoleDecoderOpts ) : string {
119+ static createTermName ( options : RTTConsoleDecoderOpts , existing : string | null ) : string {
103120 const channel = options . port || 0 ;
104121 const orig = options . label || `RTT Ch:${ channel } ` ;
105122 let ret = orig ;
106123 let count = 1 ;
107124 while ( vscode . window . terminals . findIndex ( ( t ) => t . name === ret ) >= 0 ) {
125+ if ( existing === ret ) {
126+ return existing ;
127+ }
108128 ret = `${ orig } -${ count } ` ;
109129 count = count + 1 ;
110130 }
111131 return ret ;
112132 }
113133
114- public canReuse ( options : RTTConsoleDecoderOpts ) {
115- for ( const prop of [ 'type' , 'tcpPort' , 'port' , 'label' , 'prompt' , 'noprompt' , 'logfile' , 'clear' , 'scale' ] ) {
116- if ( options [ prop ] !== this . options [ prop ] ) {
134+ // If all goes well, this will reset the terminal options. The original port (meaning channel) name
135+ // has to match and the label for the VSCode terminal has to match. If successful, tt will reset the
136+ // Terminal options and mark it as used (inUse = true) as well.
137+ public async tryReuse ( options : RTTConsoleDecoderOpts ) : Promise < boolean > {
138+ if ( this . options . port === options . port ) {
139+ // See if we are going to get the same label as before because that cannot be changed
140+ const newName = RTTTerminal . createTermName ( options , this . _rttTerminal . name ) ;
141+ if ( newName !== this . _rttTerminal . name ) {
117142 return false ;
118143 }
144+ const termOptions = new RTTTerminalOptions ( options , this . termOptions . nonce ) ;
145+ const ret = await this . sendOptions ( termOptions ) ;
146+ return ret ;
147+ }
148+ return false ;
149+ }
150+
151+ private sendOptions ( options : RTTTerminalOptions ) : boolean {
152+ const str = JSON . stringify ( options ) + '\n' ;
153+ if ( this . rttTermServer . sendToClient ( this . nonce , str ) ) {
154+ this . termOptions = options ;
155+ this . inUse = true ;
156+ return true ;
119157 }
120- return true ;
158+ return false ;
159+ }
160+ }
161+
162+ export class TerminalServer {
163+ protected server : net . Server = null ;
164+ protected port : number ;
165+ protected socketByNonce : Map < string , net . Socket > = new Map ( ) ;
166+ protected nonceBySocket : Map < net . Socket , string > = new Map ( ) ;
167+
168+ constructor ( ) {
169+ this . createServer ( ) ;
170+ }
171+
172+ public portNumber ( ) : number { return this . port ; }
173+ public isConnected ( ) : boolean { return ! ! this . server ; }
174+ private createServer ( ) {
175+ getAnyFreePort ( 55000 ) . then ( ( x ) => {
176+ this . port = x ;
177+ const newServer = net . createServer ( this . onNewClient . bind ( this ) ) ;
178+ newServer . listen ( this . port , 'localhost' , ( ) => {
179+ this . server = newServer ;
180+ } ) ;
181+ newServer . on ( ( 'error' ) , ( e ) => {
182+ console . log ( e ) ;
183+ } ) ;
184+ newServer . on ( 'close' , ( ) => { this . server = null ; } ) ;
185+ } ) . catch ( ( e ) => {
186+ } ) ;
187+ }
188+
189+ protected onNewClient ( socket : net . Socket ) {
190+ console . log ( 'New client connected' ) ;
191+ socket . setKeepAlive ( true ) ;
192+ socket . on ( 'close' , ( ) => {
193+ console . log ( 'client closed' ) ;
194+ const nonce = this . nonceBySocket . get ( socket ) ;
195+ if ( nonce ) {
196+ this . nonceBySocket . delete ( socket ) ;
197+ this . socketByNonce . delete ( nonce ) ;
198+ }
199+ } ) ;
200+ socket . on ( 'data' , ( data ) => {
201+ const str = data . toString ( ) . trim ( ) ;
202+ if ( this . socketByNonce . has ( str ) ) {
203+ // Client is alive and responded with proper nonce
204+ this . socketByNonce . set ( str , socket ) ;
205+ this . nonceBySocket . set ( socket , str ) ;
206+ } else {
207+ console . error ( `Unknown message '${ str } ' from client` ) ;
208+ }
209+ } ) ;
210+ socket . on ( 'error' , ( e ) => {
211+ console . error ( `client error ${ e } ` )
212+ } ) ;
213+ }
214+
215+ public dispose ( ) {
216+ if ( this . server ) {
217+ this . server . close ( ) ;
218+ this . server = null ;
219+ }
220+ }
221+
222+ public sendToClient ( nonce : string , data : string | Buffer ) : Promise < boolean > {
223+ return new Promise < boolean > ( ( resolve ) => {
224+ if ( ! this . socketByNonce . has ( nonce ) ) {
225+ resolve ( false ) ;
226+ return ;
227+ }
228+ let socket : net . Socket = this . socketByNonce [ nonce ] ;
229+ function send ( ) {
230+ try {
231+ socket . write ( data ) ;
232+ socket . uncork ( ) ;
233+ resolve ( true ) ;
234+ }
235+ catch ( e ) {
236+ resolve ( false ) ;
237+ }
238+ }
239+ if ( socket ) {
240+ send ( ) ;
241+ } else {
242+ // This can happen the very first time we are sending something. Technically, return
243+ // should be a promise
244+ const interval = setInterval ( ( ) => {
245+ socket = this . socketByNonce . get ( nonce ) ;
246+ if ( socket ) {
247+ clearInterval ( interval ) ;
248+ send ( ) ;
249+ }
250+ } , 1 ) ;
251+ }
252+ } ) ;
253+ }
254+
255+ public addClient ( nonce :string ) {
256+ console . log ( `adding client ${ nonce } ` ) ;
257+ this . socketByNonce . set ( nonce , null ) ;
121258 }
122259}
0 commit comments