Skip to content

Commit 4789db0

Browse files
committed
Run configureIDE with sbt server
1 parent 5c34a3a commit 4789db0

File tree

3 files changed

+243
-61
lines changed

3 files changed

+243
-61
lines changed

vscode-dotty/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"child-process-promise": "^2.2.1",
7575
"compare-versions": "^3.4.0",
7676
"vscode-languageclient": "^5.1.0",
77-
"vscode-languageserver": "^5.1.0"
77+
"vscode-languageserver": "^5.1.0",
78+
"vscode-jsonrpc": "4.0.0"
7879
},
7980
"devDependencies": {
8081
"@types/compare-versions": "^3.0.0",

vscode-dotty/src/extension.ts

Lines changed: 102 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as path from 'path';
66
import * as cpp from 'child-process-promise';
77
import * as compareVersions from 'compare-versions';
88

9+
import { ChildProcess } from "child_process";
10+
911
import { ExtensionContext } from 'vscode';
1012
import * as vscode from 'vscode';
1113
import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn,
@@ -14,10 +16,16 @@ import { enableOldServerWorkaround } from './compat'
1416

1517
import * as worksheet from './worksheet'
1618

19+
import * as rpc from 'vscode-jsonrpc'
20+
import * as sbtserver from './sbt-server'
21+
1722
let extensionContext: ExtensionContext
1823
let outputChannel: vscode.OutputChannel
1924
export let client: LanguageClient
2025

26+
/** The sbt process that may have been started by this extension */
27+
let sbtProcess: ChildProcess
28+
2129
const sbtVersion = "1.2.3"
2230
const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}`
2331
const workspaceRoot = `${vscode.workspace.rootPath}`
@@ -71,27 +79,79 @@ export function activate(context: ExtensionContext) {
7179
})
7280

7381
} else {
74-
// Check whether `.dotty-ide-artifact` exists. If it does, start the language server,
75-
// otherwise, try propose to start it if there's no build.sbt
76-
if (fs.existsSync(languageServerArtifactFile)) {
77-
runLanguageServer(coursierPath, languageServerArtifactFile)
78-
} else if (isUnconfiguredProject()) {
79-
vscode.window.showInformationMessage(
80-
"This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?",
81-
"Yes", "No"
82+
let configuredProject: Thenable<void> = Promise.resolve()
83+
if (isUnconfiguredProject()) {
84+
configuredProject = vscode.window.showInformationMessage(
85+
"This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?",
86+
"Yes", "No"
8287
).then(choice => {
83-
if (choice == "Yes") {
84-
fetchAndConfigure(coursierPath, sbtArtifact, buildSbtFileSource, dottyPluginSbtFileSource).then(() => {
85-
runLanguageServer(coursierPath, languageServerArtifactFile)
86-
})
87-
} else {
88+
if (choice === "Yes") {
89+
bootstrapSbtProject(buildSbtFileSource, dottyPluginSbtFileSource)
90+
return Promise.resolve()
91+
} else if (choice === "No") {
8892
fs.appendFile(disableDottyIDEFile, "", _ => {})
93+
return Promise.reject()
8994
}
9095
})
9196
}
97+
98+
configuredProject
99+
.then(_ => withProgress("Configuring Dotty IDE...", configureIDE(coursierPath)))
100+
.then(_ => runLanguageServer(coursierPath, languageServerArtifactFile))
92101
}
93102
}
94103

104+
export function deactivate() {
105+
// If sbt was started by this extension, kill the process.
106+
// FIXME: This will be a problem for other clients of this server.
107+
if (sbtProcess) {
108+
sbtProcess.kill()
109+
}
110+
}
111+
112+
/**
113+
* Display a progress bar with title `title` while `op` completes.
114+
*
115+
* @param title The title of the progress bar
116+
* @param op The thenable that is monitored by the progress bar.
117+
*/
118+
function withProgress<T>(title: string, op: Thenable<T>): Thenable<T> {
119+
return vscode.window.withProgress({
120+
location: vscode.ProgressLocation.Window,
121+
title: title
122+
}, _ => op)
123+
}
124+
125+
/** 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+
})
139+
}
140+
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+
})
152+
})
153+
}
154+
95155
function runLanguageServer(coursierPath: string, languageServerArtifactFile: string) {
96156
fs.readFile(languageServerArtifactFile, (err, data) => {
97157
if (err) throw err
@@ -109,10 +169,34 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
109169
})
110170
}
111171

112-
function fetchAndConfigure(coursierPath: string, sbtArtifact: string, buildSbtFileSource: string, dottyPluginSbtFileSource: string) {
113-
return fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => {
114-
return configureIDE(sbtClasspath, buildSbtFileSource, dottyPluginSbtFileSource)
172+
/**
173+
* Connects to an existing sbt server, or boots up one instance and connects to it.
174+
*/
175+
function withSbtInstance(log: vscode.OutputChannel, coursierPath: string): Thenable<rpc.MessageConnection> {
176+
const serverSocketInfo = path.join(workspaceRoot, "project", "target", "active.json")
177+
178+
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+
})
115196
})
197+
}
198+
199+
return sbtserver.connectToSbtServer(log)
116200
}
117201

118202
function fetchWithCoursier(coursierPath: string, artifact: string, extra: string[] = []) {
@@ -150,54 +234,12 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string
150234
})
151235
}
152236

153-
function configureIDE(sbtClasspath: string,
154-
buildSbtFileSource: string,
155-
dottyPluginSbtFileSource: string) {
156-
157-
return vscode.window.withProgress({
158-
location: vscode.ProgressLocation.Window,
159-
title: 'Configuring the IDE for Dotty...'
160-
}, _ => {
161-
162-
// Bootstrap an sbt build
237+
function bootstrapSbtProject(buildSbtFileSource: string,
238+
dottyPluginSbtFileSource: string) {
163239
fs.mkdirSync(sbtProjectDir)
164240
fs.appendFileSync(sbtBuildPropertiesFile, `sbt.version=${sbtVersion}`)
165241
fs.copyFileSync(buildSbtFileSource, sbtBuildSbtFile)
166242
fs.copyFileSync(dottyPluginSbtFileSource, path.join(sbtProjectDir, "plugins.sbt"))
167-
168-
// Run sbt to configure the IDE.
169-
const sbtPromise =
170-
cpp.spawn("java", [
171-
"-Dsbt.log.noformat=true",
172-
"-classpath", sbtClasspath,
173-
"xsbt.boot.Boot",
174-
"configureIDE"
175-
])
176-
177-
const sbtProc = sbtPromise.childProcess
178-
// Close stdin, otherwise in case of error sbt will block waiting for the
179-
// user input to reload or exit the build.
180-
sbtProc.stdin.end()
181-
182-
sbtProc.stdout.on('data', (data: Buffer) => {
183-
let msg = data.toString().trim()
184-
outputChannel.appendLine(msg)
185-
})
186-
sbtProc.stderr.on('data', (data: Buffer) => {
187-
let msg = data.toString().trim()
188-
outputChannel.appendLine(msg)
189-
})
190-
191-
sbtProc.on('close', (code: number) => {
192-
if (code != 0) {
193-
const msg = "Configuring the IDE failed."
194-
outputChannel.appendLine(msg)
195-
throw new Error(msg)
196-
}
197-
})
198-
199-
return sbtPromise
200-
})
201243
}
202244

203245
function run(serverOptions: ServerOptions, isOldServer: boolean) {

vscode-dotty/src/sbt-server.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* sbt
3+
* Copyright 2011 - 2018, Lightbend, Inc.
4+
* Copyright 2008 - 2010, Mark Harrah
5+
* Licensed under Apache License 2.0 (see LICENSE)
6+
*/
7+
// Copy pasted from vscode-sbt-scala
8+
9+
'use strict'
10+
11+
import * as fs from 'fs'
12+
import * as net from 'net'
13+
import * as os from 'os'
14+
import * as path from 'path'
15+
import * as url from 'url'
16+
17+
import * as rpc from 'vscode-jsonrpc'
18+
19+
import * as vscode from 'vscode'
20+
21+
/** The result of successful `sbt/exec` call. */
22+
export interface ExecResult {
23+
status: string
24+
channelName: string
25+
execId: number
26+
commandQueue: string[]
27+
exitCode: number
28+
}
29+
30+
class CommandLine {
31+
commandLine: string
32+
constructor(commandLine: string) {
33+
this.commandLine = commandLine
34+
}
35+
}
36+
37+
/**
38+
* Sends `command` to sbt with `sbt/exec`.
39+
*
40+
* @param log Where to log messages between this client and sbt server
41+
* @param connection The connection to sbt server to use
42+
* @param command The command to send to sbt
43+
*
44+
* @return The result of executing `command`.
45+
*/
46+
export function tellSbt(log: vscode.OutputChannel,
47+
connection: rpc.MessageConnection,
48+
command: string): Thenable<ExecResult> {
49+
log.appendLine(`>>> ${command}`)
50+
let req = new rpc.RequestType<CommandLine, ExecResult, any, any>("sbt/exec")
51+
return connection.sendRequest(req, new CommandLine(command))
52+
}
53+
54+
/**
55+
* Attempts to connect to an sbt server running in this workspace.
56+
*
57+
* If connection fails, shows an error message and ask the user to retry.
58+
*
59+
* @param log Where to log messages between VSCode and sbt server.
60+
*/
61+
export function connectToSbtServer(log: vscode.OutputChannel): Promise<rpc.MessageConnection> {
62+
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+
}
85+
})
86+
}
87+
88+
function connectSocket(socket: net.Socket): net.Socket {
89+
let u = discoverUrl();
90+
if (u.protocol == 'tcp:' && u.port) {
91+
socket.connect(+u.port, '127.0.0.1');
92+
} else if (u.protocol == 'local:' && u.hostname && os.platform() == 'win32') {
93+
let pipePath = '\\\\.\\pipe\\' + u.hostname;
94+
socket.connect(pipePath);
95+
} else if (u.protocol == 'local:' && u.path) {
96+
socket.connect(u.path);
97+
} else {
98+
throw 'Unknown protocol ' + u.protocol;
99+
}
100+
return socket;
101+
}
102+
103+
// the port file is hardcoded to a particular location relative to the build.
104+
function discoverUrl(): url.Url {
105+
let pf = path.join(process.cwd(), 'project', 'target', 'active.json');
106+
let portfile = JSON.parse(fs.readFileSync(pf).toString());
107+
return url.parse(portfile.uri);
108+
}
109+
110+
function delay(ms: number) {
111+
return new Promise(resolve => setTimeout(resolve, ms));
112+
}
113+
114+
async function waitForServer(): Promise<net.Socket | null> {
115+
let socket: net.Socket | null = null
116+
return vscode.window.withProgress({
117+
location: vscode.ProgressLocation.Window,
118+
title: "Connecting to sbt server..."
119+
}, async _ => {
120+
let retries = 60;
121+
while (!socket && retries > 0) {
122+
try { socket = connectSocket(new net.Socket()) }
123+
catch (e) {
124+
retries--;
125+
await delay(1000);
126+
}
127+
}
128+
return socket
129+
}).then(_ => socket)
130+
}
131+
132+
function messageTypeToString(messageType: number): string {
133+
if (messageType == 1) return "error"
134+
else if (messageType == 2) return "warn"
135+
else if (messageType == 3) return "info"
136+
else if (messageType == 4) return "log"
137+
else return "???"
138+
}
139+

0 commit comments

Comments
 (0)