@@ -24,7 +24,16 @@ let outputChannel: vscode.OutputChannel
2424export let client : LanguageClient
2525
2626/** 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"
2837
2938const sbtVersion = "1.2.3"
3039const sbtArtifact = `org.scala-sbt:sbt-launch:${ sbtVersion } `
@@ -96,11 +105,56 @@ export function activate(context: ExtensionContext) {
96105 }
97106
98107 configuredProject
99- . then ( _ => withProgress ( "Configuring Dotty IDE..." , configureIDE ( coursierPath ) ) )
108+ . then ( _ => connectToSbt ( coursierPath ) )
109+ . then ( sbt => withProgress ( "Configuring Dotty IDE..." , configureIDE ( sbt ) ) )
100110 . then ( _ => runLanguageServer ( coursierPath , languageServerArtifactFile ) )
101111 }
102112}
103113
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+
104158export function deactivate ( ) {
105159 // If sbt was started by this extension, kill the process.
106160 // 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> {
123177}
124178
125179/** 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 )
139184 }
140185
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 )
152195 } )
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+ } )
153219}
154220
155221function runLanguageServer ( coursierPath : string , languageServerArtifactFile : string ) {
@@ -169,31 +235,35 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
169235 } )
170236}
171237
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+
172259/**
173260 * Connects to an existing sbt server, or boots up one instance and connects to it.
174261 */
175262function withSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) : Thenable < rpc . MessageConnection > {
176263 const serverSocketInfo = path . join ( workspaceRoot , "project" , "target" , "active.json" )
177264
178265 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 )
197267 }
198268
199269 return sbtserver . connectToSbtServer ( log )
0 commit comments