From db2b3e47bc91dfba556ed2c9af24702f7dc2bcbe Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 28 Jun 2023 16:43:45 -0400 Subject: [PATCH] Reinstall Node whenever a NodeRuntime operation has serious error --- Cargo.lock | 1 + crates/copilot/src/copilot.rs | 2 +- crates/node_runtime/Cargo.toml | 1 + crates/node_runtime/src/node_runtime.rs | 201 ++++++++++++++---------- crates/zed/src/languages/typescript.rs | 4 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 7 files changed, 126 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4b12223e5..55e10ed326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4138,6 +4138,7 @@ dependencies = [ "async-tar", "futures 0.3.28", "gpui", + "log", "parking_lot 0.11.2", "serde", "serde_derive", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4c45bf823b..ce4938ed0d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -340,7 +340,7 @@ impl Copilot { let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let this = cx.add_model(|cx| Self { http: http.clone(), - node_runtime: NodeRuntime::new(http, cx.background().clone()), + node_runtime: NodeRuntime::instance(http, cx.background().clone()), server: CopilotServer::Running(RunningCopilotServer { lsp: Arc::new(server), sign_in_status: SignInStatus::Authorized, diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index fce0fdfe50..53635f2725 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -20,3 +20,4 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true smol.workspace = true +log.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index e2a8d0d003..27a763e7f8 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,21 +1,24 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; +use futures::lock::Mutex; use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; -use parking_lot::Mutex; use serde::Deserialize; use smol::{fs, io::BufReader, process::Command}; +use std::process::Output; use std::{ env::consts, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, OnceLock}, }; -use util::http::HttpClient; +use util::{http::HttpClient, ResultExt}; const VERSION: &str = "v18.15.0"; -#[derive(Deserialize)] +static RUNTIME_INSTANCE: OnceLock> = OnceLock::new(); + +#[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct NpmInfo { #[serde(default)] @@ -23,7 +26,7 @@ pub struct NpmInfo { versions: Vec, } -#[derive(Deserialize, Default)] +#[derive(Debug, Deserialize, Default)] pub struct NpmInfoDistTags { latest: Option, } @@ -35,12 +38,16 @@ pub struct NodeRuntime { } impl NodeRuntime { - pub fn new(http: Arc, background: Arc) -> Arc { - Arc::new(NodeRuntime { - http, - background, - installation_path: Mutex::new(None), - }) + pub fn instance(http: Arc, background: Arc) -> Arc { + RUNTIME_INSTANCE + .get_or_init(|| { + Arc::new(NodeRuntime { + http, + background, + installation_path: Mutex::new(None), + }) + }) + .clone() } pub async fn binary_path(&self) -> Result { @@ -50,55 +57,74 @@ impl NodeRuntime { pub async fn run_npm_subcommand( &self, - directory: &Path, + directory: Option<&Path>, subcommand: &str, args: &[&str], - ) -> Result<()> { + ) -> Result { + let attempt = |installation_path: PathBuf| async move { + let node_binary = installation_path.join("bin/node"); + let npm_file = installation_path.join("bin/npm"); + + if smol::fs::metadata(&node_binary).await.is_err() { + return Err(anyhow!("missing node binary file")); + } + + if smol::fs::metadata(&npm_file).await.is_err() { + return Err(anyhow!("missing npm file")); + } + + let mut command = Command::new(node_binary); + command.arg(npm_file).arg(subcommand).args(args); + + if let Some(directory) = directory { + command.current_dir(directory); + } + + command.output().await.map_err(|e| anyhow!("{e}")) + }; + let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); - - let output = Command::new(node_binary) - .arg(npm_file) - .arg(subcommand) - .args(args) - .current_dir(directory) - .output() - .await?; - - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); + let mut output = attempt(installation_path).await; + if output.is_err() { + let installation_path = self.reinstall().await?; + output = attempt(installation_path).await; + if output.is_err() { + return Err(anyhow!( + "failed to launch npm subcommand {subcommand} subcommand" + )); + } } - Ok(()) + if let Ok(output) = &output { + if !output.status.success() { + return Err(anyhow!( + "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + } + + output.map_err(|e| anyhow!("{e}")) } pub async fn npm_package_latest_version(&self, name: &str) -> Result { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); - - let output = Command::new(node_binary) - .arg(npm_file) - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .args(["-fetch-timeout", "5000"]) - .args(["info", name, "--json"]) - .output() - .await - .context("failed to run npm info")?; - - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } + let output = self + .run_npm_subcommand( + None, + "info", + &[ + name, + "--json", + "-fetch-retry-mintimeout", + "2000", + "-fetch-retry-maxtimeout", + "5000", + "-fetch-timeout", + "5000", + ], + ) + .await?; let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; info.dist_tags @@ -112,41 +138,54 @@ impl NodeRuntime { directory: &Path, packages: impl IntoIterator, ) -> Result<()> { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); + let packages: Vec<_> = packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")) + .collect(); - let output = Command::new(node_binary) - .arg(npm_file) - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .args(["-fetch-timeout", "5000"]) - .arg("install") - .arg("--prefix") - .arg(directory) - .args( - packages - .into_iter() - .map(|(name, version)| format!("{name}@{version}")), - ) - .output() - .await - .context("failed to run npm install")?; + let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); + arguments.extend_from_slice(&[ + "-fetch-retry-mintimeout", + "2000", + "-fetch-retry-maxtimeout", + "5000", + "-fetch-timeout", + "5000", + ]); - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } + self.run_npm_subcommand(Some(directory), "install", &arguments) + .await?; Ok(()) } + async fn reinstall(&self) -> Result { + log::info!("beginnning to reinstall Node runtime"); + let mut installation_path = self.installation_path.lock().await; + + if let Some(task) = installation_path.as_ref().cloned() { + if let Ok(installation_path) = task.await { + smol::fs::remove_dir_all(&installation_path) + .await + .context("node dir removal") + .log_err(); + } + } + + let http = self.http.clone(); + let task = self + .background + .spawn(async move { Self::install(http).await.map_err(Arc::new) }) + .shared(); + + *installation_path = Some(task.clone()); + task.await.map_err(|e| anyhow!("{}", e)) + } + async fn install_if_needed(&self) -> Result { let task = self .installation_path .lock() + .await .get_or_insert_with(|| { let http = self.http.clone(); self.background @@ -155,13 +194,11 @@ impl NodeRuntime { }) .clone(); - match task.await { - Ok(path) => Ok(path), - Err(error) => Err(anyhow!("{}", error)), - } + task.await.map_err(|e| anyhow!("{}", e)) } async fn install(http: Arc) -> Result { + log::info!("installing Node runtime"); let arch = match consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index e6d1466731..0a47d365b5 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -263,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter { fs::rename(first.path(), &repo_root).await?; self.node - .run_npm_subcommand(&repo_root, "install", &[]) + .run_npm_subcommand(Some(&repo_root), "install", &[]) .await?; self.node - .run_npm_subcommand(&repo_root, "run-script", &["compile"]) + .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) .await?; } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index dcdf5c1ea5..6421014a4d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -131,7 +131,7 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned()); languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bcdfe57a46..260c7edf94 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2177,7 +2177,7 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let http = FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::new(http, cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http, cx.background().to_owned()); languages::init(languages.clone(), node_runtime); for name in languages.language_names() { languages.language_for_name(&name);