Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better error message when cadl-server is not found in Visual Studio #594

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "cadl-vs",
"comment": "Improve error reporting when cadl-server is not found",
"type": "patch"
}
],
"packageName": "cadl-vs"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "cadl-vscode",
"comment": "Improve error reporting when cadl-server is not found",
"type": "patch"
}
],
"packageName": "cadl-vscode"
}
32 changes: 24 additions & 8 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions packages/cadl-vs/src/Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;

namespace Microsoft.Cadl.VisualStudio;

/// <summary>
/// Provide a typed map for Win32 error codes as described here https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d
/// </summary>
internal class Win32ErrorCodes
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
{
public const int ERROR_FILE_NOT_FOUND = 2;
}


[Serializable]
public class CadlUserErrorException : Exception
{
public CadlUserErrorException() { }

public CadlUserErrorException(string message)
: base(message)
{

}
}


[Serializable]
public class CadlServerNotFound : CadlUserErrorException
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
{
public CadlServerNotFound() { }

public CadlServerNotFound(string name)
: base(string.Join("\n", new string[]
{
$"Cadl server exectuable was not found: '{name}' is not found. Make sure either:",
" - cadl is installed globally with `npm install -g @cadl-lang/compiler'.",
" - cadl server path is configured with https://github.com/microsoft/cadl/blob/main/packages/cadl-vs/README.md#configure-cadl-visual-studio-extension."
}))
{

}
}
98 changes: 58 additions & 40 deletions packages/cadl-vs/src/VSExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.VisualStudio.Utilities;
using Task = System.Threading.Tasks.Task;
using System.Linq;
using System.ComponentModel;

namespace Microsoft.Cadl.VisualStudio
{
Expand Down Expand Up @@ -82,20 +83,32 @@ public LanguageClient([Import] IVsFolderWorkspaceService workspaceService)
};

#if DEBUG
// Use local build of cadl-server in development (lauched from F5 in VS)
if (InDevelopmentMode()) {
// --nolazy isn't supported by NODE_OPTIONS so we pass these via CLI instead
info.Environment.Remove("NODE_OPTIONS");
}
// Use local build of cadl-server in development (lauched from F5 in VS)
if (InDevelopmentMode())
{
// --nolazy isn't supported by NODE_OPTIONS so we pass these via CLI instead
info.Environment.Remove("NODE_OPTIONS");
}
#endif
try
{
var process = Process.Start(info);
process.BeginErrorReadLine();
process.ErrorDataReceived += (_, e) => LogStderrMessage(e.Data);

var process = Process.Start(info);
process.BeginErrorReadLine();
process.ErrorDataReceived += (_, e) => LogStderrMessage(e.Data);
return new Connection(
process.StandardOutput.BaseStream,
process.StandardInput.BaseStream);
}
catch (Win32Exception e)
{
if (e.NativeErrorCode == Win32ErrorCodes.ERROR_FILE_NOT_FOUND)
{
throw new CadlServerNotFound(info.FileName);
}
throw e;
}

return new Connection(
process.StandardOutput.BaseStream,
process.StandardInput.BaseStream);
}

public async Task OnLoadedAsync()
Expand All @@ -108,21 +121,24 @@ public async Task OnLoadedAsync()
}

#if VS2019
public Task OnServerInitializeFailedAsync(Exception e) {
Debug.Fail("Failed to initialize cadl-server:\r\n\r\n" + e);
return Task.CompletedTask;
}
public Task OnServerInitializeFailedAsync(Exception e)
{
var message = e is CadlUserErrorException ? e.Message : e.ToString();
Debug.Fail("Failed to initialize cadl-server:\r\n\r\n" + message);
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
return Task.CompletedTask;
}
#endif

#if VS2022
public Task<InitializationFailureContext?> OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) {
var exception = initializationState.InitializationException;
var message = exception is CadlUserErrorException
? exception.Message
: $"File issue at https://github.com/microsoft/cadl\r\n\r\n{exception?.ToString()}";
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
Debug.Fail("Failed to initialize cadl-server:\r\n\r\n" + exception);
return Task.FromResult<InitializationFailureContext?>(
new InitializationFailureContext {
FailureMessage = "Failed to activate Cadl language server!\r\n" +
"File issue at https://github.com/microsoft/cadl\r\n\r\n" +
exception
FailureMessage = "Failed to activate Cadl language server!\r\n" + message
});
}
#endif
Expand All @@ -143,35 +159,37 @@ private void LogStderrMessage(string? message)
}

#if DEBUG
private static bool InDevelopmentMode() {
return string.Equals(
Environment.GetEnvironmentVariable("CADL_DEVELOPMENT_MODE"),
"true",
StringComparison.OrdinalIgnoreCase);
}
private static bool InDevelopmentMode()
{
return string.Equals(
Environment.GetEnvironmentVariable("CADL_DEVELOPMENT_MODE"),
"true",
StringComparison.OrdinalIgnoreCase);
}

