Skip to content

Commit 26cf0f6

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 c7a0024 commit 26cf0f6

File tree

3 files changed

+135
-72
lines changed

3 files changed

+135
-72
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
if ((sources in config).value.isEmpty) Def.task { None }
274277
else Def.task {
@@ -304,7 +307,7 @@ object DottyIDEPlugin extends AutoPlugin {
304307
)
305308

306309
override def buildSettings: Seq[Setting[_]] = Seq(
307-
commands ++= Seq(configureIDE, compileForIDE, launchIDE),
310+
commands ++= Seq(configureIDE, compileForIDE, launchIDE, nopCommand),
308311

309312
excludeFromIDE := false,
310313

vscode-dotty/src/extension.ts

Lines changed: 108 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ let extensionContext: ExtensionContext
2222
let 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

2736
const sbtVersion = "1.2.3"
2837
const 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+
90144
export 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

141207
function 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
*/
161242
function 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)

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)