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'"}
pyobjc-core = {version = "*",platform_system = "== 'Darwin'"}
pyobjc-framework-launchservices = {version = "*",platform_system = "== 'Darwin'"}
requests = "*"
[dev-packages]
black = "*"

94
Pipfile.lock generated
View file

@ -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": [

View file

@ -2,10 +2,13 @@ from PyQt5 import QtCore, QtWidgets
import os
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, is_docker_ready, DockerInstaller
dangerzone_version = "0.1.0"
@ -25,6 +28,27 @@ 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) 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 = MainWindow(common)

View file

@ -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"
@ -239,8 +239,14 @@ class Common(object):
"share",
)
else:
# In linux...
prefix = os.path.join(sys.prefix, "share/dangerzone")
if platform.system() == "Darwin":
# 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)
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 time
import tempfile
import os
import pipes
import platform