Source commit: 750f29790fe2046599fbe5b5d06fdb63d92fabff Public tree identity: sha256:02326e08850bb287585789733ea5f3f153e1f7f3fd88afcc24e249107df91506
414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const childProcess = require("child_process");
|
|
|
|
const DEFAULT_OPERATOR_ENDPOINT = "https://disasmer.michelpaulissen.com:9443";
|
|
|
|
let vscode = null;
|
|
try {
|
|
vscode = require("vscode");
|
|
} catch (_) {
|
|
vscode = null;
|
|
}
|
|
|
|
function discoverEnvironmentNames(projectRoot) {
|
|
const envsDir = path.join(projectRoot, "envs");
|
|
if (!fs.existsSync(envsDir)) return [];
|
|
|
|
return fs
|
|
.readdirSync(envsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => {
|
|
const dir = path.join(envsDir, name);
|
|
return (
|
|
fs.existsSync(path.join(dir, "Containerfile")) ||
|
|
fs.existsSync(path.join(dir, "Dockerfile"))
|
|
);
|
|
})
|
|
.sort();
|
|
}
|
|
|
|
function findEnvReferences(text) {
|
|
const references = [];
|
|
const pattern = /env!\(\s*"([^"]+)"\s*\)/g;
|
|
let match;
|
|
while ((match = pattern.exec(text)) !== null) {
|
|
references.push({
|
|
name: match[1],
|
|
start: match.index,
|
|
end: pattern.lastIndex
|
|
});
|
|
}
|
|
return references;
|
|
}
|
|
|
|
function diagnoseEnvReferences(text, environmentNames) {
|
|
const known = new Set(environmentNames);
|
|
return findEnvReferences(text)
|
|
.filter((reference) => !known.has(reference.name))
|
|
.map((reference) => ({
|
|
...reference,
|
|
message: `Missing Disasmer environment "${reference.name}". Add envs/${reference.name}/Containerfile or envs/${reference.name}/Dockerfile.`
|
|
}));
|
|
}
|
|
|
|
function binaryCandidates(projectRoot, name) {
|
|
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
|
|
return names.map((candidate) => path.join(projectRoot, candidate));
|
|
}
|
|
|
|
function executableWorkspaceBinary(projectRoot, name) {
|
|
if (!projectRoot) return null;
|
|
return binaryCandidates(projectRoot, name).find((candidate) => {
|
|
try {
|
|
return fs.statSync(candidate).isFile();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}) || null;
|
|
}
|
|
|
|
function hasCargoWorkspace(root) {
|
|
return fs.existsSync(path.join(root, "Cargo.toml"));
|
|
}
|
|
|
|
function resolveProjectPath(folder, value) {
|
|
const workspaceRoot = folder && folder.uri && folder.uri.fsPath;
|
|
if (!value || value === "${workspaceFolder}") return workspaceRoot;
|
|
if (workspaceRoot && typeof value === "string") {
|
|
return value.replace(/\$\{workspaceFolder\}/g, workspaceRoot);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function bundleInspectCommand(projectRoot, adapterWorkspace = path.resolve(__dirname, "..")) {
|
|
const workspaceCli = executableWorkspaceBinary(projectRoot, "disasmer");
|
|
if (workspaceCli) {
|
|
return {
|
|
command: workspaceCli,
|
|
args: ["bundle", "inspect", "--project", projectRoot],
|
|
options: {
|
|
cwd: projectRoot,
|
|
encoding: "utf8"
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "cargo",
|
|
args: [
|
|
"run",
|
|
"-q",
|
|
"-p",
|
|
"disasmer-cli",
|
|
"--bin",
|
|
"disasmer",
|
|
"--",
|
|
"bundle",
|
|
"inspect",
|
|
"--project",
|
|
projectRoot
|
|
],
|
|
options: {
|
|
cwd: adapterWorkspace,
|
|
encoding: "utf8"
|
|
}
|
|
};
|
|
}
|
|
|
|
function refreshBundleBeforeLaunch(
|
|
projectRoot,
|
|
adapterWorkspace = path.resolve(__dirname, ".."),
|
|
runner = childProcess.spawnSync
|
|
) {
|
|
const command = bundleInspectCommand(projectRoot, adapterWorkspace);
|
|
const result = runner(command.command, command.args, command.options);
|
|
if (result.error) {
|
|
throw new Error(`Disasmer bundle refresh failed: ${result.error.message}`);
|
|
}
|
|
if (result.status !== 0) {
|
|
const detail = (result.stderr || result.stdout || "").trim();
|
|
throw new Error(
|
|
`Disasmer bundle refresh failed before debug launch${detail ? `: ${detail}` : "."}`
|
|
);
|
|
}
|
|
|
|
let inspection;
|
|
try {
|
|
inspection = JSON.parse(result.stdout);
|
|
} catch (error) {
|
|
throw new Error(`Disasmer bundle refresh returned invalid metadata: ${error.message}`);
|
|
}
|
|
if (!inspection.metadata || !inspection.metadata.identity) {
|
|
throw new Error("Disasmer bundle refresh did not return a bundle identity.");
|
|
}
|
|
return inspection;
|
|
}
|
|
|
|
function disasmerViewDescriptors() {
|
|
return [
|
|
{ id: "disasmer.nodes", emptyLabel: "No attached nodes" },
|
|
{ id: "disasmer.processes", emptyLabel: "No virtual processes" },
|
|
{ id: "disasmer.logs", emptyLabel: "No log streams" },
|
|
{ id: "disasmer.artifacts", emptyLabel: "No artifacts" },
|
|
{ id: "disasmer.inspector", emptyLabel: "No inspector state" }
|
|
];
|
|
}
|
|
|
|
function disasmerViewStatePath(projectRoot) {
|
|
return path.join(projectRoot, ".disasmer", "views.json");
|
|
}
|
|
|
|
function emptyDisasmerViewState() {
|
|
return {
|
|
nodes: [],
|
|
processes: [],
|
|
logs: [],
|
|
artifacts: [],
|
|
inspector: []
|
|
};
|
|
}
|
|
|
|
function asArray(value) {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
function loadDisasmerViewState(projectRoot, reader = fs.readFileSync) {
|
|
const statePath = disasmerViewStatePath(projectRoot);
|
|
if (!fs.existsSync(statePath)) {
|
|
return emptyDisasmerViewState();
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(reader(statePath, "utf8"));
|
|
} catch (error) {
|
|
return {
|
|
...emptyDisasmerViewState(),
|
|
inspector: [
|
|
{
|
|
label: "View state error",
|
|
value: error.message
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
return {
|
|
nodes: asArray(parsed.nodes),
|
|
processes: asArray(parsed.processes),
|
|
logs: asArray(parsed.logs),
|
|
artifacts: asArray(parsed.artifacts),
|
|
inspector: asArray(parsed.inspector)
|
|
};
|
|
}
|
|
|
|
function item(label, description = "") {
|
|
return {
|
|
label: String(label || ""),
|
|
description: String(description || "")
|
|
};
|
|
}
|
|
|
|
function disasmerViewItems(state, viewId) {
|
|
switch (viewId) {
|
|
case "disasmer.nodes":
|
|
return state.nodes.map((node) =>
|
|
item(node.id || node.name || "node", [node.status, node.capabilities].filter(Boolean).join(" "))
|
|
);
|
|
case "disasmer.processes":
|
|
return state.processes.map((process) =>
|
|
item(process.id || process.process || "process", [process.status, process.entry].filter(Boolean).join(" "))
|
|
);
|
|
case "disasmer.logs":
|
|
return state.logs.map((log) =>
|
|
item(log.task || log.stream || "log", [log.message, log.bytes].filter(Boolean).join(" "))
|
|
);
|
|
case "disasmer.artifacts":
|
|
return state.artifacts.map((artifact) =>
|
|
item(artifact.path || artifact.id || "artifact", [artifact.status, artifact.size].filter(Boolean).join(" "))
|
|
);
|
|
case "disasmer.inspector":
|
|
return state.inspector.map((entry) =>
|
|
item(entry.label || entry.name || "inspector", entry.value || entry.detail || "")
|
|
);
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function treeItemsForView(projectRoot, descriptor) {
|
|
const state = projectRoot ? loadDisasmerViewState(projectRoot) : emptyDisasmerViewState();
|
|
const items = disasmerViewItems(state, descriptor.id);
|
|
return items.length > 0 ? items : [item(descriptor.emptyLabel)];
|
|
}
|
|
|
|
function toVsCodeTreeItem(viewItem) {
|
|
const treeItem = new vscode.TreeItem(
|
|
viewItem.label,
|
|
vscode.TreeItemCollapsibleState.None
|
|
);
|
|
treeItem.description = viewItem.description;
|
|
return treeItem;
|
|
}
|
|
|
|
function resolveDisasmerDebugConfiguration(folder, config = {}) {
|
|
const project = resolveProjectPath(folder, config.project);
|
|
return {
|
|
...config,
|
|
name: config.name || "Disasmer: Launch Virtual Process",
|
|
type: "disasmer",
|
|
request: "launch",
|
|
entry: config.entry || "build",
|
|
project,
|
|
runtimeBackend: config.runtimeBackend || "local-services",
|
|
operatorEndpoint: config.operatorEndpoint || DEFAULT_OPERATOR_ENDPOINT
|
|
};
|
|
}
|
|
|
|
function defaultOperatorEndpoint() {
|
|
return DEFAULT_OPERATOR_ENDPOINT;
|
|
}
|
|
|
|
function debugAdapterExecutableSpec(
|
|
projectRoot,
|
|
adapterWorkspace = path.resolve(__dirname, "..")
|
|
) {
|
|
const workspaceAdapter = executableWorkspaceBinary(projectRoot, "disasmer-debug-dap");
|
|
if (workspaceAdapter) {
|
|
return {
|
|
command: workspaceAdapter,
|
|
args: [],
|
|
options: { cwd: projectRoot }
|
|
};
|
|
}
|
|
|
|
if (!hasCargoWorkspace(adapterWorkspace)) {
|
|
return {
|
|
command: "disasmer-debug-dap",
|
|
args: [],
|
|
options: { cwd: projectRoot || process.cwd() }
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "cargo",
|
|
args: ["run", "-q", "-p", "disasmer-dap", "--bin", "disasmer-debug-dap"],
|
|
options: { cwd: adapterWorkspace }
|
|
};
|
|
}
|
|
|
|
function registerDisasmerViews(context) {
|
|
if (!vscode) return;
|
|
const workspaceRoot =
|
|
vscode.workspace.workspaceFolders &&
|
|
vscode.workspace.workspaceFolders[0] &&
|
|
vscode.workspace.workspaceFolders[0].uri.fsPath;
|
|
const emitters = [];
|
|
for (const descriptor of disasmerViewDescriptors()) {
|
|
const emitter = new vscode.EventEmitter();
|
|
emitters.push(emitter);
|
|
context.subscriptions.push(
|
|
vscode.window.registerTreeDataProvider(descriptor.id, {
|
|
onDidChangeTreeData: emitter.event,
|
|
getTreeItem(item) {
|
|
return item;
|
|
},
|
|
getChildren() {
|
|
return treeItemsForView(workspaceRoot, descriptor).map(toVsCodeTreeItem);
|
|
}
|
|
}),
|
|
emitter
|
|
);
|
|
}
|
|
if (workspaceRoot && vscode.workspace.createFileSystemWatcher) {
|
|
const watcher = vscode.workspace.createFileSystemWatcher(
|
|
new vscode.RelativePattern(workspaceRoot, ".disasmer/views.json")
|
|
);
|
|
const refresh = () => emitters.forEach((emitter) => emitter.fire());
|
|
context.subscriptions.push(
|
|
watcher,
|
|
watcher.onDidChange(refresh),
|
|
watcher.onDidCreate(refresh),
|
|
watcher.onDidDelete(refresh)
|
|
);
|
|
}
|
|
}
|
|
|
|
function activate(context) {
|
|
if (!vscode) return;
|
|
|
|
const diagnostics = vscode.languages.createDiagnosticCollection("disasmer");
|
|
context.subscriptions.push(diagnostics);
|
|
registerDisasmerViews(context);
|
|
|
|
const refresh = (document) => {
|
|
if (document.languageId !== "rust") return;
|
|
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
if (!folder) return;
|
|
const environmentNames = discoverEnvironmentNames(folder.uri.fsPath);
|
|
const items = diagnoseEnvReferences(document.getText(), environmentNames).map((diagnostic) => {
|
|
const start = document.positionAt(diagnostic.start);
|
|
const end = document.positionAt(diagnostic.end);
|
|
return new vscode.Diagnostic(
|
|
new vscode.Range(start, end),
|
|
diagnostic.message,
|
|
vscode.DiagnosticSeverity.Error
|
|
);
|
|
});
|
|
diagnostics.set(document.uri, items);
|
|
};
|
|
|
|
context.subscriptions.push(
|
|
vscode.workspace.onDidOpenTextDocument(refresh),
|
|
vscode.workspace.onDidChangeTextDocument((event) => refresh(event.document)),
|
|
vscode.workspace.onDidSaveTextDocument(refresh),
|
|
vscode.debug.registerDebugAdapterDescriptorFactory("disasmer", {
|
|
createDebugAdapterDescriptor(session) {
|
|
const folder = session.workspaceFolder;
|
|
const project = session.configuration.project || (folder && folder.uri.fsPath);
|
|
if (!project) {
|
|
throw new Error("Disasmer debug launch requires a workspace folder or project path.");
|
|
}
|
|
const adapterWorkspace = path.resolve(__dirname, "..");
|
|
refreshBundleBeforeLaunch(project, adapterWorkspace);
|
|
const spec = debugAdapterExecutableSpec(project, adapterWorkspace);
|
|
return new vscode.DebugAdapterExecutable(
|
|
spec.command,
|
|
spec.args,
|
|
spec.options
|
|
);
|
|
}
|
|
}),
|
|
vscode.debug.registerDebugConfigurationProvider("disasmer", {
|
|
resolveDebugConfiguration(folder, config) {
|
|
return resolveDisasmerDebugConfiguration(folder, config);
|
|
}
|
|
})
|
|
);
|
|
|
|
for (const document of vscode.workspace.textDocuments) {
|
|
refresh(document);
|
|
}
|
|
}
|
|
|
|
function deactivate() {}
|
|
|
|
module.exports = {
|
|
activate,
|
|
deactivate,
|
|
bundleInspectCommand,
|
|
debugAdapterExecutableSpec,
|
|
defaultOperatorEndpoint,
|
|
diagnoseEnvReferences,
|
|
disasmerViewDescriptors,
|
|
disasmerViewItems,
|
|
disasmerViewStatePath,
|
|
discoverEnvironmentNames,
|
|
findEnvReferences,
|
|
loadDisasmerViewState,
|
|
registerDisasmerViews,
|
|
refreshBundleBeforeLaunch,
|
|
resolveDisasmerDebugConfiguration
|
|
};
|