extension release bootstrapping and updating

This commit is contained in:
Noah Santschi-Cooney 2021-02-07 02:05:18 +00:00
parent 8fab7bd0f2
commit 4573dd86fd
No known key found for this signature in database
GPG key ID: 3B22282472C8AE48
8 changed files with 209 additions and 49 deletions

View file

@ -1,13 +1,30 @@
import { mkdirSync, promises as fs } from 'fs'
import * as vscode from 'vscode'
import * as lsp from 'vscode-languageclient'
import * as commands from './commands'
import { log } from './log'
import { LanguageClient } from './lspClient'
import { download, getReleaseInfo } from './net'
import { PersistentState } from './persistent_state'
import * as path from 'path'
const platforms: { [key: string]: string } = {
'x64 win32': 'x86_64-pc-windows-gnu',
'x64 linux': 'x86_64-unknown-linux-gnu',
'x64 darwin': 'x86_64-apple-darwin',
}
export class Extension {
private statusBarItem: vscode.StatusBarItem | null = null
private extensionContext: vscode.ExtensionContext | null = null
private client: lsp.LanguageClient
private state: PersistentState
readonly extensionID = 'strum355.vscode-mc-shader'
readonly package: {
version: string
} = vscode.extensions.getExtension(this.extensionID)!.packageJSON;
public get context(): vscode.ExtensionContext {
return this.extensionContext
@ -19,6 +36,9 @@ export class Extension {
public activate = async (context: vscode.ExtensionContext) => {
this.extensionContext = context
this.state = new PersistentState(context.globalState)
await this.bootstrap()
this.registerCommand('graphDot', commands.generateGraphDot)
this.registerCommand('restart', commands.restartExtension)
@ -54,6 +74,40 @@ export class Extension {
public clearStatus = () => {
this.statusBarItem?.dispose()
}
private bootstrap = async () => {
mkdirSync(this.extensionContext.globalStoragePath, { recursive: true })
const dest = path.join(this.extensionContext.globalStoragePath, 'mcshader-lsp' + (process.platform === 'win32' ? '.exe' : ''))
const exists = await fs.stat(dest).then(() => true, () => false)
if (!exists) await this.state.updateServerVersion(undefined)
this.state.updateServerVersion('borger')
const release = await getReleaseInfo(this.package.version)
const platform = platforms[`${process.arch} ${process.platform}`]
if (platform === undefined) {
vscode.window.showErrorMessage('Unfortunately we don\'t ship binaries for your platform yet.')
return
}
if (release.tag_name === this.state.serverVersion) return
const artifact = release.assets.find(artifact => artifact.name === `mcshader-lsp-${platform}`)
const userResponse = await vscode.window.showInformationMessage(
this.state.serverVersion == undefined ?
`Language server version ${this.package.version} is not installed.` :
`An update is available. Upgrade from ${this.state.serverVersion} to ${release.tag_name}?`,
'Download now'
)
if (userResponse !== 'Download now') return
await download(artifact.browser_download_url, dest)
this.state.updateServerVersion(release.tag_name)
}
}
export const activate = new Extension().activate

View file

@ -10,13 +10,15 @@ export class LanguageClient extends lsp.LanguageClient {
constructor(ext: Extension) {
super('vscode-mc-shader', 'VSCode MC Shader', {
command: ext.context.asAbsolutePath(path.join('server', 'target', 'debug', 'vscode-mc-shader')),
command: process.env['MCSHADER_DEBUG'] ?
ext.context.asAbsolutePath(path.join('server', 'target', 'debug', 'vscode-mc-shader')) :
path.join(ext.context.globalStoragePath, 'mcshader-lsp')
}, {
documentSelector: [{scheme: 'file', language: 'glsl'}],
outputChannel: lspOutputChannel,
synchronize: {
configurationSection: 'mcglsl',
fileEvents: workspace.createFileSystemWatcher('**/*.{fsh,gsh,vsh,glsl}')
fileEvents: workspace.createFileSystemWatcher('**/*.{fsh,gsh,vsh,glsl,inc}')
},
})
this.extension = ext

101
client/src/net.ts Normal file
View file

@ -0,0 +1,101 @@
import { log } from './log'
import fetch from 'node-fetch'
import * as vscode from 'vscode'
import * as stream from 'stream'
import * as fs from 'fs'
import * as util from 'util'
const pipeline = util.promisify(stream.pipeline)
interface GithubRelease {
tag_name: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
}
export async function getReleaseInfo(releaseTag: string): Promise<GithubRelease> {
const response = await fetch(`https://api.github.com/repos/strum355/mcshader-lsp/releases/tags/${releaseTag}`, {
headers: {Accept: 'application/vnd.github.v3+json'}
})
const isRelease = (obj: unknown): obj is GithubRelease => {
return obj != null && typeof obj === 'object'
&& typeof (obj as GithubRelease).tag_name === 'string'
&& Array.isArray((obj as GithubRelease).assets)
&& (obj as GithubRelease).assets.every((a) => typeof a.name === 'string' && typeof a.browser_download_url === 'string')
}
const json = await response.json()
if(!isRelease(json)) {
throw new TypeError('Received malformed request from Github Release API')
}
return json
}
export async function download(url: string, downloadDest: string) {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
cancellable: false,
title: `Downloading ${url}`
},
async (progress, _) => {
let lastPercentage = 0
await downloadFile(url, downloadDest, (readBytes, totalBytes) => {
const newPercentage = Math.round((readBytes / totalBytes) * 100)
if (newPercentage !== lastPercentage) {
progress.report({
message: `${newPercentage.toFixed(0)}%`,
increment: newPercentage - lastPercentage
})
lastPercentage = newPercentage
}
})
}
)
}
async function downloadFile(
url: string,
destFilePath: fs.PathLike,
onProgress: (readBytes: number, totalBytes: number) => void
): Promise<void> {
const res = await fetch(url)
if (!res.ok) {
log.error(res.status, 'while downloading file from', url)
log.error({ body: await res.text(), headers: res.headers })
throw new Error(`Got response ${res.status} when trying to download ${url}.`)
}
const totalBytes = Number(res.headers.get('content-length'))
log.debug('downloading file of', totalBytes, 'bytes size from', url, 'to', destFilePath)
let readBytes = 0
res.body.on('data', (chunk: Buffer) => {
readBytes += chunk.length
onProgress(readBytes, totalBytes)
})
const destFileStream = fs.createWriteStream(destFilePath, { mode: 0o755 })
await pipeline(res.body, destFileStream)
// Don't apply the workaround in fixed versions of nodejs, since the process
// freezes on them, the process waits for no-longer emitted `close` event.
// The fix was applied in commit 7eed9d6bcc in v13.11.0
// See the nodejs changelog:
// https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md
const [, major, minor] = /v(\d+)\.(\d+)\.(\d+)/.exec(process.version)!
if (+major > 13 || (+major === 13 && +minor >= 11)) return
await new Promise<void>(resolve => {
destFileStream.on('close', resolve)
destFileStream.destroy()
// This workaround is awaiting to be removed when vscode moves to newer nodejs version:
// https://github.com/rust-analyzer/rust-analyzer/issues/3167
})
}

View file

@ -0,0 +1,16 @@
import { Memento } from 'vscode'
import { log } from './log'
export class PersistentState {
constructor(private readonly state: Memento) {
const { serverVersion } = this
log.info('working with state', { serverVersion })
}
get serverVersion(): string | undefined {
return this.state.get('serverVersion')
}
async updateServerVersion(value: string | undefined) {
await this.state.update('serverVersion', value)
}
}