Merge pull request #7 from firstlookmedia/2_macos_docker

Install docker desktop in macOS if it's not already installed
This commit is contained in:
Micah Lee 2020-02-07 15:46:42 -08:00 committed by GitHub
commit a47325545c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 337 additions and 33 deletions

View file

@ -10,6 +10,7 @@ appdirs = "*"
pyxdg = {version = "*",platform_system = "== 'Linux'"} pyxdg = {version = "*",platform_system = "== 'Linux'"}
pyobjc-core = {version = "*",platform_system = "== 'Darwin'"} pyobjc-core = {version = "*",platform_system = "== 'Darwin'"}
pyobjc-framework-launchservices = {version = "*",platform_system = "== 'Darwin'"} pyobjc-framework-launchservices = {version = "*",platform_system = "== 'Darwin'"}
requests = "*"
[dev-packages] [dev-packages]
black = "*" black = "*"

94
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0ec85e528a50d2e61151ca453c5d6fc217b1695fee0673e86db6eea60491de02" "sha256": "695116394343f7849640651aff1c6957762111bbe2905e3e4d4473be4d6dfb43"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -24,6 +24,20 @@
"index": "pypi", "index": "pypi",
"version": "==1.4.3" "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": { "click": {
"hashes": [ "hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
@ -32,6 +46,13 @@
"index": "pypi", "index": "pypi",
"version": "==7.0" "version": "==7.0"
}, },
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"pyobjc-core": { "pyobjc-core": {
"hashes": [ "hashes": [
"sha256:1a0fbf012fb575e0adf8c18cfd4453e657cc2c0deb2660c529bf524ba4c9149a", "sha256:1a0fbf012fb575e0adf8c18cfd4453e657cc2c0deb2660c529bf524ba4c9149a",
@ -81,36 +102,36 @@
}, },
"pyqt5": { "pyqt5": {
"hashes": [ "hashes": [
"sha256:2b79209aa6e4688f6ac46e6d2694236dcf91db5f3a87270150d0f82082e3d360", "sha256:2d94ec761fb656707050c68b41958e3a9f755bb1df96c064470f4096d2899e32",
"sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b", "sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b",
"sha256:3d6e315e6e2d6489a2e1e0148d00e784e277c6590c189227d6060f15b9be690a", "sha256:31b142a868152d60c6323e0527edb692fdf05fd7cb4fe2fe9ce07d1ce560221a",
"sha256:812233bd155735377e2e9c7eea7a28815f357440334db51788d941e2a8b62f64", "sha256:713b9a201f5e7b2fca8691373e5d5c8c2552a51d87ca9ffbb1461e34e3241211",
"sha256:be10fa95e6bdc9cad616ebf368c51b3f5748138b2b3a600cf7c4f80b78cb9852" "sha256:a0bfe9fd718bca4de3e33000347e048f73126b6dc46530eb020b0251a638ee9d"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.14.1" "version": "==5.14.1"
}, },
"pyqt5-sip": { "pyqt5-sip": {
"hashes": [ "hashes": [
"sha256:02d94786bada670ab17a2b62ce95b3cf8e3b40c99d36007593a6334d551840bb", "sha256:1115728644bbadcde5fc8a16e7918bd31915a42dd6fb36b10d4afb78c582753e",
"sha256:06bc66b50556fb949f14875a4c224423dbf03f972497ccb883fb19b7b7c3b346", "sha256:1f4289276d355b6521dc2cc956189315da6f13adfb6bbab8f25ebd15e3bce1d4",
"sha256:091fbbe10a7aebadc0e8897a9449cda08d3c3f663460d812eca3001ca1ed3526", "sha256:288c6dc18a8d6a20981c07b715b5695d9b66880778565f3792bc6e38f14f20fb",
"sha256:0a067ade558befe4d46335b13d8b602b5044363bfd601419b556d4ec659bca18", "sha256:3f665376d9e52faa9855c3736a66ce6d825f85c86d7774d3c393f09da23f4f86",
"sha256:1910c1cb5a388d4e59ebb2895d7015f360f3f6eeb1700e7e33e866c53137eb9e", "sha256:6b4860c4305980db509415d0af802f111d15f92016c9422eb753bc8883463456",
"sha256:1c7ad791ec86247f35243bbbdd29cd59989afbe0ab678e0a41211f4407f21dd8", "sha256:7ffa39763097f64de129cf5cc770a651c3f65d2466b4fe05bef2bd2efbaa38e6",
"sha256:3c330ff1f70b3eaa6f63dce9274df996dffea82ad9726aa8e3d6cbe38e986b2f", "sha256:8a18e6f45d482ddfe381789979d09ee13aa6450caa3a0476503891bccb3ac709",
"sha256:482a910fa73ee0e36c258d7646ef38f8061774bbc1765a7da68c65056b573341", "sha256:8da842d3d7bf8931d1093105fb92702276b6dbb7e801abbaaa869405d616171a",
"sha256:7695dfafb4f5549ce1290ae643d6508dfc2646a9003c989218be3ce42a1aa422", "sha256:b42021229424aa44e99b3b49520b799fd64ff6ae8b53f79f903bbd85719a28e4",
"sha256:8274ed50f4ffbe91d0f4cc5454394631edfecd75dc327aa01be8bc5818a57e88", "sha256:b5b4906445fe980aee76f20400116b6904bf5f30d0767489c13370e42a764020",
"sha256:9047d887d97663790d811ac4e0d2e895f1bf2ecac4041691487de40c30239480", "sha256:c1e730a9eb2ec3869ed5d81b0f99f6e2460fb4d77750444c0ec183b771d798f7",
"sha256:9f6ab1417ecfa6c1ce6ce941e0cebc03e3ec9cd9925058043229a5f003ae5e40", "sha256:cbeeae6b45234a1654657f79943f8bccd3d14b4e7496746c62cf6fbce69442c7",
"sha256:b43ba2f18999d41c3df72f590348152e14cd4f6dcea2058c734d688dfb1ec61f", "sha256:d46b0f8effc554de52a1466b1bd80e5cb4bce635a75ac4e7ad6247c965dec5b9",
"sha256:c3ab9ea1bc3f4ce8c57ebc66fb25cd044ef92ed1ca2afa3729854ecc59658905", "sha256:e28c3abc9b62a1b7e796891648b9f14f8167b31c8e7990fae79654777252bb4d",
"sha256:da69ba17f6ece9a85617743cb19de689f2d63025bf8001e2facee2ec9bcff18f", "sha256:e6078f5ee7d31c102910d0c277a110e1c2a20a3fc88cd017a39e170120586d3f",
"sha256:ef3c7a0bf78674b0dda86ff5809d8495019903a096c128e1f160984b37848f73", "sha256:ee1a12f09d5af2304273bfd2f6b43835c1467d5ed501a6c95f5405637fa7750a",
"sha256:fabff832046643cdb93920ddaa8f77344df90768930fbe6bb33d211c4dcd0b5e" "sha256:f314f31f5fd39b06897f013f425137e511d45967150eb4e424a363d8138521c6"
], ],
"version": "==12.7.0" "version": "==12.7.1"
}, },
"pyxdg": { "pyxdg": {
"hashes": [ "hashes": [
@ -120,15 +141,30 @@
"index": "pypi", "index": "pypi",
"markers": "platform_system == 'Linux'", "markers": "platform_system == 'Linux'",
"version": "==0.26" "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": { "develop": {
"altgraph": { "altgraph": {
"hashes": [ "hashes": [
"sha256:d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa",
"sha256:ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c" "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"
], ],
"version": "==0.16.1" "version": "==0.17"
}, },
"appdirs": { "appdirs": {
"hashes": [ "hashes": [
@ -163,10 +199,10 @@
}, },
"macholib": { "macholib": {
"hashes": [ "hashes": [
"sha256:b71afea242d5ad4caacbdb79d80e75815d033fbc30f45954b2f3397f39683fd6", "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432",
"sha256:c72bda118afe7799570fcb4114315d5c9c5416e48eacf1198da39b4d77201559" "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"
], ],
"version": "==1.13" "version": "==1.14"
}, },
"pathspec": { "pathspec": {
"hashes": [ "hashes": [

View file

@ -2,10 +2,13 @@ from PyQt5 import QtCore, QtWidgets
import os import os
import sys import sys
import signal import signal
import platform
import click import click
import time
from .common import Common from .common import Common
from .main_window import MainWindow from .main_window import MainWindow
from .docker_installer import is_docker_installed, is_docker_ready, DockerInstaller
dangerzone_version = "0.1.0" dangerzone_version = "0.1.0"
@ -25,6 +28,27 @@ def main(filename):
# Common object # Common object
common = Common(app) common = Common(app)
# See if we need to install Docker...
if platform.system() == "Darwin" and (
not is_docker_installed(common) or not is_docker_ready(common)
):
print("Docker is either not installed or not running")
docker_installer = DockerInstaller(common)
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 # Main window
main_window = MainWindow(common) main_window = MainWindow(common)

View file

@ -50,7 +50,7 @@ class Common(object):
# Container runtime # Container runtime
if platform.system() == "Darwin": if platform.system() == "Darwin":
self.container_runtime = "docker" self.container_runtime = "/usr/local/bin/docker"
else: else:
self.container_runtime = "podman" self.container_runtime = "podman"
@ -239,8 +239,14 @@ class Common(object):
"share", "share",
) )
else: else:
# In linux... if platform.system() == "Darwin":
prefix = os.path.join(sys.prefix, "share/dangerzone") # macOS
prefix = os.path.join(
os.path.dirname(os.path.dirname(sys.executable)), "Resources/share"
)
else:
# Linux
prefix = os.path.join(sys.prefix, "share/dangerzone")
resource_path = os.path.join(prefix, filename) resource_path = os.path.join(prefix, filename)
return resource_path return resource_path

View file

@ -0,0 +1,238 @@
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):
# Does 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
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__()
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; }")
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)
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()
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(self.cancel_button)
buttons_layout.addStretch()
layout = QtWidgets.QVBoxLayout()
layout.addWidget(label)
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
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):
print(f"Download failed: status code {status_code}")
self.download_t = None
def download(self):
self.task_label.setText("Downloading Docker")
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_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 install_failed(self, exception):
print(f"Install failed: {exception}")
self.task_label.setText(f"Install failed: {exception}")
self.install_t = None
self.progress.hide()
self.cancel_button.setEnabled(True)
def install_clicked(self):
self.task_label.setText("Installing Docker")
self.progress.show()
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):
self.reject()
if self.download_t:
self.download_t.quit()
if self.install_t:
self.install_t.quit()
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
class Downloader(QtCore.QThread):
download_finished = QtCore.pyqtSignal()
download_failed = QtCore.pyqtSignal(int)
update_progress = QtCore.pyqtSignal(int, int)
def __init__(self, dmg_filename):
super(Downloader, self).__init__()
self.dmg_filename = dmg_filename
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()
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

View file

@ -1,6 +1,5 @@
import subprocess import subprocess
import time import time
import tempfile
import os import os
import pipes import pipes
import platform import platform