@@ -24,7 +24,16 @@ let outputChannel: vscode.OutputChannel
24
24
export let client : LanguageClient
25
25
26
26
/** The sbt process that may have been started by this extension */
27
- let sbtProcess : ChildProcess
27
+ let sbtProcess : ChildProcess | undefined
28
+
29
+ /** The status bar where the show the status of sbt server */
30
+ let sbtStatusBar : vscode . StatusBarItem
31
+
32
+ /** Interval in ms to check that sbt is alive */
33
+ const sbtCheckIntervalMs = 10 * 1000
34
+
35
+ /** A command that we use to check that sbt is still alive. */
36
+ export const nopCommand = "nop"
28
37
29
38
const sbtVersion = "1.2.3"
30
39
const sbtArtifact = `org.scala-sbt:sbt-launch:${ sbtVersion } `
@@ -96,11 +105,56 @@ export function activate(context: ExtensionContext) {
96
105
}
97
106
98
107
configuredProject
99
- . then ( _ => withProgress ( "Configuring Dotty IDE..." , configureIDE ( coursierPath ) ) )
108
+ . then ( _ => connectToSbt ( coursierPath ) )
109
+ . then ( sbt => withProgress ( "Configuring Dotty IDE..." , configureIDE ( sbt ) ) )
100
110
. then ( _ => runLanguageServer ( coursierPath , languageServerArtifactFile ) )
101
111
}
102
112
}
103
113
114
+ /**
115
+ * Connect to sbt server (possibly by starting a new instance) and keep verifying that the
116
+ * connection is still alive. If it dies, restart sbt server.
117
+ */
118
+ function connectToSbt ( coursierPath : string ) : Thenable < rpc . MessageConnection > {
119
+ if ( ! sbtStatusBar ) sbtStatusBar = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Right )
120
+ sbtStatusBar . text = "sbt server: connecting $(sync)"
121
+ sbtStatusBar . show ( )
122
+
123
+ return offeringToRetry ( ( ) => {
124
+ return withSbtInstance ( outputChannel , coursierPath ) . then ( connection => {
125
+ markSbtUp ( )
126
+ const interval = setInterval ( ( ) => checkSbt ( interval , connection , coursierPath ) , sbtCheckIntervalMs )
127
+ return connection
128
+ } )
129
+ } , "Couldn't connect to sbt server (see log for details)" )
130
+ }
131
+
132
+ /** Mark sbt server as alive in the status bar */
133
+ function markSbtUp ( timeout ?: NodeJS . Timer ) {
134
+ sbtStatusBar . text = "sbt server: up $(check)"
135
+ if ( timeout ) clearTimeout ( timeout )
136
+ }
137
+
138
+ /** Mark sbt server as dead and try to reconnect */
139
+ function markSbtDownAndReconnect ( coursierPath : string ) {
140
+ sbtStatusBar . text = "sbt server: down $(x)"
141
+ if ( sbtProcess ) {
142
+ sbtProcess . kill ( )
143
+ sbtProcess = undefined
144
+ }
145
+ connectToSbt ( coursierPath )
146
+ }
147
+
148
+ /** Check that sbt is alive, try to reconnect if it is dead. */
149
+ function checkSbt ( interval : NodeJS . Timer , connection : rpc . MessageConnection , coursierPath : string ) {
150
+ sbtserver . tellSbt ( outputChannel , connection , nopCommand )
151
+ . then ( _ => markSbtUp ( ) ,
152
+ _ => {
153
+ clearInterval ( interval )
154
+ markSbtDownAndReconnect ( coursierPath )
155
+ } )
156
+ }
157
+
104
158
export function deactivate ( ) {
105
159
// If sbt was started by this extension, kill the process.
106
160
// FIXME: This will be a problem for other clients of this server.
@@ -123,33 +177,45 @@ function withProgress<T>(title: string, op: Thenable<T>): Thenable<T> {
123
177
}
124
178
125
179
/** Connect to an sbt server and run `configureIDE`. */
126
- function configureIDE ( coursierPath : string ) : Thenable < sbtserver . ExecResult > {
127
-
128
- function offeringToRetry ( client : rpc . MessageConnection , command : string ) : Thenable < sbtserver . ExecResult > {
129
- return sbtserver . tellSbt ( outputChannel , client , command )
130
- . then ( success => Promise . resolve ( success ) ,
131
- _ => {
132
- outputChannel . show ( )
133
- return vscode . window . showErrorMessage ( "IDE configuration failed (see logs for details)" , "Retry?" )
134
- . then ( retry => {
135
- if ( retry ) return offeringToRetry ( client , command )
136
- else return Promise . reject ( )
137
- } )
138
- } )
180
+ function configureIDE ( sbt : rpc . MessageConnection ) : Thenable < sbtserver . ExecResult > {
181
+
182
+ const tellSbt = ( command : string ) => {
183
+ return ( ) => sbtserver . tellSbt ( outputChannel , sbt , command )
139
184
}
140
185
141
- return withSbtInstance ( outputChannel , coursierPath )
142
- . then ( client => {
143
- // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
144
- // until sbt/sbt#4370 is fixed.
145
- // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
146
- // in case of failure), and we're pretty sure configureIDE will pass if they passed.
147
- return offeringToRetry ( client , "compile" ) . then ( _ => {
148
- return offeringToRetry ( client , "test:compile" ) . then ( _ => {
149
- return offeringToRetry ( client , "configureIDE" )
150
- } )
151
- } )
186
+ const failMessage = "`configureIDE` failed (see log for details)"
187
+
188
+ // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
189
+ // until sbt/sbt#4370 is fixed.
190
+ // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
191
+ // in case of failure), and we're pretty sure configureIDE will pass if they passed.
192
+ return offeringToRetry ( tellSbt ( "compile" ) , failMessage ) . then ( _ => {
193
+ return offeringToRetry ( tellSbt ( "test:compile" ) , failMessage ) . then ( _ => {
194
+ return offeringToRetry ( tellSbt ( "configureIDE" ) , failMessage )
152
195
} )
196
+ } )
197
+ }
198
+
199
+ /**
200
+ * Present the user with a dialog to retry `op` after a failure, returns its result in case of
201
+ * success.
202
+ *
203
+ * @param op The operation to perform
204
+ * @param failMessage The message to display in the dialog offering to retry `op`.
205
+ * @return A promise that will either resolve to the result of `op`, or a dialog that will let
206
+ * the user retry the operation.
207
+ */
208
+ function offeringToRetry < T > ( op : ( ) => Thenable < T > , failMessage : string ) : Thenable < T > {
209
+ return op ( )
210
+ . then ( success => Promise . resolve ( success ) ,
211
+ _ => {
212
+ outputChannel . show ( )
213
+ return vscode . window . showErrorMessage ( failMessage , "Retry?" )
214
+ . then ( retry => {
215
+ if ( retry ) return offeringToRetry ( op , failMessage )
216
+ else return Promise . reject ( )
217
+ } )
218
+ } )
153
219
}
154
220
155
221
function runLanguageServer ( coursierPath : string , languageServerArtifactFile : string ) {
@@ -169,31 +235,35 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
169
235
} )
170
236
}
171
237
238
+ function startNewSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) {
239
+ fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
240
+ sbtProcess = cpp . spawn ( "java" , [
241
+ "-Dsbt.log.noformat=true" ,
242
+ "-classpath" , sbtClasspath ,
243
+ "xsbt.boot.Boot"
244
+ ] ) . childProcess
245
+
246
+ // Close stdin, otherwise in case of error sbt will block waiting for the
247
+ // user input to reload or exit the build.
248
+ sbtProcess . stdin . end ( )
249
+
250
+ sbtProcess . stdout . on ( 'data' , data => {
251
+ log . append ( data . toString ( ) )
252
+ } )
253
+ sbtProcess . stderr . on ( 'data' , data => {
254
+ log . append ( data . toString ( ) )
255
+ } )
256
+ } )
257
+ }
258
+
172
259
/**
173
260
* Connects to an existing sbt server, or boots up one instance and connects to it.
174
261
*/
175
262
function withSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) : Thenable < rpc . MessageConnection > {
176
263
const serverSocketInfo = path . join ( workspaceRoot , "project" , "target" , "active.json" )
177
264
178
265
if ( ! fs . existsSync ( serverSocketInfo ) ) {
179
- fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
180
- sbtProcess = cpp . spawn ( "java" , [
181
- "-Dsbt.log.noformat=true" ,
182
- "-classpath" , sbtClasspath ,
183
- "xsbt.boot.Boot"
184
- ] ) . childProcess
185
-
186
- // Close stdin, otherwise in case of error sbt will block waiting for the
187
- // user input to reload or exit the build.
188
- sbtProcess . stdin . end ( )
189
-
190
- sbtProcess . stdout . on ( 'data' , data => {
191
- log . appendLine ( data . toString ( ) )
192
- } )
193
- sbtProcess . stderr . on ( 'data' , data => {
194
- log . appendLine ( data . toString ( ) )
195
- } )
196
- } )
266
+ startNewSbtInstance ( log , coursierPath )
197
267
}
198
268
199
269
return sbtserver . connectToSbtServer ( log )
0 commit comments