private static string GetDevelopmentCadlServerPath() {
// Even when debugging, we get deployed to an extension folder outside the
// source tree, so we stash the source directory in a file in debug builds
// so we can use it to run cadl-server against the live developer build in
// the source tree.
var thisDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var srcDir = File.ReadAllText(Path.Combine(thisDir, "DebugSourceDirectory.txt")).Trim();
return Path.GetFullPath(Path.Combine(srcDir, "../compiler/cmd/cadl-server.js"));
}
private static string GetDevelopmentCadlServerPath()
{
// Even when debugging, we get deployed to an extension folder outside the
// source tree, so we stash the source directory in a file in debug builds
// so we can use it to run cadl-server against the live developer build in
// the source tree.
var thisDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var srcDir = File.ReadAllText(Path.Combine(thisDir, "DebugSourceDirectory.txt")).Trim();
return Path.GetFullPath(Path.Combine(srcDir, "../compiler/cmd/cadl-server.js"));
}
#endif

private (string, string[]) resolveCadlServer(IWorkspaceSettings? settings)
{
var args = new string[] { "--stdio" };

#if DEBUG
// Use local build of cadl-server in development (lauched from F5 in VS)
if (InDevelopmentMode()) {
var options = Environment.GetEnvironmentVariable("CADL_SERVER_NODE_OPTIONS");
var module = GetDevelopmentCadlServerPath();
return ("node.exe", new string[] { module, options }.Concat(args).ToArray());
}
// Use local build of cadl-server in development (lauched from F5 in VS)
if (InDevelopmentMode())
{
var options = Environment.GetEnvironmentVariable("CADL_SERVER_NODE_OPTIONS");
var module = GetDevelopmentCadlServerPath();
return ("node.exe", new string[] { module, options }.Concat(args).ToArray());
}
#endif

var serverPath = settings?.Property<string>("cadl.cadl-server.path");
Expand Down
2 changes: 1 addition & 1 deletion packages/cadl-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"tmlanguage-generator": "~0.3.0",
"typescript": "~4.7.2",
"vsce": "~2.6.7",
"vscode-languageclient": "~7.0.0",
"vscode-languageclient": "~8.0.0",
"vscode-oniguruma": "~1.6.1",
"vscode-textmate": "~6.0.0"
}
Expand Down
52 changes: 41 additions & 11 deletions packages/cadl-vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { stat } from "fs/promises";
import { join } from "path";
import { commands, ExtensionContext, workspace } from "vscode";
import vscode, { commands, ExtensionContext, workspace } from "vscode";
import {
Executable,
ExecutableOptions,
Expand All @@ -11,9 +11,30 @@ import {
let client: LanguageClient | undefined;

export async function activate(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand("cadl.restartServer", restartCadlServer));

return await vscode.window.withProgress(
{
title: "Launching Cadl language service...",
location: vscode.ProgressLocation.Notification,
},
async () => launchLanguageClient(context)
);
}

async function restartCadlServer(): Promise<void> {
if (client) {
await client.stop();
client.start();
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
}
}

async function launchLanguageClient(context: ExtensionContext) {
const exe = await resolveCadlServer(context);
const options: LanguageClientOptions = {
synchronize: {
// Synchronize the setting section 'cadl' to the server
configurationSection: "cadl",
fileEvents: [
workspace.createFileSystemWatcher("**/*.cadl"),
workspace.createFileSystemWatcher("**/cadl-project.yaml"),
Expand All @@ -26,24 +47,33 @@ export async function activate(context: ExtensionContext) {
],
};

context.subscriptions.push(commands.registerCommand("cadl.restartServer", restartCadlServer));

const name = "Cadl";
const id = "cadlLanguageServer";
client = new LanguageClient(id, name, { run: exe, debug: exe }, options);
client.start();
}

async function restartCadlServer(): Promise<void> {
if (client) {
await client.stop();
client.start();
try {
client = new LanguageClient(id, name, { run: exe, debug: exe }, options);
await client.start();
} catch (e) {
if (typeof e === "string" && e.startsWith("Launching server using command")) {
client?.error(
[
`Cadl server exectuable was not found: '${exe.command}' is not found. Make sure either:`,
" - cadl is installed globally with `npm install -g @cadl-lang/compiler'.",
" - cadl server path is configured with https://github.com/microsoft/cadl#installing-vs-code-extension.",
].join("\n"),
undefined,
false
);
throw `Cadl server exectuable was not found: '${exe.command}' is not found.`;
} else {
throw e;
}
}
}

async function resolveCadlServer(context: ExtensionContext): Promise<Executable> {
const nodeOptions = process.env.CADL_SERVER_NODE_OPTIONS;
const args = ["--stdio"];

// In development mode (F5 launch from source), resolve to locally built server.js.
if (process.env.CADL_DEVELOPMENT_MODE) {
const script = context.asAbsolutePath("../compiler/dist/server/server.js");
Expand Down