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 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,130 +46,28 @@ 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)
else:
def exec_container(args, stdout_callback=None):
if container_tech == "podman":
container_runtime = shutil.which("podman")
else:
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())
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")
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()
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)
@ -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,23 +109,17 @@ 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)
# We did it
success = True
# Clean up
if vm_info:
vm_rmdir(guest_tmpdir, vm_info)
else:
shutil.rmtree(tmpdir.name)
return success

View file

@ -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)

View file

@ -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)

View file

@ -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":
if not os.path.exists(self.docker_path):
self.task_label.setText(
"<a href='https://docs.docker.com/docker-for-windows/install/'>Download Docker</a>, install it, and then run Dangerzone again."
"<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</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:
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()

View file

@ -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)

View file

@ -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()