@@ -22,7 +22,16 @@ let extensionContext: ExtensionContext
2222let outputChannel : vscode . OutputChannel
2323
2424/** The sbt process that may have been started by this extension */
25- let sbtProcess : ChildProcess
25+ let sbtProcess : ChildProcess | undefined
26+
27+ /** The status bar where the show the status of sbt server */
28+ let sbtStatusBar : vscode . StatusBarItem
29+
30+ /** Interval in ms to check that sbt is alive */
31+ const sbtCheckIntervalMs = 10 * 1000
32+
33+ /** A command that we use to check that sbt is still alive. */
34+ export const nopCommand = "nop"
2635
2736const sbtVersion = "1.2.3"
2837const sbtArtifact = `org.scala-sbt:sbt-launch:${ sbtVersion } `
@@ -82,11 +91,56 @@ export function activate(context: ExtensionContext) {
8291 }
8392
8493 configuredProject
85- . then ( _ => withProgress ( "Configuring Dotty IDE..." , configureIDE ( coursierPath ) ) )
94+ . then ( _ => connectToSbt ( coursierPath ) )
95+ . then ( sbt => withProgress ( "Configuring Dotty IDE..." , configureIDE ( sbt ) ) )
8696 . then ( _ => runLanguageServer ( coursierPath , languageServerArtifactFile ) )
8797 }
8898}
8999
100+ /**
101+ * Connect to sbt server (possibly by starting a new instance) and keep verifying that the
102+ * connection is still alive. If it dies, restart sbt server.
103+ */
104+ function connectToSbt ( coursierPath : string ) : Thenable < rpc . MessageConnection > {
105+ if ( ! sbtStatusBar ) sbtStatusBar = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Right )
106+ sbtStatusBar . text = "sbt server: connecting $(sync)"
107+ sbtStatusBar . show ( )
108+
109+ return offeringToRetry ( ( ) => {
110+ return withSbtInstance ( outputChannel , coursierPath ) . then ( connection => {
111+ markSbtUp ( )
112+ const interval = setInterval ( ( ) => checkSbt ( interval , connection , coursierPath ) , sbtCheckIntervalMs )
113+ return connection
114+ } )
115+ } , "Couldn't connect to sbt server (see log for details)" )
116+ }
117+
118+ /** Mark sbt server as alive in the status bar */
119+ function markSbtUp ( timeout ?: NodeJS . Timer ) {
120+ sbtStatusBar . text = "sbt server: up $(check)"
121+ if ( timeout ) clearTimeout ( timeout )
122+ }
123+
124+ /** Mark sbt server as dead and try to reconnect */
125+ function markSbtDownAndReconnect ( coursierPath : string ) {
126+ sbtStatusBar . text = "sbt server: down $(x)"
127+ if ( sbtProcess ) {
128+ sbtProcess . kill ( )
129+ sbtProcess = undefined
130+ }
131+ connectToSbt ( coursierPath )
132+ }
133+
134+ /** Check that sbt is alive, try to reconnect if it is dead. */
135+ function checkSbt ( interval : NodeJS . Timer , connection : rpc . MessageConnection , coursierPath : string ) {
136+ sbtserver . tellSbt ( outputChannel , connection , nopCommand )
137+ . then ( _ => markSbtUp ( ) ,
138+ _ => {
139+ clearInterval ( interval )
140+ markSbtDownAndReconnect ( coursierPath )
141+ } )
142+ }
143+
90144export function deactivate ( ) {
91145 // If sbt was started by this extension, kill the process.
92146 // FIXME: This will be a problem for other clients of this server.
@@ -109,33 +163,45 @@ function withProgress<T>(title: string, op: Thenable<T>): Thenable<T> {
109163}
110164
111165/** Connect to an sbt server and run `configureIDE`. */
112- function configureIDE ( coursierPath : string ) : Thenable < sbtserver . ExecResult > {
113-
114- function offeringToRetry ( client : rpc . MessageConnection , command : string ) : Thenable < sbtserver . ExecResult > {
115- return sbtserver . tellSbt ( outputChannel , client , command )
116- . then ( success => Promise . resolve ( success ) ,
117- _ => {
118- outputChannel . show ( )
119- return vscode . window . showErrorMessage ( "IDE configuration failed (see logs for details)" , "Retry?" )
120- . then ( retry => {
121- if ( retry ) return offeringToRetry ( client , command )
122- else return Promise . reject ( )
123- } )
124- } )
166+ function configureIDE ( sbt : rpc . MessageConnection ) : Thenable < sbtserver . ExecResult > {
167+
168+ const tellSbt = ( command : string ) => {
169+ return ( ) => sbtserver . tellSbt ( outputChannel , sbt , command )
125170 }
126171
127- return withSbtInstance ( outputChannel , coursierPath )
128- . then ( client => {
129- // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
130- // until sbt/sbt#4370 is fixed.
131- // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
132- // in case of failure), and we're pretty sure configureIDE will pass if they passed.
133- return offeringToRetry ( client , "compile" ) . then ( _ => {
134- return offeringToRetry ( client , "test:compile" ) . then ( _ => {
135- return offeringToRetry ( client , "configureIDE" )
136- } )
137- } )
172+ const failMessage = "`configureIDE` failed (see log for details)"
173+
174+ // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
175+ // until sbt/sbt#4370 is fixed.
176+ // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
177+ // in case of failure), and we're pretty sure configureIDE will pass if they passed.
178+ return offeringToRetry ( tellSbt ( "compile" ) , failMessage ) . then ( _ => {
179+ return offeringToRetry ( tellSbt ( "test:compile" ) , failMessage ) . then ( _ => {
180+ return offeringToRetry ( tellSbt ( "configureIDE" ) , failMessage )
138181 } )
182+ } )
183+ }
184+
185+ /**
186+ * Present the user with a dialog to retry `op` after a failure, returns its result in case of
187+ * success.
188+ *
189+ * @param op The operation to perform
190+ * @param failMessage The message to display in the dialog offering to retry `op`.
191+ * @return A promise that will either resolve to the result of `op`, or a dialog that will let
192+ * the user retry the operation.
193+ */
194+ function offeringToRetry < T > ( op : ( ) => Thenable < T > , failMessage : string ) : Thenable < T > {
195+ return op ( )
196+ . then ( success => Promise . resolve ( success ) ,
197+ _ => {
198+ outputChannel . show ( )
199+ return vscode . window . showErrorMessage ( failMessage , "Retry?" )
200+ . then ( retry => {
201+ if ( retry ) return offeringToRetry ( op , failMessage )
202+ else return Promise . reject ( )
203+ } )
204+ } )
139205}
140206
141207function runLanguageServer ( coursierPath : string , languageServerArtifactFile : string ) {
@@ -155,25 +221,29 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
155221 } )
156222}
157223
224+ function startNewSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) {
225+ fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
226+ sbtProcess = cpp . spawn ( "java" , [
227+ "-classpath" , sbtClasspath ,
228+ "xsbt.boot.Boot"
229+ ] ) . childProcess
230+ sbtProcess . stdout . on ( 'data' , data => {
231+ log . append ( data . toString ( ) )
232+ } )
233+ sbtProcess . stderr . on ( 'data' , data => {
234+ log . append ( data . toString ( ) )
235+ } )
236+ } )
237+ }
238+
158239/**
159240 * Connects to an existing sbt server, or boots up one instance and connects to it.
160241 */
161242function withSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) : Thenable < rpc . MessageConnection > {
162243 const serverSocketInfo = path . join ( workspaceRoot , "project" , "target" , "active.json" )
163244
164245 if ( ! fs . existsSync ( serverSocketInfo ) ) {
165- fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
166- sbtProcess = cpp . spawn ( "java" , [
167- "-classpath" , sbtClasspath ,
168- "xsbt.boot.Boot"
169- ] ) . childProcess
170- sbtProcess . stdout . on ( 'data' , data => {
171- log . append ( data . toString ( ) )
172- } )
173- sbtProcess . stderr . on ( 'data' , data => {
174- log . append ( data . toString ( ) )
175- } )
176- } )
246+ startNewSbtInstance ( log , coursierPath )
177247 }
178248
179249 return sbtserver . connectToSbtServer ( log )
0 commit comments