diff --git a/typescript/vscode-extension/CHANGELOG.md b/typescript/vscode-extension/CHANGELOG.md index b38ad29d..aac4991a 100644 --- a/typescript/vscode-extension/CHANGELOG.md +++ b/typescript/vscode-extension/CHANGELOG.md @@ -4,8 +4,10 @@ All notable changes to the "suibase" extension will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -## [Unreleased] - +## 0.1.4 + - Warn user if some Sui prerequisites are not installed (git and rustc) + - Start suibase-daemon if not running. + ## 0.1.2 - Warn about requiring suibase to be installed. diff --git a/typescript/vscode-extension/package.json b/typescript/vscode-extension/package.json index 0815bbec..2f834240 100644 --- a/typescript/vscode-extension/package.json +++ b/typescript/vscode-extension/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "icon": "media/logo_128.png", - "version": "0.1.3", + "version": "0.1.4", "repository": { "type": "git", "url": "https://github.com/ChainMovers/suibase.git" diff --git a/typescript/vscode-extension/src/BackendSync.ts b/typescript/vscode-extension/src/BackendSync.ts index 114d4000..da02235c 100644 --- a/typescript/vscode-extension/src/BackendSync.ts +++ b/typescript/vscode-extension/src/BackendSync.ts @@ -9,6 +9,7 @@ import { UpdateWorkdirStatus, } from "./common/ViewMessages"; import { SuibaseJson, SuibaseJsonVersions } from "./common/SuibaseJson"; +import { SuibaseExec } from "./SuibaseExec"; // One instance per workdir, instantiated in same size and order as WORKDIRS_KEYS. class BackendWorkdirTracking { @@ -120,7 +121,11 @@ export class BackendSync { private async asyncLoop(forceRefresh: boolean): Promise { await this.loopMutex.runExclusive(async () => { - await this.update(forceRefresh); + try { + await this.update(forceRefresh); + } catch (error) { + console.error(`Catch done in asyncLoop: ${JSON.stringify(error)}`); + } if (forceRefresh === false) { // Schedule another call in one second. @@ -213,13 +218,37 @@ export class BackendSync { } } - private diagnoseBackendError(workdirIdx: number) { + private async diagnoseBackendError(workdirIdx: number) { // This is a helper function to diagnose the backend error. + // + // It may attempts fixes, and it is assumed the caller will + // retry to contact the backend periodically until success. + // // It will send a message to the views to display the error message. - // For now, always broadcast a message to all views that the backend is not responding. + // For now, always broadcast problems to all views. this.mForceRefreshOnNextReconnect = true; + const msg = new UpdateVersions(WEBVIEW_BACKEND, workdirIdx, undefined); - msg.setSetupIssue("Suibase not responding. Is it installed?"); + + const sb = SuibaseExec.getInstance(); + if (sb === undefined) { + msg.setSetupIssue("Internal error. Shell commands failed."); + } else if ((await sb.isSuibaseInstalled()) === false) { + msg.setSetupIssue("Suibase not installed?\nCheck https://suibase.io/how-to/install"); + } else if ((await sb.isGitInstalled()) === false) { + msg.setSetupIssue( + "Git not installed?\nPlease install Sui prerequisites\nhttps://docs.sui.io/guides/developer/getting-started/sui-install" + ); + } else if ((await sb.isSuibaseBackendRunning()) === false) { + if ((await sb.startDaemon()) === true) { + msg.setSetupIssue("Suibase initializing..."); + } else { + msg.setSetupIssue("Suibase backend not starting"); + } + } else { + msg.setSetupIssue("Suibase backend not responding"); + } + BaseWebview.broadcastMessage(msg); } @@ -232,14 +261,17 @@ export class BackendSync { for (let workdirIdx = 0; workdirIdx < WORKDIRS_KEYS.length; workdirIdx++) { const workdir = WORKDIRS_KEYS[workdirIdx]; let data = undefined; + try { data = await this.fetchGetVersions(workdir); } catch (error) { - this.diagnoseBackendError(workdirIdx); + await this.diagnoseBackendError(workdirIdx); return; } + if (data) { try { + //console.log("update versions: ", JSON.stringify(data)); // This is an example of data: // {"jsonrpc":"2.0","result":{ // "header":{"method":"getVersions", "methodUuid":"...","dataUuid":"...","key":"localnet"}, @@ -258,6 +290,7 @@ export class BackendSync { // The views will then decide if they need to synchronize further with the extension. if (hasChanged || forceRefresh || this.mForceRefreshOnNextReconnect) { this.mForceRefreshOnNextReconnect = false; + BaseWebview.broadcastMessage(new UpdateVersions(WEBVIEW_BACKEND, workdirIdx, data.result)); } } catch (error) { @@ -298,6 +331,7 @@ export class BackendSync { // {"label":"Multi-link RPC","status":"OK","statusInfo":null,"helpInfo":null,"pid":null}]},"id":2} // // Update the SuibaseJson instance for the workdir. + //console.log("replyWorkdirStatus: ", JSON.stringify(data)); BaseWebview.postMessageTo(sender, new UpdateWorkdirStatus(WEBVIEW_BACKEND, workdirIdx, data)); } catch (error) { const errorMsg = `Error in replyWorkdirStatus: ${JSON.stringify(error)}. Data: ${JSON.stringify(data)}`; @@ -342,7 +376,9 @@ export class BackendSync { // Update the SuibaseJson instance for the workdir. BaseWebview.postMessageTo(sender, new UpdateWorkdirPackages(WEBVIEW_BACKEND, workdirIdx, data)); } catch (error) { - const errorMsg = `Error in replyWorkdirPackages: ${JSON.stringify(error)}. Data: ${JSON.stringify(data)}`; + const errorMsg = `Error in replyWorkdirPackages: ${JSON.stringify(error)}. Data: ${JSON.stringify( + data + )}`; console.error(errorMsg); //throw new Error(errorMsg); } diff --git a/typescript/vscode-extension/src/SuibaseExec.ts b/typescript/vscode-extension/src/SuibaseExec.ts index c3249c7d..e00bef2d 100644 --- a/typescript/vscode-extension/src/SuibaseExec.ts +++ b/typescript/vscode-extension/src/SuibaseExec.ts @@ -23,6 +23,17 @@ const execShell = (cmd: string) => }); }); +const execShellBackground = (cmd: string) => + // eslint-disable-next-line @typescript-eslint/no-unused-vars + new Promise((resolve, _reject) => { + cp.exec(cmd, (err, stdout, stderr) => { + if (err) { + console.warn(err); + } + resolve(stdout ? stdout : stderr); + }); + }); + export class SuibaseExec { private static instance?: SuibaseExec; private static context?: vscode.ExtensionContext; @@ -93,20 +104,56 @@ export class SuibaseExec { return SuibaseExec.instance; } - public async version(): Promise { + public async isRustInstalled(): Promise { + // Returns true if the rust compiler can be call. + // Returns false on any error. try { - const result = await execShell("localnet --version"); - console.log(result); - return Promise.resolve(result); - } catch (err) { - return Promise.reject(err); + const result = await execShell("rustc --version"); + if (result.startsWith("rustc") && !result.includes("error")) { + return true; + } + } catch (error) { + console.error("rustc not installed"); } + return false; } - private async startDaemon() { - // Check if suibase-daemon is running, if not, attempt - // to start it and return once confirmed ready to - // process requests. + public async isGitInstalled(): Promise { + // Returns true if the git can be call. + // Returns false on any error. + try { + const result = await execShell("git --version"); + if (result.startsWith("git") && !result.includes("error")) { + return true; + } + } catch (error) { + console.error("git not installed"); + } + return false; + } + + public async isSuibaseInstalled(): Promise { + // Verify if Suibase itself is installed. + // Returns true if all the following files exists: + // ~/suibase/install + // ~/suibase/scripts/common/run-daemon.sh + try { + let result = await execShell("ls ~/suibase/install"); + if (result.includes("suibase/install")) { + return true; + } + result = await execShell("ls ~/suibase/scripts/common/run-daemon.sh"); + if (result.includes("suibase/scripts/common/run-daemon.sh")) { + return true; + } + } catch (error) { + console.error("suibase not installed"); + } + return false; + } + + public async isSuibaseBackendRunning(): Promise { + // Returns true if suibase-daemon is running. let suibaseRunning = false; try { const result = await execShell("lsof /tmp/.suibase/suibase-daemon.lock"); @@ -122,18 +169,45 @@ export class SuibaseExec { } catch (err) { /* Do nothing */ } + return suibaseRunning; + } - if (!suibaseRunning) { - // Start suibase daemon - await execShell("~/suibase/scripts/common/run-daemon.sh suibase &"); + public async version(): Promise { + try { + const result = await execShell("localnet --version"); + console.log(result); + return Promise.resolve(result); + } catch (err) { + return Promise.reject(err); + } + } - // TODO Implement retry and error handling of run-daemon.sh for faster startup. + public async startDaemon(): Promise { + // Check if suibase-daemon is running, if not, attempt + // to start it and return once confirmed ready to + // process requests. + let suibaseRunning = await this.isSuibaseBackendRunning(); - // Sleep 500 milliseconds to give it a chance to start. - await new Promise((r) => setTimeout(r, 500)); + if (!suibaseRunning) { + // Start suibase daemon + void execShellBackground("~/suibase/scripts/common/run-daemon.sh suibase"); + + // Check for up to ~5 seconds that it is started. + let attempts = 10; + while (!suibaseRunning && attempts > 0) { + // Sleep 500 millisecs to give it a chance to start. + await new Promise((r) => setTimeout(r, 500)); + suibaseRunning = await this.isSuibaseBackendRunning(); + attempts--; + } + } - // TODO Confirm that suibase-daemon is responding to requests. + if (suibaseRunning) { + return true; } + + console.error("Failed to start suibase.daemon"); + return false; } private makeJsonRpcCall() {