From d1c33bfcf5380d4b3ac93c4d43ac106f49924799 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 22 Nov 2021 13:36:21 -0800 Subject: [PATCH] Begin ripping out VM logic, go back to Docker Desktop for Mac --- dangerzone/container.py | 161 +++++-------------------- dangerzone/global_common.py | 5 - dangerzone/gui/__init__.py | 20 +--- dangerzone/gui/docker_installer.py | 186 +++-------------------------- dangerzone/gui/main_window.py | 2 +- dangerzone/gui/systray.py | 13 -- 6 files changed, 49 insertions(+), 338 deletions(-) diff --git a/dangerzone/container.py b/dangerzone/container.py index 8d1fa9d..9fd106e 100644 --- a/dangerzone/container.py +++ b/dangerzone/container.py @@ -1,20 +1,16 @@ import platform import subprocess -import sys import pipes import shutil -import json import os -import uuid import tempfile +import appdirs # What container tech is used for this platform? -if platform.system() == "Darwin": - container_tech = "dangerzone-vm" -elif platform.system() == "Linux": +if platform.system() == "Linux": container_tech = "podman" 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" # Define startupinfo for subprocesses @@ -25,7 +21,14 @@ else: startupinfo = None +# Name of the dangerzone container +container_name = "dangerzone.rocks/dangerzone" + + def exec(args, stdout_callback=None): + args_str = " ".join(pipes.quote(s) for s in args) + print("> " + args_str) + with subprocess.Popen( args, stdin=None, @@ -43,136 +46,34 @@ def exec(args, stdout_callback=None): return p.returncode -def vm_ssh_args(vm_info): - return [ - "/usr/bin/ssh", - "-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) +def exec_container(args, stdout_callback=None): + if container_tech == "podman": + container_runtime = shutil.which("podman") else: - if container_tech == "podman": - container_runtime = shutil.which("podman") - else: - container_runtime = shutil.which("docker") + container_runtime = shutil.which("docker") - args = [container_runtime] + args - return host_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()) + args = [container_runtime] + args + return exec(args, stdout_callback) def convert(global_common, input_filename, output_filename, ocr_lang, stdout_callback): success = False - container_name = "dangerzone.rocks/dangerzone" if ocr_lang: ocr = "1" else: ocr = "0" - if global_common.vm: - vm_info = load_vm_info(global_common.vm.vm_info_path) - else: - vm_info = None + dz_tmp = os.path.join(appdirs.user_config_dir("dangerzone"), "tmp") + os.makedirs(dz_tmp, exist_ok=True) - # If we're using the VM, create temp dirs in the guest and upload the input file - # Otherwise, create temp dirs - if vm_info: - ssh_args_str = " ".join(pipes.quote(s) for s in vm_ssh_args(vm_info)) - print("\nIf you want to SSH to the VM:\n" + ssh_args_str + "\n") + tmpdir = tempfile.TemporaryDirectory(dir=dz_tmp) + 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) - guest_tmpdir, input_dir, pixel_dir, safe_dir = vm_mkdirs(vm_info) - 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") + container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf") # Convert document to pixels args = [ @@ -186,7 +87,7 @@ def convert(global_common, input_filename, output_filename, ocr_lang, stdout_cal container_name, "document-to-pixels", ] - ret = exec_container(args, vm_info, stdout_callback) + ret = exec_container(args, stdout_callback) if ret != 0: print("documents-to-pixels failed") else: @@ -208,24 +109,18 @@ def convert(global_common, input_filename, output_filename, ocr_lang, stdout_cal container_name, "pixels-to-pdf", ] - ret = exec_container(args, vm_info, stdout_callback) + ret = exec_container(args, stdout_callback) if ret != 0: print("pixels-to-pdf failed") else: # Move the final file to the right place - if vm_info: - vm_download(container_output_filename, output_filename, vm_info) - else: - os.rename(container_output_filename, output_filename) + os.rename(container_output_filename, output_filename) # We did it success = True # Clean up - if vm_info: - vm_rmdir(guest_tmpdir, vm_info) - else: - shutil.rmtree(tmpdir.name) + shutil.rmtree(tmpdir.name) return success diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index f7e8a69..53af5c0 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -38,9 +38,6 @@ class GlobalCommon(object): # In case we have a custom container self.custom_container = None - # VM object, if available - self.vm = None - # Languages supported by tesseract self.ocr_languages = { "Afrikaans": "ar", @@ -418,8 +415,6 @@ class GlobalCommon(object): convert(self, input_filename, output_filename, ocr_lang) 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) print(Style.DIM + "> " + Style.NORMAL + Fore.CYAN + args_str) diff --git a/dangerzone/gui/__init__.py b/dangerzone/gui/__init__.py index 3f7c9d3..b21996b 100644 --- a/dangerzone/gui/__init__.py +++ b/dangerzone/gui/__init__.py @@ -8,7 +8,6 @@ from PySide2 import QtCore, QtWidgets from .common import GuiCommon from .main_window import MainWindow -from .vm import Vm from .systray import SysTray from .docker_installer import ( is_docker_installed, @@ -51,8 +50,7 @@ class ApplicationWrapper(QtCore.QObject): @click.command() @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, allow_vm_login): +def gui_main(filename): if platform.system() == "Darwin": # Required for macOS Big Sur: https://stackoverflow.com/a/64878899 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) # 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) ): click.echo("Docker is either not installed or not running") @@ -97,20 +95,9 @@ def gui_main(filename, allow_vm_login): docker_installer.start() 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 systray = SysTray(global_common, gui_common, app, app_wrapper) - # Start the VM - if vm: - vm.start() - closed_windows = {} windows = {} @@ -170,7 +157,4 @@ def gui_main(filename, allow_vm_login): # Launch the GUI ret = app.exec_() - if vm: - vm.stop() - sys.exit(ret) diff --git a/dangerzone/gui/docker_installer.py b/dangerzone/gui/docker_installer.py index 9f9413d..d113e36 100644 --- a/dangerzone/gui/docker_installer.py +++ b/dangerzone/gui/docker_installer.py @@ -1,36 +1,24 @@ import os -import stat -import requests import subprocess import shutil import platform -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2 import QtCore, QtWidgets class AuthorizationFailed(Exception): pass +container_runtime = shutil.which("docker") + + def is_docker_installed(): - container_runtime = shutil.which("docker.exe") - 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 + return container_runtime is not None def is_docker_ready(global_common): # 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() # The user canceled, or permission denied @@ -41,8 +29,8 @@ def is_docker_ready(global_common): if p.returncode == 0: return True else: - print(outs.decode()) - print(errs.decode()) + print(outs) + print(errs) return False @@ -57,7 +45,7 @@ class DockerInstaller(QtWidgets.QDialog): def __init__(self, gui_common): super(DockerInstaller, self).__init__() - self.setWindowTitle("dangerzone") + self.setWindowTitle("Dangerzone") self.setWindowIcon(gui_common.get_window_icon()) # self.setMinimumHeight(170) @@ -74,176 +62,38 @@ class DockerInstaller(QtWidgets.QDialog): self.task_label.setWordWrap(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.clicked.connect(self.ok_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addStretch() - buttons_layout.addWidget(self.open_finder_button) buttons_layout.addWidget(self.ok_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) if platform.system() == "Darwin": - self.installer_filename = os.path.join( - os.path.expanduser("~/Downloads"), "Docker.dmg" - ) - 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 + self.docker_path = "/Applications/Docker.app/Contents/Resources/bin/docker" + elif platform.system() == "Windows": + self.docker_path = shutil.which("docker.exe") def ok_clicked(self): 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): - if platform.system() == "Darwin": - docker_app_path = "/Applications/Docker.app" - else: - docker_app_path = "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe" - - if not os.path.exists(docker_app_path): - if platform.system() == "Windows": - self.task_label.setText( - "Download Docker, 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() + if not os.path.exists(self.docker_path): + self.task_label.setText( + "Download Docker Desktop, install it, and then run Dangerzone again." + ) + self.task_label.setTextFormat(QtCore.Qt.RichText) else: 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 - - -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() diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 27a2151..0dcf881 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -20,7 +20,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window_id = window_id self.common = Common() - self.setWindowTitle("dangerzone") + self.setWindowTitle("Dangerzone") self.setWindowIcon(self.gui_common.get_window_icon()) self.setMinimumWidth(600) diff --git a/dangerzone/gui/systray.py b/dangerzone/gui/systray.py index 7905c43..677704d 100644 --- a/dangerzone/gui/systray.py +++ b/dangerzone/gui/systray.py @@ -28,19 +28,6 @@ class SysTray(QtWidgets.QSystemTrayIcon): self.setContextMenu(menu) 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): self.app_wrapper.new_window.emit()