Skip to content

Commit 7370074

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 ed6ec62 commit 7370074

File tree

3 files changed

+136
-73
lines changed

3 files changed

+136
-73
lines changed

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

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

228+
/** An sbt command that does nothing. We use it to check that sbt server is still alive. */
229+
def nopCommand = Command.command("nop")(state => state)
230+
228231
private def projectConfigTask(config: Configuration): Initialize[Task[Option[ProjectConfig]]] = Def.taskDyn {
229232
if ((sources in config).value.isEmpty) Def.task { None }
230233
else Def.task {
@@ -260,7 +263,7 @@ object DottyIDEPlugin extends AutoPlugin {
260263
)
261264

262265
override def buildSettings: Seq[Setting[_]] = Seq(
263-
commands ++= Seq(configureIDE, compileForIDE, launchIDE),
266+
commands ++= Seq(configureIDE, compileForIDE, launchIDE, nopCommand),
264267

265268
excludeFromIDE := false,
266269

vscode-dotty/src/extension.ts

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ let extensionContext: ExtensionContext
1919
let outputChannel: vscode.OutputChannel
2020

2121
/** The sbt process that may have been started by this extension */
22-
let sbtProcess: ChildProcess
22+
let sbtProcess: ChildProcess | undefined
23+
24+
/** The status bar where the show the status of sbt server */
25+
let sbtStatusBar: vscode.StatusBarItem
26+
27+
/** Interval in ms to check that sbt is alive */
28+
const sbtCheckIntervalMs = 10 * 1000
29+
30+
/** A command that we use to check that sbt is still alive. */
31+
export const nopCommand = "nop"
2332

2433
const sbtVersion = "1.2.3"
2534
const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}`
@@ -79,11 +88,56 @@ export function activate(context: ExtensionContext) {
7988
}
8089

8190
configuredProject
82-
.then(_ => withProgress("Configuring Dotty IDE...", configureIDE(coursierPath)))
91+
.then(_ => connectToSbt(coursierPath))
92+
.then(sbt => withProgress("Configuring Dotty IDE...", configureIDE(sbt)))
8393
.then(_ => runLanguageServer(coursierPath, languageServerArtifactFile))
8494
}
8595
}
8696

97+
/**
98+
* Connect to sbt server (possibly by starting a new instance) and keep verifying that the
99+
* connection is still alive. If it dies, restart sbt server.
100+
*/
101+
function connectToSbt(coursierPath: string): Thenable<rpc.MessageConnection> {
102+
if (!sbtStatusBar) sbtStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right)
103+
sbtStatusBar.text = "sbt server: connecting $(sync)"
104+
sbtStatusBar.show()
105+
106+
return offeringToRetry(() => {
107+
return withSbtInstance(outputChannel, coursierPath).then(connection => {
108+
markSbtUp()
109+
const interval = setInterval(() => checkSbt(interval, connection, coursierPath), sbtCheckIntervalMs)
110+
return connection
111+
})
112+
}, "Couldn't connect to sbt server (see log for details)")
113+
}
114+
115+
/** Mark sbt server as alive in the status bar */
116+
function markSbtUp(timeout?: NodeJS.Timer) {
117+
sbtStatusBar.text = "sbt server: up $(check)"
118+
if (timeout) clearTimeout(timeout)
119+
}
120+
121+
/** Mark sbt server as dead and try to reconnect */
122+
function markSbtDownAndReconnect(coursierPath: string) {
123+
sbtStatusBar.text = "sbt server: down $(x)"
124+
if (sbtProcess) {
125+
sbtProcess.kill()
126+
sbtProcess = undefined
127+
}
128+
connectToSbt(coursierPath)
129+
}
130+
131+
/** Check that sbt is alive, try to reconnect if it is dead. */
132+
function checkSbt(interval: NodeJS.Timer, connection: rpc.MessageConnection, coursierPath: string) {
133+
sbtserver.tellSbt(outputChannel, connection, nopCommand)
134+
.then(_ => markSbtUp(),
135+
_ => {
136+
clearInterval(interval)
137+
markSbtDownAndReconnect(coursierPath)
138+
})
139+
}
140+
87141
export function deactivate() {
88142
// If sbt was started by this extension, kill the process.
89143
// FIXME: This will be a problem for other clients of this server.
@@ -106,33 +160,45 @@ function withProgress<T>(title: string, op: Thenable<T>): Thenable<T> {
106160
}
107161

108162
/** Connect to an sbt server and run `configureIDE`. */
109-
function configureIDE(coursierPath: string): Thenable<sbtserver.ExecResult> {
110-
111-
function offeringToRetry(client: rpc.MessageConnection, command: string): Thenable<sbtserver.ExecResult> {
112-
return sbtserver.tellSbt(outputChannel, client, command)
113-
.then(success => Promise.resolve(success),
114-
_ => {
115-
outputChannel.show()
116-
return vscode.window.showErrorMessage("IDE configuration failed (see logs for details)", "Retry?")
117-
.then(retry => {
118-
if (retry) return offeringToRetry(client, command)
119-
else return Promise.reject()
120-
})
121-
})
163+
function configureIDE(sbt: rpc.MessageConnection): Thenable<sbtserver.ExecResult> {
164+
165+
const tellSbt = (command: string) => {
166+
return () => sbtserver.tellSbt(outputChannel, sbt, command)
122167
}
123168

124-
return withSbtInstance(outputChannel, coursierPath)
125-
.then(client => {
126-
// `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
127-
// until sbt/sbt#4370 is fixed.
128-
// We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
129-
// in case of failure), and we're pretty sure configureIDE will pass if they passed.
130-
return offeringToRetry(client, "compile").then(_ => {
131-
return offeringToRetry(client, "test:compile").then(_ => {
132-
return offeringToRetry(client, "configureIDE")
133-
})
134-
})
169+
const failMessage = "`configureIDE` failed (see log for details)"
170+
171+
// `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
172+
// until sbt/sbt#4370 is fixed.
173+
// We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
174+
// in case of failure), and we're pretty sure configureIDE will pass if they passed.
175+
return offeringToRetry(tellSbt("compile"), failMessage).then(_ => {
176+
return offeringToRetry(tellSbt("test:compile"), failMessage).then(_ => {
177+
return offeringToRetry(tellSbt("configureIDE"), failMessage)
135178
})
179+
})
180+
}
181+
182+
/**
183+
* Present the user with a dialog to retry `op` after a failure, returns its result in case of
184+
* success.
185+
*
186+
* @param op The operation to perform
187+
* @param failMessage The message to display in the dialog offering to retry `op`.
188+
* @return A promise that will either resolve to the result of `op`, or a dialog that will let
189+
* the user retry the operation.
190+
*/
191+
function offeringToRetry<T>(op: () => Thenable<T>, failMessage: string): Thenable<T> {
192+
return op()
193+
.then(success => Promise.resolve(success),
194+
_ => {
195+
outputChannel.show()
196+
return vscode.window.showErrorMessage(failMessage, "Retry?")
197+
.then(retry => {
198+
if (retry) return offeringToRetry(op, failMessage)
199+
else return Promise.reject()
200+
})
201+
})
136202
}
137203

138204
function runLanguageServer(coursierPath: string, languageServerArtifactFile: string) {
@@ -150,25 +216,29 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
150216
})
151217
}
152218

