From 13aac3348a169dc19784c7279543f4310bcbe692 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 7 Feb 2020 11:29:43 -0800 Subject: [PATCH] After downloading Docker, install it, launch it for the first time, and once it's ready, re-launch dangerzone --- dangerzone/__init__.py | 18 +++- dangerzone/docker_installer.py | 149 ++++++++++++++++++++++++++++----- 2 files changed, 141 insertions(+), 26 deletions(-) diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 25a51ef..97c09ed 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -4,10 +4,11 @@ import sys import signal import platform import click +import time from .common import Common from .main_window import MainWindow -from .docker_installer import is_docker_installed, DockerInstaller +from .docker_installer import is_docker_installed, is_docker_ready, DockerInstaller dangerzone_version = "0.1.0" @@ -31,8 +32,19 @@ def main(filename): if platform.system() == "Darwin" and not is_docker_installed(common): print("Docker is not installed!") docker_installer = DockerInstaller(common) - if docker_installer.launch(): - main(filename) + if docker_installer.start(): + # When installer finished, wait up to 20 minutes for the user to launch it + for i in range(120): + if is_docker_installed(common) and is_docker_ready(common): + main(filename) + return + + print("Waiting for docker to be available ...") + time.sleep(1) + + # Give up + print("Docker not available, giving up") + return # Main window diff --git a/dangerzone/docker_installer.py b/dangerzone/docker_installer.py index 380cda3..b3463b5 100644 --- a/dangerzone/docker_installer.py +++ b/dangerzone/docker_installer.py @@ -2,11 +2,14 @@ import os import stat import requests import tempfile +import subprocess +import shutil +import time from PyQt5 import QtCore, QtGui, QtWidgets def is_docker_installed(common): - # Soes the docker binary exist? + # Does the docker binary exist? if os.path.isdir("/Applications/Docker.app") and os.path.exists( common.container_runtime ): @@ -16,6 +19,15 @@ def is_docker_installed(common): return False +def is_docker_ready(common): + # Run `docker ps` without an error + try: + subprocess.run([common.container_runtime, "ps"], check=True) + return True + except subprocess.CalledProcessError: + return False + + class DockerInstaller(QtWidgets.QDialog): def __init__(self, common): super(DockerInstaller, self).__init__() @@ -26,8 +38,10 @@ class DockerInstaller(QtWidgets.QDialog): label = QtWidgets.QLabel("Dangerzone for macOS requires Docker") label.setStyleSheet("QLabel { font-weight: bold; }") + label.setAlignment(QtCore.Qt.AlignCenter) self.task_label = QtWidgets.QLabel() + self.task_label.setAlignment(QtCore.Qt.AlignCenter) self.progress = QtWidgets.QProgressBar() self.progress.setMinimum(0) @@ -40,14 +54,14 @@ class DockerInstaller(QtWidgets.QDialog): self.launch_button.setStyleSheet("QPushButton { font-weight: bold; }") self.launch_button.clicked.connect(self.launch_clicked) self.launch_button.hide() - cancel_button = QtWidgets.QPushButton("Cancel") - cancel_button.clicked.connect(self.cancel_clicked) + self.cancel_button = QtWidgets.QPushButton("Cancel") + self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addStretch() buttons_layout.addWidget(self.install_button) buttons_layout.addWidget(self.launch_button) - buttons_layout.addWidget(cancel_button) + buttons_layout.addWidget(self.cancel_button) buttons_layout.addStretch() layout = QtWidgets.QVBoxLayout() @@ -55,29 +69,27 @@ class DockerInstaller(QtWidgets.QDialog): layout.addWidget(self.task_label) layout.addWidget(self.progress) layout.addLayout(buttons_layout) + layout.addStretch() self.setLayout(layout) + self.tmp_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-docker-") + self.dmg_filename = os.path.join(self.tmp_dir.name, "Docker.dmg") + # Threads self.download_t = None - - def cancel_clicked(self): - if self.download_t: - self.download_t.terminate() - self.reject() - - def install_clicked(self): - print("Install clicked") - - def launch_clicked(self): - print("Launch clicked") + self.install_t = None def update_progress(self, value, maximum): self.progress.setMaximum(maximum) self.progress.setValue(value) + def update_task_label(self, s): + self.task_label.setText(s) + def download_finished(self): self.task_label.setText("Finished downloading Docker") self.download_t = None + self.progress.hide() self.install_button.show() def download_failed(self, status_code): @@ -86,17 +98,72 @@ class DockerInstaller(QtWidgets.QDialog): def download(self): self.task_label.setText("Downloading Docker") - self.download_t = Downloader() + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.start_download) + self.timer.setSingleShot(True) + self.timer.start(10) + + def start_download(self): + self.download_t = Downloader(self.dmg_filename) self.download_t.download_finished.connect(self.download_finished) self.download_t.download_failed.connect(self.download_failed) self.download_t.update_progress.connect(self.update_progress) self.download_t.start() - def install(self): - pass + def install_finished(self): + self.task_label.setText("Finished installing Docker") + self.install_t = None + self.progress.hide() + self.install_button.hide() + self.launch_button.show() + self.cancel_button.setEnabled(True) - def launch(self): - self.download() + def install_failed(self, exception): + print(f"Install failed: {exception}") + self.task_label.setText(f"Install failed: {exception}") + self.install_t = None + self.cancel_button.setEnabled(True) + + def install_clicked(self): + self.task_label.setText("Installing Docker") + self.install_button.hide() + self.cancel_button.setEnabled(False) + + self.progress.setMinimum(0) + self.progress.setMaximum(0) + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.start_installer) + self.timer.setSingleShot(True) + self.timer.start(10) + + def start_installer(self): + self.install_t = Installer(self.dmg_filename) + self.install_t.install_finished.connect(self.install_finished) + self.install_t.install_failed.connect(self.install_failed) + self.install_t.update_task_label.connect(self.update_task_label) + self.install_t.start() + + def launch_clicked(self): + print("Launching Docker") + self.accept() + subprocess.Popen(["open", "-a", "Docker.app"]) + + def cancel_clicked(self): + if self.download_t: + self.download_t.terminate() + if self.install_t: + self.install_t.terminate() + self.reject() + + def start(self): + if not os.path.isdir("/Applications/Docker.app"): + self.download() + else: + self.task_label.setText("Docker is installed, but you must launch it first") + self.progress.hide() + self.launch_button.show() return self.exec_() == QtWidgets.QDialog.Accepted @@ -105,10 +172,9 @@ class Downloader(QtCore.QThread): download_failed = QtCore.pyqtSignal(int) update_progress = QtCore.pyqtSignal(int, int) - def __init__(self): + def __init__(self, dmg_filename): super(Downloader, self).__init__() - self.tmp_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-docker-") - self.dmg_filename = os.path.join(self.tmp_dir.name, "Docker.dmg") + self.dmg_filename = dmg_filename def run(self): print(f"Downloading docker to {self.dmg_filename}") @@ -130,3 +196,40 @@ class Downloader(QtCore.QThread): self.download_finished.emit() + +class Installer(QtCore.QThread): + install_finished = QtCore.pyqtSignal() + install_failed = QtCore.pyqtSignal(str) + update_task_label = QtCore.pyqtSignal(str) + + def __init__(self, dmg_filename): + super(Installer, self).__init__() + self.dmg_filename = dmg_filename + + def run(self): + print(f"Installing Docker") + try: + # Mount the dmg + self.update_task_label.emit(f"Mounting Docker.dmg") + subprocess.run(["hdiutil", "attach", "-nobrowse", self.dmg_filename]) + + # Copy Docker.app to Applications + self.update_task_label.emit("Copying Docker into Applications") + shutil.copytree("/Volumes/Docker/Docker.app", "/Applications/Docker.app") + + # Sync + self.update_task_label.emit("Syncing filesystem") + subprocess.run(["sync"]) + + # Wait, to prevent early crash + time.sleep(1) + + # Unmount the dmg + self.update_task_label.emit(f"Unmounting /Volumes/Docker") + subprocess.run(["hdiutil", "detach", "/Volumes/Docker"]) + + self.install_finished.emit() + + except Exception as e: + self.install_failed.emit(str(e)) + return