Repository navigation
fix(desktop): start an installed Docker Desktop instead of installing Podman beside it #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dustywusty
wants to merge
6
commits into
main
Choose a base branch
from
dustywusty/docker-desktop-not-podman
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+527
−84
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3479d27
fix(desktop): start Docker Desktop when it is installed, install it w…
dustywusty 3e892f4
fix(desktop): name the exit status when opening Docker Desktop off macOS
dustywusty 36f6969
fix(desktop): Docker first, Podman only when there is no Docker
dustywusty 8ce0725
fix(desktop): skip the Podman machine once Docker Desktop answers; fi…
dustywusty 048bf48
fix(desktop): place no Compose after starting Docker Desktop
dustywusty e1df54e
Merge upstream main into dustywusty/docker-desktop-not-podman
dustywusty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| //! Docker Desktop: started when it is installed and not running. | ||
| //! | ||
| //! **Docker first, Podman when there is no Docker.** A Mac or a Windows machine with Docker Desktop | ||
| //! installed but not running is not a machine without an engine: starting it is the repair, not | ||
| //! installing a second engine beside it. A machine with no Docker at all gets Podman (see | ||
| //! `install::install_engine_with`). OpenBot never installs Docker Desktop itself. | ||
| //! | ||
| //! Linux is not handled here: Docker there is the distribution's Docker Engine, not Docker Desktop. | ||
|
|
||
| use std::path::PathBuf; | ||
| use std::time::{Duration, Instant}; | ||
|
|
||
| use crate::engine::{self, Engine}; | ||
| use crate::problem::Problem; | ||
|
|
||
| /// How long an installed Docker Desktop gets to start answering. A first start shows Docker's | ||
| /// terms and a helper prompt in Docker's own window, so this allows for somebody reading them. | ||
| const START_PATIENCE: Duration = Duration::from_secs(5 * 60); | ||
| const POLL: Duration = Duration::from_secs(2); | ||
|
|
||
| /// Where Docker Desktop is installed on this machine, if it is. | ||
| pub fn installed_app() -> Option<PathBuf> { | ||
| installed_app_in(&app_places()) | ||
| } | ||
|
|
||
| fn installed_app_in(places: &[PathBuf]) -> Option<PathBuf> { | ||
| places.iter().find(|place| place.exists()).cloned() | ||
| } | ||
|
|
||
| pub(crate) fn app_places() -> Vec<PathBuf> { | ||
| #[cfg(target_os = "macos")] | ||
| { | ||
| let mut places = vec![PathBuf::from("/Applications/Docker.app")]; | ||
| if let Some(home) = std::env::var_os("HOME") { | ||
| places.push(PathBuf::from(home).join("Applications/Docker.app")); | ||
| } | ||
| places | ||
| } | ||
| #[cfg(target_os = "windows")] | ||
| { | ||
| std::env::var_os("ProgramFiles") | ||
| .map(|files| PathBuf::from(files).join("Docker\\Docker\\Docker Desktop.exe")) | ||
| .into_iter() | ||
| .collect() | ||
| } | ||
| #[cfg(target_os = "linux")] | ||
| { | ||
| Vec::new() | ||
| } | ||
| } | ||
|
|
||
| /// Whether Docker answers now. | ||
| pub fn answers() -> bool { | ||
| engine::Address::new(Engine::Docker, None).responds() | ||
| } | ||
|
|
||
| /// What `start_with` needs from the machine, so its decisions can be tested without one. | ||
| pub(crate) struct Machine<'a> { | ||
| pub answers: &'a dyn Fn() -> bool, | ||
| pub open: &'a dyn Fn() -> Result<(), Problem>, | ||
| pub sleep: &'a dyn Fn(Duration), | ||
| pub now: &'a dyn Fn() -> Instant, | ||
| } | ||
|
|
||
| /// Docker answering, by opening the installed Docker Desktop. Answers with the sentence for the | ||
| /// step row; `progress` is each line before that. See the module comment. | ||
| pub fn start(progress: &mut dyn FnMut(String)) -> Result<String, Problem> { | ||
| start_with( | ||
| &Machine { | ||
| answers: &answers, | ||
| open: &open, | ||
| sleep: &std::thread::sleep, | ||
| now: &Instant::now, | ||
| }, | ||
| progress, | ||
| ) | ||
| } | ||
|
|
||
| pub(crate) fn start_with( | ||
| machine: &Machine, | ||
| progress: &mut dyn FnMut(String), | ||
| ) -> Result<String, Problem> { | ||
| if (machine.answers)() { | ||
| return Ok("Docker Desktop is running.".into()); | ||
| } | ||
| progress("Starting Docker Desktop. Accept anything it asks in its own window.".into()); | ||
| (machine.open)()?; | ||
|
|
||
| let started = (machine.now)(); | ||
| while (machine.now)().duration_since(started) < START_PATIENCE { | ||
| if (machine.answers)() { | ||
| return Ok("Docker Desktop is running.".into()); | ||
| } | ||
| (machine.sleep)(POLL); | ||
| } | ||
| Err(Problem::with( | ||
| "Docker Desktop did not start. Open Docker Desktop, finish anything it asks for, then try \ | ||
| again.", | ||
| format!( | ||
| "Docker did not answer within {}s of opening Docker Desktop", | ||
| START_PATIENCE.as_secs() | ||
| ), | ||
| )) | ||
| } | ||
|
|
||
| /// Open Docker Desktop the way the person would. | ||
| fn open() -> Result<(), Problem> { | ||
| let Some(app) = installed_app() else { | ||
| return Err(Problem::plain( | ||
| "OpenBot cannot find Docker Desktop. Install it, then try again.", | ||
| )); | ||
| }; | ||
| #[cfg(target_os = "macos")] | ||
| let spawned = crate::quiet::command("open").arg("-a").arg(&app).status(); | ||
| // Starting Docker Desktop's own executable is the whole job, so a started process counts as | ||
| // success and its exit is not waited for. The status is named: `Default::default()` here left | ||
| // the type to inference, which had nothing to go on, and failed to compile off macOS. | ||
| #[cfg(not(target_os = "macos"))] | ||
| let spawned = crate::quiet::command(&app) | ||
| .spawn() | ||
| .map(|_| std::process::ExitStatus::default()); | ||
| match spawned { | ||
| Ok(status) if status.success() => Ok(()), | ||
| Ok(status) => Err(Problem::with( | ||
| "OpenBot could not open Docker Desktop. Open it yourself, then try again.", | ||
| format!("opening {} ended with {status}", app.display()), | ||
| )), | ||
| Err(error) => Err(Problem::with( | ||
| "OpenBot could not open Docker Desktop. Open it yourself, then try again.", | ||
| format!("opening {}: {error}", app.display()), | ||
| )), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use std::cell::{Cell, RefCell}; | ||
|
|
||
| /// A machine whose Docker answers after `answers_after` polls, counting what was done to it. | ||
| struct Fake { | ||
| polls: Cell<u32>, | ||
| answers_after: Option<u32>, | ||
| opened: Cell<u32>, | ||
| clock: Cell<Instant>, | ||
| } | ||
|
|
||
| impl Fake { | ||
| fn new(answers_after: Option<u32>) -> Self { | ||
| Self { | ||
| polls: Cell::new(0), | ||
| answers_after, | ||
| opened: Cell::new(0), | ||
| clock: Cell::new(Instant::now()), | ||
| } | ||
| } | ||
|
|
||
| fn run(&self) -> (Result<String, Problem>, Vec<String>) { | ||
| let said = RefCell::new(Vec::new()); | ||
| let answers = || { | ||
| let polls = self.polls.get(); | ||
| self.polls.set(polls + 1); | ||
| self.opened.get() > 0 && self.answers_after.is_some_and(|after| polls >= after) | ||
| }; | ||
| let result = start_with( | ||
| &Machine { | ||
| answers: &answers, | ||
| open: &|| { | ||
| self.opened.set(self.opened.get() + 1); | ||
| Ok(()) | ||
| }, | ||
| sleep: &|step| self.clock.set(self.clock.get() + step), | ||
| now: &|| self.clock.get(), | ||
| }, | ||
| &mut |line| said.borrow_mut().push(line), | ||
| ); | ||
| (result, said.into_inner()) | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn an_answering_docker_is_used_as_it_is() { | ||
| let fake = Fake::new(Some(0)); | ||
| fake.opened.set(1); | ||
| let (result, said) = fake.run(); | ||
| assert_eq!(result.unwrap(), "Docker Desktop is running."); | ||
| assert_eq!(fake.opened.get(), 1, "nothing was opened again"); | ||
| assert!(said.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn an_installed_docker_desktop_that_is_stopped_is_started() { | ||
| let fake = Fake::new(Some(3)); | ||
| let (result, said) = fake.run(); | ||
| assert_eq!(result.unwrap(), "Docker Desktop is running."); | ||
| assert_eq!(fake.opened.get(), 1); | ||
| assert_eq!( | ||
| said, | ||
| ["Starting Docker Desktop. Accept anything it asks in its own window."] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_docker_desktop_that_never_answers_says_so_after_its_patience() { | ||
| let fake = Fake::new(None); | ||
| let (result, _) = fake.run(); | ||
| let problem = result.unwrap_err(); | ||
| assert!(problem.said.starts_with("Docker Desktop did not start.")); | ||
| assert!(problem.detail.unwrap().contains("300s")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn an_installed_app_is_found_in_any_of_its_places() { | ||
| let root = crate::test_support::temp_root("docker-desktop-places"); | ||
| let first = root.join("Applications/Docker.app"); | ||
| let second = root.join("home/Applications/Docker.app"); | ||
| assert_eq!(installed_app_in(&[first.clone(), second.clone()]), None); | ||
| std::fs::create_dir_all(&second).unwrap(); | ||
| assert_eq!(installed_app_in(&[first, second.clone()]), Some(second)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.