Begin ripping out VM logic, go back to Docker Desktop for Mac

This commit is contained in:
Micah Lee 2021-11-22 13:36:21 -08:00
parent 112291f82a
commit d1c33bfcf5
No known key found for this signature in database
GPG key ID: 403C2657CD994F73
6 changed files with 49 additions and 338 deletions

View file

@ -1,20 +1,16 @@
import platform import platform
import subprocess import subprocess
import sys
import pipes import pipes
import shutil import shutil
import json
import os import os
import uuid
import tempfile import tempfile
import appdirs
# What container tech is used for this platform? # What container tech is used for this platform?
if platform.system() == "Darwin": if platform.system() == "Linux":
container_tech = "dangerzone-vm"
elif platform.system() == "Linux":
container_tech = "podman" container_tech = "podman"
else: else:
# Windows and unknown use docker for now, dangerzone-vm eventually # Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
container_tech = "docker" container_tech = "docker"
# Define startupinfo for subprocesses # Define startupinfo for subprocesses
@ -25,7 +21,14 @@ else:
startupinfo = None startupinfo = None
# Name of the dangerzone container
container_name = "dangerzone.rocks/dangerzone"
def exec(args, stdout_callback=None): def exec(args, stdout_callback=None):
args_str = " ".join(pipes.quote(s) for s in args)
print("> " + args_str)
with subprocess.Popen( with subprocess.Popen(
args, args,
stdin=None, stdin=None,
@ -43,136 +46,34 @@ def exec(args, stdout_callback=None):
return p.returncode return p.returncode
def vm_ssh_args(vm_info): def exec_container(args, stdout_callback=None):
return [ if container_tech == "podman":
"/usr/bin/ssh", container_runtime = shutil.which("podman")
"-q",
"-i",
vm_info["client_key_path"],
"-p",
str(vm_info["tunnel_port"]),
"-o",
"StrictHostKeyChecking=no",
"user@127.0.0.1",
]
def vm_scp_args(vm_info):
return [
"/usr/bin/scp",
"-i",
vm_info["client_key_path"],
"-P",
str(vm_info["tunnel_port"]),
"-o",
"StrictHostKeyChecking=no",
]
def host_exec(args, stdout_callback=None):
args_str = " ".join(pipes.quote(s) for s in args)
print("> " + args_str)
return exec(args, stdout_callback)
def vm_exec(args, vm_info, stdout_callback=None):
if container_tech == "dangerzone-vm" and vm_info is None:
print("--vm-info-path required on this platform")
return
args_str = " ".join(pipes.quote(s) for s in args)
print("VM > " + args_str)
args = vm_ssh_args(vm_info) + args
return exec(args, stdout_callback)
def vm_mkdirs(vm_info):
guest_path = os.path.join("/home/user/", str(uuid.uuid4()))
input_dir = os.path.join(guest_path, "input")
pixel_dir = os.path.join(guest_path, "pixel")
safe_dir = os.path.join(guest_path, "safe")
vm_exec(["/bin/mkdir", guest_path, input_dir, pixel_dir, safe_dir], vm_info)
return guest_path, input_dir, pixel_dir, safe_dir
def vm_rmdir(guest_path, vm_info):
vm_exec(["/bin/rm", "-r", guest_path], vm_info)
def vm_upload(host_path, guest_path, vm_info):
args = vm_scp_args(vm_info) + [host_path, f"user@127.0.0.1:{guest_path}"]
print(f"Uploading '{host_path}' to VM at '{guest_path}'")
host_exec(args)
def vm_download(guest_path, host_path, vm_info):
args = vm_scp_args(vm_info) + [f"user@127.0.0.1:{guest_path}", host_path]
print(f"Downloading '{guest_path}' from VM to '{host_path}'")
host_exec(args)
def exec_container(args, vm_info=None, stdout_callback=None):
if container_tech == "dangerzone-vm" and vm_info is None:
print("Invalid VM info")
return
if container_tech == "dangerzone-vm":
args = ["/usr/bin/podman"] + args
return vm_exec(args, vm_info, stdout_callback)
else: else:
if container_tech == "podman": container_runtime = shutil.which("docker")
container_runtime = shutil.which("podman")
else:
container_runtime = shutil.which("docker")
args = [container_runtime] + args args = [container_runtime] + args
return host_exec(args, stdout_callback) return exec(args, stdout_callback)
def load_vm_info(vm_info_path):
if not vm_info_path:
return None
with open(vm_info_path) as f:
return json.loads(f.read())
def convert(global_common, input_filename, output_filename, ocr_lang, stdout_callback): def convert(global_common, input_filename, output_filename, ocr_lang, stdout_callback):
success = False success = False
container_name = "dangerzone.rocks/dangerzone"
if ocr_lang: if ocr_lang:
ocr = "1" ocr = "1"
else: else:
ocr = "0" ocr = "0"
if global_common.vm: dz_tmp = os.path.join(appdirs.user_config_dir("dangerzone"), "tmp")
vm_info = load_vm_info(global_common.vm.vm_info_path) os.makedirs(dz_tmp, exist_ok=True)
else:
vm_info = None
# If we're using the VM, create temp dirs in the guest and upload the input file tmpdir = tempfile.TemporaryDirectory(dir=dz_tmp)
# Otherwise, create temp dirs pixel_dir = os.path.join(tmpdir.name, "pixels")
if vm_info: safe_dir = os.path.join(tmpdir.name, "safe")
ssh_args_str = " ".join(pipes.quote(s) for s in vm_ssh_args(vm_info)) os.makedirs(pixel_dir, exist_ok=True)
print("\nIf you want to SSH to the VM:\n" + ssh_args_str + "\n") os.makedirs(safe_dir, exist_ok=True)
guest_tmpdir, input_dir, pixel_dir, safe_dir = vm_mkdirs(vm_info) container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf")
guest_input_filename = os.path.join(input_dir, "input_file")
container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf")
vm_upload(input_filename, guest_input_filename, vm_info)
input_filename = guest_input_filename
else:
tmpdir = tempfile.TemporaryDirectory()
pixel_dir = os.path.join(tmpdir.name, "pixels")
safe_dir = os.path.join(tmpdir.name, "safe")
os.makedirs(pixel_dir, exist_ok=True)
os.makedirs(safe_dir, exist_ok=True)
container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf")
# Convert document to pixels # Convert document to pixels
args = [ args = [
@ -186,7 +87,7 @@ def convert(global_common, input_filename, output_filename, ocr_lang, stdout_cal
container_name, container_name,
"document-to-pixels", "document-to-pixels",
] ]
ret = exec_container(args, vm_info, stdout_callback) ret = exec_container(args, stdout_callback)
if ret != 0: if ret != 0:
print("documents-to-pixels failed") print("documents-to-pixels failed")
else: else:
@ -208,24 +109,18 @@ def convert(global_common, input_filename, output_filename, ocr_lang, stdout_cal
container_name, container_name,
"pixels-to-pdf", "pixels-to-pdf",
] ]
ret = exec_container(args, vm_info, stdout_callback) ret = exec_container(args, stdout_callback)
if ret != 0: if ret != 0:
print("pixels-to-pdf failed") print("pixels-to-pdf failed")
else: else:
# Move the final file to the right place # Move the final file to the right place
if vm_info: os.rename(container_output_filename, output_filename)
vm_download(container_output_filename, output_filename, vm_info)
else:
os.rename(container_output_filename, output_filename)
# We did it # We did it
success = True success = True
# Clean up # Clean up
if vm_info: shutil.rmtree(tmpdir.name)
vm_rmdir(guest_tmpdir, vm_info)
else:
shutil.rmtree(tmpdir.name)
return success return success

View file

@ -38,9 +38,6 @@ class GlobalCommon(object):
# In case we have a custom container # In case we have a custom container
self.custom_container = None self.custom_container = None
# VM object, if available
self.vm = None
# Languages supported by tesseract # Languages supported by tesseract
self.ocr_languages = { self.ocr_languages = {
"Afrikaans": "ar", "Afrikaans": "ar",
@ -418,8 +415,6 @@ class GlobalCommon(object):
convert(self, input_filename, output_filename, ocr_lang) convert(self, input_filename, output_filename, ocr_lang)
args = [self.dz_container_path] + args args = [self.dz_container_path] + args
if self.vm:
args += ["--vm-info-path", self.vm.vm_info_path]
args_str = " ".join(pipes.quote(s) for s in args) args_str = " ".join(pipes.quote(s) for s in args)
print(Style.DIM + "> " + Style.NORMAL + Fore.CYAN + args_str) print(Style.DIM + "> " + Style.NORMAL + Fore.CYAN + args_str)

View file

@ -8,7 +8,6 @@ from PySide2 import QtCore, QtWidgets
from .common import GuiCommon from .common import GuiCommon
from .main_window import MainWindow from .main_window import MainWindow
from .vm import Vm
from .systray import SysTray from .systray import SysTray
from .docker_installer import ( from .docker_installer import (
is_docker_installed, is_docker_installed,
@ -51,8 +50,7 @@ class ApplicationWrapper(QtCore.QObject):
@click.command() @click.command()
@click.argument("filename", required=False) @click.argument("filename", required=False)
@click.option("--allow-vm-login", is_flag=True, help="Allow logging into the VM as root to troubleshoot") def gui_main(filename):
def gui_main(filename, allow_vm_login):
if platform.system() == "Darwin": if platform.system() == "Darwin":
# Required for macOS Big Sur: https://stackoverflow.com/a/64878899 # Required for macOS Big Sur: https://stackoverflow.com/a/64878899
os.environ["QT_MAC_WANTS_LAYER"] = "1" os.environ["QT_MAC_WANTS_LAYER"] = "1"
@ -89,7 +87,7 @@ def gui_main(filename, allow_vm_login):
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
# See if we need to install Docker (Windows-only) # See if we need to install Docker (Windows-only)
if platform.system() == "Windows" and ( if (platform.system() == "Windows" or platform.system() == "Darwin") and (
not is_docker_installed() or not is_docker_ready(global_common) not is_docker_installed() or not is_docker_ready(global_common)
): ):
click.echo("Docker is either not installed or not running") click.echo("Docker is either not installed or not running")
@ -97,20 +95,9 @@ def gui_main(filename, allow_vm_login):
docker_installer.start() docker_installer.start()
return return
# The dangerzone VM (Mac-only)
if platform.system() == "Darwin":
vm = Vm(global_common, allow_vm_login)
global_common.vm = vm
else:
vm = None
# Create the system tray # Create the system tray
systray = SysTray(global_common, gui_common, app, app_wrapper) systray = SysTray(global_common, gui_common, app, app_wrapper)
# Start the VM
if vm:
vm.start()
closed_windows = {} closed_windows = {}
windows = {} windows = {}
@ -170,7 +157,4 @@ def gui_main(filename, allow_vm_login):
# Launch the GUI # Launch the GUI
ret = app.exec_() ret = app.exec_()
if vm:
vm.stop()
sys.exit(ret) sys.exit(ret)

View file

@ -1,36 +1,24 @@
import os import os
import stat
import requests
import subprocess import subprocess
import shutil import shutil
import platform import platform
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtWidgets
class AuthorizationFailed(Exception): class AuthorizationFailed(Exception):
pass pass
container_runtime = shutil.which("docker")
def is_docker_installed(): def is_docker_installed():
container_runtime = shutil.which("docker.exe") return container_runtime is not None
if platform.system() == "Darwin":
# Does the docker binary exist?
if os.path.isdir("/Applications/Docker.app") and os.path.exists(
container_runtime
):
# Is it executable?
st = os.stat(container_runtime)
return bool(st.st_mode & stat.S_IXOTH)
if platform.system() == "Windows":
return os.path.exists(container_runtime)
return False
def is_docker_ready(global_common): def is_docker_ready(global_common):
# Run `docker image ls` without an error # Run `docker image ls` without an error
with global_common.exec_dangerzone_container(["ls"]) as p: with subprocess.Popen([container_runtime, "image", "ls"]) as p:
outs, errs = p.communicate() outs, errs = p.communicate()
# The user canceled, or permission denied # The user canceled, or permission denied
@ -41,8 +29,8 @@ def is_docker_ready(global_common):
if p.returncode == 0: if p.returncode == 0:
return True return True
else: else:
print(outs.decode()) print(outs)
print(errs.decode()) print(errs)
return False return False
@ -57,7 +45,7 @@ class DockerInstaller(QtWidgets.QDialog):
def __init__(self, gui_common): def __init__(self, gui_common):
super(DockerInstaller, self).__init__() super(DockerInstaller, self).__init__()
self.setWindowTitle("dangerzone") self.setWindowTitle("Dangerzone")
self.setWindowIcon(gui_common.get_window_icon()) self.setWindowIcon(gui_common.get_window_icon())
# self.setMinimumHeight(170) # self.setMinimumHeight(170)
@ -74,176 +62,38 @@ class DockerInstaller(QtWidgets.QDialog):
self.task_label.setWordWrap(True) self.task_label.setWordWrap(True)
self.task_label.setOpenExternalLinks(True) self.task_label.setOpenExternalLinks(True)
self.progress = QtWidgets.QProgressBar()
self.progress.setMinimum(0)
self.open_finder_button = QtWidgets.QPushButton()
if platform.system() == "Darwin":
self.open_finder_button.setText("Show in Finder")
else:
self.open_finder_button.setText("Show in Explorer")
self.open_finder_button.setStyleSheet("QPushButton { font-weight: bold; }")
self.open_finder_button.clicked.connect(self.open_finder_clicked)
self.open_finder_button.hide()
self.cancel_button = QtWidgets.QPushButton("Cancel")
self.cancel_button.clicked.connect(self.cancel_clicked)
self.ok_button = QtWidgets.QPushButton("OK") self.ok_button = QtWidgets.QPushButton("OK")
self.ok_button.clicked.connect(self.ok_clicked) self.ok_button.clicked.connect(self.ok_clicked)
buttons_layout = QtWidgets.QHBoxLayout() buttons_layout = QtWidgets.QHBoxLayout()
buttons_layout.addStretch() buttons_layout.addStretch()
buttons_layout.addWidget(self.open_finder_button)
buttons_layout.addWidget(self.ok_button) buttons_layout.addWidget(self.ok_button)
buttons_layout.addWidget(self.cancel_button)
buttons_layout.addStretch() buttons_layout.addStretch()
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(label) layout.addWidget(label)
layout.addWidget(self.task_label) layout.addWidget(self.task_label)
layout.addWidget(self.progress)
layout.addLayout(buttons_layout) layout.addLayout(buttons_layout)
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
if platform.system() == "Darwin": if platform.system() == "Darwin":
self.installer_filename = os.path.join( self.docker_path = "/Applications/Docker.app/Contents/Resources/bin/docker"
os.path.expanduser("~/Downloads"), "Docker.dmg" elif platform.system() == "Windows":
) self.docker_path = shutil.which("docker.exe")
else:
self.installer_filename = os.path.join(
os.path.expanduser("~\\Downloads"), "Docker for Windows Installer.exe"
)
# Threads
self.download_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. Install it, make sure it's running, and then open Dangerzone again."
)
self.download_t = None
self.progress.hide()
self.cancel_button.hide()
self.open_finder_path = self.installer_filename
self.open_finder_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.installer_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 cancel_clicked(self):
self.reject()
if self.download_t:
self.download_t.quit()
try:
os.remove(self.installer_filename)
except:
pass
def ok_clicked(self): def ok_clicked(self):
self.accept() self.accept()
if self.download_t:
self.download_t.quit()
try:
os.remove(self.installer_filename)
except:
pass
def open_finder_clicked(self):
if platform.system() == "Darwin":
subprocess.call(["open", "-R", self.open_finder_path])
else:
subprocess.Popen(
f'explorer.exe /select,"{self.open_finder_path}"', shell=True
)
self.accept()
def start(self): def start(self):
if platform.system() == "Darwin": if not os.path.exists(self.docker_path):
docker_app_path = "/Applications/Docker.app" self.task_label.setText(
else: "<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</a>, install it, and then run Dangerzone again."
docker_app_path = "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe" )
self.task_label.setTextFormat(QtCore.Qt.RichText)
if not os.path.exists(docker_app_path):
if platform.system() == "Windows":
self.task_label.setText(
"<a href='https://docs.docker.com/docker-for-windows/install/'>Download Docker</a>, install it, and then run Dangerzone again."
)
self.task_label.setTextFormat(QtCore.Qt.RichText)
self.progress.hide()
self.cancel_button.hide()
else:
self.ok_button.hide()
self.download()
else: else:
self.task_label.setText( self.task_label.setText(
"Docker is installed, but you must launch it first. Open Docker, make sure it's running, and then open Dangerzone again." "Docker Desktop is installed, but you must launch it first. Open Docker, make sure it's running, and then open Dangerzone again."
) )
self.progress.hide()
self.ok_button.hide()
self.cancel_button.hide()
self.open_finder_path = docker_app_path
self.open_finder_button.show()
return self.exec_() == QtWidgets.QDialog.Accepted return self.exec_() == QtWidgets.QDialog.Accepted
class Downloader(QtCore.QThread):
download_finished = QtCore.Signal()
download_failed = QtCore.Signal(int)
update_progress = QtCore.Signal(int, int)
def __init__(self, installer_filename):
super(Downloader, self).__init__()
self.installer_filename = installer_filename
if platform.system() == "Darwin":
self.installer_url = "https://download.docker.com/mac/stable/Docker.dmg"
elif platform.system() == "Windows":
self.installer_url = "https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe"
def run(self):
print(f"Downloading docker to {self.installer_filename}")
with requests.get(self.installer_url, 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.installer_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()

View file

@ -20,7 +20,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window_id = window_id self.window_id = window_id
self.common = Common() self.common = Common()
self.setWindowTitle("dangerzone") self.setWindowTitle("Dangerzone")
self.setWindowIcon(self.gui_common.get_window_icon()) self.setWindowIcon(self.gui_common.get_window_icon())
self.setMinimumWidth(600) self.setMinimumWidth(600)

View file

@ -28,19 +28,6 @@ class SysTray(QtWidgets.QSystemTrayIcon):
self.setContextMenu(menu) self.setContextMenu(menu)
self.show() self.show()
if self.global_common.vm:
self.global_common.vm.vm_state_change.connect(self.vm_state_change)
def vm_state_change(self, state):
if state == self.global_common.vm.STATE_OFF:
self.status_action.setText("Dangerzone VM is off")
elif state == self.global_common.vm.STATE_STARTING:
self.status_action.setText("Dangerzone VM is starting...")
elif state == self.global_common.vm.STATE_ON:
self.status_action.setText("Dangerzone VM is running")
elif state == self.global_common.vm.STATE_FAIL:
self.status_action.setText("Dangerzone VM failed to start")
def new_window(self): def new_window(self):
self.app_wrapper.new_window.emit() self.app_wrapper.new_window.emit()