225 lines
8.0 KiB
TypeScript
225 lines
8.0 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as https from 'https';
|
|
import * as os from 'os';
|
|
// @ts-ignore
|
|
import decompress = require('decompress');
|
|
|
|
interface GitHubRelease {
|
|
tag_name: string;
|
|
assets: {
|
|
name: string;
|
|
browser_download_url: string;
|
|
}[];
|
|
}
|
|
|
|
|
|
export async function ensureServerBinary(
|
|
context: vscode.ExtensionContext,
|
|
channel: vscode.OutputChannel
|
|
): Promise<string | undefined> {
|
|
|
|
const storagePath = context.globalStorageUri.fsPath;
|
|
const platform = os.platform();
|
|
const arch = os.arch();
|
|
|
|
channel.appendLine(`[Download] Initializing clice downloader...`);
|
|
channel.appendLine(`[Download] Platform: ${platform}, Arch: ${arch}, Storage: ${storagePath}`);
|
|
|
|
let platformKeyword = '';
|
|
let archKeyword = '';
|
|
let binaryName = 'clice';
|
|
|
|
if (platform === 'win32') {
|
|
platformKeyword = 'windows';
|
|
archKeyword = 'x64';
|
|
binaryName = 'clice.exe';
|
|
} else if (platform === 'darwin') {
|
|
platformKeyword = 'macos';
|
|
archKeyword = arch;
|
|
} else if (platform === 'linux') {
|
|
platformKeyword = 'linux';
|
|
archKeyword = arch === 'x64' ? 'x86_64' : arch;
|
|
} else {
|
|
const msg = `Unsupported platform: ${platform}`;
|
|
channel.appendLine(`[Download] Error: ${msg}`);
|
|
vscode.window.showErrorMessage(msg);
|
|
return undefined;
|
|
}
|
|
|
|
const executablePath = path.join(storagePath, "bin", binaryName);
|
|
|
|
if (fs.existsSync(executablePath)) {
|
|
channel.appendLine(`[Download] Found existing binary at: ${executablePath}`);
|
|
// TODO: check tag update
|
|
return executablePath;
|
|
}
|
|
|
|
if (!fs.existsSync(storagePath)) {
|
|
channel.appendLine(`[Download] Creating storage directory: ${storagePath}`);
|
|
fs.mkdirSync(storagePath, { recursive: true });
|
|
}
|
|
|
|
const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
|
|
|
try {
|
|
statusItem.text = "$(sync~spin) Checking clice updates...";
|
|
statusItem.show();
|
|
|
|
channel.appendLine(`[Download] Fetching latest release from GitHub...`);
|
|
const release = await fetchReleaseInfo(channel);
|
|
channel.appendLine(`[Download] Latest tag: ${release.tag_name}`);
|
|
|
|
const asset = release.assets.find(a => {
|
|
const name = a.name.toLowerCase();
|
|
return name.includes(platformKeyword) &&
|
|
name.includes(archKeyword) &&
|
|
!name.includes('symbol');
|
|
});
|
|
|
|
if (!asset) {
|
|
throw new Error(`No compatible asset found for ${platform}-${archKeyword} in release ${release.tag_name}`);
|
|
}
|
|
|
|
channel.appendLine(`[Download] Found asset: ${asset.name}`);
|
|
channel.appendLine(`[Download] Download URL: ${asset.browser_download_url}`);
|
|
|
|
const tempArchiveName = asset.name;
|
|
const tempArchivePath = path.join(storagePath, tempArchiveName);
|
|
|
|
statusItem.text = `$(cloud-download) Downloading clice...`;
|
|
await downloadFile(asset.browser_download_url, tempArchivePath, channel);
|
|
|
|
statusItem.text = `$(file-zip) Extracting...`;
|
|
channel.appendLine(`[Download] Extracting ${tempArchivePath} to ${storagePath}...`);
|
|
|
|
await decompress(tempArchivePath, storagePath);
|
|
channel.appendLine(`[Download] Extraction complete.`);
|
|
|
|
fs.unlinkSync(tempArchivePath);
|
|
|
|
if (!fs.existsSync(executablePath)) {
|
|
throw new Error(`Executable not found at ${executablePath} after extraction.`);
|
|
}
|
|
|
|
if (platform !== 'win32') {
|
|
channel.appendLine(`[Download] Setting executable permissions (chmod 755)...`);
|
|
fs.chmodSync(executablePath, '755');
|
|
}
|
|
|
|
channel.appendLine(`[Download] Setup successful. Binary ready at: ${executablePath}`);
|
|
vscode.window.showInformationMessage(`Clice language server updated to ${release.tag_name}`);
|
|
return executablePath;
|
|
|
|
} catch (error) {
|
|
channel.appendLine(`[Download] CRITICAL ERROR during setup:`);
|
|
if (error instanceof Error) {
|
|
channel.appendLine(`[Download] Message: ${error.message}`);
|
|
if (error.stack) {
|
|
channel.appendLine(`[Download] Stack: ${error.stack}`);
|
|
}
|
|
} else {
|
|
channel.appendLine(`[Download] Unknown error: ${JSON.stringify(error)}`);
|
|
}
|
|
|
|
vscode.window.showErrorMessage(`Failed to download clice server. Check "clice" output channel for details.`, "Open Output").then(selection => {
|
|
if (selection === "Open Output") {
|
|
channel.show();
|
|
}
|
|
});
|
|
|
|
return undefined;
|
|
} finally {
|
|
statusItem.dispose();
|
|
}
|
|
}
|
|
|
|
function downloadFile(url: string, destPath: string, channel: vscode.OutputChannel, maxRedirects = 5): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (maxRedirects <= 0) {
|
|
reject(new Error('Too many redirects'));
|
|
return;
|
|
}
|
|
|
|
const file = fs.createWriteStream(destPath);
|
|
channel.appendLine(`[Download] Start downloading to ${destPath}`);
|
|
|
|
https.get(url, { headers: { 'User-Agent': 'VSCode-Extension' } }, (response) => {
|
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
channel.appendLine(`[Download] Redirecting to ${response.headers.location}`);
|
|
file.close();
|
|
downloadFile(response.headers.location!, destPath, channel, maxRedirects - 1).then(resolve).catch(reject);
|
|
return;
|
|
}
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`Download failed with status code ${response.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
response.pipe(file);
|
|
file.on('finish', () => {
|
|
file.close();
|
|
channel.appendLine(`[Download] Download finished.`);
|
|
resolve();
|
|
});
|
|
}).on('error', (err) => {
|
|
file.close();
|
|
fs.unlink(destPath, () => { });
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function fetchReleaseInfo(channel: vscode.OutputChannel): Promise<GitHubRelease> {
|
|
try {
|
|
channel.appendLine('[Download] Attempting to fetch latest stable release...');
|
|
const release = await fetchJson<GitHubRelease>('/repos/clice-io/clice/releases/latest');
|
|
channel.appendLine(`[Download] Found stable release: ${release.tag_name}`);
|
|
return release;
|
|
} catch (error: any) {
|
|
if (error.message && error.message.includes('404')) {
|
|
channel.appendLine('[Download] Latest stable release not found (404). Checking for pre-releases...');
|
|
|
|
const releases = await fetchJson<GitHubRelease[]>('/repos/clice-io/clice/releases?per_page=1');
|
|
|
|
if (Array.isArray(releases) && releases.length > 0) {
|
|
const latestPre = releases[0];
|
|
channel.appendLine(`[Download] Found pre-release: ${latestPre.tag_name}`);
|
|
return latestPre;
|
|
} else {
|
|
throw new Error('No releases found in repository.');
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function fetchJson<T>(apiPath: string): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
hostname: 'api.github.com',
|
|
path: apiPath,
|
|
headers: { 'User-Agent': 'VSCode-Extension' }
|
|
};
|
|
|
|
https.get(options, (res) => {
|
|
let data = '';
|
|
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
res.resume();
|
|
reject(new Error(`GitHub API returned ${res.statusCode} for ${apiPath}`));
|
|
return;
|
|
}
|
|
|
|
res.on('data', (chunk) => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(new Error(`Failed to parse GitHub API response: ${e}`));
|
|
}
|
|
});
|
|
}).on('error', reject);
|
|
});
|
|
}
|