From abb56b68aca6e0e8088a374970f6be6200a5f1e3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 6 Feb 2020 17:11:46 -0800 Subject: [PATCH] In macOS, download Docker Desktop for the user if it is not installed --- Pipfile | 1 + Pipfile.lock | 94 +++++++++++++++-------- dangerzone/__init__.py | 10 +++ dangerzone/common.py | 2 +- dangerzone/docker_installer.py | 132 +++++++++++++++++++++++++++++++++ dangerzone/tasks.py | 1 - 6 files changed, 209 insertions(+), 31 deletions(-) create mode 100644 dangerzone/docker_installer.py diff --git a/Pipfile b/Pipfile index 244ea81..0ac87f2 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ appdirs = "*" pyxdg = {version = "*",platform_system = "== 'Linux'"} pyobjc-core = {version = "*",platform_system = "== 'Darwin'"} pyobjc-framework-launchservices = {version = "*",platform_system = "== 'Darwin'"} +requests = "*" [dev-packages] black = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d1abe3f..87570ae 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0ec85e528a50d2e61151ca453c5d6fc217b1695fee0673e86db6eea60491de02" + "sha256": "695116394343f7849640651aff1c6957762111bbe2905e3e4d4473be4d6dfb43" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,20 @@ "index": "pypi", "version": "==1.4.3" }, + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -32,6 +46,13 @@ "index": "pypi", "version": "==7.0" }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, "pyobjc-core": { "hashes": [ "sha256:1a0fbf012fb575e0adf8c18cfd4453e657cc2c0deb2660c529bf524ba4c9149a", @@ -81,36 +102,36 @@ }, "pyqt5": { "hashes": [ - "sha256:2b79209aa6e4688f6ac46e6d2694236dcf91db5f3a87270150d0f82082e3d360", + "sha256:2d94ec761fb656707050c68b41958e3a9f755bb1df96c064470f4096d2899e32", "sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b", - "sha256:3d6e315e6e2d6489a2e1e0148d00e784e277c6590c189227d6060f15b9be690a", - "sha256:812233bd155735377e2e9c7eea7a28815f357440334db51788d941e2a8b62f64", - "sha256:be10fa95e6bdc9cad616ebf368c51b3f5748138b2b3a600cf7c4f80b78cb9852" + "sha256:31b142a868152d60c6323e0527edb692fdf05fd7cb4fe2fe9ce07d1ce560221a", + "sha256:713b9a201f5e7b2fca8691373e5d5c8c2552a51d87ca9ffbb1461e34e3241211", + "sha256:a0bfe9fd718bca4de3e33000347e048f73126b6dc46530eb020b0251a638ee9d" ], "index": "pypi", "version": "==5.14.1" }, "pyqt5-sip": { "hashes": [ - "sha256:02d94786bada670ab17a2b62ce95b3cf8e3b40c99d36007593a6334d551840bb", - "sha256:06bc66b50556fb949f14875a4c224423dbf03f972497ccb883fb19b7b7c3b346", - "sha256:091fbbe10a7aebadc0e8897a9449cda08d3c3f663460d812eca3001ca1ed3526", - "sha256:0a067ade558befe4d46335b13d8b602b5044363bfd601419b556d4ec659bca18", - "sha256:1910c1cb5a388d4e59ebb2895d7015f360f3f6eeb1700e7e33e866c53137eb9e", - "sha256:1c7ad791ec86247f35243bbbdd29cd59989afbe0ab678e0a41211f4407f21dd8", - "sha256:3c330ff1f70b3eaa6f63dce9274df996dffea82ad9726aa8e3d6cbe38e986b2f", - "sha256:482a910fa73ee0e36c258d7646ef38f8061774bbc1765a7da68c65056b573341", - "sha256:7695dfafb4f5549ce1290ae643d6508dfc2646a9003c989218be3ce42a1aa422", - "sha256:8274ed50f4ffbe91d0f4cc5454394631edfecd75dc327aa01be8bc5818a57e88", - "sha256:9047d887d97663790d811ac4e0d2e895f1bf2ecac4041691487de40c30239480", - "sha256:9f6ab1417ecfa6c1ce6ce941e0cebc03e3ec9cd9925058043229a5f003ae5e40", - "sha256:b43ba2f18999d41c3df72f590348152e14cd4f6dcea2058c734d688dfb1ec61f", - "sha256:c3ab9ea1bc3f4ce8c57ebc66fb25cd044ef92ed1ca2afa3729854ecc59658905", - "sha256:da69ba17f6ece9a85617743cb19de689f2d63025bf8001e2facee2ec9bcff18f", - "sha256:ef3c7a0bf78674b0dda86ff5809d8495019903a096c128e1f160984b37848f73", - "sha256:fabff832046643cdb93920ddaa8f77344df90768930fbe6bb33d211c4dcd0b5e" + "sha256:1115728644bbadcde5fc8a16e7918bd31915a42dd6fb36b10d4afb78c582753e", + "sha256:1f4289276d355b6521dc2cc956189315da6f13adfb6bbab8f25ebd15e3bce1d4", + "sha256:288c6dc18a8d6a20981c07b715b5695d9b66880778565f3792bc6e38f14f20fb", + "sha256:3f665376d9e52faa9855c3736a66ce6d825f85c86d7774d3c393f09da23f4f86", + "sha256:6b4860c4305980db509415d0af802f111d15f92016c9422eb753bc8883463456", + "sha256:7ffa39763097f64de129cf5cc770a651c3f65d2466b4fe05bef2bd2efbaa38e6", + "sha256:8a18e6f45d482ddfe381789979d09ee13aa6450caa3a0476503891bccb3ac709", + "sha256:8da842d3d7bf8931d1093105fb92702276b6dbb7e801abbaaa869405d616171a", + "sha256:b42021229424aa44e99b3b49520b799fd64ff6ae8b53f79f903bbd85719a28e4", + "sha256:b5b4906445fe980aee76f20400116b6904bf5f30d0767489c13370e42a764020", + "sha256:c1e730a9eb2ec3869ed5d81b0f99f6e2460fb4d77750444c0ec183b771d798f7", + "sha256:cbeeae6b45234a1654657f79943f8bccd3d14b4e7496746c62cf6fbce69442c7", + "sha256:d46b0f8effc554de52a1466b1bd80e5cb4bce635a75ac4e7ad6247c965dec5b9", + "sha256:e28c3abc9b62a1b7e796891648b9f14f8167b31c8e7990fae79654777252bb4d", + "sha256:e6078f5ee7d31c102910d0c277a110e1c2a20a3fc88cd017a39e170120586d3f", + "sha256:ee1a12f09d5af2304273bfd2f6b43835c1467d5ed501a6c95f5405637fa7750a", + "sha256:f314f31f5fd39b06897f013f425137e511d45967150eb4e424a363d8138521c6" ], - "version": "==12.7.0" + "version": "==12.7.1" }, "pyxdg": { "hashes": [ @@ -120,15 +141,30 @@ "index": "pypi", "markers": "platform_system == 'Linux'", "version": "==0.26" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "version": "==1.25.8" } }, "develop": { "altgraph": { "hashes": [ - "sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", - "sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c" + "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa", + "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe" ], - "version": "==0.16.1" + "version": "==0.17" }, "appdirs": { "hashes": [ @@ -163,10 +199,10 @@ }, "macholib": { "hashes": [ - "sha256:b71afea242d5ad4caacbdb79d80e75815d033fbc30f45954b2f3397f39683fd6", - "sha256:c72bda118afe7799570fcb4114315d5c9c5416e48eacf1198da39b4d77201559" + "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432", + "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281" ], - "version": "==1.13" + "version": "==1.14" }, "pathspec": { "hashes": [ diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 82e582e..25a51ef 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -2,10 +2,12 @@ from PyQt5 import QtCore, QtWidgets import os import sys import signal +import platform import click from .common import Common from .main_window import MainWindow +from .docker_installer import is_docker_installed, DockerInstaller dangerzone_version = "0.1.0" @@ -25,6 +27,14 @@ def main(filename): # Common object common = Common(app) + # See if we need to install Docker... + 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) + return + # Main window main_window = MainWindow(common) diff --git a/dangerzone/common.py b/dangerzone/common.py index 6d64c0c..91f79c8 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -50,7 +50,7 @@ class Common(object): # Container runtime if platform.system() == "Darwin": - self.container_runtime = "docker" + self.container_runtime = "/usr/local/bin/docker" else: self.container_runtime = "podman" diff --git a/dangerzone/docker_installer.py b/dangerzone/docker_installer.py new file mode 100644 index 0000000..380cda3 --- /dev/null +++ b/dangerzone/docker_installer.py @@ -0,0 +1,132 @@ +import os +import stat +import requests +import tempfile +from PyQt5 import QtCore, QtGui, QtWidgets + + +def is_docker_installed(common): + # Soes the docker binary exist? + if os.path.isdir("/Applications/Docker.app") and os.path.exists( + common.container_runtime + ): + # Is it executable? + st = os.stat(common.container_runtime) + return bool(st.st_mode & stat.S_IXOTH) + return False + + +class DockerInstaller(QtWidgets.QDialog): + def __init__(self, common): + super(DockerInstaller, self).__init__() + self.common = common + + self.setWindowTitle("dangerzone") + self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path("logo.png"))) + + label = QtWidgets.QLabel("Dangerzone for macOS requires Docker") + label.setStyleSheet("QLabel { font-weight: bold; }") + + self.task_label = QtWidgets.QLabel() + + self.progress = QtWidgets.QProgressBar() + self.progress.setMinimum(0) + + self.install_button = QtWidgets.QPushButton("Install Docker") + self.install_button.setStyleSheet("QPushButton { font-weight: bold; }") + self.install_button.clicked.connect(self.install_clicked) + self.install_button.hide() + self.launch_button = QtWidgets.QPushButton("Launch Docker") + 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) + + 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.addStretch() + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(label) + layout.addWidget(self.task_label) + layout.addWidget(self.progress) + layout.addLayout(buttons_layout) + self.setLayout(layout) + + # 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") + + def update_progress(self, value, maximum): + self.progress.setMaximum(maximum) + self.progress.setValue(value) + + def download_finished(self): + self.task_label.setText("Finished downloading Docker") + self.download_t = None + self.install_button.show() + + def download_failed(self, status_code): + print(f"Download failed: status code {status_code}") + self.download_t = None + + def download(self): + self.task_label.setText("Downloading Docker") + self.download_t = Downloader() + 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 launch(self): + self.download() + return self.exec_() == QtWidgets.QDialog.Accepted + + +class Downloader(QtCore.QThread): + download_finished = QtCore.pyqtSignal() + download_failed = QtCore.pyqtSignal(int) + update_progress = QtCore.pyqtSignal(int, int) + + def __init__(self): + 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") + + def run(self): + print(f"Downloading docker to {self.dmg_filename}") + with requests.get( + "https://download.docker.com/mac/stable/Docker.dmg", stream=True + ) as r: + if r.status_code != 200: + self.download_failed.emit(r.status_code) + return + total_bytes = int(r.headers.get("content-length")) + downloaded_bytes = 0 + + with open(self.dmg_filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: # filter out keep-alive new chunks + downloaded_bytes += f.write(chunk) + + self.update_progress.emit(downloaded_bytes, total_bytes) + + self.download_finished.emit() + diff --git a/dangerzone/tasks.py b/dangerzone/tasks.py index 20adbb3..1b6f720 100644 --- a/dangerzone/tasks.py +++ b/dangerzone/tasks.py @@ -1,6 +1,5 @@ import subprocess import time -import tempfile import os import pipes import platform