219+
function startNewSbtInstance(log: vscode.OutputChannel, coursierPath: string) {
220+
fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => {
221+
sbtProcess = cpp.spawn("java", [
222+
"-classpath", sbtClasspath,
223+
"xsbt.boot.Boot"
224+
]).childProcess
225+
sbtProcess.stdout.on('data', data => {
226+
log.append(data.toString())
227+
})
228+
sbtProcess.stderr.on('data', data => {
229+
log.append(data.toString())
230+
})
231+
})
232+
}
233+
153234
/**
154235
* Connects to an existing sbt server, or boots up one instance and connects to it.
155236
*/
156237
function withSbtInstance(log: vscode.OutputChannel, coursierPath: string): Thenable<rpc.MessageConnection> {
157238
const serverSocketInfo = path.join(workspaceRoot, "project", "target", "active.json")
158239

159240
if (!fs.existsSync(serverSocketInfo)) {
160-
fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => {
161-
sbtProcess = cpp.spawn("java", [
162-
"-classpath", sbtClasspath,
163-
"xsbt.boot.Boot"
164-
]).childProcess
165-
sbtProcess.stdout.on('data', data => {
166-
log.append(data.toString())
167-
})
168-
sbtProcess.stderr.on('data', data => {
169-
log.append(data.toString())
170-
})
171-
})
241+
startNewSbtInstance(log, coursierPath)
172242
}
173243

174244
return sbtserver.connectToSbtServer(log)
@@ -225,7 +295,7 @@ function run(serverOptions: ServerOptions) {
225295
revealOutputChannelOn: RevealOutputChannelOn.Never
226296
}
227297

228-
outputChannel.dispose()
298+
// outputChannel.dispose()
229299

230300
const client = new LanguageClient('dotty', 'Dotty Language Server', serverOptions, clientOptions);
231301

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)