Skip to content

Commit 6c96109

Browse files
committed
Check sbt is alive and restart it if necessary
This commis adds a status bar that shows the status of sbt server, and a timer that periodically checks that sbt server is still alive, starting a new instance if necessary.
1 parent 4789db0 commit 6c96109

File tree

3 files changed

+141
-78
lines changed

3 files changed

+141
-78
lines changed

sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ object DottyIDEPlugin extends AutoPlugin {
269269
Command.process("runCode", state1)
270270
}
271271

272+
/** An sbt command that does nothing. We use it to check that sbt server is still alive. */
273+
def nopCommand = Command.command("nop")(state => state)
274+
272275
private def projectConfigTask(config: Configuration): Initialize[Task[Option[ProjectConfig]]] = Def.taskDyn {
273276
val depClasspath = Attributed.data((dependencyClasspath in config).value)
274277

@@ -311,7 +314,7 @@ object DottyIDEPlugin extends AutoPlugin {
311314
)
312315

313316
override def buildSettings: Seq[Setting[_]] = Seq(
314-
commands ++= Seq(configureIDE, compileForIDE, launchIDE),
317+
commands ++= Seq(configureIDE, compileForIDE, launchIDE, nopCommand),
315318

316319
excludeFromIDE := false,
317320

vscode-dotty/src/extension.ts

Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,16 @@ let outputChannel: vscode.OutputChannel
2424
export 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

2938
const sbtVersion = "1.2.3"
3039
const 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+
104158
export 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

155221
function 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
*/
175262
function 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)

vscode-dotty/src/sbt-server.ts

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import * as rpc from 'vscode-jsonrpc'
1818

1919
import * as vscode from 'vscode'
2020

21+
import { nopCommand } from './extension'
22+
2123
/** The result of successful `sbt/exec` call. */
2224
export interface ExecResult {
2325
status: string
@@ -43,45 +45,32 @@ class CommandLine {
4345
*
4446
* @return The result of executing `command`.
4547
*/
46-
export function tellSbt(log: vscode.OutputChannel,
47-
connection: rpc.MessageConnection,
48-
command: string): Thenable<ExecResult> {
48+
export async function tellSbt(log: vscode.OutputChannel,
49+
connection: rpc.MessageConnection,
50+
command: string): Promise<ExecResult> {
4951
log.appendLine(`>>> ${command}`)
50-
let req = new rpc.RequestType<CommandLine, ExecResult, any, any>("sbt/exec")
51-
return connection.sendRequest(req, new CommandLine(command))
52+
const req = new rpc.RequestType<CommandLine, ExecResult, any, any>("sbt/exec")
53+
return await connection.sendRequest(req, new CommandLine(command))
5254
}
5355

5456
/**
5557
* Attempts to connect to an sbt server running in this workspace.
5658
*
57-
* If connection fails, shows an error message and ask the user to retry.
58-
*
5959
* @param log Where to log messages between VSCode and sbt server.
6060
*/
6161
export function connectToSbtServer(log: vscode.OutputChannel): Promise<rpc.MessageConnection> {
6262
return waitForServer().then(socket => {
63-
if (socket) {
64-
let connection = rpc.createMessageConnection(
65-
new rpc.StreamMessageReader(socket),
66-
new rpc.StreamMessageWriter(socket))
67-
68-
connection.listen()
69-
70-
connection.onNotification("window/logMessage", (params) => {
71-
log.appendLine(`<<< [${messageTypeToString(params.type)}] ${params.message}`)
72-
})
73-
74-
return connection
75-
} else {
76-
return vscode.window.showErrorMessage("Couldn't connect to sbt server.", "Retry?").then(answer => {
77-
if (answer) {
78-
return connectToSbtServer(log)
79-
} else {
80-
log.show()
81-
return Promise.reject()
82-
}
83-
})
84-
}
63+
let connection = rpc.createMessageConnection(
64+
new rpc.StreamMessageReader(socket),
65+
new rpc.StreamMessageWriter(socket))
66+
67+
connection.listen()
68+
69+
connection.onNotification("window/logMessage", (params) => {
70+
log.appendLine(`<<< [${messageTypeToString(params.type)}] ${params.message}`)
71+
})
72+
73+
return tellSbt(log, connection, nopCommand).then(_ => connection)
8574
})
8675
}
8776

@@ -111,8 +100,8 @@ function delay(ms: number) {
111100
return new Promise(resolve => setTimeout(resolve, ms));
112101
}
113102

114-
async function waitForServer(): Promise<net.Socket | null> {
115-
let socket: net.Socket | null = null
103+
async function waitForServer(): Promise<net.Socket> {
104+
let socket: net.Socket
116105
return vscode.window.withProgress({
117106
location: vscode.ProgressLocation.Window,
118107
title: "Connecting to sbt server..."
@@ -125,8 +114,9 @@ async function waitForServer(): Promise<net.Socket | null> {
125114
await delay(1000);
126115
}
127116
}
128-
return socket
129-
}).then(_ => socket)
117+
if (socket) return Promise.resolve(socket)
118+
else return Promise.reject()
119+
})
130120
}
131121

132122
function messageTypeToString(messageType: number): string {

0 commit comments

Comments
 (0)