Use more elaborate messages for permission denied errors on startup (#22368)

Kirill Bulatov created

Also show all paths with issues, grouping them by the issue kind.

Inspired by https://github.com/zed-industries/zed/issues/22365 ,
https://github.com/zed-industries/zed/issues/20162,
https://github.com/zed-industries/zed/issues/13426, ...

Attempts to provide example `chown` and `chmod` commands on this error,
instead of encouraging to submit another issue.
For now, uses a hardcoded command examples, but we might provide a
better workflow later, e.g. try to automatically run those commands.

I seem to be unable to select the size of the modal, but for something
that's supposed to appear only once, it seems to be ok.


![regular](https://github.com/user-attachments/assets/484feb95-b9c5-445c-87f8-cd1db9337529)


![also_regular](https://github.com/user-attachments/assets/680ea95d-91fe-4b16-b3bc-e241ecd841d4)


![many_kinds](https://github.com/user-attachments/assets/7103161f-754d-413a-94df-62a44e353674)


![exaggerated](https://github.com/user-attachments/assets/b6e70be7-9ab4-4861-afae-5285a554fdba)



Release Notes:

- N/A

Change summary

crates/zed/src/main.rs | 90 +++++++++++++++++++++++++++++++------------
1 file changed, 65 insertions(+), 25 deletions(-)

Detailed changes

crates/zed/src/main.rs 🔗

@@ -10,6 +10,7 @@ use clap::{command, Parser};
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 use client::{parse_zed_link, Client, ProxySettings, UserStore};
 use collab_ui::channel_view::ChannelView;
+use collections::HashMap;
 use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
 use editor::Editor;
 use env_logger::Builder;
@@ -40,7 +41,7 @@ use simplelog::ConfigBuilder;
 use std::{
     env,
     fs::OpenOptions,
-    io::{IsTerminal, Write},
+    io::{self, IsTerminal, Write},
     path::{Path, PathBuf},
     process,
     sync::Arc,
@@ -69,22 +70,59 @@ use util::load_shell_from_passwd;
 #[global_allocator]
 static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
 
-fn fail_to_launch(e: anyhow::Error) {
-    eprintln!("Zed failed to launch: {e:?}");
-    App::new().run(move |cx| {
-        if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| cx.new_view(|_| gpui::Empty)) {
-            window.update(cx, |_, cx| {
-                let response = cx.prompt(gpui::PromptLevel::Critical, "Zed failed to launch", Some(&format!("{e}\n\nFor help resolving this, please open an issue on https://github.com/zed-industries/zed")), &["Exit"]);
+fn files_not_createad_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
+    let message = "Zed failed to launch";
+    let error_details = errors
+        .into_iter()
+        .flat_map(|(kind, paths)| {
+            #[allow(unused_mut)] // for non-unix platforms
+            let mut error_kind_details = match paths.len() {
+                0 => return None,
+                1 => format!(
+                    "{kind} when creating directory {:?}",
+                    paths.first().expect("match arm checks for a single entry")
+                ),
+                _many => format!("{kind} when creating directories {paths:?}"),
+            };
 
-                cx.spawn(|_, mut cx| async move {
-                    response.await?;
-                    cx.update(|cx| {
-                        cx.quit()
+            #[cfg(unix)]
+            {
+                match kind {
+                    io::ErrorKind::PermissionDenied => {
+                        error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\
+                            \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`");
+                    }
+                    _ => {}
+                }
+            }
+
+            Some(error_kind_details)
+        })
+        .collect::<Vec<_>>().join("\n\n");
+
+    eprintln!("{message}: {error_details}");
+    App::new().run(move |cx| {
+        if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| {
+            cx.new_view(|_| gpui::Empty)
+        }) {
+            window
+                .update(cx, |_, cx| {
+                    let response = cx.prompt(
+                        gpui::PromptLevel::Critical,
+                        message,
+                        Some(&error_details),
+                        &["Exit"],
+                    );
+
+                    cx.spawn(|_, mut cx| async move {
+                        response.await?;
+                        cx.update(|cx| cx.quit())
                     })
-                }).detach_and_log_err(cx);
-            }).log_err();
+                    .detach_and_log_err(cx);
+                })
+                .log_err();
         } else {
-            fail_to_open_window(e, cx)
+            fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx)
         }
     })
 }
@@ -139,8 +177,9 @@ fn main() {
     menu::init();
     zed_actions::init();
 
-    if let Err(e) = init_paths() {
-        fail_to_launch(e);
+    let file_errors = init_paths();
+    if !file_errors.is_empty() {
+        files_not_createad_on_launch(file_errors);
         return;
     }
 
@@ -907,8 +946,8 @@ pub(crate) async fn restorable_workspace_locations(
     }
 }
 
-fn init_paths() -> anyhow::Result<()> {
-    for path in [
+fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
+    [
         paths::config_dir(),
         paths::extensions_dir(),
         paths::languages_dir(),
@@ -916,12 +955,13 @@ fn init_paths() -> anyhow::Result<()> {
         paths::logs_dir(),
         paths::temp_dir(),
     ]
-    .iter()
-    {
-        std::fs::create_dir_all(path)
-            .map_err(|e| anyhow!("Could not create directory {:?}: {}", path, e))?;
-    }
-    Ok(())
+    .into_iter()
+    .fold(HashMap::default(), |mut errors, path| {
+        if let Err(e) = std::fs::create_dir_all(path) {
+            errors.entry(e.kind()).or_insert_with(Vec::new).push(path);
+        }
+        errors
+    })
 }
 
 fn init_logger() {
@@ -1001,7 +1041,7 @@ fn init_stdout_logger() {
 }
 
 fn stdout_is_a_pty() -> bool {
-    std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal()
+    std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
 }
 
 #[derive(Parser, Debug)]