cli/util/
errors.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5use crate::{
6	constants::{APPLICATION_NAME, CONTROL_PORT, DOCUMENTATION_URL, QUALITYLESS_PRODUCT_NAME},
7	rpc::ResponseError,
8};
9use std::fmt::Display;
10use thiserror::Error;
11
12// Wraps another error with additional info.
13#[derive(Debug, Clone)]
14pub struct WrappedError {
15	message: String,
16	original: String,
17}
18
19impl std::fmt::Display for WrappedError {
20	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
21		write!(f, "{}: {}", self.message, self.original)
22	}
23}
24
25impl std::error::Error for WrappedError {
26	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
27		None
28	}
29}
30
31impl WrappedError {
32	// fn new(original: Box<dyn std::error::Error>, message: String) -> WrappedError {
33	//     WrappedError { message, original }
34	// }
35}
36
37impl From<reqwest::Error> for WrappedError {
38	fn from(e: reqwest::Error) -> WrappedError {
39		WrappedError {
40			message: format!(
41				"error requesting {}",
42				e.url().map_or("<unknown>", |u| u.as_str())
43			),
44			original: format!("{}", e),
45		}
46	}
47}
48
49pub fn wrapdbg<T, S>(original: T, message: S) -> WrappedError
50where
51	T: std::fmt::Debug,
52	S: Into<String>,
53{
54	WrappedError {
55		message: message.into(),
56		original: format!("{:?}", original),
57	}
58}
59
60pub fn wrap<T, S>(original: T, message: S) -> WrappedError
61where
62	T: Display,
63	S: Into<String>,
64{
65	WrappedError {
66		message: message.into(),
67		original: format!("{}", original),
68	}
69}
70
71// Error generated by an unsuccessful HTTP response
72#[derive(Debug)]
73pub struct StatusError {
74	pub url: String,
75	pub status_code: u16,
76	pub body: String,
77}
78
79impl std::fmt::Display for StatusError {
80	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
81		write!(
82			f,
83			"error requesting {}: {} {}",
84			self.url, self.status_code, self.body
85		)
86	}
87}
88
89impl StatusError {
90	pub async fn from_res(res: reqwest::Response) -> Result<StatusError, AnyError> {
91		let status_code = res.status().as_u16();
92		let url = res.url().to_string();
93		let body = res.text().await.map_err(|e| {
94			wrap(
95				e,
96				format!(
97					"failed to read response body on {} code from {}",
98					status_code, url
99				),
100			)
101		})?;
102
103		Ok(StatusError {
104			url,
105			status_code,
106			body,
107		})
108	}
109}
110
111// When the provided connection token doesn't match the one used to set up the original VS Code Server
112// This is most likely due to a new user joining.
113#[derive(Debug)]
114pub struct MismatchConnectionToken(pub String);
115
116impl std::fmt::Display for MismatchConnectionToken {
117	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
118		write!(f, "{}", self.0)
119	}
120}
121
122// When the VS Code server has an unrecognized extension (rather than zip or gz)
123#[derive(Debug)]
124pub struct InvalidServerExtensionError(pub String);
125
126impl std::fmt::Display for InvalidServerExtensionError {
127	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
128		write!(f, "invalid server extension '{}'", self.0)
129	}
130}
131
132// When the tunnel fails to open
133#[derive(Debug, Clone)]
134pub struct DevTunnelError(pub String);
135
136impl std::fmt::Display for DevTunnelError {
137	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
138		write!(f, "could not open tunnel: {}", self.0)
139	}
140}
141
142impl std::error::Error for DevTunnelError {
143	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144		None
145	}
146}
147
148// When the server was downloaded, but the entrypoint scripts don't exist.
149#[derive(Debug)]
150pub struct MissingEntrypointError();
151
152impl std::fmt::Display for MissingEntrypointError {
153	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
154		write!(f, "Missing entrypoints in server download. Most likely this is a corrupted download. Please retry")
155	}
156}
157
158#[derive(Debug)]
159pub struct SetupError(pub String);
160
161impl std::fmt::Display for SetupError {
162	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
163		write!(
164			f,
165			"{}\n\nMore info at {}/remote/linux",
166			DOCUMENTATION_URL.unwrap_or("<docs>"),
167			self.0
168		)
169	}
170}
171
172#[derive(Debug)]
173pub struct NoHomeForLauncherError();
174
175impl std::fmt::Display for NoHomeForLauncherError {
176	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177		write!(
178            f,
179            "No $HOME variable was found in your environment. Either set it, or specify a `--data-dir` manually when invoking the launcher.",
180        )
181	}
182}
183
184#[derive(Debug)]
185pub struct InvalidTunnelName(pub String);
186
187impl std::fmt::Display for InvalidTunnelName {
188	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
189		write!(f, "{}", &self.0)
190	}
191}
192
193#[derive(Debug)]
194pub struct TunnelCreationFailed(pub String, pub String);
195
196impl std::fmt::Display for TunnelCreationFailed {
197	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
198		write!(
199			f,
200			"Could not create tunnel with name: {}\nReason: {}",
201			&self.0, &self.1
202		)
203	}
204}
205
206#[derive(Debug)]
207pub struct TunnelHostFailed(pub String);
208
209impl std::fmt::Display for TunnelHostFailed {
210	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
211		write!(f, "{}", &self.0)
212	}
213}
214
215#[derive(Debug)]
216pub struct ExtensionInstallFailed(pub String);
217
218impl std::fmt::Display for ExtensionInstallFailed {
219	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
220		write!(f, "Extension install failed: {}", &self.0)
221	}
222}
223
224#[derive(Debug)]
225pub struct MismatchedLaunchModeError();
226
227impl std::fmt::Display for MismatchedLaunchModeError {
228	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
229		write!(f, "A server is already running, but it was not launched in the same listening mode (port vs. socket) as this request")
230	}
231}
232
233#[derive(Debug)]
234pub struct NoAttachedServerError();
235
236impl std::fmt::Display for NoAttachedServerError {
237	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
238		write!(f, "No server is running")
239	}
240}
241
242#[derive(Debug)]
243pub struct RefreshTokenNotAvailableError();
244
245impl std::fmt::Display for RefreshTokenNotAvailableError {
246	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
247		write!(f, "Refresh token not available, authentication is required")
248	}
249}
250
251#[derive(Debug)]
252pub struct NoInstallInUserProvidedPath(pub String);
253
254impl std::fmt::Display for NoInstallInUserProvidedPath {
255	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
256		write!(
257            f,
258            "No {} installation could be found in {}. You can run `{} --use-quality=stable` to switch to the latest stable version of {}.",
259						QUALITYLESS_PRODUCT_NAME,
260            self.0,
261						APPLICATION_NAME,
262						QUALITYLESS_PRODUCT_NAME
263        )
264	}
265}
266
267#[derive(Debug)]
268pub struct InvalidRequestedVersion();
269
270impl std::fmt::Display for InvalidRequestedVersion {
271	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
272		write!(
273            f,
274            "The reqested version is invalid, expected one of 'stable', 'insiders', version number (x.y.z), or absolute path.",
275        )
276	}
277}
278
279#[derive(Debug)]
280pub struct UserCancelledInstallation();
281
282impl std::fmt::Display for UserCancelledInstallation {
283	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
284		write!(f, "Installation aborted.")
285	}
286}
287
288#[derive(Debug)]
289pub struct CannotForwardControlPort();
290
291impl std::fmt::Display for CannotForwardControlPort {
292	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
293		write!(f, "Cannot forward or unforward port {}.", CONTROL_PORT)
294	}
295}
296
297#[derive(Debug)]
298pub struct ServerHasClosed();
299
300impl std::fmt::Display for ServerHasClosed {
301	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
302		write!(f, "Request cancelled because the server has closed")
303	}
304}
305
306#[derive(Debug)]
307pub struct ServiceAlreadyRegistered();
308
309impl std::fmt::Display for ServiceAlreadyRegistered {
310	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
311		write!(f, "Already registered the service. Run `{} tunnel service uninstall` to unregister it first", APPLICATION_NAME)
312	}
313}
314
315#[derive(Debug)]
316pub struct WindowsNeedsElevation(pub String);
317
318impl std::fmt::Display for WindowsNeedsElevation {
319	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
320		writeln!(f, "{}", self.0)?;
321		writeln!(f)?;
322		writeln!(f, "You may need to run this command as an administrator:")?;
323		writeln!(f, " 1. Open the start menu and search for Powershell")?;
324		writeln!(f, " 2. Right click and 'Run as administrator'")?;
325		if let Ok(exe) = std::env::current_exe() {
326			writeln!(
327				f,
328				" 3. Run &'{}' '{}'",
329				exe.display(),
330				std::env::args().skip(1).collect::<Vec<_>>().join("' '")
331			)
332		} else {
333			writeln!(f, " 3. Run the same command again",)
334		}
335	}
336}
337
338#[derive(Debug)]
339pub struct InvalidRpcDataError(pub String);
340
341impl std::fmt::Display for InvalidRpcDataError {
342	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343		write!(f, "parse error: {}", self.0)
344	}
345}
346
347#[derive(Debug)]
348pub struct CorruptDownload(pub String);
349
350impl std::fmt::Display for CorruptDownload {
351	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
352		write!(
353			f,
354			"Error updating the {} CLI: {}",
355			QUALITYLESS_PRODUCT_NAME, self.0
356		)
357	}
358}
359
360#[derive(Debug)]
361pub struct MissingHomeDirectory();
362
363impl std::fmt::Display for MissingHomeDirectory {
364	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
365		write!(f, "Could not find your home directory. Please ensure this command is running in the context of an normal user.")
366	}
367}
368
369#[derive(Debug)]
370pub struct OAuthError {
371	pub error: String,
372	pub error_description: Option<String>,
373}
374
375impl std::fmt::Display for OAuthError {
376	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
377		write!(
378			f,
379			"Error getting authorization: {} {}",
380			self.error,
381			self.error_description.as_deref().unwrap_or("")
382		)
383	}
384}
385
386// Makes an "AnyError" enum that contains any of the given errors, in the form
387// `enum AnyError { FooError(FooError) }` (when given `makeAnyError!(FooError)`).
388// Useful to easily deal with application error types without making tons of "From"
389// clauses.
390macro_rules! makeAnyError {
391    ($($e:ident),*) => {
392
393        #[derive(Debug)]
394        #[allow(clippy::enum_variant_names)]
395        pub enum AnyError {
396            $($e($e),)*
397        }
398
399        impl std::fmt::Display for AnyError {
400            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
401                match *self {
402                    $(AnyError::$e(ref e) => e.fmt(f),)*
403                }
404            }
405        }
406
407        impl std::error::Error for AnyError {
408            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
409                None
410            }
411        }
412
413        $(impl From<$e> for AnyError {
414            fn from(e: $e) -> AnyError {
415                AnyError::$e(e)
416            }
417        })*
418    };
419}
420
421#[derive(Debug)]
422pub struct DbusConnectFailedError(pub String);
423
424impl Display for DbusConnectFailedError {
425	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
426		let mut str = String::new();
427		str.push_str("Error creating dbus session. This command uses systemd for managing services, you should check that systemd is installed and under your user.");
428
429		if std::env::var("WSL_DISTRO_NAME").is_ok() {
430			str.push_str("\n\nTo enable systemd on WSL, check out: https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/.\n\n");
431		}
432
433		str.push_str("If running `systemctl status` works, systemd is ok, but your session dbus may not be. You might need to:\n\n- Install the `dbus-user-session` package, and reboot if it was not installed\n- Start the user dbus session with `systemctl --user enable dbus --now`.\n\nThe error encountered was: ");
434		str.push_str(&self.0);
435		str.push('\n');
436
437		write!(f, "{}", str)
438	}
439}
440
441/// Internal errors in the VS Code CLI.
442/// Note: other error should be migrated to this type gradually
443#[derive(Error, Debug)]
444pub enum CodeError {
445	#[error("could not connect to socket/pipe: {0:?}")]
446	AsyncPipeFailed(std::io::Error),
447	#[error("could not listen on socket/pipe: {0:?}")]
448	AsyncPipeListenerFailed(std::io::Error),
449	#[error("could not create singleton lock file: {0:?}")]
450	SingletonLockfileOpenFailed(std::io::Error),
451	#[error("could not read singleton lock file: {0:?}")]
452	SingletonLockfileReadFailed(rmp_serde::decode::Error),
453	#[error("the process holding the singleton lock file (pid={0}) exited")]
454	SingletonLockedProcessExited(u32),
455	#[error("no tunnel process is currently running")]
456	NoRunningTunnel,
457	#[error("rpc call failed: {0:?}")]
458	TunnelRpcCallFailed(ResponseError),
459	#[cfg(windows)]
460	#[error("the windows app lock {0} already exists")]
461	AppAlreadyLocked(String),
462	#[cfg(windows)]
463	#[error("could not get windows app lock: {0:?}")]
464	AppLockFailed(std::io::Error),
465	#[error("failed to run command \"{command}\" (code {code}): {output}")]
466	CommandFailed {
467		command: String,
468		code: i32,
469		output: String,
470	},
471
472	#[error("platform not currently supported: {0}")]
473	UnsupportedPlatform(String),
474	#[error("This machine does not meet {name}'s prerequisites, expected either...: {bullets}")]
475	PrerequisitesFailed { name: &'static str, bullets: String },
476	#[error("failed to spawn process: {0:?}")]
477	ProcessSpawnFailed(std::io::Error),
478	#[error("failed to handshake spawned process: {0:?}")]
479	ProcessSpawnHandshakeFailed(std::io::Error),
480	#[error("download appears corrupted, please retry ({0})")]
481	CorruptDownload(&'static str),
482	#[error("port forwarding is not available in this context")]
483	PortForwardingNotAvailable,
484	#[error("'auth' call required")]
485	ServerAuthRequired,
486	#[error("challenge not yet issued")]
487	AuthChallengeNotIssued,
488	#[error("challenge token is invalid")]
489	AuthChallengeBadToken,
490	#[error("unauthorized client refused")]
491	AuthMismatch,
492	#[error("keyring communication timed out after 5s")]
493	KeyringTimeout,
494	#[error("no host is connected to the tunnel relay")]
495	NoTunnelEndpoint,
496	#[error("could not parse `host`: {0}")]
497	InvalidHostAddress(std::net::AddrParseError),
498	#[error("could not start server on the given host/port: {0}")]
499	CouldNotListenOnInterface(hyper::Error),
500	#[error(
501		"Run this command again with --accept-server-license-terms to indicate your agreement."
502	)]
503	NeedsInteractiveLegalConsent,
504	#[error("Sorry, you cannot use this CLI without accepting the terms.")]
505	DeniedLegalConset,
506	#[error("The server is not yet downloaded, try again shortly.")]
507	ServerNotYetDownloaded,
508	#[error("An error was encountered downloading the server, please retry: {0}")]
509	ServerDownloadError(String),
510	#[error("Updates are are not available: {0}")]
511	UpdatesNotConfigured(&'static str),
512	// todo: can be specialized when update service is moved to CodeErrors
513	#[error("Could not check for update: {0}")]
514	UpdateCheckFailed(String),
515	#[error("Could not write connection token file: {0}")]
516	CouldNotCreateConnectionTokenFile(std::io::Error)
517}
518
519makeAnyError!(
520	MismatchConnectionToken,
521	DevTunnelError,
522	StatusError,
523	WrappedError,
524	InvalidServerExtensionError,
525	MissingEntrypointError,
526	SetupError,
527	NoHomeForLauncherError,
528	TunnelCreationFailed,
529	TunnelHostFailed,
530	InvalidTunnelName,
531	ExtensionInstallFailed,
532	MismatchedLaunchModeError,
533	NoAttachedServerError,
534	RefreshTokenNotAvailableError,
535	NoInstallInUserProvidedPath,
536	UserCancelledInstallation,
537	InvalidRequestedVersion,
538	CannotForwardControlPort,
539	ServerHasClosed,
540	ServiceAlreadyRegistered,
541	WindowsNeedsElevation,
542	CorruptDownload,
543	MissingHomeDirectory,
544	OAuthError,
545	InvalidRpcDataError,
546	CodeError,
547	DbusConnectFailedError
548);
549
550impl From<reqwest::Error> for AnyError {
551	fn from(e: reqwest::Error) -> AnyError {
552		AnyError::WrappedError(WrappedError::from(e))
553	}
554}