@@ -7,8 +7,11 @@ import * as archiver from 'archiver';
7
7
import * as WebSocket from 'ws' ;
8
8
import * as request from 'request' ;
9
9
10
+ import { extensionName } from './extension' ;
10
11
import { TracingConsentCache } from './tracing-consent' ;
11
12
13
+ const consentCommandName = `${ extensionName } .adjust-consent` ;
14
+
12
15
export interface Ctx {
13
16
readonly extensionContext : vscode . ExtensionContext ,
14
17
readonly extensionConfig : vscode . WorkspaceConfiguration ,
@@ -24,13 +27,20 @@ export class Tracer {
24
27
25
28
private tracingConsent : TracingConsentCache
26
29
27
- private remoteTracingUrl ?: string
30
+ private readonly remoteTracingUrl : string | undefined
31
+ private readonly remoteWorkspaceDumpUrl : string | undefined
32
+ private get isTracingEnabled ( ) : boolean {
33
+ return Boolean ( this . remoteWorkspaceDumpUrl || this . remoteTracingUrl ) ;
34
+ }
28
35
29
36
constructor ( ctx : Ctx ) {
30
37
this . ctx = ctx ;
31
38
32
39
this . tracingConsent = new TracingConsentCache ( ctx . extensionContext . workspaceState ) ;
33
40
41
+ this . remoteWorkspaceDumpUrl = this . ctx . extensionConfig . get < string > ( 'remoteWorkspaceDumpUrl' ) ;
42
+ this . remoteTracingUrl = this . ctx . extensionConfig . get < string > ( 'remoteTracingUrl' ) ;
43
+
34
44
this . machineId = ( ( ) => {
35
45
const machineIdKey = 'tracing.machineId' ;
36
46
function persisted ( value : string ) : string {
@@ -60,35 +70,82 @@ export class Tracer {
60
70
this . sessionId = new Date ( ) . toISOString ( ) ;
61
71
}
62
72
63
- initializeAsyncWorkspaceDump ( ) {
64
- const remoteWorkspaceDumpUrl = this . ctx . extensionConfig . get < string > ( 'remoteWorkspaceDumpUrl' ) ;
65
- if ( remoteWorkspaceDumpUrl === undefined ) return ;
73
+ run ( ) : { lspOutputChannel ?: vscode . OutputChannel } {
74
+ const consentCommandDisposable = vscode . commands . registerCommand ( consentCommandName , ( ) => this . askForTracingConsent ( ) ) ;
75
+ if ( this . isTracingEnabled && this . tracingConsent . get ( ) === 'no-answer' ) this . askForTracingConsent ( ) ;
76
+ this . initializeAsyncWorkspaceDump ( ) ;
77
+ const lspOutputChannel = this . createLspOutputChannel ( ) ;
78
+ const statusBarItem = this . createStatusBarItem ( ) ;
79
+ for ( const disposable of [ consentCommandDisposable , lspOutputChannel , statusBarItem ] ) {
80
+ if ( disposable ) this . ctx . extensionContext . subscriptions . push ( disposable ) ;
81
+ }
82
+ return { lspOutputChannel } ;
83
+ }
84
+
85
+ private askForTracingConsent ( ) : void {
86
+ vscode . window . showInformationMessage (
87
+ 'Do you want to help EPFL develop Dotty LSP plugin by uploading your LSP communication? ' +
88
+ 'PLEASE BE AWARE that the data sent contains your entire codebase and ALL the IDE actions, ' +
89
+ 'including every single keystroke.' ,
90
+ 'yes' , 'no'
91
+ ) . then ( ( value : string | undefined ) => {
92
+ if ( value === 'yes' || value === 'no' ) this . tracingConsent . set ( value ) ;
93
+ } ) ;
94
+ }
66
95
67
- try {
68
- this . asyncUploadWorkspaceDump ( remoteWorkspaceDumpUrl ) ;
69
- } catch ( err ) {
70
- this . logError ( 'error during workspace dump' , safeError ( err ) ) ;
96
+ private initializeAsyncWorkspaceDump ( ) {
97
+ if ( this . remoteWorkspaceDumpUrl === undefined ) return ;
98
+ // convince TS that this is a string
99
+ const definedUrl : string = this . remoteWorkspaceDumpUrl ;
100
+
101
+ const doInitialize = ( ) => {
102
+ try {
103
+ this . asyncUploadWorkspaceDump ( definedUrl ) ;
104
+ } catch ( err ) {
105
+ this . logError ( 'error during workspace dump' , safeError ( err ) ) ;
106
+ }
107
+ } ;
108
+
109
+ if ( this . tracingConsent . get ( ) === 'yes' ) {
110
+ doInitialize ( )
111
+ } else {
112
+ let didInitialize = false ;
113
+ this . tracingConsent . subscribe ( ( ) => {
114
+ if ( didInitialize ) return ;
115
+ didInitialize = true ;
116
+ doInitialize ( ) ;
117
+ } )
71
118
}
72
119
}
73
120
74
- createLspOutputChannel ( ) : vscode . OutputChannel | undefined {
75
- const remoteTracingUrl = this . ctx . extensionConfig . get < string > ( 'remoteTracingUrl' ) ;
76
- if ( ! remoteTracingUrl ) return undefined ;
77
-
78
- if ( this . tracingConsent . get ( ) === 'no-answer' ) {
79
- vscode . window . showInformationMessage (
80
- 'Do you want to help EPFL develop this plugin by uploading your usage data? ' +
81
- 'PLEASE BE AWARE that this will upload all of your keystrokes and all of your code, ' +
82
- 'among other things.' ,
83
- 'yes' , 'no'
84
- ) . then ( ( value : string | undefined ) => {
85
- if ( value === 'yes' || value === 'no' ) this . tracingConsent . set ( value ) ;
86
- } ) ;
121
+ private createStatusBarItem ( ) : vscode . StatusBarItem | undefined {
122
+ if ( ! this . isTracingEnabled ) return undefined ;
123
+ const item = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 0 )
124
+ item . command = consentCommandName ;
125
+ const renderStatusBarItem = ( ) => {
126
+ item . text = ( ( ) => {
127
+ const desc = this . tracingConsent . get ( ) === 'yes' ? 'ON' : 'OFF' ;
128
+ return `$(radio-tower) Dotty trace: ${ desc } ` ;
129
+ } ) ( ) ;
130
+
131
+ item . tooltip = ( ( ) => {
132
+ const desc = this . tracingConsent . get ( ) === 'yes' ? 'consented' : 'not consented' ;
133
+ return `This workspace is configured for remote tracing of Dotty LSP and you have ${ desc } to it. ` +
134
+ 'Click to adjust your consent.' ;
135
+ } ) ( ) ;
87
136
}
137
+ renderStatusBarItem ( ) ;
138
+ this . tracingConsent . subscribe ( renderStatusBarItem ) ;
139
+ item . show ( ) ;
140
+ return item ;
141
+ }
142
+
143
+ private createLspOutputChannel ( ) : vscode . OutputChannel | undefined {
144
+ if ( ! this . remoteTracingUrl ) return undefined ;
88
145
89
146
const localLspOutputChannel = vscode . window . createOutputChannel ( 'Dotty LSP Communication' )
90
147
try {
91
- return this . createRemoteLspOutputChannel ( remoteTracingUrl , localLspOutputChannel ) ;
148
+ return this . createRemoteLspOutputChannel ( this . remoteTracingUrl , localLspOutputChannel ) ;
92
149
} catch ( err ) {
93
150
this . logError ( 'error during remote output channel creation' , safeError ( err ) ) ;
94
151
return localLspOutputChannel ;
@@ -97,6 +154,7 @@ export class Tracer {
97
154
98
155
private asyncUploadWorkspaceDump ( url : string ) {
99
156
const storagePath = this . ctx . extensionContext . storagePath ;
157
+ // TODO: handle multi-root workspaces
100
158
const rootPath = vscode . workspace . rootPath ;
101
159
if ( storagePath === undefined || rootPath === undefined ) {
102
160
this . logError ( 'Cannot start workspace dump b/c of workspace state:' , { storagePath, rootPath } ) ;
@@ -106,7 +164,7 @@ export class Tracer {
106
164
if ( ! fs . existsSync ( storagePath ) ) fs . mkdirSync ( storagePath ) ;
107
165
const outputPath = path . join ( storagePath , 'workspace-dump.zip' ) ;
108
166
if ( fs . existsSync ( outputPath ) ) fs . unlinkSync ( outputPath ) ;
109
- let output = fs . createWriteStream ( outputPath ) ;
167
+ const output = fs . createWriteStream ( outputPath ) ;
110
168
output . on ( 'end' , ( ) => {
111
169
this . ctx . extensionOut . appendLine ( 'zip - data has been drained' ) ;
112
170
} ) ;
@@ -138,57 +196,81 @@ export class Tracer {
138
196
) ;
139
197
} ) ;
140
198
zip . pipe ( output ) ;
141
- zip . glob ( './**/*.{scala,sbt}' , { cwd : rootPath } ) ;
199
+ zip . glob ( './**/*.{scala,sc,sbt,java}' , { cwd : rootPath } ) ;
200
+ zip . glob ( './**/.dotty-ide{.json,-artifact}' , { cwd : rootPath } ) ;
142
201
zip . finalize ( ) ;
143
202
}
144
203
145
204
private createRemoteLspOutputChannel (
146
205
remoteTracingUrl : string ,
147
206
localOutputChannel : vscode . OutputChannel
148
207
) : vscode . OutputChannel {
149
- const socketHeaders = {
150
- 'X-DLS-Project-ID' : this . projectId ,
151
- 'X-DLS-Client-ID' : this . machineId ,
152
- 'X-DLS-Session-ID' : this . sessionId ,
153
- } ;
208
+ const createSocket = ( ) => {
209
+ const socket = new WebSocket ( remoteTracingUrl , {
210
+ headers : {
211
+ 'X-DLS-Project-ID' : this . projectId ,
212
+ 'X-DLS-Client-ID' : this . machineId ,
213
+ 'X-DLS-Session-ID' : this . sessionId ,
214
+ } ,
215
+ } ) ;
216
+
217
+ const timer = setInterval (
218
+ ( ) => {
219
+ if ( socket . readyState === WebSocket . OPEN ) {
220
+ socket . send ( '' ) ;
221
+ } else if ( socket . readyState === WebSocket . CLOSED ) {
222
+ clearInterval ( timer ) ;
223
+ }
224
+ } ,
225
+ 10 * 1000 /*ms*/ ,
226
+ )
227
+
228
+ socket . onerror = ( event ) => {
229
+ this . logErrorWithoutNotifying (
230
+ 'socket error' ,
231
+ remoteTracingUrl ,
232
+ new SafeJsonifier ( event , ( event ) => ( {
233
+ error : safeError ( event . error ) ,
234
+ message : event . message ,
235
+ type : event . type
236
+ } ) )
237
+ ) ;
238
+ vscode . window . showWarningMessage ( 'An error occured in Dotty LSP remote tracing connection.' ) ;
239
+ }
154
240
155
- const socket = new WebSocket ( remoteTracingUrl , { headers : socketHeaders } ) ;
241
+ socket . onclose = ( event ) => {
242
+ this . logErrorWithoutNotifying (
243
+ 'socket closed' ,
244
+ remoteTracingUrl ,
245
+ new SafeJsonifier ( event , ( event ) => ( {
246
+ wasClean : event . wasClean ,
247
+ code : event . code ,
248
+ reason : event . reason
249
+ } ) )
250
+ ) ;
251
+ vscode . window . showWarningMessage ( 'Dotty LSP remote tracing connection was dropped.' ) ;
252
+ }
156
253
157
- const timer = setInterval (
158
- ( ) => {
159
- if ( socket . readyState === WebSocket . OPEN ) {
160
- socket . send ( '' ) ;
161
- } else if ( socket . readyState === WebSocket . CLOSED ) {
162
- clearInterval ( timer ) ;
254
+ return socket ;
255
+ } ;
256
+
257
+ let alreadyCreated = false ;
258
+ let socket : WebSocket ;
259
+ // note: creating socket lazily is important for correctness
260
+ // if the user did not initially give his consent on IDE start, but gives it afterwards
261
+ // we only want to start a connection and upload data *after* being given consent
262
+ const withSocket : ( thunk : ( socket : WebSocket ) => any ) => void = ( thunk ) => {
263
+ // only try to create the socket _once_ to avoid endlessly looping
264
+ if ( ! alreadyCreated ) {
265
+ alreadyCreated = true ;
266
+ try {
267
+ socket = createSocket ( ) ;
268
+ } catch ( err ) {
269
+ this . logError ( 'socket create error' , safeError ( err ) ) ;
163
270
}
164
- } ,
165
- 10 * 1000 /*ms*/ ,
166
- )
167
-
168
- socket . onerror = ( event ) => {
169
- this . logErrorWithoutNotifying (
170
- 'socket error' ,
171
- remoteTracingUrl ,
172
- new SafeJsonifier ( event , ( event ) => ( {
173
- error : safeError ( event . error ) ,
174
- message : event . message ,
175
- type : event . type
176
- } ) )
177
- ) ;
178
- vscode . window . showWarningMessage ( 'An error occured in Dotty LSP remote tracing connection.' ) ;
179
- }
271
+ }
180
272
181
- socket . onclose = ( event ) => {
182
- this . logErrorWithoutNotifying (
183
- 'socket closed' ,
184
- remoteTracingUrl ,
185
- new SafeJsonifier ( event , ( event ) => ( {
186
- wasClean : event . wasClean ,
187
- code : event . code ,
188
- reason : event . reason
189
- } ) )
190
- ) ;
191
- vscode . window . showWarningMessage ( 'Dotty LSP remote tracing connection was dropped.' ) ;
273
+ if ( socket ) thunk ( socket ) ;
192
274
}
193
275
194
276
let log : string = '' ;
@@ -210,21 +292,23 @@ export class Tracer {
210
292
211
293
log += value ;
212
294
log += '\n' ;
213
- if ( this . tracingConsent . get ( ) === 'yes' && socket . readyState === WebSocket . OPEN ) {
214
- socket . send ( log , ( err ) => {
215
- if ( err ) {
216
- this . logError ( 'socket send error' , err )
217
- }
218
- } ) ;
219
- log = '' ;
220
- }
295
+ if ( this . tracingConsent . get ( ) === 'yes' ) withSocket ( ( socket ) => {
296
+ if ( socket . readyState === WebSocket . OPEN ) {
297
+ socket . send ( log , ( err ) => {
298
+ if ( err ) {
299
+ this . logError ( 'socket send error' , err )
300
+ }
301
+ } ) ;
302
+ log = '' ;
303
+ }
304
+ } ) ;
221
305
} ,
222
306
223
307
clear ( ) { } ,
224
308
show ( ) { } ,
225
309
hide ( ) { } ,
226
310
dispose ( ) {
227
- socket . close ( ) ;
311
+ if ( socket ) socket . close ( ) ;
228
312
localOutputChannel . dispose ( ) ;
229
313
}
230
314
} ;
0 commit comments