mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Begin ripping out VM logic, go back to Docker Desktop for Mac
This commit is contained in:
parent
112291f82a
commit
d1c33bfcf5
6 changed files with 49 additions and 338 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue