From 007337336efe13bb9a27268e4257022234bf2410 Mon Sep 17 00:00:00 2001 From: Ivan Trubach Date: Mon, 15 Aug 2022 15:49:41 +0300 Subject: [PATCH 01/42] vm(qemu): do not require clipboard sharing for 9p Updates #2184 --- .../UTMQemuConfiguration+Arguments.swift | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 8d2486a03..1aeb8621b 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -688,27 +688,28 @@ extension UTMQemuConfiguration { f("virtserialport,chardev=charchannel1,id=channel1,name=org.spice-space.webdav.0") f("-chardev") f("spiceport,name=org.spice-space.webdav.0,id=charchannel1") - } else if sharing.directoryShareMode == .virtfs, let url = sharing.directoryShareUrl { - f("-fsdev") - "local" - "id=virtfs0" - "path=" - url - "security_model=mapped-xattr" - if sharing.isDirectoryShareReadOnly { - "readonly=on" - } - f() - f("-device") - if system.architecture == .s390x { - "virtio-9p-ccw" - } else { - "virtio-9p-pci" - } - "fsdev=virtfs0" - "mount_tag=share" } } + if sharing.directoryShareMode == .virtfs, let url = sharing.directoryShareUrl { + f("-fsdev") + "local" + "id=virtfs0" + "path=" + url + "security_model=mapped-xattr" + if sharing.isDirectoryShareReadOnly { + "readonly=on" + } + f() + f("-device") + if system.architecture == .s390x { + "virtio-9p-ccw" + } else { + "virtio-9p-pci" + } + "fsdev=virtfs0" + "mount_tag=share" + } } @QEMUArgumentBuilder private var miscArguments: [QEMUArgument] { From 7aade210504fb05144f6350200361bb701939b85 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Tue, 16 Aug 2022 13:22:34 -0700 Subject: [PATCH 02/42] github: force rebuild sysroot should still cache --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff560b354..e28a7b734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,6 @@ jobs: run: | [[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}" - name: Cache Sysroot - if: github.event.inputs.rebuild_sysroot != 'true' id: cache-sysroot uses: actions/cache@v3 with: @@ -61,14 +60,14 @@ jobs: run: | echo "/usr/local/opt/bison/bin:/opt/homebrew/opt/bison/bin" >> $GITHUB_PATH - name: Install Requirements - if: steps.cache-sysroot.outputs.cache-hit != 'true' && env.INSTALL_REQUIREMENTS == 'true' + if: (steps.cache-sysroot.outputs.cache-hit != 'true' || github.event.inputs.rebuild_sysroot == 'true') && env.INSTALL_REQUIREMENTS == 'true' run: | brew uninstall cmake brew install bison pkg-config gettext glib-utils libgpg-error nasm make meson pip3 install --user six pyparsing rm -f /usr/local/lib/pkgconfig/*.pc - name: Build Sysroot - if: steps.cache-sysroot.outputs.cache-hit != 'true' + if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event.inputs.rebuild_sysroot == 'true' run: ./scripts/build_dependencies.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }} env: NCPU: ${{ matrix.platform == 'ios-tci' && '1' || '0' }} # limit 1 CPU for TCI build due to memory issues, 0 = unlimited for other builds @@ -89,7 +88,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Cache Sysroot (Universal Mac) - if: github.event.inputs.rebuild_sysroot != 'true' id: cache-sysroot-universal uses: actions/cache@v3 with: From 58a96e63c7a84eb986c2f3ed7a6a457e5fe01ccf Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Tue, 16 Aug 2022 13:58:39 -0700 Subject: [PATCH 03/42] qemu: fix PPC not working with 256 colours Thanks @cat7 for suggesting the patch. Fixes #4277 --- patches/qemu-7.0.0-utm.patch | 128 +++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/patches/qemu-7.0.0-utm.patch b/patches/qemu-7.0.0-utm.patch index 7842f1a84..fb86c0fca 100644 --- a/patches/qemu-7.0.0-utm.patch +++ b/patches/qemu-7.0.0-utm.patch @@ -466,3 +466,131 @@ index 8692ea2561..5ee2cc88d5 100644 -- 2.28.0 +From patchwork Mon Jul 25 11:58:15 2022 +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: 8bit +X-Patchwork-Submitter: =?utf-8?q?Marc-Andr=C3=A9_Lureau?= + +X-Patchwork-Id: 12927993 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on + aws-us-west-2-korg-lkml-1.web.codeaurora.org +Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + (No client certificate requested) + by smtp.lore.kernel.org (Postfix) with ESMTPS id 96039C433EF + for ; Mon, 25 Jul 2022 12:02:30 +0000 (UTC) +Received: from localhost ([::1]:34130 helo=lists1p.gnu.org) + by lists.gnu.org with esmtp (Exim 4.90_1) + (envelope-from + ) + id 1oFwn7-0005u8-7P + for qemu-devel@archiver.kernel.org; Mon, 25 Jul 2022 08:02:29 -0400 +Received: from eggs.gnu.org ([2001:470:142:3::10]:43214) + by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.90_1) (envelope-from ) + id 1oFwjE-000402-Ro + for qemu-devel@nongnu.org; Mon, 25 Jul 2022 07:58:29 -0400 +Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]:28817) + by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.90_1) (envelope-from ) + id 1oFwjB-0001fy-2n + for qemu-devel@nongnu.org; Mon, 25 Jul 2022 07:58:26 -0400 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; + s=mimecast20190719; t=1658750303; + h=from:from:reply-to:subject:subject:date:date:message-id:message-id: + to:to:cc:cc:mime-version:mime-version:content-type:content-type: + content-transfer-encoding:content-transfer-encoding; + bh=gh1CF9/skxxtps1WwbLkaNJbKti0bR9znmqXATo6rcQ=; + b=LgYgJZzZrctg1tE4HAY1PkI6H3tOjolPgQjL64z/I1S9bm0QKIYl+VsoGu3ZLpQx3zfk8D + Ji+UVlY6GneLzPy179oRKEzyb4PYYqv9vMAQ3hH4caGM0FYSjnGLgEMCIFg5zZuimbRUKK + XSBcTXiIS38Y1SU5vl9E3YJkkHHcdMU= +Received: from mimecast-mx02.redhat.com (mx3-rdu2.redhat.com + [66.187.233.73]) by relay.mimecast.com with ESMTP with STARTTLS + (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id + us-mta-448-Z7e9RGT7MxKUAawP3wpCiA-1; Mon, 25 Jul 2022 07:58:20 -0400 +X-MC-Unique: Z7e9RGT7MxKUAawP3wpCiA-1 +Received: from smtp.corp.redhat.com (int-mx10.intmail.prod.int.rdu2.redhat.com + [10.11.54.10]) + (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by mimecast-mx02.redhat.com (Postfix) with ESMTPS id EBF4F3801F4B; + Mon, 25 Jul 2022 11:58:19 +0000 (UTC) +Received: from localhost (unknown [10.39.208.20]) + by smtp.corp.redhat.com (Postfix) with ESMTP id 0053A492C3B; + Mon, 25 Jul 2022 11:58:18 +0000 (UTC) +From: marcandre.lureau@redhat.com +To: qemu-devel@nongnu.org +Cc: kraxel@redhat.com, mark.cave-ayland@ilande.co.uk, =?utf-8?q?Marc-Andr?= + =?utf-8?q?=C3=A9_Lureau?= +Subject: [PATCH] ui/console: fix qemu_console_resize() regression +Date: Mon, 25 Jul 2022 15:58:15 +0400 +Message-Id: <20220725115815.2461322-1-marcandre.lureau@redhat.com> +MIME-Version: 1.0 +X-Scanned-By: MIMEDefang 2.85 on 10.11.54.10 +Received-SPF: pass client-ip=170.10.133.124; + envelope-from=marcandre.lureau@redhat.com; + helo=us-smtp-delivery-124.mimecast.com +X-Spam_score_int: -21 +X-Spam_score: -2.2 +X-Spam_bar: -- +X-Spam_report: (-2.2 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.082, + DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, + RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, + T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no +X-Spam_action: no action +X-BeenThere: qemu-devel@nongnu.org +X-Mailman-Version: 2.1.29 +Precedence: list +List-Id: +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org +Sender: "Qemu-devel" + + +From: Marc-André Lureau + +The display may be corrupted when changing screen colour depth in +qemu-system-ppc/MacOS since 7.0. + +Do not short-cut qemu_console_resize() if the surface is backed by vga +vram. When the scanout isn't set, or it is already allocated, or opengl, +and the size is fitting, we still avoid the reallocation & replace path. + +Fixes: commit cb8962c1 ("ui: do not create a surface when resizing a GL scanout") + +Reported-by: Mark Cave-Ayland +Signed-off-by: Marc-André Lureau +Tested-by: Mark Cave-Ayland +Acked-by: Gerd Hoffmann +--- + ui/console.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/ui/console.c b/ui/console.c +index e139f7115e1f..765892f84f1c 100644 +--- a/ui/console.c ++++ b/ui/console.c +@@ -2575,11 +2575,13 @@ static void vc_chr_open(Chardev *chr, + + void qemu_console_resize(QemuConsole *s, int width, int height) + { +- DisplaySurface *surface; ++ DisplaySurface *surface = qemu_console_surface(s); + + assert(s->console_type == GRAPHIC_CONSOLE); + +- if (qemu_console_get_width(s, -1) == width && ++ if ((s->scanout.kind != SCANOUT_SURFACE || ++ (surface && surface->flags & QEMU_ALLOCATED_FLAG)) && ++ qemu_console_get_width(s, -1) == width && + qemu_console_get_height(s, -1) == height) { + return; + } From 02e3b5d5db4c2a83e51cd2ba805685641a93c992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jezer=20Mej=C3=ADa?= Date: Tue, 16 Aug 2022 16:47:28 -0600 Subject: [PATCH 04/42] Add Spanish (Latin America) localization. Based on English, French and Japanese localizations. --- Platform/es-419.lproj/InfoPlist.strings | 14 + Platform/es-419.lproj/Localizable.strings | 1374 +++++++++++++++++ Platform/es-419.lproj/Localizable.stringsdict | 22 + .../VMDisplayMetalViewInputAccessory.strings | 162 ++ Platform/iOS/es-419.lproj/InfoPlist.strings | 20 + .../es-419.lproj/VMDisplayWindow.strings | 78 + Platform/macOS/es-419.lproj/InfoPlist.strings | 5 + QEMUHelper/es-419.lproj/InfoPlist.strings | 8 + QEMUHelper/es-419.lproj/Localizable.strings | 8 + UTM.xcodeproj/project.pbxproj | 35 +- 10 files changed, 1718 insertions(+), 8 deletions(-) create mode 100644 Platform/es-419.lproj/InfoPlist.strings create mode 100644 Platform/es-419.lproj/Localizable.strings create mode 100644 Platform/es-419.lproj/Localizable.stringsdict create mode 100644 Platform/iOS/Display/es-419.lproj/VMDisplayMetalViewInputAccessory.strings create mode 100644 Platform/iOS/es-419.lproj/InfoPlist.strings create mode 100644 Platform/macOS/Display/es-419.lproj/VMDisplayWindow.strings create mode 100644 Platform/macOS/es-419.lproj/InfoPlist.strings create mode 100644 QEMUHelper/es-419.lproj/InfoPlist.strings create mode 100644 QEMUHelper/es-419.lproj/Localizable.strings diff --git a/Platform/es-419.lproj/InfoPlist.strings b/Platform/es-419.lproj/InfoPlist.strings new file mode 100644 index 000000000..2a3c6372a --- /dev/null +++ b/Platform/es-419.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Privacy - Location Always and When In Use Usage Description */ +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Debido al funcionamiento de iOS, mantener la máquina virtual en segundo plano requiere de los servicios de localización. Esto no transmitirá su información de ubicación."; + +/* Privacy - Location Always Usage Description */ +"NSLocationAlwaysUsageDescription" = "Debido al funcionamiento de iOS, mantener la máquina virtual en segundo plano requiere de los servicios de localización. Esto no transmitirá su información de ubicación."; + +/* Privacy - Location When In Use Usage Description */ +"NSLocationWhenInUseUsageDescription" = "Debido al funcionamiento de iOS, mantener la máquina virtual en segundo plano requiere de los servicios de localización. Esto no transmitirá su información de ubicación."; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "UTM requiere su permiso para usar el micrófono."; + +/* (No Comment) */ +"UTM virtual machine" = "Máquina virtual de UTM"; diff --git a/Platform/es-419.lproj/Localizable.strings b/Platform/es-419.lproj/Localizable.strings new file mode 100644 index 000000000..6d36d79b6 --- /dev/null +++ b/Platform/es-419.lproj/Localizable.strings @@ -0,0 +1,1374 @@ +/* No comment provided by engineer. */ +"-" = "-"; + +/* A removable drive that has no image file inserted. */ +"(empty)" = "(vacío)"; + +/* UTMQemuConfiguration+Drives */ +"%@ Drive" = "Unidad %@"; + +/* VMConfigDriveCreateViewController */ +"A file already exists for this name, if you proceed, it will be replaced." = "Ya existe un archivo con este nombre, si continúas, será reemplazado."; + +/* VMListViewController */ +"A VM already exists with this name." = "Ya existe una VM con este nombre."; + +/* No comment provided by engineer. */ +"Additional Settings" = "Configuraciones Adicionales"; + +/* No comment provided by engineer. */ +"Advanced: Bypass configuration and manually specify arguments" = "Avanzado: Omitir configuración y especificar manualmente los argumentos"; + +/* VMConfigSystemView */ +"Allocating too much memory will crash the VM. Your device has %llu MB of memory and the estimated usage is %llu MB." = "Asignar demasiada memoria bloqueará la VM. Tu dispositivo tiene %llu MB de memoria y el uso estimado es de %llu MB."; + +/* UTMData */ +"AltJIT error: (error.localizedDescription)" = "Error de AltJIT: (error.localizedDescription)"; + +/* CSConnection */ +"An error occurred trying to connect to SPICE." = "Ocurrió un error al intentar conectarse a SPICE."; + +/* UTMData */ +"An existing virtual machine already exists with this name." = "Ya existe una máquina virtual con este nombre."; + +/* VMDisplayViewController */ +"An internal error has occured. UTM will terminate." = "Ocurrió un error interno. UTM se cerrará."; + +/* UTMConfiguration */ +"An internal error has occurred." = "Ha ocurrido un error interno."; + +/* No comment provided by engineer. */ +"Architecture" = "Arquitectura"; + +/* VMConfigDirectoryPickerViewController */ +"Are you sure you want to delete this directory? All files and subdirectories WILL be deleted." = "¿Estás seguro/a de querer eliminar este directorio? Todos los archivos y subdirectorios serán ELIMINADOS."; + +/* Delete confirmation */ +"Are you sure you want to delete this VM? Any drives associated will also be deleted." = "¿Estás seguro/a de eliminar esta VM? Cualquier unidad asociada también será eliminada."; + +/* VMDisplayViewController */ +"Are you sure you want to exit UTM?" = "¿Estás seguro/a de querer cerrar UTM?"; + +/* VMConfigDrivePickerViewController */ +"Are you sure you want to permanently delete this disk image?" = "¿Estás seguro/a de querer eliminar permanentemente esta imagen de disco?"; + +/* VMDisplayViewController */ +"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "¿Estás seguro/a de querer reiniciar esta VM? Se perderá cualquier cambio no guardado."; + +/* VMDisplayViewController */ +"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "¿Estás seguro/a de querer detener esta VM y salir? Se perderá cualquier cambio no guardado."; + +/* No comment provided by engineer. */ +"Argument" = "Argumento"; + +/* UTMQemuConfiguration */ +"BIOS" = "BIOS"; + +/* No comment provided by engineer. */ +"Blinking Cursor" = "Cursor parpadeante"; + +/* No comment provided by engineer. */ +"Boot" = "Boot"; + +/* No comment provided by engineer. */ +"Boot Arguments" = "Argumentos de arranque"; + +/* No comment provided by engineer. */ +"Boot from kernel image" = "Iniciar desde una imagen de kernel"; + +/* No comment provided by engineer. */ +"Boot Image" = "Imagen de arranque"; + +/* No comment provided by engineer. */ +"Boot ISO Image:" = "Imagen ISO de arranque:"; + +/* No comment provided by engineer. */ +"Boot VHDX Image:" = "Imagen VHDX de arranque:"; + +/* UTMQemuConfiguration */ +"Bridged (Advanced)" = "Puenteada (Avanzado)"; + +/* No comment provided by engineer. */ +"Bridged Interface" = "Interfaz de puente"; + +/* No comment provided by engineer. */ +"Browse" = "Navegar"; + +/* No comment provided by engineer. */ +"Browse UTM Gallery" = "Navegar la librería de UTM"; + +/* VMConfigSharingViewController */ +"Browse…" = "Buscar..."; + +/* Cancel button + VMConfigDirectoryPickerViewController + VMConfigPortForwardingViewController + VMDisplayMetalWindowController + VMRemovableDrivesViewController */ +"Cancel" = "Cancelar"; + +/* No comment provided by engineer. */ +"Cancel download" = "Cancelar descarga"; + +/* VMConfigDriveCreateViewController */ +"Cannot create directory for disk image." = "No se puede crear el directorio para la imagen de disco."; + +/* UTMData */ +"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "No es posible encontrar AltServer habilitado para JIT. No puedes ejecutar las VMs hasta que JIT esté habilitado."; + +/* VMListViewController */ +"Cannot find VM." = "No se puede encontrar la VM."; + +/* UTMData */ +"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "No se puede importar esta VM. La configuración es inválida, fue creado con una nueva versión de UTM, o en una plataforma que es incompatible con esta versión de UTM."; + +/* UTMVirtualMachine+Sharing */ +"Cannot start shared directory before SPICE starts." = "No se puede iniciar un directorio compartido antes de que SPICE inicie."; + +/* Configuration boot device */ +"CD/DVD" = "CD/DVD"; + +/* UTMQemuConfiguration */ +"CD/DVD (ISO) Image" = "Imagen (ISO) de CD/DVD"; + +/* VMRemovableDrivesViewController */ +"Change" = "Cambiar"; + +/* No comment provided by engineer. */ +"Clear" = "Limpiar"; + +/* No comment provided by engineer. */ +"Clipboard Sharing" = "Compartir portapapeles"; + +/* Clone context menu */ +"Clone" = "Clonar"; + +/* No comment provided by engineer. */ +"Clone selected VM" = "Clonar la VM seleccionada"; + +/* No comment provided by engineer. */ +"Clone…" = "Clonar..."; + +/* No comment provided by engineer. */ +"Close" = "Cerrar"; + +/* VMDisplayWindowController */ +"Closing this window will kill the VM." = "Cerrar esta ventana matará la VM."; + +/* No comment provided by engineer. */ +"Command to send when resizing the console. Placeholder $COLS is the number of columns and $ROWS is the number of rows." = "Comando a enviar al redimensionar la consola. La variable $COLS es el número de columnas y $ROWS es el número de filas."; + +/* UTMVirtualMachine */ +"Config format incorrect." = "El formato de configuración es incorrecto."; + +/* VMDisplayMetalWindowController */ +"Confirm" = "Confirmar"; + +/* No comment provided by engineer. */ +"Confirm Delete" = "Confirmar eliminación"; + +/* VMDisplayWindowController */ +"Confirmation" = "Confirmación"; + +/* No comment provided by engineer. */ +"Console Only" = "Sólo consola"; + +/* VMWizardSummaryView */ +"Core" = "Núcleo"; + +/* VMWizardSummaryView */ +"Cores" = "Núcleos"; + +/* No comment provided by engineer. */ +"CPU" = "CPU"; + +/* No comment provided by engineer. */ +"CPU Cores" = "Núcleos de CPU"; + +/* No comment provided by engineer. */ +"CPU Flags" = "Argumentos de CPU"; + +/* Create button */ +"Create" = "Crear"; + +/* No comment provided by engineer. */ +"Create a New Virtual Machine" = "Crear una nueva Máquina Virtual (VM)"; + +/* VMConfigDirectoryPickerViewController */ +"Create Directory" = "Crear directorio"; + +/* VMConfigDriveCreateViewController */ +"Creating disk…" = "Creando disco..."; + +/* No comment provided by engineer. */ +"Debug Logging" = "Registro de depuración"; + +/* No comment provided by engineer. */ +"Default" = "Por defecto"; + +/* Delete button + Delete context menu + VMConfigDirectoryPickerViewController */ +"Delete" = "Eliminar"; + +/* VMConfigDrivesViewController */ +"Delete Data" = "Eliminar los datos"; + +/* No comment provided by engineer. */ +"Delete selected VM" = "Eliminar la VM seleccionada"; + +/* No comment provided by engineer. */ +"Delete…" = "Eliminar..."; + +/* Delete VM overlay */ +"Deleting %@…" = "Eliminando %@..."; + +/* No comment provided by engineer. */ +"DHCP Domain Name" = "Nombre de dominio DHCP"; + +/* No comment provided by engineer. */ +"DHCP Host" = "Servidor DHCP"; + +/* No comment provided by engineer. */ +"DHCP Start" = "Inicio de DHCP"; + +/* No comment provided by engineer. */ +"Directory" = "Directorio"; + +/* VMConfigDirectoryPickerViewController */ +"Directory Name" = "Nombre de directorio"; + +/* VMDisplayTerminalViewController */ +"Disable this bar in Settings -> General -> Keyboards -> Shortcuts" = "Deshabilita esta barra en Configuración -> General -> Teclado -> Funciones rápidas"; + +/* No comment provided by engineer. */ +"Disk" = "Disco"; + +/* UTMData + VMConfigDriveCreateViewController + VMWizardState */ +"Disk creation failed." = "La creación del disco ha fallado"; + +/* UTMQemuConfiguration */ +"Disk Image" = "Imagen de disco"; + +/* No comment provided by engineer. */ +"Display" = "Monitor"; + +/* No comment provided by engineer. */ +"DNS Search Domains" = "Dominios de búsqueda DNS"; + +/* No comment provided by engineer. */ +"DNS Server" = "Servidor DNS"; + +/* No comment provided by engineer. */ +"DNS Server (IPv6)" = "Servidor DNS (IPv6)"; + +/* VMDisplayMetalWindowController */ +"Do Not Show Again" = "No mostrar de nuevo"; + +/* VMConfigDrivesViewController */ +"Do you want to also delete the disk image data? If yes, the data will be lost. Otherwise, you can create a new drive with the existing data." = "¿Desea también eliminar los datos de la imagen de disco? Si es así, los datos se perderán. De lo contrario, puedes crear una nueva unidad con los datos existentes."; + +/* No comment provided by engineer. */ +"Do you want to delete this VM and all its data?" = "¿Desea eliminar esta VM y todos sus datos?"; + +/* No comment provided by engineer. */ +"Do you want to duplicate this VM and all its data?" = "¿Desea duplicar esta VM y todos sus datos?"; + +/* No comment provided by engineer. */ +"Do you want to force stop this VM and lose all unsaved data?" = "¿Desea detener forzosamente esta VM y perder todos los datos no guardados?"; + +/* VMConfigDirectoryPickerViewController + VMConfigPortForwardingViewController */ +"Done" = "Completado"; + +/* No comment provided by engineer. */ +"Download prebuilt from UTM Gallery…" = "Descargar una VM lista para usar desde la librería de UTM..."; + +/* No comment provided by engineer. */ +"Download Ubuntu Server for ARM" = "Descargar Ubuntu Server para ARM"; + +/* No comment provided by engineer. */ +"Download Windows 11 for ARM64 Preview VHDX" = "Descargar imagen VHDX de la Preview de Windows 11 para ARM64"; + +/* No comment provided by engineer. */ +"Downscaling" = "Reducción de escala"; + +/* VMRemovableDrivesViewController */ +"Drive Options" = "Opciones de unidad de disco"; + +/* No comment provided by engineer. */ +"Drives" = "Unidades de disco"; + +/* No comment provided by engineer. */ +"Edit" = "Editar"; + +/* No comment provided by engineer. */ +"Edit selected VM" = "Editar la VM seleccionada"; + +/* VMRemovableDrivesViewController */ +"Eject" = "Expulsar"; + +/* No comment provided by engineer. */ +"Emulate" = "Emular"; + +/* No comment provided by engineer. */ +"Emulated Audio Card" = "Tarjeta de audio emulada"; + +/* No comment provided by engineer. */ +"Emulated Display Card" = "Tarjeta gráfica emulada"; + +/* No comment provided by engineer. */ +"Emulated Network Card" = "Tarjeta de red emulada"; + +/* UTMQemuConfiguration */ +"Emulated VLAN" = "VLAN emulado"; + +/* No comment provided by engineer. */ +"Enable Clipboard Sharing" = "Habilitar uso compartido del portapapeles"; + +/* No comment provided by engineer. */ +"Enable Directory Sharing" = "Habilitar directorio compartido"; + +/* No comment provided by engineer. */ +"Enable hardware OpenGL acceleration (experimental)" = "Habilitar la aceleración OpenGL por hardware (experimental)"; + +/* No comment provided by engineer. */ +"Enabled" = "Habilitado"; + +/* No comment provided by engineer. */ +"Engine" = "Motor"; + +/* VMDisplayWindowController */ +"Error" = "Error"; + +/* UTMJSONStream */ +"Error parsing JSON." = "Error al analizar el archivo JSON"; + +/* VMConfigDriveCreateViewController */ +"Error renaming file" = "Error al renombrar archivo"; + +/* UTMVirtualMachine */ +"Error trying to restore removable drives: %@" = "Error al intentar recuperar las unidades extraíbles: %@"; + +/* UTMVirtualMachine */ +"Error trying to start shared directory: %@" = "Error al intentar iniciar el directorio compartido: %@"; + +/* No comment provided by engineer. */ +"Export Debug Log" = "Exportar registro de depuración"; + +/* No comment provided by engineer. */ +"Export QEMU Command" = "Exportar comando de QEMU"; + +/* UTMVirtualMachine+Drives */ +"Failed create bookmark." = "No se pudo crear el marcador."; + +/* UTMVirtualMachine+Drives */ +"Failed to access drive image path." = "No se pudo acceder a la ruta de la imagen de disco."; + +/* VMConfigInfoView */ +"Failed to check name." = "No se pudo verificar el nombre."; + +/* UTMData */ +"Failed to clone VM." = "No se pudo clonar la VM."; + +/* UTMSpiceIO */ +"Failed to connect to SPICE server." = "No se pudo conectar al servidor SPICE."; + +/* UTMDataExtension */ +"Failed to delete saved state." = "No se pudo eliminar el estado guardado."; + +/* VMWizardState */ +"Failed to get latest macOS version from Apple." = "No se pudo obtener la última versión de macOS desde Apple."; + +/* VMRemovableDrivesViewController */ +"Failed to get VM object." = "No se pudo obtener el objeto de la VM."; + +/* UTMVirtualMachine */ +"Failed to load plist" = "No se pudo cargar la plist"; + +/* UTMData */ +"Failed to parse imported VM." = "No se pudo analizar la VM importada."; + +/* VMDisplayViewController */ +"Failed to save VM snapshot. Usually this means at least one device does not support snapshots." = "No se pudo guardar la instantánea de la VM."; + +/* No comment provided by engineer. */ +"Faster, but can only run the native CPU architecture." = "Más rápido, pero sólo puede ejecutar la arquitectura de CPU nativa."; + +/* No comment provided by engineer. */ +"Fit To Screen" = "Adaptar a la pantalla"; + +/* Configuration boot device */ +"Floppy" = "Disquete"; + +/* No comment provided by engineer. */ +"Font" = "Fuente de letra"; + +/* No comment provided by engineer. */ +"Font Size" = "Tamaño de letra"; + +/* No comment provided by engineer. */ +"Force Multicore" = "Forzar multinúcleo"; + +/* No comment provided by engineer. */ +"Full Graphics" = "Gráficos completos"; + +/* No comment provided by engineer. */ +"GB" = "GB"; + +/* No comment provided by engineer. */ +"Generate Windows Installer ISO" = "Generar imagen ISO del instalador de Windows"; + +/* No comment provided by engineer. */ +"Gesture and Cursor Settings" = "Configuración de gestos y cursor"; + +/* No comment provided by engineer. */ +"Guest Address" = "Dirección de invitado"; + +/* VMConfigPortForwardingViewController */ +"Guest address (optional)" = "Dirección de invitado (opcional)"; + +/* No comment provided by engineer. */ +"Guest Network" = "Red de invitado"; + +/* No comment provided by engineer. */ +"Guest Network (IPv6)" = "Red de invitado (IPv6)"; + +/* UTMQemuManager */ +"Guest panic" = "Pánico del invitado"; + +/* No comment provided by engineer. */ +"Guest Port" = "Puerto de invitado"; + +/* VMConfigPortForwardingViewController */ +"Guest port (required)" = "Puerto de invitado (requerido)"; + +/* Configuration boot device */ +"Hard Disk" = "Disco duro"; + +/* No comment provided by engineer. */ +"Hardware" = "Hardware"; + +/* No comment provided by engineer. */ +"Hide" = "Ocultar"; + +/* System pane. */ +"Hide Unused…" = "Ocultar no usado..."; + +/* VMDisplayViewController */ +"Hint: To show the toolbar again, use a three-finger swipe down on the screen." = "Pista: Para mostrar de nuevo la barra de herramientas, desliza hacia abajo con 3 dedos en la pantalla."; + +/* No comment provided by engineer. */ +"Host Address" = "Dirección de host"; + +/* No comment provided by engineer. */ +"Host Address (IPv6)" = "Dirección de host (IPv6)"; + +/* VMConfigPortForwardingViewController */ +"Host address (optional)" = "Dirección de host (opcional)"; + +/* No comment provided by engineer. */ +"Host Port" = "Puerto de host"; + +/* VMConfigPortForwardingViewController */ +"Host port (required)" = "Puerto de host (requerido)"; + +/* No comment provided by engineer. */ +"Hypervisor" = "Hipervisor"; + +/* No comment provided by engineer. */ +"I want to…" = "Quiero..."; + +/* No comment provided by engineer. */ +"Icon" = "Icono"; + +/* No comment provided by engineer. */ +"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "Si está habilitado, se iniciará directamente desde una imagen bruta de kernel e initrd. De lo contrario, se iniciará desde una imagen ISO compatible."; + +/* No comment provided by engineer. */ +"Image Type" = "Tipo de imagen"; + +/* Import button */ +"Import…" = "Importar..."; + +/* No comment provided by engineer. */ +"Import Drive" = "Importar unidad"; + +/* No comment provided by engineer. */ +"Import VHDX Image" = "Importar una imagen VHDX"; + +/* No comment provided by engineer. */ +"Import Virtual Machine…" = "Importar máquina virtual..."; + +/* Save VM overlay */ +"Importing %@…" = "Importando %@..."; + +/* No comment provided by engineer. */ +"Inactive" = "Inactivo"; + +/* No comment provided by engineer. */ +"Information" = "Información"; + +/* No comment provided by engineer. */ +"Initial Ramdisk" = "RAMDisk inicial"; + +/* No comment provided by engineer. */ +"Input" = "Entrada"; + +/* No comment provided by engineer. */ +"Interface" = "Interfaz"; + +/* UTMQemu */ +"Internal error has occurred." = "Ocurrió un error interno."; + +/* UTMVirtualMachine */ +"Internal error starting main loop." = "Error interno al iniciar el bucle principal."; + +/* UTMVirtualMachine */ +"Internal error starting VM." = "Error interno al iniciar la VM."; + +/* VMConfigSystemViewController */ +"Invalid core count." = "Cantidad inválida de núcleos."; + +/* UTMData */ +"Invalid drive size." = "Tamaño inválido del disco."; + +/* VMRemovableDrivesViewController */ +"Invalid file selected." = "Archivo seleccionado inválido"; + +/* VMConfigSystemViewController */ +"Invalid memory size." = "Tamaño inválido de memoria."; + +/* VMConfigDriveCreateViewController */ +"Invalid name" = "Nombre inválido"; + +/* VMConfigDriveCreateViewController */ +"Invalid size" = "Tamaño inválido"; + +/* VMListViewController */ +"Invalid UTM not imported." = "UTM inválido no importado."; + +/* No comment provided by engineer. */ +"Invert Mouse Scroll" = "Invertir el desplazamiento del mouse"; + +/* No comment provided by engineer. */ +"IP Configuration" = "Configuración de dirección IP"; + +/* No comment provided by engineer. */ +"Isolate Guest from Host" = "Aislar invitado del host"; + +/* No comment provided by engineer. */ +"JIT Cache" = "Caché de JIT"; + +/* VMConfigSystemViewController */ +"JIT cache size cannot be larger than 2GB." = "El tamaño de caché de JIT no puede ser mayor a 2GB."; + +/* VMConfigSystemViewController */ +"JIT cache size too small." = "El tamaño de caché de JIT es muy pequeño."; + +/* No comment provided by engineer. */ +"Kernel" = "Núcleo (Kernel)"; + +/* No comment provided by engineer. */ +"Keyboard" = "Teclado"; + +/* No comment provided by engineer. */ +"Legacy" = "Heredado"; + +/* No comment provided by engineer. */ +"Legacy (PS/2) Mode" = "Modo heredado (PS/2)"; + +/* No comment provided by engineer. */ +"License" = "Licencia"; + +/* UTMQemuConfiguration */ +"Linear" = "Linear"; + +/* No comment provided by engineer. */ +"Linux" = "Linux"; + +/* UTMQemuConfiguration */ +"Linux Device Tree Binary" = "Binario del árbol de dispositivos de Linux"; + +/* No comment provided by engineer. */ +"Linux initial ramdisk:" = "RAMDisk de Linux inicial:"; + +/* UTMQemuConfiguration */ +"Linux Kernel" = "Kernel de Linux"; + +/* No comment provided by engineer. */ +"Linux kernel (required):" = "Kernel de Linux (requerido)"; + +/* UTMQemuConfiguration */ +"Linux RAM Disk" = "Disco de memoria de Linux"; + +/* No comment provided by engineer. */ +"Linux Root FS Image:" = "Imagen Root FS de Linux"; + +/* No comment provided by engineer. */ +"Logging" = "Registro"; + +/* No comment provided by engineer. */ +"MAC Address" = "Dirección MAC"; + +/* No comment provided by engineer. */ +"Machine" = "Máquina"; + +/* VMWizardState */ +"macOS is not supported with QEMU." = "macOS no está soportado con QEMU."; + +/* UTMQemuManager */ +"Manager being deallocated, killing pending RPC." = "El controlador está siendo deasignado, matando RPC pendiente."; + +/* No comment provided by engineer. */ +"Maximum Shared USB Devices" = "Número máximo de dispositivos USB compartidos"; + +/* No comment provided by engineer. */ +"MB" = "MB"; + +/* No comment provided by engineer. */ +"Memory" = "Memoria"; + +/* No comment provided by engineer. */ +"Mouse Wheel" = "Rueda de ratón"; + +/* Save VM overlay */ +"Moving %@…" = "Moviendo %@..."; + +/* Clone VM name prompt title */ +"Name" = "Nombre"; + +/* VMConfigInfoView */ +"Name is an invalid filename." = "El nombre de archivo no es válido."; + +/* UTMQemuConfiguration */ +"Nearest Neighbor" = "Vecino más cercano"; + +/* No comment provided by engineer. */ +"Network" = "Red"; + +/* No comment provided by engineer. */ +"Network Mode" = "Modo de red"; + +/* No comment provided by engineer. */ +"New" = "Nuevo"; + +/* No comment provided by engineer. */ +"New Drive" = "Nueva unidad"; + +/* VMConfigPortForwardingViewController */ +"New port forward" = "Nuevo puerto de redirección"; + +/* No comment provided by engineer. */ +"New Virtual Machine" = "Nueva máquina virtual (VM)"; + +/* No comment provided by engineer. */ +"New VM" = "Nueva VM"; + +/* Clone VM name prompt message */ +"New VM name" = "Nuevo nombre de VM"; + +/* No comment provided by engineer. */ +"New…" = "Nuevo..."; + +/* No comment provided by engineer. */ +"Open…" = "Abrir..."; + +/* No comment provided by engineer. */ +"Continue" = "Continuar"; + +/* No button + VMDisplayViewController + VMListViewController */ +"No" = "No"; + +/* UTMQemuManager */ +"No connection for RPC." = "Sin conexión para RPC."; + +/* VMConfigExistingViewController */ +"No debug log found!" = "¡No se encontró ningún registro de depurado!"; + +/* No comment provided by engineer. */ +"No drives added." = "Sin unidades añadidas."; + +/* UTMData */ +"No log found!" = "¡No se encontró ningún registro!"; + +/* UTMDrive */ +"none" = "ninguno"; + +/* UTMQemuConfiguration */ +"None" = "Ninguno"; + +/* No comment provided by engineer. */ +"Not running" = "No está en ejecución"; + +/* No comment provided by engineer. */ +"Note: Boot order is as listed." = "Nota: El orden de arranque es el que se indica."; + +/* No comment provided by engineer. */ +"Note: select the path to share from the main screen." = "Nota: selecciona la ruta a compartir desde la pantalla principal."; + +/* No comment provided by engineer. */ +"Notes" = "Notas"; + +/* OK button + OK Button */ +"OK" = "OK"; + +/* No comment provided by engineer. */ +"Open VM Settings" = "Abrir configuración de la VM"; + +/* No comment provided by engineer. */ +"Operating System" = "Sistema operativo"; + +/* No comment provided by engineer. */ +"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "Opcionalmente, selecciona un directorio a hacer accessible dentro de la VM. Ten en cuenta que el soporte de los directorios compartidos varía según el sistema operativo invitado y puede requerir drivers adicionales a ser instalados. Ver las páginas de soporte de UTM para más información."; + +/* No comment provided by engineer. */ +"Other" = "Otro"; + +/* No comment provided by engineer. */ +"Pause" = "Pausar"; + +/* No comment provided by engineer. */ +"Pending" = "Pendiente"; + +/* No comment provided by engineer. */ +"Play" = "Iniciar"; + +/* VMWizardState */ +"Please select a boot image." = "Por favor selecciona una imagen de arranque."; + +/* VMWizardState */ +"Please select a kernel file." = "Por favor selecciona un archivo de kernel."; + +/* VMWizardState */ +"Please select a system to emulate." = "Por favor selecciona un sistema a emular."; + +/* No comment provided by engineer. */ +"Port Forward" = "Redirección de puerto"; + +/* No comment provided by engineer. */ +"Power Off" = "Apagar"; + +/* No comment provided by engineer. */ +"Protocol" = "Protocolo"; + +/* No comment provided by engineer. */ +"PS/2 has higher compatibility with older operating systems but does not support custom cursor settings." = "PS/2 tiene una mayor compatibilidad con sistemas operativos antiguos, pero no soporta configuraciones personalizadas del cursor."; + +/* No comment provided by engineer. */ +"QEMU" = "QEMU"; + +/* No comment provided by engineer. */ +"QEMU Arguments" = "Argumentos de QEMU"; + +/* UTMQemu */ +"QEMU exited from an error: %@" = "QEMU salió por un error: %@"; + +/* No comment provided by engineer. */ +"QEMU Machine Properties" = "Propiedades de la máquina QEMU"; + +/* No comment provided by engineer. */ +"Quit" = "Salir"; + +/* No comment provided by engineer. */ +"RAM" = "RAM"; + +/* No comment provided by engineer. */ +"Random" = "Aleatorio"; + +/* No comment provided by engineer. */ +"Read Only" = "Sólo lectura"; + +/* No comment provided by engineer. */ +"Share is read only" = "Compartir en modo sólo lectura"; + +/* No comment provided by engineer. */ +"Removable" = "Removible"; + +/* VMConfigDrivesView + VMConfigDrivesViewController */ +"Removable Drive" = "Unidad removible"; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed." = "Se requiere tener instalado las herramientas de agente invitado de SPICE."; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed. Retina Mode is recommended only if the guest OS supports HiDPI." = "Se requiere tener instalado las herramientas de agente invitado de SPICE. El Modo Retina es recomendado sólo si el sistema operativo invitado soporta HiDPI."; + +/* No comment provided by engineer. */ +"Always use native (HiDPI) resolution" = "Siempre usar resolución nativa (HiDPI)"; + +/* No comment provided by engineer. */ +"Requires SPICE WebDAV service to be installed." = "Se requiere tener instalado el servicio WebDAV de SPICE."; + +/* No comment provided by engineer. */ +"Resize Console Command" = "Comando de redimensionamiento de la consola"; + +/* No comment provided by engineer. */ +"Resolution" = "Resolución"; + +/* No comment provided by engineer. */ +"Restart" = "Reiniciar"; + +/* No comment provided by engineer. */ +"Retina Mode" = "Modo Retina"; + +/* No comment provided by engineer. */ +"Root Image" = "Imagen Root"; + +/* No comment provided by engineer. */ +"Run" = "Ejecutar"; + +/* No comment provided by engineer. */ +"Run selected VM" = "Ejecutar VM seleccionada"; + +/* No comment provided by engineer. */ +"Running" = "En ejecución"; + +/* VMDisplayViewController */ +"Running low on memory! UTM might soon be killed by iOS. You can prevent this by decreasing the amount of memory and/or JIT cache assigned to this VM" = "¡El dispositivo se está quedando sin memoria! UTM pronto podría ser matado por iOS. Puedes prevenir esto al disminuir la cantidad de memoria y/o el caché de JIT asignado a esta VM"; + +/* No comment provided by engineer. */ +"Save" = "Guardar"; + +/* Save VM overlay */ +"Saving %@…" = "Guardando %@..."; + +/* No comment provided by engineer. */ +"Scaling" = "Escalado"; + +/* No comment provided by engineer. */ +"Selected:" = "Seleccionado:"; + +/* No comment provided by engineer. */ +"Set to 0 for default which is 1/4 of the allocated Memory size. This is in addition to the host memory!" = "Establecido en 0 por defecto, que es 1/4 del tamaño de la memoria asignada. ¡Esto se suma a la memoria del host!"; + +/* No comment provided by engineer. */ +"Set to 0 to use maximum supported CPUs. Force multicore might result in incorrect emulation." = "Establecer en 0 para usar la cantidad máxima de CPUs soportados. Forzar multinúcleo puede resultar en una emulación incorrecta."; + +/* No comment provided by engineer. */ +"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "Forzar multinúcleo puede mejorar la velocidad de la emulación, pero también puede resultar en una emulación inestable e incorrecta."; + +/* No comment provided by engineer. */ +"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "Por defecto es 1/4 del tamaño de la RAM (de arriba). ¡El tamaño de caché de JIT se añade al total del uso de memoria!"; + +/* No comment provided by engineer. */ +"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "Estas son configuraciones avanzadas que afectan a QEMU y deben mantenerse predeterminadas a menos que tengas problemas."; + +/* No comment provided by engineer. */ +"This is appended to the -machine argument." = "Esto se adjunta al argumento -machine."; + +/* VMDisplayWindowController */ +"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "Esto puede corromper la VM y se perderá cualquier cambio no guardado. Para salir con seguridad, apaga la VM desde el invitado."; + +/* VMDisplayWindowController */ +"This will reset the VM and any unsaved state will be lost." = "Esto reiniciará la VM y se perderá cualquier estado no guardado."; + +/* No comment provided by engineer. */ +"If enabled, the default input devices will be emulated on the USB bus." = "Si está habilitado, los dispositivos de entrada predeterminados serán emulados en el bus de USB."; + +/* No comment provided by engineer. */ +"Settings" = "Configuraciones"; + +/* Share context menu */ +"Share" = "Compartir"; + +/* No comment provided by engineer. */ +"Share Directory" = "Compartir directorio"; + +/* No comment provided by engineer. */ +"Share selected VM" = "Compartir la VM seleccionada"; + +/* No comment provided by engineer. */ +"Shared Directory" = "Directorio compartido"; + +/* UTMQemuConfiguration */ +"Shared Network" = "Red compartida"; + +/* VMConfigSharingViewController */ +"Shared path has moved. Please re-choose." = "La ruta compartida ha sido movida. Por favor selecciona una nueva ruta."; + +/* VMConfigSharingViewController */ +"Shared path is no longer valid. Please re-choose." = "La ruta compartda ya no es válida. Por favor selecciona una nueva ruta."; + +/* No comment provided by engineer. */ +"Sharing" = "Compartiendo"; + +/* No comment provided by engineer. */ +"Show Advanced Settings" = "Mostrar configuraciones avanzadas"; + +/* System pane. */ +"Show All…" = "Mostrar todo..."; + +/* No comment provided by engineer. */ +"Size" = "Tamaño"; + +/* No comment provided by engineer. */ +"Skip Boot Image" = "Ignorar la imagen de arranque"; + +/* No comment provided by engineer. */ +"Skip ISO boot (advanced)" = "Ignorar el arranque ISO (avanzado)"; + +/* No comment provided by engineer. */ +"Slower, but can run other CPU architectures." = "Más lento, pero puede correr otras arquitecturas de CPU."; + +/* No comment provided by engineer. */ +"Sound" = "Audio"; + +/* No comment provided by engineer. */ +"Specify the size of the drive where data will be stored into." = "Especifica el tamaño del disco en donde los datos serán guardados."; + +/* No comment provided by engineer. */ +"Stop" = "Detener"; + +/* No comment provided by engineer. */ +"Stop selected VM" = "Detener la VM seleccionada."; + +/* No comment provided by engineer. */ +"Stop…" = "Detener..."; + +/* No comment provided by engineer. */ +"Storage" = "Almacenamiento"; + +/* No comment provided by engineer. */ +"stty cols $COLS rows $ROWS\n" = "stty cols $COLS rows $ROWS\n"; + +/* No comment provided by engineer. */ +"Style" = "Estilo"; + +/* No comment provided by engineer. */ +"Summary" = "Resumen"; + +/* No comment provided by engineer. */ +"Support" = "Soporte"; + +/* No comment provided by engineer. */ +"Suspended" = "Suspendido"; + +/* No comment provided by engineer. */ +"System" = "Sistema"; + +/* VMConfigPortForwardingViewController */ +"TCP Forward" = "Redirección TCP"; + +/* No comment provided by engineer. */ +"Test" = "Test"; + +/* No comment provided by engineer. */ +"The selected architecture is unsupported in this version of UTM." = "La arquitectura seleccionada no está soportada en esta versión de UTM."; + +/* VMConfigSystemViewController */ +"The total memory usage is close to your device's limit. iOS will kill the VM if it consumes too much memory." = "El uso de memoria total está cerca del límite del dispositivo. iOS matará la VM si consume demasiada memoria."; + +/* No comment provided by engineer. */ +"Theme" = "Tema"; + +/* Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine. */ +"There is no UTM file in the downloaded ZIP archive." = "No hay un archivo UTM en el archivo ZIP descargado."; + +/* No comment provided by engineer. */ +"These settings are unavailable in console display mode." = "Estas configuraciones no están disponibles en el modo de consola."; + +/* UTMQemuSystem */ +"This version of macOS does not support audio in console mode. Please change the VM configuration or upgrade macOS." = "Esta versión de macOS no soporta audio en el modo de consola. Por favor cambia la configuración de la VM o actualiza macOS."; + +/* UTMQemuSystem */ +"This version of macOS does not support GPU acceleration. Please change the VM configuration or upgrade macOS." = "Esta versión de macOS no soporta aceleración por GPU. Por favor cambia la configuración de la VM o actualiza macOS."; + +/* No comment provided by engineer. */ +"This virtual machine has been deleted." = "Esta máquina virtual ha sido eliminada."; + +/* UTMQemuManager */ +"Timed out waiting for RPC." = "Se agotó el tiempo de espera para RPC."; + +/* No comment provided by engineer. */ +"Tweaks" = "Retoques"; + +/* No comment provided by engineer. */ +"Type" = "Tipo"; + +/* VMConfigPortForwardingViewController */ +"UDP Forward" = "Redirección UDP"; + +/* No comment provided by engineer. */ +"UEFI Boot" = "Arranque UEFI"; + +/* No comment provided by engineer. */ +"Should be off for older operating systems such as Windows 7 or lower." = "Debería estar desactivado para sistemas operativos más antiguos, como Windows 7 o inferior."; + +/* UTMQemuSystem */ +"UEFI is not supported with this architecture." = "UEFI no está soportado con esta arquitectura."; + +/* VMWizardState */ +"Unavailable for this platform." = "No disponible para esta plataforma."; + +/* UTMVirtualMachineExtension */ +"Unknown" = "Desconocido"; + +/* No comment provided by engineer. */ +"Upscaling" = "Aumento de escala"; + +/* No comment provided by engineer. */ +"USB" = "USB"; + +/* No comment provided by engineer. */ +"USB 3.0 (XHCI) Support" = "Soporte de USB 3.0 (XHCI)"; + +/* No comment provided by engineer. */ +"USB not supported in console display mode." = "USB no soportado en el modo consola."; + +/* No comment provided by engineer. */ +"USB not supported in this build of UTM." = "USB no soportado en esta versión de UTM."; + +/* No comment provided by engineer. */ +"USB Sharing" = "Compartir USB"; + +/* No comment provided by engineer. */ +"Use Hypervisor" = "Usar Hipervisor"; + +/* No comment provided by engineer. */ +"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "Sólo disponible si la arquitectura host coincide con la del invitado. De otro modo, se utiliza la emulación TCG."; + +/* No comment provided by engineer. */ +"Use Virtualization" = "Utilizar la Virtualización"; + +/* No comment provided by engineer. */ +"User Guide" = "Guía de usuario"; + +/* No comment provided by engineer. */ +"Virtual Machine Gallery" = "Librería de máquinas virtuales"; + +/* No comment provided by engineer. */ +"Virtualization is not supported on your system." = "La Virtualización no está soportada en tu sistema."; + +/* No comment provided by engineer. */ +"Virtualize" = "Virtualizar"; + +/* UTMVirtualMachine+Sharing */ +"VM frontend does not support shared directories." = "El frontend no soporta los directorios compartidos."; + +/* VMConfigSystemViewController */ +"Warning: iOS will kill apps that use more than 80% of the device's total memory." = "Advertencia: iOS matará las aplicaciones que usan más del 80% de la memoria total del dispositivo."; + +/* No comment provided by engineer. */ +"Welcome to UTM" = "Bienvenido/a a UTM"; + +/* Startup message */ +"Welcome to UTM! Due to a bug in iOS, if you force kill this app, the system will be unstable and you cannot launch UTM again until you reboot. The recommended way to terminate this app is the button on the top left." = "¡Bienvenido/a a UTM! Debido a un bug en iOS, si matas forzosamente esta app el sistema se inestabilizará y no podrás lanzar UTM de nuevo hazta que reinicies. La forma recomendada de terminar esta app es con el botón arriba a la izquierda."; + +/* No comment provided by engineer. */ +"Windows" = "Windows"; + +/* VMDisplayMetalWindowController */ +"Would you like to connect '%@' to this virtual machine?" = "¿Te gustaría conectar '%@' a esta máquina virtual?"; + +/* VMConfigDrivePickerViewController */ +"Would you like to import an existing disk image or create a new one?" = "¿Te gustaría importar una imagen de disco existente o crear una nueva?"; + +/* VMDisplayViewController + VMListViewController + Yes button */ +"Yes" = "Sí"; + +/* UTMData + VMConfigDrivePickerViewController */ +"You cannot import a .utm package as a drive. Did you mean to open the package with UTM?" = "No puedes importar un paquete .utm como una unidad. ¿Quizá querías abrir el paquete con UTM?"; + +/* UTMData + VMConfigDrivePickerViewController */ +"You cannot import a directory as a drive." = "No puedes importar un directorio como una unidad."; + +/* VMConfigDriveDetailsViewController */ +"You must select a disk image." = "Debes de seleccionar una imagen de disco."; + +/* VMDisplayViewController */ +"You must terminate the running VM before you can import a new VM." = "Debes de terminar la VM en ejecución antes de poder importar una nueva VM."; + +/* ContentView */ +"Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached." = "Tu versión de iOS no soporta ejecutar VMs sin modificar. Debes de ejecutar UTM con Jailbreak o con un depurador remoto conectado."; + +/* No comment provided by engineer. */ +"Zoom" = "Zoom"; + +/* Manually added: Common > Button */ +"Go Back" = "Atrás"; + +/* Manually added: Create a New Virtual Machine > macOS */ +"To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple." = "Para instalar macOS, debes descargar un IPSW de recuperación. Si no seleccionas un IPSW existente, el IPSW más reciente de macOS será descargado desde Apple."; + +/* Manually added: Create a New Virtual Machine > Virtualization > Linux */ +"Use Apple Virtualization" = "Usar la Virtualización de Apple"; + +/* Manually added: Configuration > Drive */ +"Delete Drive" = "Eliminar unidad"; + +/* Manually added: Configuration > Drive */ +"Move Up" = "Ascender"; + +/* Manually added: Configuration > Drive */ +"Move Down" = "Descender"; + +/* Manually added: VM's Context Menu */ +"Show in Finder" = "Mostrar en Finder"; + +/* No comment provided by engineer. */ +"Generic" = "Genérico"; + +/* No comment provided by engineer. */ +"Custom" = "Personalizado"; + +/* No comment provided by engineer. */ +"Status" = "Estado"; + +/* No comment provided by engineer. */ +"Stopped" = "Detenido"; + +/* No comment provided by engineer. */ +"Acceleration" = "Aceleración"; + +/* No comment provided by engineer. */ +"Force PS/2 controller" = "Forzar el controlador PS/2"; + +/* No comment provided by engineer. */ +"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "Iniciar el controlador PS/2 incluso si la entrada USB está soportada. Requerido para versiones de Windows antiguas."; + +/* No comment provided by engineer. */ +"Use local time for base clock" = "Usar tiempo local para el reloj base"; + +/* No comment provided by engineer. */ +"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "Si está habilitado, usar la hora local para RTC que es requerido en Windows. De otro modo, usar el reloj UTC."; + +/* No comment provided by engineer. */ +"RNG Device" = "Dispositivo RNG"; + +/* No comment provided by engineer. */ +"Should be on always unless the guest cannot boot because of this." = "Debería de estar siempre activo, a menos que el invitado no pueda arrancar debido a esto."; + +/* No comment provided by engineer. */ +"Boot UEFI" = "Arranque UEFI"; + +/* No comment provided by engineer. */ +"Do not generate any arguments based on current configuration" = "No generar ningún argumento basado en la configuración actual"; + +/* No comment provided by engineer. */ +"Keep UTM running after last window is closed and all VMs are shut down" = "Continuar ejecutando UTM después de que se cierre la última ventana y se apaguen todas las VMs"; + +/* No comment provided by engineer. */ +"VM display size is fixed" = "El tamaño de pantalla de la VM es fijo"; + +/* No comment provided by engineer. */ +"Do not save VM screenshot to disk" = "No guardar la captura de pantalla de la VM en el disco"; + +/* No comment provided by engineer. */ +"Default VM Configuration" = "Configuración por defecto de la VM"; + +/* No comment provided by engineer. */ +"Force slower emulation by default (deprecated: now configured per-VM)" = "Forzar una emulación más lenta por defecto (obsoleto: configurado por VM)"; + +/* No comment provided by engineer. */ +"Use only performance cores by default (deprecated: now configured per-VM)" = "Usar sólo los núcleos de rendimiento por defecto (obsoleto: configurado por VM)"; + +/* No comment provided by engineer. */ +"Hold Control (⌃) for right click" = "Mantener presionado Control (⌃) para hacer click derecho"; + +/* No comment provided by engineer. */ +"Use Command+Option (⌘+⌥) for input capture/release" = "Usa Command+Option (⌘+⌥) para capturar/librerar la entrada"; + +/* No comment provided by engineer. */ +"Caps Lock (⇪) is treated as a key" = "Bloq Mayús (⇪) es tratado como una tecla"; + +/* No comment provided by engineer. */ +"Do not show prompt when USB device is plugged in" = "No mostrar mensaje cuando un dispositivo USB es conectado"; + +/* No comment provided by engineer. */ +"USB Support" = "Soporte de USB"; + +/* No comment provided by engineer. */ +"Auto Resolution" = "Resolución automática"; + +/* No comment provided by engineer. */ +"Resize display to window size automatically" = "Redimensionar el tamaño de la pantalla al tamaño de la ventana automáticamente"; + +/* No comment provided by engineer. */ +"Share USB devices from host" = "Compartir dispositivos USB del host"; + +/* No comment provided by engineer. */ +"Reclaim Space" = "Reclamar espacio"; + +/* No comment provided by engineer. */ +"Error" = "Error"; + +/* Main pane. */ +"Move selected VM" = "Mover la VM seleccionada"; + +/* Preferences pane. */ +"Invert scrolling" = "Invertir el desplazamiento del mouse"; + +/* New VM window. */ +"Start" = "Inicio"; + +/* New VM window. */ +"Existing" = "Existente"; + +/* New VM window. */ +"Preconfigured" = "Preconfigurado"; + +/* New VM window. */ +"Import IPSW" = "Importar un IPSW"; + +/* New VM window. */ +"Drag and drop IPSW file here" = "Arrastre y suelte el archivo IPSW aquí"; + +/* New VM window. */ +"Empty" = "Vacío"; + +/* New VM window. */ +"Advanced" = "Avanzado"; + +/* New VM window. */ +"Skip ISO boot" = "Ignorar el arranque ISO"; + +/* New VM window. */ +"Image File Type" = "Tipo de archivo de imagen"; + +/* New VM window. */ +"Some older systems do not support UEFI boot, such as Windows 7 and below." = "Algunos sistemas antiguos no soportan el arranque UEFI, como Windows 7 y anteriores."; + +/* New VM window. */ +"File Imported" = "Archivo importado"; + +/* New VM window. */ +"Hint: For the best Windows experience, make sure to download and install the latest [SPICE tools and QEMU drivers](https://mac.getutm.app/support/)." = "Pista: Para la mejor experiencia en Windows, asegúrate de descargar e instalar las más recientes [herramientas de SPICE y drivers de QEMU](https://mac.getutm.app/support/)"; + +/* New VM window. */ +"Virtualization Engine" = "Motor de Virtualización"; + +/* New VM window. */ +"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "La Virtualización de Apple es experimental y sólo para casos de uso avanzado. Deje sin marcar para usar QEMU, que es lo recomendado."; + +/* New VM window. */ +"Boot Image Type" = "Tipo de imagen de arranque"; + +/* New VM window. */ +"Linux kernel (required):" = "Kernel de Linux (requerido):"; + +/* New VM window. */ +"Linux initial ramdisk (optional):" = "RAMDisk inicial de Linux (opcional):"; + +/* New VM window. */ +"Linux Root FS image (optional):" = "Imagen Root FS de Linux (opcional):"; + +/* New VM window. */ +"Boot ISO Image (optional):" = "Imagen ISO de arranque (opcional):"; + +/* System pane. */ +"Force Enable CPU Flags" = "Forzar habilitado de argumentos de CPU"; + +/* System pane. */ +"Force Disable CPU Flags" = "Forzar deshabilitado de argumentos de CPU"; + +/* System pane. */ +"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "Si está marcado, el argumento de CPU se habilitará. De lo contrario, el valor predeterminado será usado."; + +/* System pane. */ +"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "Si está marcado, el argumento de CPU se deshabilitará. De lo contrario, el valor predeterminado será usado."; + +/* QEMU pane. */ +"Balloon Device" = "Dispositivo Balloon"; + +/* Input pane. */ +"Disabled" = "Desactivado"; + +/* Share pane. */ +"Directory Share Mode" = "Modo de directorio compartido"; + +/* Share pane. */ +"SPICE WebDAV (Legacy)" = "SPICE WebDAV (Heredado)"; + +/* Share pane. */ +"VirtFS (Recommended)" = "VirtFS (Recomendado)"; + +/* Video pane. */ +"VGA Device RAM (MB)" = "RAM del dispositivo VGA (en MB)"; + +/* Network pane. */ +"Host Only" = "Sólo host"; + +/* Left pane. */ +"Serial" = "Serial"; + +/* Left pane. */ +"Devices" = "Dispositivos"; + +/* Serial pane. */ +"Built-in Terminal" = "Terminal incorporado"; + +/* Serial pane. */ +"TCP Client Connection" = "Conexión de cliente TCP"; + +/* Serial pane. */ +"TCP Server Connection" = "Conexión de servidor TCP"; + +/* Serial pane. */ +"Pseudo-TTY Device" = "Dispositivo pseudo-TTY"; + +/* Serial pane. */ +"Target" = "Destino"; +/* Serial pane. */ +"Mode" = "Modo"; + +/* Serial pane. */ +"Automatic Serial Device (max 4)" = "Dispositivo serial automático (máx 4)"; + +/* Serial pane. */ +"Manual Serial Device (advanced)" = "Dispositivo serial manual (avanzado)"; + +/* Serial pane. */ +"Emulated Serial Device" = "Dispositivo serial emulado"; + +/* Serial pane. */ +"GDB Debug Stub" = "GDB Debug Stub"; + +/* Serial pane. */ +"QEMU Monitor (HMP)" = "Monitor QEMU (HMP)"; + +/* Serial pane. */ +"Server Address" = "Dirección del servidor"; + +/* Serial pane. */ +"Wait for Connection" = "Esperar conexión"; + +/* Serial pane. */ +"Text Color" = "Color del texto"; + +/* Serial pane. */ +"Background Color" = "Color de fondo"; + +/* Drive pane. */ +"None (Advanced)" = "Ninguno (avanzado)"; + +/* Drive pane. */ +"SD Card" = "Tarjeta SD"; + +/* Drive pane. */ +"IDE Drive" = "Unidad IDE"; + +/* Drive pane. */ +"SCSI Drive" = "Unidad SCSI"; + +/* Drive pane. */ +"VirtIO Drive" = "Unidad VirtIO"; + +/* Drive pane. */ +"NVMe Drive" = "Unidad NVMe"; + +/* Drive pane. */ +"USB Drive" = "Unidad USB"; + +/* Share and Drive panes. */ +"Path" = "Ruta"; diff --git a/Platform/es-419.lproj/Localizable.stringsdict b/Platform/es-419.lproj/Localizable.stringsdict new file mode 100644 index 000000000..45479e04c --- /dev/null +++ b/Platform/es-419.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + %lld Cores + + NSStringLocalizedFormatKey + %#@cores@ + cores + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + %lld Core + other + %lld Cores + + + + diff --git a/Platform/iOS/Display/es-419.lproj/VMDisplayMetalViewInputAccessory.strings b/Platform/iOS/Display/es-419.lproj/VMDisplayMetalViewInputAccessory.strings new file mode 100644 index 000000000..7b36f2c87 --- /dev/null +++ b/Platform/iOS/Display/es-419.lproj/VMDisplayMetalViewInputAccessory.strings @@ -0,0 +1,162 @@ + +/* Class = "UIButton"; normalTitle = "F7"; ObjectID = "3yi-Pr-1ih"; */ +"3yi-Pr-1ih.normalTitle" = "F7"; + +/* Class = "UIButton"; accessibilityLabel = "Paste"; ObjectID = "740-aI-39P"; */ +"740-aI-39P.accessibilityLabel" = "Pegar"; + +/* Class = "UIButton"; accessibilityLabel = "Tab"; ObjectID = "7pj-Jz-7JR"; */ +"7pj-Jz-7JR.accessibilityLabel" = "Tab"; + +/* Class = "UIButton"; normalTitle = "⇥"; ObjectID = "7pj-Jz-7JR"; */ +"7pj-Jz-7JR.normalTitle" = "⇥"; + +/* Class = "UIButton"; accessibilityLabel = "Right"; ObjectID = "8Lh-4D-Fz6"; */ +"8Lh-4D-Fz6.accessibilityLabel" = "Derecha"; + +/* Class = "UIButton"; normalTitle = "→"; ObjectID = "8Lh-4D-Fz6"; */ +"8Lh-4D-Fz6.normalTitle" = "→"; + +/* Class = "UIButton"; accessibilityLabel = "Right"; ObjectID = "AY8-eJ-bAP"; */ +"AY8-eJ-bAP.accessibilityLabel" = "Derecha"; + +/* Class = "UIButton"; normalTitle = "Del"; ObjectID = "AY8-eJ-bAP"; */ +"AY8-eJ-bAP.normalTitle" = "Borrar"; + +/* Class = "UIButton"; normalTitle = "F10"; ObjectID = "AhH-ij-IF8"; */ +"AhH-ij-IF8.normalTitle" = "F10"; + +/* Class = "UIButton"; accessibilityLabel = "Up"; ObjectID = "BUL-js-yMh"; */ +"BUL-js-yMh.accessibilityLabel" = "Arriba"; + +/* Class = "UIButton"; normalTitle = "↑"; ObjectID = "BUL-js-yMh"; */ +"BUL-js-yMh.normalTitle" = "↑"; + +/* Class = "UIButton"; accessibilityLabel = "Num Lock"; ObjectID = "BUk-Vf-yE5"; */ +"BUk-Vf-yE5.accessibilityLabel" = "Bloq Num"; + +/* Class = "UIButton"; normalTitle = "Num"; ObjectID = "BUk-Vf-yE5"; */ +"BUk-Vf-yE5.normalTitle" = "Num"; + +/* Class = "UIButton"; normalTitle = "F5"; ObjectID = "DxX-zu-urb"; */ +"DxX-zu-urb.normalTitle" = "F5"; + +/* Class = "UIButton"; normalTitle = "F12"; ObjectID = "EDi-KP-KwO"; */ +"EDi-KP-KwO.normalTitle" = "F12"; + +/* Class = "UIButton"; accessibilityLabel = "Left"; ObjectID = "EVa-2J-CRA"; */ +"EVa-2J-CRA.accessibilityLabel" = "Izquierda"; + +/* Class = "UIButton"; normalTitle = "←"; ObjectID = "EVa-2J-CRA"; */ +"EVa-2J-CRA.normalTitle" = "←"; + +/* Class = "UIButton"; accessibilityLabel = "Caps Lock"; ObjectID = "FDV-W6-qlO"; */ +"FDV-W6-qlO.accessibilityLabel" = "Bloq Mayús"; + +/* Class = "UIButton"; normalTitle = "Caps"; ObjectID = "FDV-W6-qlO"; */ +"FDV-W6-qlO.normalTitle" = "Mayús"; + +/* Class = "UIButton"; accessibilityLabel = "Home"; ObjectID = "LU6-kH-vN3"; */ +"LU6-kH-vN3.accessibilityLabel" = "Inicio"; + +/* Class = "UIButton"; normalTitle = "Home"; ObjectID = "LU6-kH-vN3"; */ +"LU6-kH-vN3.normalTitle" = "Inicio"; + +/* Class = "UIButton"; normalTitle = "F8"; ObjectID = "LlV-Ae-CrL"; */ +"LlV-Ae-CrL.normalTitle" = "F8"; + +/* Class = "UIButton"; normalTitle = "F1"; ObjectID = "PWe-Va-Qi1"; */ +"PWe-Va-Qi1.normalTitle" = "F1"; + +/* Class = "UIButton"; accessibilityLabel = "Print Screen"; ObjectID = "Pes-KN-KzU"; */ +"Pes-KN-KzU.accessibilityLabel" = "Imprimir Pantalla"; + +/* Class = "UIButton"; normalTitle = "Pr Scr"; ObjectID = "Pes-KN-KzU"; */ +"Pes-KN-KzU.normalTitle" = "Impr Pant"; + +/* Class = "UIButton"; accessibilityLabel = "Command"; ObjectID = "Pjh-3m-tFX"; */ +"Pjh-3m-tFX.accessibilityLabel" = "Command"; + +/* Class = "UIButton"; normalTitle = "⌘"; ObjectID = "Pjh-3m-tFX"; */ +"Pjh-3m-tFX.normalTitle" = "⌘"; + +/* Class = "UIButton"; accessibilityLabel = "Shift"; ObjectID = "QPo-cD-UlK"; */ +"QPo-cD-UlK.accessibilityLabel" = "Shift"; + +/* Class = "UIButton"; normalTitle = "⇧"; ObjectID = "QPo-cD-UlK"; */ +"QPo-cD-UlK.normalTitle" = "⇧"; + +/* Class = "UIButton"; accessibilityLabel = "Down"; ObjectID = "RCo-l7-gvf"; */ +"RCo-l7-gvf.accessibilityLabel" = "Abajo"; + +/* Class = "UIButton"; normalTitle = "↓"; ObjectID = "RCo-l7-gvf"; */ +"RCo-l7-gvf.normalTitle" = "↓"; + +/* Class = "UIButton"; normalTitle = "F6"; ObjectID = "Rb5-vO-sIx"; */ +"Rb5-vO-sIx.normalTitle" = "F6"; + +/* Class = "UIButton"; accessibilityLabel = "End"; ObjectID = "TOV-fV-TTa"; */ +"TOV-fV-TTa.accessibilityLabel" = "Fin"; + +/* Class = "UIButton"; normalTitle = "End"; ObjectID = "TOV-fV-TTa"; */ +"TOV-fV-TTa.normalTitle" = "Fin"; + +/* Class = "UIButton"; normalTitle = "F9"; ObjectID = "UNT-ei-lIn"; */ +"UNT-ei-lIn.normalTitle" = "F9"; + +/* Class = "UIButton"; accessibilityLabel = "Control"; ObjectID = "bCv-uH-SSy"; */ +"bCv-uH-SSy.accessibilityLabel" = "Control"; + +/* Class = "UIButton"; normalTitle = "⌃"; ObjectID = "bCv-uH-SSy"; */ +"bCv-uH-SSy.normalTitle" = "⌃"; + +/* Class = "UIButton"; normalTitle = "F4"; ObjectID = "c7C-CG-EBg"; */ +"c7C-CG-EBg.normalTitle" = "F4"; + +/* Class = "UIButton"; normalTitle = "F3"; ObjectID = "gUX-ez-mbt"; */ +"gUX-ez-mbt.normalTitle" = "F3"; + +/* Class = "UIButton"; accessibilityLabel = "Page Down"; ObjectID = "h4q-XF-UMn"; */ +"h4q-XF-UMn.accessibilityLabel" = "Avanzar Página"; + +/* Class = "UIButton"; normalTitle = "Pg Dn"; ObjectID = "h4q-XF-UMn"; */ +"h4q-XF-UMn.normalTitle" = "Av Pág"; + +/* Class = "UIButton"; accessibilityLabel = "Option"; ObjectID = "jxu-AQ-u8c"; */ +"jxu-AQ-u8c.accessibilityLabel" = "Option"; + +/* Class = "UIButton"; normalTitle = "⌥"; ObjectID = "jxu-AQ-u8c"; */ +"jxu-AQ-u8c.normalTitle" = "⌥"; + +/* Class = "UIButton"; accessibilityLabel = "Insert"; ObjectID = "kO0-HZ-5w2"; */ +"kO0-HZ-5w2.accessibilityLabel" = "Insertar"; + +/* Class = "UIButton"; normalTitle = "Ins"; ObjectID = "kO0-HZ-5w2"; */ +"kO0-HZ-5w2.normalTitle" = "Ins"; + +/* Class = "UIButton"; normalTitle = "F2"; ObjectID = "kd1-fj-kXM"; */ +"kd1-fj-kXM.normalTitle" = "F2"; + +/* Class = "UIButton"; accessibilityLabel = "Escape"; ObjectID = "n12-9R-99C"; */ +"n12-9R-99C.accessibilityLabel" = "Escape"; + +/* Class = "UIButton"; normalTitle = "⎋"; ObjectID = "n12-9R-99C"; */ +"n12-9R-99C.normalTitle" = "⎋"; + +/* Class = "UIButton"; accessibilityLabel = "Page Up"; ObjectID = "pX1-7o-dbU"; */ +"pX1-7o-dbU.accessibilityLabel" = "Retroceder Página"; + +/* Class = "UIButton"; normalTitle = "Pg Up"; ObjectID = "pX1-7o-dbU"; */ +"pX1-7o-dbU.normalTitle" = "Re Pág"; + +/* Class = "UIButton"; normalTitle = "F11"; ObjectID = "rfk-su-cFq"; */ +"rfk-su-cFq.normalTitle" = "F11"; + +/* Class = "UIButton"; accessibilityLabel = "Hide Keyboard"; ObjectID = "rtU-Yt-FhT"; */ +"rtU-Yt-FhT.accessibilityLabel" = "Ocultar Teclado"; + +/* Class = "UIButton"; accessibilityLabel = "Scroll Lock"; ObjectID = "sF1-tj-hUG"; */ +"sF1-tj-hUG.accessibilityLabel" = "Bloq Despl"; + +/* Class = "UIButton"; normalTitle = "Scroll"; ObjectID = "sF1-tj-hUG"; */ +"sF1-tj-hUG.normalTitle" = "Desplazamiento"; diff --git a/Platform/iOS/es-419.lproj/InfoPlist.strings b/Platform/iOS/es-419.lproj/InfoPlist.strings new file mode 100644 index 000000000..b81675292 --- /dev/null +++ b/Platform/iOS/es-419.lproj/InfoPlist.strings @@ -0,0 +1,20 @@ +/* Bundle name */ +"CFBundleName" = "UTM"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "UTM usa la red nativa para encontrar y comunicarse con AltServer."; + +/* Privacy - Location Always and When In Use Usage Description */ +"NSLocationAlwaysAndWhenInUseUsageDescription" = "El acceso en segundo plano a la máquina virtual requiere de los servicios de localización. Los datos de ubicación no se transfieren fuera del dispositivo."; + +/* Privacy - Location Always Usage Description */ +"NSLocationAlwaysUsageDescription" = "El acceso en segundo plano a la máquina virtual requiere de los servicios de localización. Los datos de ubicación no se transfieren fuera del dispositivo."; + +/* Privacy - Location When In Use Usage Description */ +"NSLocationWhenInUseUsageDescription" = "El acceso en segundo plano a la máquina virtual requiere de los servicios de localización. Los datos de ubicación no se transfieren fuera del dispositivo."; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "La máquina virtual necesita el acceso al micrófono."; + +/* (No Comment) */ +"UTM virtual machine" = "Máquina virtual de UTM"; diff --git a/Platform/macOS/Display/es-419.lproj/VMDisplayWindow.strings b/Platform/macOS/Display/es-419.lproj/VMDisplayWindow.strings new file mode 100644 index 000000000..7dff78c46 --- /dev/null +++ b/Platform/macOS/Display/es-419.lproj/VMDisplayWindow.strings @@ -0,0 +1,78 @@ + +/* Class = "NSToolbarItem"; label = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.label" = "Carpeta compartida"; + +/* Class = "NSToolbarItem"; paletteLabel = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.paletteLabel" = "Carpeta compartida"; + +/* Class = "NSToolbarItem"; toolTip = "Shared folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.toolTip" = "Carpeta compartida"; + +/* Class = "NSToolbarItem"; label = "Stop"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.label" = "Detener"; + +/* Class = "NSToolbarItem"; paletteLabel = "Stop"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.paletteLabel" = "Detener"; + +/* Class = "NSToolbarItem"; toolTip = "Shuts down and stops the VM"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.toolTip" = "Apaga y detiene la VM"; + +/* Class = "NSToolbarItem"; label = "Capture Mouse"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.label" = "Capturar mouse"; + +/* Class = "NSToolbarItem"; paletteLabel = "Capture Mouse"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.paletteLabel" = "Capturar mouse"; + +/* Class = "NSToolbarItem"; toolTip = "Capture mouse cursor"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.toolTip" = "Capturar cursor del mouse"; + +/* Class = "NSToolbarItem"; label = "Restart"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.label" = "Reiniciar"; + +/* Class = "NSToolbarItem"; paletteLabel = "Restart"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.paletteLabel" = "Reiniciar"; + +/* Class = "NSToolbarItem"; toolTip = "Restarts the VM"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.toolTip" = "Reinicia la VM"; + +/* Class = "NSWindow"; title = "UTM"; ObjectID = "QvC-M9-y7g"; */ +"QvC-M9-y7g.title" = "UTM"; + +/* Class = "NSToolbarItem"; label = "Resize Console"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.label" = "Redimensionar la consola"; + +/* Class = "NSToolbarItem"; paletteLabel = "Resize Console"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.paletteLabel" = "Redimensionar la consola"; + +/* Class = "NSToolbarItem"; toolTip = "Send console resize command"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.toolTip" = "Enviar el comando de redimensionamiento de la consola"; + +/* Class = "NSButton"; ibShadowedToolTip = "Starts/resumes the VM"; ObjectID = "ZTi-Hs-ge6"; */ +"ZTi-Hs-ge6.ibShadowedToolTip" = "Inicia/resume la VM"; + +/* Class = "NSToolbarItem"; label = "Drives"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.label" = "Unidades"; + +/* Class = "NSToolbarItem"; paletteLabel = "Drives"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.paletteLabel" = "Unidades"; + +/* Class = "NSToolbarItem"; toolTip = "Drive image options"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.toolTip" = "Opciones de unidades de disco"; + +/* Class = "NSToolbarItem"; label = "Start/Pause"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.label" = "Iniciar/Pausar"; + +/* Class = "NSToolbarItem"; paletteLabel = "Start/Pause"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.paletteLabel" = "Iniciar/Pausar"; + +/* Class = "NSToolbarItem"; toolTip = "Start/pause the VM"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.toolTip" = "Inicia/pausa la VM"; + +/* Class = "NSToolbarItem"; label = "USB"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.label" = "USB"; + +/* Class = "NSToolbarItem"; paletteLabel = "USB"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.paletteLabel" = "USB"; + +/* Class = "NSToolbarItem"; toolTip = "USB devices"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.toolTip" = "Dispositivos USB"; diff --git a/Platform/macOS/es-419.lproj/InfoPlist.strings b/Platform/macOS/es-419.lproj/InfoPlist.strings new file mode 100644 index 000000000..12fd50037 --- /dev/null +++ b/Platform/macOS/es-419.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +/* Bundle name */ +"CFBundleName" = "UTM"; + +/* (No Comment) */ +"UTM virtual machine" = "Máquina virtual de UTM"; diff --git a/QEMUHelper/es-419.lproj/InfoPlist.strings b/QEMUHelper/es-419.lproj/InfoPlist.strings new file mode 100644 index 000000000..aa2985f9f --- /dev/null +++ b/QEMUHelper/es-419.lproj/InfoPlist.strings @@ -0,0 +1,8 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "QEMUHelper"; + +/* Bundle name */ +"CFBundleName" = "QEMUHelper"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2020 osy. Todos los derechos reservados."; diff --git a/QEMUHelper/es-419.lproj/Localizable.strings b/QEMUHelper/es-419.lproj/Localizable.strings new file mode 100644 index 000000000..2985fb49b --- /dev/null +++ b/QEMUHelper/es-419.lproj/Localizable.strings @@ -0,0 +1,8 @@ +/* QEMUHelper */ +"Cannot find QEMU support libraries." = "No es posible encontrar las librerías necesarias de QEMU."; + +/* QEMUHelper */ +"Error starting QEMU." = "Error al iniciar QEMU."; + +/* QEMUHelper */ +"QEMU exited unexpectedly." = "QEMU salió inesperadamente."; diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index d977f41f2..76990aee4 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -2197,6 +2197,15 @@ E2D64BC7241DB24B0034E0C6 /* UTMSpiceIO.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMSpiceIO.h; sourceTree = ""; }; E2D64BC8241DB24B0034E0C6 /* UTMSpiceIO.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMSpiceIO.m; sourceTree = ""; }; E2D64BE0241EAEBE0034E0C6 /* UTMSpiceIODelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMSpiceIODelegate.h; sourceTree = ""; }; + E68D491B28AC018600D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/VMDisplayMetalViewInputAccessory.strings"; sourceTree = ""; }; + E68D491C28AC018700D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/VMDisplayWindow.strings"; sourceTree = ""; }; + E68D491D28AC018D00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; + E68D491E28AC018D00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; + E68D491F28AC018D00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + E68D492028AC018D00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = ""; }; + E68D492128AC018D00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; + E68D492228AC018E00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + E68D492328AC018E00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; FFB02A8B266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; FFB02A8F266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; FFB02A92266CB09C006CD71A /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -3409,6 +3418,7 @@ "zh-Hant", ja, fr, + "es-419", ); mainGroup = CE550BC0225947990063E575; packageReferences = ( @@ -4419,6 +4429,7 @@ CEF84ADA2887D7D300578F41 /* ja */, 84937F0C28975DCB003148F4 /* fr */, 84937F19289764D9003148F4 /* en */, + E68D491F28AC018D00D34C54 /* es-419 */, ); name = Localizable.strings; sourceTree = ""; @@ -4431,23 +4442,17 @@ 52873FDA247F5B1B0063E4C8 /* zh-Hant */, 84937F0D28975DD2003148F4 /* fr */, 84937F1A289764DE003148F4 /* en */, + E68D492128AC018D00D34C54 /* es-419 */, ); name = InfoPlist.strings; sourceTree = ""; }; - CED8DF7928A120C100C34345 /* Localizable.stringsdict */ = { - isa = PBXVariantGroup; - children = ( - CED8DF7828A120C100C34345 /* en */, - ); - name = Localizable.stringsdict; - sourceTree = ""; - }; CE061CDD289E6DC30000351C /* VMDisplayWindow.xib */ = { isa = PBXVariantGroup; children = ( CE061CDC289E6DC30000351C /* Base */, CE061CDF289E6DCF0000351C /* ja */, + E68D491C28AC018700D34C54 /* es-419 */, ); name = VMDisplayWindow.xib; sourceTree = ""; @@ -4457,10 +4462,20 @@ children = ( CE061CE8289EB6250000351C /* Base */, CE061CEB289EB62E0000351C /* ja */, + E68D491B28AC018600D34C54 /* es-419 */, ); name = VMDisplayMetalViewInputAccessory.xib; sourceTree = ""; }; + CED8DF7928A120C100C34345 /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + CED8DF7828A120C100C34345 /* en */, + E68D492028AC018D00D34C54 /* es-419 */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; FFB02A8A266CB09C006CD71A /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( @@ -4469,6 +4484,7 @@ CEDC9BA2288B74E50030F494 /* ja */, 84937F0E289761C0003148F4 /* fr */, 84937F17289764A9003148F4 /* en */, + E68D491D28AC018D00D34C54 /* es-419 */, ); name = InfoPlist.strings; sourceTree = ""; @@ -4481,6 +4497,7 @@ CEDC9BA4288BBD6B0030F494 /* ja */, 84937F0F289761D1003148F4 /* fr */, 84937F18289764CF003148F4 /* en */, + E68D491E28AC018D00D34C54 /* es-419 */, ); name = InfoPlist.strings; sourceTree = ""; @@ -4492,6 +4509,7 @@ C03453AF2709E35100AD51AD /* zh-Hans */, 84937F10289761FF003148F4 /* fr */, 84937F1C289764E8003148F4 /* en */, + E68D492328AC018E00D34C54 /* es-419 */, ); name = InfoPlist.strings; sourceTree = ""; @@ -4504,6 +4522,7 @@ CEDC9BA3288BBD130030F494 /* ja */, 84937F1128976204003148F4 /* fr */, 84937F1B289764E4003148F4 /* en */, + E68D492228AC018E00D34C54 /* es-419 */, ); name = Localizable.strings; sourceTree = ""; From 9f6ab1d5b804dbf97ced3bae9468555eff15963b Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Fri, 19 Aug 2022 21:26:50 -0700 Subject: [PATCH 05/42] qemu: fix resource exhaustion building QEMU TCTI --- .github/workflows/build.yml | 2 -- patches/qemu-7.0.0-utm.patch | 67 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e28a7b734..e7e1ff935 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,8 +69,6 @@ jobs: - name: Build Sysroot if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event.inputs.rebuild_sysroot == 'true' run: ./scripts/build_dependencies.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }} - env: - NCPU: ${{ matrix.platform == 'ios-tci' && '1' || '0' }} # limit 1 CPU for TCI build due to memory issues, 0 = unlimited for other builds - name: Compress Sysroot if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event_name == 'release' || github.event.inputs.test_release == 'true' run: tar -acf sysroot.tgz sysroot* diff --git a/patches/qemu-7.0.0-utm.patch b/patches/qemu-7.0.0-utm.patch index fb86c0fca..b7b3dc489 100644 --- a/patches/qemu-7.0.0-utm.patch +++ b/patches/qemu-7.0.0-utm.patch @@ -594,3 +594,70 @@ index e139f7115e1f..765892f84f1c 100644 qemu_console_get_height(s, -1) == height) { return; } +From 7f65c39e6865763cb8a614a8903b25f23a9a8683 Mon Sep 17 00:00:00 2001 +From: osy <50960678+osy@users.noreply.github.com> +Date: Fri, 19 Aug 2022 14:07:10 -0700 +Subject: [PATCH] tcg-tcti: fix memory usage issues with clang build + +Build with -O0 to avoid blowing up the memory during build. +--- + meson.build | 12 ++++++++++++ + tcg/meson.build | 8 +++++++- + 2 files changed, 19 insertions(+), 1 deletion(-) + +diff --git a/meson.build b/meson.build +index d66b73d009..b144789c5e 100644 +--- a/meson.build ++++ b/meson.build +@@ -2649,6 +2649,7 @@ qom_ss = ss.source_set() + softmmu_ss = ss.source_set() + specific_fuzz_ss = ss.source_set() + specific_ss = ss.source_set() ++specific_nooptimize_ss = ss.source_set() + stub_ss = ss.source_set() + trace_ss = ss.source_set() + user_ss = ss.source_set() +@@ -3217,6 +3218,17 @@ foreach target : target_dirs + arch_srcs += target_specific.sources() + arch_deps += target_specific.dependencies() + ++ nooptimize_ss = specific_nooptimize_ss.apply(config_target, strict: false) ++ if nooptimize_ss.sources().length() > 0 ++ libnooptimize = static_library('qemu-' + target + '-nooptimize', ++ sources: genh + nooptimize_ss.sources(), ++ include_directories: target_inc, ++ dependencies: nooptimize_ss.dependencies(), ++ c_args: c_args + '-O0') ++ nooptimize = declare_dependency(link_whole: libnooptimize) ++ arch_deps += nooptimize ++ endif ++ + if config_all.has_key('CONFIG_SHARED_LIBRARY_BUILD') + build_lib_args = { + 'target_type': 'shared_library', +diff --git a/tcg/meson.build b/tcg/meson.build +index c4c63b19d4..16c226e114 100644 +--- a/tcg/meson.build ++++ b/tcg/meson.build +@@ -3,7 +3,6 @@ tcg_ss = ss.source_set() + tcg_ss.add(files( + 'optimize.c', + 'region.c', +- 'tcg.c', + 'tcg-common.c', + 'tcg-op.c', + 'tcg-op-gvec.c', +@@ -18,3 +17,10 @@ if get_option('tcg_interpreter') + endif + + specific_ss.add_all(when: 'CONFIG_TCG', if_true: tcg_ss) ++ ++# TCTI requires -O0 or otherwise clang memory usage blows up ++if get_option('tcg_threaded_interpreter') ++ specific_nooptimize_ss.add(when: 'CONFIG_TCG', if_true: files('tcg.c')) ++else ++ specific_ss.add(when: 'CONFIG_TCG', if_true: files('tcg.c')) ++endif +-- +2.28.0 + From e499fd04e74c32ea0a753298a6b4cecdea386c67 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 20 Aug 2022 16:59:56 -0700 Subject: [PATCH 06/42] vm(qemu): implement registry for external drives This shall replace the legacy UTMViewState and UTMDrive implementation. --- Configuration/UTMQemuConfigurationDrive.swift | 30 ++- Managers/UTMQemuVirtualMachine-Protected.h | 33 ++++ Managers/UTMQemuVirtualMachine.m | 15 +- Managers/UTMQemuVirtualMachine.swift | 111 +++++++++++ Managers/UTMRegistry.swift | 29 +++ Managers/UTMRegistryEntry.swift | 177 ++++++++++++++++++ Managers/UTMVirtualMachine.h | 2 + Platform/Shared/VMDetailsView.swift | 12 +- Platform/Shared/VMRemovableDrivesView.swift | 127 ++++++------- Platform/Swift-Bridging-Header.h | 3 + Platform/iOS/VMToolbarDriveMenuView.swift | 56 +++--- Platform/iOS/VMToolbarView.swift | 2 +- .../VMDisplayQemuDisplayController.swift | 46 +++-- UTM.xcodeproj/project.pbxproj | 26 +++ 14 files changed, 533 insertions(+), 136 deletions(-) create mode 100644 Managers/UTMQemuVirtualMachine-Protected.h create mode 100644 Managers/UTMQemuVirtualMachine.swift create mode 100644 Managers/UTMRegistry.swift create mode 100644 Managers/UTMRegistryEntry.swift diff --git a/Configuration/UTMQemuConfigurationDrive.swift b/Configuration/UTMQemuConfigurationDrive.swift index 851e5ae9f..3bc8b08e3 100644 --- a/Configuration/UTMQemuConfigurationDrive.swift +++ b/Configuration/UTMQemuConfigurationDrive.swift @@ -55,7 +55,6 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { enum CodingKeys: String, CodingKey { case imageName = "ImageName" - case bookmark = "Bookmark" case imageType = "ImageType" case interface = "Interface" case identifier = "Identifier" @@ -73,11 +72,6 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { self.imageName = imageName imageURL = dataURL.appendingPathComponent(imageName) isExternal = false - } else if let bookmark = try values.decodeIfPresent(Data.self, forKey: .bookmark) { - var stale: Bool = false - imageURL = try? URL(resolvingBookmarkData: bookmark, options: kUTMBookmarkResolutionOptions, bookmarkDataIsStale: &stale) - imageName = imageURL?.lastPathComponent - isExternal = true } else { isExternal = true } @@ -90,19 +84,6 @@ struct UTMQemuConfigurationDrive: UTMConfigurationDrive { var container = encoder.container(keyedBy: CodingKeys.self) if !isExternal { try container.encodeIfPresent(imageURL?.lastPathComponent, forKey: .imageName) - } else { - var options = kUTMBookmarkCreationOptions - #if os(macOS) - if isReadOnly { - options.insert(.securityScopeAllowOnlyReadAccess) - } - #endif - _ = imageURL?.startAccessingSecurityScopedResource() - defer { - imageURL?.stopAccessingSecurityScopedResource() - } - let bookmark = try imageURL?.bookmarkData(options: options) - try container.encodeIfPresent(bookmark, forKey: .bookmark) } try container.encode(imageType, forKey: .imageType) if imageType == .cd || imageType == .disk { @@ -237,3 +218,14 @@ extension UTMQemuConfigurationDrive { self.interface = defaultInterfaceForImageType!(imageType) } } + +// MARK: - Drive label for display + +extension UTMQemuConfigurationDrive { + var label: String { + String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "UTMQemuConfigurationDrive"), + imageType.prettyValue, + interface.prettyValue, + imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "UTMQemuConfigurationDrive")) + } +} diff --git a/Managers/UTMQemuVirtualMachine-Protected.h b/Managers/UTMQemuVirtualMachine-Protected.h new file mode 100644 index 000000000..e0bfcbd4a --- /dev/null +++ b/Managers/UTMQemuVirtualMachine-Protected.h @@ -0,0 +1,33 @@ +// +// Copyright © 2022 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "UTMQemuVirtualMachine.h" + +@class UTMQemu; +@class UTMQemuManager; + +NS_ASSUME_NONNULL_BEGIN + +@interface UTMQemuVirtualMachine (Protected) + +@property (nonatomic, readonly, nullable) UTMQemuManager *qemu; +@property (nonatomic, readonly, nullable) UTMQemu *system; + +- (void)saveViewState; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 4dd795ac7..2d74f241d 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -268,8 +268,19 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { completion([self errorWithMessage:errMsg]); return; } - if (![self restoreRemovableDrivesFromBookmarksWithError:&err]) { - errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore removable drives: %@", @"UTMVirtualMachine"), err.localizedDescription]; + __block NSError *restoreExternalDrivesError = nil; + dispatch_semaphore_t restoreExternalDrivesEvent = dispatch_semaphore_create(0); + [self restoreExternalDrivesWithCompletion:^(NSError *err) { + restoreExternalDrivesError = err; + dispatch_semaphore_signal(restoreExternalDrivesEvent); + }]; + if (dispatch_semaphore_wait(restoreExternalDrivesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { + UTMLog(@"Timed out waiting for external drives to be restored."); + completion([self errorGeneric]); + return; + } + if (restoreExternalDrivesError) { + errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore removable drives: %@", @"UTMVirtualMachine"), restoreExternalDrivesError.localizedDescription]; completion([self errorWithMessage:errMsg]); return; } diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift new file mode 100644 index 000000000..46f79ad74 --- /dev/null +++ b/Managers/UTMQemuVirtualMachine.swift @@ -0,0 +1,111 @@ +// +// Copyright © 2022 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - External drives +extension UTMQemuVirtualMachine { + var qemuConfig: UTMQemuConfiguration { + config.qemuConfig! + } + + func eject(_ drive: inout UTMQemuConfigurationDrive, isForced: Bool = false) throws { + guard let oldURL = drive.imageURL else { + return // nothing to eject + } + guard drive.isExternal else { + return + } + drive.imageURL = nil + registryEntry?.externalDrives.removeValue(forKey: drive.id) + system?.stopAccessingPath(oldURL.path) + guard let qemu = qemu, qemu.isConnected else { + return + } + try qemu.ejectDrive("drive\(drive.id)", force: isForced) + } + + func changeMedium(_ drive: inout UTMQemuConfigurationDrive, with url: URL) async throws { + _ = url.startAccessingSecurityScopedResource() + defer { + url.stopAccessingSecurityScopedResource() + } + let tempBookmark = try url.bookmarkData() + try eject(&drive, isForced: true) + try await changeMedium(&drive, with: tempBookmark, isSecurityScoped: false) + drive.imageURL = url + } + + private func changeMedium(_ drive: inout UTMQemuConfigurationDrive, with bookmark: Data, isSecurityScoped: Bool) async throws { + guard let system = system else { + return + } + let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) + guard let bookmark = bookmark, let path = path, success else { + throw UTMQemuVirtualMachineError.accessDriveImageFailed + } + let file = UTMRegistryEntry.File(path: path, bookmark: bookmark, isReadOnly: drive.isReadOnly) + registryEntry?.externalDrives[drive.id] = file + if let qemu = qemu, qemu.isConnected { + try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) + } + } + + func restoreExternalDrives() async throws { + guard system != nil && qemu != nil && qemu!.isConnected else { + throw UTMQemuVirtualMachineError.invalidVmState + } + let qemuConfig = config.qemuConfig! + for i in qemuConfig.drives.indices { + if !qemuConfig.drives[i].isExternal { + continue + } + let id = qemuConfig.drives[i].id + if let url = qemuConfig.drives[i].imageURL { + // an image was selected while the VM was stopped + try await changeMedium(&qemuConfig.drives[i], with: url) + } else if let bookmark = registryEntry?.externalDrives[id]?.bookmark { + // an image bookmark was saved + try await changeMedium(&qemuConfig.drives[i], with: bookmark, isSecurityScoped: true) + } + } + } + + @objc func restoreExternalDrives(completion: @escaping (Error?) -> Void) { + Task.detached { + do { + try await self.restoreExternalDrives() + completion(nil) + } catch { + completion(error) + } + } + } +} + +enum UTMQemuVirtualMachineError: Error { + case accessDriveImageFailed + case invalidVmState +} + +extension UTMQemuVirtualMachineError: LocalizedError { + var errorDescription: String? { + switch self { + case .accessDriveImageFailed: return NSLocalizedString("Failed to access drive image path.", comment: "UTMQemuVirtualMachine") + case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine") + } + } +} diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift new file mode 100644 index 000000000..f714c3593 --- /dev/null +++ b/Managers/UTMRegistry.swift @@ -0,0 +1,29 @@ +// +// Copyright © 2022 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class UTMRegistry { + static let `default` = UTMRegistry() + + private init() { + + } + + func update(entry: UTMRegistryEntry) { + + } +} diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift new file mode 100644 index 000000000..1c48053a2 --- /dev/null +++ b/Managers/UTMRegistryEntry.swift @@ -0,0 +1,177 @@ +// +// Copyright © 2022 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc class UTMRegistryEntry: NSObject, Codable, ObservableObject { + @UTMRegistryValue var name: String + + @UTMRegistryValue var package: File + + @UTMRegistryValue var uuid: String + + @UTMRegistryValue var externalDrives: [String: File] + + @UTMRegistryValue var sharedDirectories: [File] + + @UTMRegistryValue var windowSettings: [Int: Window] + + private enum CodingKeys: String, CodingKey { + case name = "Name" + case package = "Package" + case uuid = "UUID" + case externalDrives = "ExternalDrives" + case sharedDirectories = "SharedDirectories" + case windowSettings = "WindowSettings" + } + + init?(newFrom vm: UTMVirtualMachine) { + guard let bookmark = vm.bookmark else { + return nil + } + let path = vm.path.path + name = vm.detailsTitleLabel + package = File(path: path, bookmark: bookmark, isReadOnly: false) + uuid = vm.config.uuid.uuidString + externalDrives = [:] + sharedDirectories = [] + windowSettings = [:] + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + package = try container.decode(File.self, forKey: .package) + uuid = try container.decode(String.self, forKey: .uuid) + externalDrives = try container.decode([String: File].self, forKey: .externalDrives) + sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) + windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(package, forKey: .package) + try container.encode(uuid, forKey: .uuid) + try container.encode(externalDrives, forKey: .externalDrives) + try container.encode(sharedDirectories, forKey: .sharedDirectories) + try container.encode(windowSettings, forKey: .windowSettings) + } +} + +extension UTMRegistryEntry { + struct File: Codable { + var path: String + + var bookmark: Data + + var isReadOnly: Bool + + private enum CodingKeys: String, CodingKey { + case path = "Path" + case bookmark = "Bookmark" + case isReadOnly = "ReadOnly" + } + + init(path: String, bookmark: Data, isReadOnly: Bool = false) { + self.path = path + self.bookmark = bookmark + self.isReadOnly = isReadOnly + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + path = try container.decode(String.self, forKey: .path) + bookmark = try container.decode(Data.self, forKey: .bookmark) + isReadOnly = try container.decode(Bool.self, forKey: .isReadOnly) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(path, forKey: .path) + try container.encode(bookmark, forKey: .bookmark) + try container.encode(isReadOnly, forKey: .isReadOnly) + } + } + + struct Window: Codable { + var scale: CGFloat = 1.0 + + var origin: CGPoint = .zero + + var isToolbarVisible: Bool = true + + var isKeyboardVisible: Bool = false + + var isDisplayZoomLocked: Bool = true + + private enum CodingKeys: String, CodingKey { + case scale = "Scale" + case origin = "Origin" + case isToolbarVisible = "ToolbarVisible" + case isKeyboardVisible = "KeyboardVisible" + case isDisplayZoomLocked = "DisplayZoomLocked" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + scale = try container.decode(CGFloat.self, forKey: .scale) + origin = try container.decode(CGPoint.self, forKey: .origin) + isToolbarVisible = try container.decode(Bool.self, forKey: .isToolbarVisible) + isKeyboardVisible = try container.decode(Bool.self, forKey: .isKeyboardVisible) + isDisplayZoomLocked = try container.decode(Bool.self, forKey: .isDisplayZoomLocked) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(scale, forKey: .scale) + try container.encode(origin, forKey: .origin) + try container.encode(isToolbarVisible, forKey: .isToolbarVisible) + try container.encode(isKeyboardVisible, forKey: .isKeyboardVisible) + try container.encode(isDisplayZoomLocked, forKey: .isDisplayZoomLocked) + } + } +} + +@propertyWrapper struct UTMRegistryValue { + static subscript( + _enclosingInstance instance: UTMRegistryEntry, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + instance[keyPath: storageKeyPath].storage + } + set { + instance[keyPath: storageKeyPath].storage = newValue + UTMRegistry.default.update(entry: instance) + } + } + + @available(*, unavailable, + message: "@UTMRegistryValue can only be applied to classes" + ) + var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } + + private var storage: Value + + init(wrappedValue: Value) { + storage = wrappedValue + } +} diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index e5ac4c32e..db813981b 100644 --- a/Managers/UTMVirtualMachine.h +++ b/Managers/UTMVirtualMachine.h @@ -20,6 +20,7 @@ @class UTMConfigurationWrapper; @class UTMLogging; @class UTMViewState; +@class UTMRegistryEntry; @class CSScreenshot; NS_ASSUME_NONNULL_BEGIN @@ -53,6 +54,7 @@ NS_ASSUME_NONNULL_BEGIN /// This includes display size, bookmarks to removable drives, etc. /// This property is observable and must only be accessed on the main thread. @property (nonatomic, readonly) UTMViewState *viewState; +@property (nonatomic, nullable) UTMRegistryEntry *registryEntry; /// Current VM state, can observe this property for state changes or use the delegate /// diff --git a/Platform/Shared/VMDetailsView.swift b/Platform/Shared/VMDetailsView.swift index 9136aea12..353f6944c 100644 --- a/Platform/Shared/VMDetailsView.swift +++ b/Platform/Shared/VMDetailsView.swift @@ -69,12 +69,13 @@ struct VMDetailsView: View { if let appleVM = vm as? UTMAppleVirtualMachine { VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig) .padding([.leading, .trailing, .bottom]) - } else { - VMRemovableDrivesView(vm: vm as! UTMQemuVirtualMachine) + } else if let qemuVM = vm as? UTMQemuVirtualMachine { + VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) .padding([.leading, .trailing, .bottom]) } #else - VMRemovableDrivesView(vm: vm as! UTMQemuVirtualMachine) + let qemuVM = vm as! UTMQemuVirtualMachine + VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) .padding([.leading, .trailing, .bottom]) #endif } else { @@ -89,10 +90,11 @@ struct VMDetailsView: View { if let appleVM = vm as? UTMAppleVirtualMachine { VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig) } else if let qemuVM = vm as? UTMQemuVirtualMachine { - VMRemovableDrivesView(vm: qemuVM) + VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) } #else - VMRemovableDrivesView(vm: vm as! UTMQemuVirtualMachine) + let qemuVM = vm as! UTMQemuVirtualMachine + VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) #endif }.padding([.leading, .trailing, .bottom]) } diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 883c74719..9460be6b8 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -18,12 +18,13 @@ import SwiftUI struct VMRemovableDrivesView: View { @ObservedObject var vm: UTMQemuVirtualMachine + @ObservedObject var config: UTMQemuConfiguration @EnvironmentObject private var data: UTMData @State private var shareDirectoryFileImportPresented: Bool = false @State private var diskImageFileImportPresented: Bool = false /// Explanation see "SwiftUI FileImporter modal bug" in the `body` @State private var workaroundFileImporterBug: Bool = false - @State private var currentDrive: UTMDrive? + @State private var currentDriveBinding: Binding? var fileManager: FileManager { FileManager.default @@ -69,59 +70,61 @@ struct VMRemovableDrivesView: View { } }.fileImporter(isPresented: $shareDirectoryFileImportPresented, allowedContentTypes: [.folder], onCompletion: selectShareDirectory) } - ForEach(vm.drives.filter { $0.status != .fixed }) { drive in - HStack { - // Drive menu - Menu { - // Browse button - Button(action: { - currentDrive = drive - // MARK: SwiftUI FileImporter modal bug - /// At this point in the execution, `diskImageFileImportPresented` must be `false`. - /// However there is a SwiftUI FileImporter modal bug: - /// if the user taps outside the import modal to cancel instead of tapping the actual cancel button, - /// the `.fileImporter` doesn't actually set the isPresented Binding to `false`. - if (diskImageFileImportPresented) { - /// bug! Let's set the bool to false ourselves. - diskImageFileImportPresented = false - /// One more thing: we can't immediately set it to `true` again because then the state won't have changed. - /// So we have to use the workaround, which is caught in the `.onChange` below. - workaroundFileImporterBug = true - } else { - diskImageFileImportPresented = true - } - }, label: { - Label("Browse…", systemImage: "doc.badge.plus") - }) - .onChange(of: workaroundFileImporterBug) { doWorkaround in - /// Explanation see "SwiftUI FileImporter modal bug" above - if doWorkaround { - DispatchQueue.main.async { - workaroundFileImporterBug = false + ForEach($config.drives) { $drive in + if drive.isExternal { + HStack { + // Drive menu + Menu { + // Browse button + Button(action: { + currentDriveBinding = $drive + // MARK: SwiftUI FileImporter modal bug + /// At this point in the execution, `diskImageFileImportPresented` must be `false`. + /// However there is a SwiftUI FileImporter modal bug: + /// if the user taps outside the import modal to cancel instead of tapping the actual cancel button, + /// the `.fileImporter` doesn't actually set the isPresented Binding to `false`. + if (diskImageFileImportPresented) { + /// bug! Let's set the bool to false ourselves. + diskImageFileImportPresented = false + /// One more thing: we can't immediately set it to `true` again because then the state won't have changed. + /// So we have to use the workaround, which is caught in the `.onChange` below. + workaroundFileImporterBug = true + } else { diskImageFileImportPresented = true } - } - } - // Eject button - if drive.status != .ejected { - Button(action: { clearRemovableImage(forDrive: drive) }, label: { - Label("Clear", systemImage: "eject") + }, label: { + Label("Browse…", systemImage: "doc.badge.plus") }) + .onChange(of: workaroundFileImporterBug) { doWorkaround in + /// Explanation see "SwiftUI FileImporter modal bug" above + if doWorkaround { + DispatchQueue.main.async { + workaroundFileImporterBug = false + diskImageFileImportPresented = true + } + } + } + // Eject button + if drive.imageURL != nil { + Button(action: { clearRemovableImage(forDrive: $drive) }, label: { + Label("Clear", systemImage: "eject") + }) + } + } label: { + DriveLabel(drive: drive) + }.disabled(vm.viewState.hasSaveState) + Spacer() + // Disk image path, or (empty) + Text(pathFor(drive)) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.secondary) + }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [.data]) { result in + if let currentDrive = self.currentDriveBinding { + selectRemovableImage(forDrive: currentDrive, result: result) + self.currentDriveBinding = nil } - } label: { - DriveLabel(drive: drive) - }.disabled(vm.viewState.hasSaveState) - Spacer() - // Disk image path, or (empty) - Text(pathFor(drive)) - .lineLimit(1) - .truncationMode(.tail) - .foregroundColor(.secondary) - } - }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [.data]) { result in - if let currentDrive = self.currentDrive { - selectRemovableImage(forDrive: currentDrive, result: result) - self.currentDrive = nil + } } } } @@ -148,10 +151,8 @@ struct VMRemovableDrivesView: View { } } - private func pathFor(_ drive: UTMDrive) -> String { - let path = vm.viewState.path(forRemovableDrive: drive.name ?? "") ?? "" - if path.count > 0 { - let url = URL(fileURLWithPath: path) + private func pathFor(_ drive: UTMQemuConfigurationDrive) -> String { + if let url = drive.imageURL { return url.lastPathComponent } else { return NSLocalizedString("(empty)", comment: "A removable drive that has no image file inserted.") @@ -159,11 +160,11 @@ struct VMRemovableDrivesView: View { } private struct DriveLabel: View { - let drive: UTMDrive + let drive: UTMQemuConfigurationDrive var body: some View { - if drive.imageType == .CD { - return Label("CD/DVD", systemImage: drive.status == .ejected ? "opticaldiscdrive" : "opticaldiscdrive.fill") + if drive.imageType == .cd { + return Label("CD/DVD", systemImage: drive.imageURL == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") } else { return Label("Removable", systemImage: "externaldrive") } @@ -186,11 +187,11 @@ struct VMRemovableDrivesView: View { vm.clearSharedDirectory() } - private func selectRemovableImage(forDrive drive: UTMDrive, result: Result) { - data.busyWork { + private func selectRemovableImage(forDrive drive: Binding, result: Result) { + data.busyWorkAsync { switch result { case .success(let url): - try vm.changeMedium(for: drive, url: url) + try await vm.changeMedium(&drive.wrappedValue, with: url) break case .failure(let err): throw err @@ -198,9 +199,9 @@ struct VMRemovableDrivesView: View { } } - private func clearRemovableImage(forDrive drive: UTMDrive) { - data.busyWork { - try vm.ejectDrive(drive, force: true) + private func clearRemovableImage(forDrive drive: Binding) { + data.busyWorkAsync { + try await vm.eject(&drive.wrappedValue) } } } diff --git a/Platform/Swift-Bridging-Header.h b/Platform/Swift-Bridging-Header.h index c0cd5aff8..91448f283 100644 --- a/Platform/Swift-Bridging-Header.h +++ b/Platform/Swift-Bridging-Header.h @@ -28,6 +28,8 @@ #include "UTMDrive.h" #include "UTMQcow2.h" #include "UTMQemu.h" +#include "UTMQemuManager.h" +#include "UTMQemuManager+BlockDevices.h" #include "UTMQemuSystem.h" #include "UTMJailbreak.h" #include "UTMLogging.h" @@ -35,6 +37,7 @@ #include "UTMVirtualMachine.h" #include "UTMVirtualMachine-Protected.h" #include "UTMQemuVirtualMachine.h" +#include "UTMQemuVirtualMachine-Protected.h" #include "UTMQemuVirtualMachine+Drives.h" #include "UTMQemuVirtualMachine+SPICE.h" #include "UTMSpiceIO.h" diff --git a/Platform/iOS/VMToolbarDriveMenuView.swift b/Platform/iOS/VMToolbarDriveMenuView.swift index 3d68b68ef..ce33d05a5 100644 --- a/Platform/iOS/VMToolbarDriveMenuView.swift +++ b/Platform/iOS/VMToolbarDriveMenuView.swift @@ -17,34 +17,35 @@ import SwiftUI struct VMToolbarDriveMenuView: View { + @State var config: UTMQemuConfiguration @EnvironmentObject private var session: VMSessionState @State private var isFileImporterShown: Bool = false - @State private var selectedDrive: UTMDrive? + @State private var selectedDrive: Binding? @State private var isRefreshRequired: Bool = false var body: some View { Menu { - ForEach(session.vm.drives) { legacyDrive in - if legacyDrive.status != .fixed { + ForEach($config.drives) { $drive in + if drive.isExternal { Menu { Button { - selectedDrive = legacyDrive + selectedDrive = $drive isFileImporterShown.toggle() } label: { MenuLabel("Change…", systemImage: "opticaldisc") } Button { - ejectDriveImage(for: legacyDrive) + ejectDriveImage(for: $drive) } label: { MenuLabel("Eject…", systemImage: "eject") } } label: { - MenuLabel(legacyDrive.label, systemImage: legacyDrive.status == .ejected ? "opticaldiscdrive" : "opticaldiscdrive.fill") + MenuLabel(drive.label, systemImage: drive.imageURL == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") } - } else { + } else if drive.imageType == .disk || drive.imageType == .cd { Button { } label: { - MenuLabel(legacyDrive.label, systemImage: "internaldrive") + MenuLabel(drive.label, systemImage: "internaldrive") }.disabled(true) } } @@ -64,27 +65,40 @@ struct VMToolbarDriveMenuView: View { } } - private func changeDriveImage(for legacyDrive: UTMDrive, with imageURL: URL) { - do { - try session.vm.changeMedium(for: legacyDrive, url: imageURL) - isRefreshRequired.toggle() - } catch { - session.nonfatalError = error.localizedDescription + private func changeDriveImage(for driveBinding: Binding, with imageURL: URL) { + Task.detached(priority: .background) { + do { + try await session.vm.changeMedium(&driveBinding.wrappedValue, with: imageURL) + Task { @MainActor in + isRefreshRequired.toggle() + } + } catch { + Task { @MainActor in + session.nonfatalError = error.localizedDescription + } + } } } - private func ejectDriveImage(for legacyDrive: UTMDrive) { - do { - try session.vm.ejectDrive(legacyDrive, force: false) - isRefreshRequired.toggle() - } catch { - session.nonfatalError = error.localizedDescription + private func ejectDriveImage(for driveBinding: Binding) { + Task.detached(priority: .background) { + do { + try await session.vm.eject(&driveBinding.wrappedValue) + Task { @MainActor in + isRefreshRequired.toggle() + } + } catch { + Task { @MainActor in + session.nonfatalError = error.localizedDescription + } + } } } } struct VMToolbarDriveMenuView_Previews: PreviewProvider { + @StateObject static var config = UTMQemuConfiguration() static var previews: some View { - VMToolbarDriveMenuView() + VMToolbarDriveMenuView(config: config) } } diff --git a/Platform/iOS/VMToolbarView.swift b/Platform/iOS/VMToolbarView.swift index 875ccdb69..2f05c8efa 100644 --- a/Platform/iOS/VMToolbarView.swift +++ b/Platform/iOS/VMToolbarView.swift @@ -116,7 +116,7 @@ struct VMToolbarView: View { .offset(offset(for: 4)) } #endif - VMToolbarDriveMenuView() + VMToolbarDriveMenuView(config: session.qemuConfig) .offset(offset(for: 3)) VMToolbarDisplayMenuView(state: $state) .offset(offset(for: 2)) diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index b194ff7b4..1dd2f04c2 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -91,16 +91,11 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { item.title = NSLocalizedString("Querying drives status...", comment: "VMDisplayWindowController") item.isEnabled = false menu.addItem(item) - DispatchQueue.global(qos: .userInitiated).async { - let drives = self.qemuVM.drives - DispatchQueue.main.async { - self.updateDrivesMenu(menu, drives: drives) - } - } + updateDrivesMenu(menu, drives: vmQemuConfig.drives) menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) } - func updateDrivesMenu(_ menu: NSMenu, drives: [UTMDrive]) { + @nonobjc func updateDrivesMenu(_ menu: NSMenu, drives: [UTMQemuConfigurationDrive]) { menu.removeAllItems() if drives.count == 0 { let item = NSMenuItem() @@ -108,13 +103,14 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { item.isEnabled = false menu.addItem(item) } - for drive in drives { - if drive.imageType != .disk && drive.imageType != .CD && drive.status == .fixed { + for i in drives.indices { + let drive = drives[i] + if drive.imageType != .disk && drive.imageType != .cd && !drive.isExternal { continue // skip non-disks } let item = NSMenuItem() item.title = drive.label - if drive.status == .fixed { + if !drive.isExternal { item.isEnabled = false } else { let submenu = NSMenu() @@ -123,14 +119,14 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { action: #selector(ejectDrive), keyEquivalent: "") eject.target = self - eject.tag = drive.index - eject.isEnabled = drive.status != .ejected + eject.tag = i + eject.isEnabled = drive.imageURL != nil submenu.addItem(eject) let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"), action: #selector(changeDriveImage), keyEquivalent: "") change.target = self - change.tag = drive.index + change.tag = i change.isEnabled = true submenu.addItem(change) item.submenu = submenu @@ -145,19 +141,19 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { logger.error("wrong sender for ejectDrive") return } - let drive = qemuVM.drives[menu.tag] - DispatchQueue.global(qos: .background).async { + let config = vmQemuConfig! + Task.detached(priority: .background) { [self] in do { - try self.qemuVM.ejectDrive(drive, force: false) + try await qemuVM.eject(&config.drives[menu.tag]) } catch { - DispatchQueue.main.async { - self.showErrorAlert(error.localizedDescription) + Task { @MainActor in + showErrorAlert(error.localizedDescription) } } } } - func openDriveImage(forDrive drive: UTMDrive) { + func openDriveImage(forDriveIndex index: Int) { let openPanel = NSOpenPanel() openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController") openPanel.allowedContentTypes = [.data] @@ -169,12 +165,13 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { logger.debug("no file selected") return } - DispatchQueue.global(qos: .background).async { + let config = self.vmQemuConfig! + Task.detached(priority: .background) { [self] in do { - try self.qemuVM.changeMedium(for: drive, url: url) + try await qemuVM.changeMedium(&config.drives[index], with: url) } catch { - DispatchQueue.main.async { - self.showErrorAlert(error.localizedDescription) + Task { @MainActor in + showErrorAlert(error.localizedDescription) } } } @@ -186,8 +183,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { logger.error("wrong sender for ejectDrive") return } - let drive = qemuVM.drives[menu.tag] - openDriveImage(forDrive: drive) + openDriveImage(forDriveIndex: menu.tag) } } diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index d977f41f2..4e2680d29 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -90,6 +90,15 @@ 841E58CF28937FED00137A20 /* UTMMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58CD28937FED00137A20 /* UTMMainView.swift */; }; 841E58D52893D01A00137A20 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58D02893AF5400137A20 /* UTMApp.swift */; }; 841E58D62893D01B00137A20 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58D02893AF5400137A20 /* UTMApp.swift */; }; + 841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997428AA1191003C6CB6 /* UTMRegistry.swift */; }; + 841E997628AA1191003C6CB6 /* UTMRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997428AA1191003C6CB6 /* UTMRegistry.swift */; }; + 841E997728AA1191003C6CB6 /* UTMRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997428AA1191003C6CB6 /* UTMRegistry.swift */; }; + 841E997928AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */; }; + 841E997A28AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */; }; + 841E997B28AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */; }; + 841E999828AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */; }; + 841E999928AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */; }; + 841E999A28AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */; }; 84258C42288F806400C66366 /* VMToolbarUSBMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84258C41288F806400C66366 /* VMToolbarUSBMenuView.swift */; }; 843BF82428441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82328441EAD0029D60D /* UTMQemuConfigurationDisplay.swift */; }; 843BF82528441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82328441EAD0029D60D /* UTMQemuConfigurationDisplay.swift */; }; @@ -1595,6 +1604,10 @@ 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTMExternalSceneDelegate.swift; sourceTree = ""; }; 841E58CD28937FED00137A20 /* UTMMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMMainView.swift; sourceTree = ""; }; 841E58D02893AF5400137A20 /* UTMApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMApp.swift; sourceTree = ""; }; + 841E997428AA1191003C6CB6 /* UTMRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRegistry.swift; sourceTree = ""; }; + 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRegistryEntry.swift; sourceTree = ""; }; + 841E999628AC80CA003C6CB6 /* UTMQemuVirtualMachine-Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMQemuVirtualMachine-Protected.h"; sourceTree = ""; }; + 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMQemuVirtualMachine.swift; sourceTree = ""; }; 84258C41288F806400C66366 /* VMToolbarUSBMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMToolbarUSBMenuView.swift; sourceTree = ""; }; 843BF82328441EAD0029D60D /* UTMQemuConfigurationDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMQemuConfigurationDisplay.swift; sourceTree = ""; }; 843BF82728441FAF0029D60D /* QEMUConstantGenerated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QEMUConstantGenerated.swift; sourceTree = ""; }; @@ -3037,8 +3050,10 @@ CE8E6620227E5DF2003B9903 /* UTMQemuManagerDelegate.h */, CE03D05424D90BE000F76B84 /* UTMQemuSystem.h */, CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */, + 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */, 84FCABB8268CE05E0036196C /* UTMQemuVirtualMachine.h */, 84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */, + 841E999628AC80CA003C6CB6 /* UTMQemuVirtualMachine-Protected.h */, CEF83EBC24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.h */, CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */, CEF83EC924FB1BB200557D15 /* UTMQemuVirtualMachine+SPICE.h */, @@ -3059,6 +3074,8 @@ CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */, 835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */, 84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */, + 841E997428AA1191003C6CB6 /* UTMRegistry.swift */, + 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */, ); path = Managers; sourceTree = ""; @@ -3568,6 +3585,7 @@ CE2D928C24AD46670059923A /* qapi-visit-rdma.c in Sources */, CE2D928D24AD46670059923A /* qapi-types-trace.c in Sources */, CE2D928E24AD46670059923A /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */, + 841E997928AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */, CEBBF1A524B56A2900C15049 /* UTMDataExtension.swift in Sources */, CE2D928F24AD46670059923A /* qapi-types-migration.c in Sources */, 84CF5DD3288DCE6400D01721 /* VMDisplayHostedView.swift in Sources */, @@ -3673,6 +3691,7 @@ CE2D92D024AD46670059923A /* qapi-types-machine.c in Sources */, CE2D92D124AD46670059923A /* qapi-commands-transaction.c in Sources */, CE2D92D224AD46670059923A /* UTMVirtualMachine.m in Sources */, + 841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */, 8401868F288A50B90050AC51 /* VMDisplayViewControllerDelegate.swift in Sources */, CE2D92D324AD46670059923A /* qapi-events-qom.c in Sources */, CE2D92D424AD46670059923A /* qapi-commands-migration.c in Sources */, @@ -3746,6 +3765,7 @@ CE2D930324AD46670059923A /* qapi-events-crypto.c in Sources */, CE2D930424AD46670059923A /* UTMLegacyQemuConfiguration.m in Sources */, CEF83EC724FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m in Sources */, + 841E999828AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */, CE2D957924AD4F990059923A /* VMDetailsView.swift in Sources */, CE2D930524AD46670059923A /* VMDisplayMetalViewController.m in Sources */, CE2D930624AD46670059923A /* qapi-commands-misc.c in Sources */, @@ -3982,6 +4002,7 @@ CE0B6D5224AD584C00FE012D /* qapi-visit-sockets.c in Sources */, CE0B6D7F24AD584D00FE012D /* qapi-events-qom.c in Sources */, CE2D956C24AD4F990059923A /* VMCardView.swift in Sources */, + 841E999A28AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */, 843BF82628441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */, CE0B6D7124AD584D00FE012D /* qapi-types-introspect.c in Sources */, CE0B6D0D24AD56C300FE012D /* qapi-util.c in Sources */, @@ -4053,6 +4074,8 @@ 845F1707289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift in Sources */, 843BF83E2845494C0029D60D /* UTMQemuConfigurationSerial.swift in Sources */, CE0B6D3524AD57FC00FE012D /* qapi-events-authz.c in Sources */, + 841E997728AA1191003C6CB6 /* UTMRegistry.swift in Sources */, + 841E997B28AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */, CE2D956424AD4F990059923A /* VMConfigNetworkPortForwardView.swift in Sources */, CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */, 53A0BDD726D79FE40010EDC5 /* SavePanel.swift in Sources */, @@ -4117,6 +4140,7 @@ 843BF8312844853E0029D60D /* UTMQemuConfigurationNetwork.swift in Sources */, CEA45E32263519B5002FA97D /* qapi-visit-tpm.c in Sources */, CEA45E35263519B5002FA97D /* qapi-visit-trace.c in Sources */, + 841E997A28AA119B003C6CB6 /* UTMRegistryEntry.swift in Sources */, 84FCABBB268CE05E0036196C /* UTMQemuVirtualMachine.m in Sources */, CEA45E36263519B5002FA97D /* qapi-events-rocker.c in Sources */, 8469CACB277D301300BA5601 /* qapi-types-compat.c in Sources */, @@ -4281,6 +4305,7 @@ CEA45ECD263519B5002FA97D /* VMConfirmActionModifier.swift in Sources */, 843BF83528450C0B0029D60D /* UTMQemuConfigurationSound.swift in Sources */, CEA45ECF263519B5002FA97D /* VMConfigPortForwardForm.swift in Sources */, + 841E999928AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */, CEA45ED0263519B5002FA97D /* qapi-types-machine-target.c in Sources */, CEA45ED1263519B5002FA97D /* UTMLocationManager.m in Sources */, CEA45ED2263519B5002FA97D /* qapi-events-block.c in Sources */, @@ -4313,6 +4338,7 @@ 843BF82928441FAF0029D60D /* QEMUConstantGenerated.swift in Sources */, CEA45EEC263519B5002FA97D /* UTMData.swift in Sources */, CEA45EED263519B5002FA97D /* qapi-events-migration.c in Sources */, + 841E997628AA1191003C6CB6 /* UTMRegistry.swift in Sources */, CEA45EEE263519B5002FA97D /* qapi-commands.c in Sources */, CEA45EEF263519B5002FA97D /* qapi-commands-audio.c in Sources */, CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */, From 057ca49e98403efc444650f3355a943fba4568f0 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:43:37 -0700 Subject: [PATCH 07/42] vm(qemu): support saving external drive bookmark before starting QEMU This requires us to save two bookmarks: one for the main process and one for the helper process. This also simplifies the logic for changing images and we no longer need to pass around references to the UTMQemuConfigurationDrive. --- Configuration/UTMQemuConfigurationDrive.swift | 11 -- Managers/UTMQemuVirtualMachine.swift | 52 ++++---- Managers/UTMRegistryEntry.swift | 23 +++- Managers/UTMVirtualMachine.h | 2 +- Managers/UTMVirtualMachine.m | 1 + Managers/UTMVirtualMachineExtension.swift | 38 ++++++ Platform/Shared/VMRemovableDrivesView.swift | 113 +++++++++--------- Platform/iOS/VMToolbarDriveMenuView.swift | 28 +++-- .../VMDisplayQemuDisplayController.swift | 20 +++- 9 files changed, 177 insertions(+), 111 deletions(-) diff --git a/Configuration/UTMQemuConfigurationDrive.swift b/Configuration/UTMQemuConfigurationDrive.swift index 3bc8b08e3..80cae44df 100644 --- a/Configuration/UTMQemuConfigurationDrive.swift +++ b/Configuration/UTMQemuConfigurationDrive.swift @@ -218,14 +218,3 @@ extension UTMQemuConfigurationDrive { self.interface = defaultInterfaceForImageType!(imageType) } } - -// MARK: - Drive label for display - -extension UTMQemuConfigurationDrive { - var label: String { - String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "UTMQemuConfigurationDrive"), - imageType.prettyValue, - interface.prettyValue, - imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "UTMQemuConfigurationDrive")) - } -} diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 46f79ad74..a8ff3b188 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -22,34 +22,33 @@ extension UTMQemuVirtualMachine { config.qemuConfig! } - func eject(_ drive: inout UTMQemuConfigurationDrive, isForced: Bool = false) throws { - guard let oldURL = drive.imageURL else { - return // nothing to eject - } + func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) throws { guard drive.isExternal else { return } - drive.imageURL = nil - registryEntry?.externalDrives.removeValue(forKey: drive.id) - system?.stopAccessingPath(oldURL.path) + if let oldPath = registryEntry.externalDrives[drive.id]?.path { + system?.stopAccessingPath(oldPath) + } + registryEntry.externalDrives.removeValue(forKey: drive.id) guard let qemu = qemu, qemu.isConnected else { return } try qemu.ejectDrive("drive\(drive.id)", force: isForced) } - func changeMedium(_ drive: inout UTMQemuConfigurationDrive, with url: URL) async throws { + func changeMedium(_ drive: UTMQemuConfigurationDrive, with url: URL) async throws { _ = url.startAccessingSecurityScopedResource() defer { url.stopAccessingSecurityScopedResource() } let tempBookmark = try url.bookmarkData() - try eject(&drive, isForced: true) - try await changeMedium(&drive, with: tempBookmark, isSecurityScoped: false) - drive.imageURL = url + try eject(drive, isForced: true) + let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly) + registryEntry.externalDrives[drive.id] = file + try await changeMedium(drive, with: tempBookmark, isSecurityScoped: false) } - private func changeMedium(_ drive: inout UTMQemuConfigurationDrive, with bookmark: Data, isSecurityScoped: Bool) async throws { + private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, isSecurityScoped: Bool) async throws { guard let system = system else { return } @@ -57,8 +56,9 @@ extension UTMQemuVirtualMachine { guard let bookmark = bookmark, let path = path, success else { throw UTMQemuVirtualMachineError.accessDriveImageFailed } - let file = UTMRegistryEntry.File(path: path, bookmark: bookmark, isReadOnly: drive.isReadOnly) - registryEntry?.externalDrives[drive.id] = file + if registryEntry.externalDrives[drive.id] != nil { + registryEntry.externalDrives[drive.id]!.remoteBookmark = bookmark + } if let qemu = qemu, qemu.isConnected { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) } @@ -68,18 +68,18 @@ extension UTMQemuVirtualMachine { guard system != nil && qemu != nil && qemu!.isConnected else { throw UTMQemuVirtualMachineError.invalidVmState } - let qemuConfig = config.qemuConfig! - for i in qemuConfig.drives.indices { - if !qemuConfig.drives[i].isExternal { + for drive in qemuConfig.drives { + if !drive.isExternal { continue } - let id = qemuConfig.drives[i].id - if let url = qemuConfig.drives[i].imageURL { - // an image was selected while the VM was stopped - try await changeMedium(&qemuConfig.drives[i], with: url) - } else if let bookmark = registryEntry?.externalDrives[id]?.bookmark { - // an image bookmark was saved - try await changeMedium(&qemuConfig.drives[i], with: bookmark, isSecurityScoped: true) + let id = drive.id + if let bookmark = registryEntry.externalDrives[id]?.remoteBookmark { + // an image bookmark was saved while QEMU was running + try await changeMedium(drive, with: bookmark, isSecurityScoped: true) + } else if let localBookmark = registryEntry.externalDrives[id]?.bookmark { + // an image bookmark was saved while QEMU was NOT running + let url = try URL(resolvingPersistentBookmarkData: localBookmark) + try await changeMedium(drive, with: url) } } } @@ -94,6 +94,10 @@ extension UTMQemuVirtualMachine { } } } + + func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? { + registryEntry.externalDrives[drive.id]?.url + } } enum UTMQemuVirtualMachineError: Error { diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 1c48053a2..1b087ec71 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -44,7 +44,10 @@ import Foundation } let path = vm.path.path name = vm.detailsTitleLabel - package = File(path: path, bookmark: bookmark, isReadOnly: false) + guard let package = try? File(path: path, bookmark: bookmark, isReadOnly: false) else { + return nil + } + self.package = package; uuid = vm.config.uuid.uuidString externalDrives = [:] sharedDirectories = [] @@ -74,22 +77,35 @@ import Foundation extension UTMRegistryEntry { struct File: Codable { + var url: URL + var path: String var bookmark: Data + var remoteBookmark: Data? + var isReadOnly: Bool private enum CodingKeys: String, CodingKey { case path = "Path" case bookmark = "Bookmark" + case remoteBookmark = "BookmarkRemote" case isReadOnly = "ReadOnly" } - init(path: String, bookmark: Data, isReadOnly: Bool = false) { + init(path: String, bookmark: Data, isReadOnly: Bool = false) throws { self.path = path self.bookmark = bookmark self.isReadOnly = isReadOnly + self.url = try URL(resolvingPersistentBookmarkData: bookmark) + } + + init(url: URL, isReadOnly: Bool = false) throws { + self.path = url.path + self.bookmark = try url.persistentBookmarkData(isReadyOnly: isReadOnly) + self.isReadOnly = isReadOnly + self.url = url } init(from decoder: Decoder) throws { @@ -97,6 +113,8 @@ extension UTMRegistryEntry { path = try container.decode(String.self, forKey: .path) bookmark = try container.decode(Data.self, forKey: .bookmark) isReadOnly = try container.decode(Bool.self, forKey: .isReadOnly) + remoteBookmark = try container.decodeIfPresent(Data.self, forKey: .remoteBookmark) + url = try URL(resolvingPersistentBookmarkData: bookmark) } func encode(to encoder: Encoder) throws { @@ -104,6 +122,7 @@ extension UTMRegistryEntry { try container.encode(path, forKey: .path) try container.encode(bookmark, forKey: .bookmark) try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encodeIfPresent(remoteBookmark, forKey: .remoteBookmark) } } diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index db813981b..2c06972de 100644 --- a/Managers/UTMVirtualMachine.h +++ b/Managers/UTMVirtualMachine.h @@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN /// This includes display size, bookmarks to removable drives, etc. /// This property is observable and must only be accessed on the main thread. @property (nonatomic, readonly) UTMViewState *viewState; -@property (nonatomic, nullable) UTMRegistryEntry *registryEntry; +@property (nonatomic, readonly) UTMRegistryEntry *registryEntry; /// Current VM state, can observe this property for state changes or use the delegate /// diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index 2203d4830..0d2917253 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -49,6 +49,7 @@ @interface UTMVirtualMachine () @property (nonatomic, readonly) BOOL isScreenshotSaveEnabled; @property (nonatomic, nullable) void (^screenshotTimerHandler)(void); @property (nonatomic) BOOL isScopedAccess; +@property (nonatomic, readwrite) UTMRegistryEntry *registryEntry; @end diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index b37133d77..7fb4c952e 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -210,6 +210,44 @@ public extension UTMQemuVirtualMachine { } } +// MARK: - Bookmark handling +extension URL { + private static var defaultCreationOptions: BookmarkCreationOptions { + #if os(iOS) + return .minimalBookmark + #else + return .withSecurityScope + #endif + } + + private static var defaultResolutionOptions: BookmarkResolutionOptions { + #if os(iOS) + return [] + #else + return .withSecurityScope + #endif + } + + func persistentBookmarkData(isReadyOnly: Bool = false) throws -> Data { + var options = Self.defaultCreationOptions + #if os(macOS) + if isReadyOnly { + options.insert(.securityScopeAllowOnlyReadAccess) + } + #endif + return try self.bookmarkData(options: options, + includingResourceValuesForKeys: nil, + relativeTo: nil) + } + + init(resolvingPersistentBookmarkData bookmark: Data) throws { + var stale: Bool = false + try self.init(resolvingBookmarkData: bookmark, + options: Self.defaultResolutionOptions, + bookmarkDataIsStale: &stale) + } +} + extension UTMDrive: Identifiable { public var id: Int { self.index diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 9460be6b8..3d552031f 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -24,7 +24,7 @@ struct VMRemovableDrivesView: View { @State private var diskImageFileImportPresented: Bool = false /// Explanation see "SwiftUI FileImporter modal bug" in the `body` @State private var workaroundFileImporterBug: Bool = false - @State private var currentDriveBinding: Binding? + @State private var currentDrive: UTMQemuConfigurationDrive? var fileManager: FileManager { FileManager.default @@ -70,60 +70,58 @@ struct VMRemovableDrivesView: View { } }.fileImporter(isPresented: $shareDirectoryFileImportPresented, allowedContentTypes: [.folder], onCompletion: selectShareDirectory) } - ForEach($config.drives) { $drive in - if drive.isExternal { - HStack { - // Drive menu - Menu { - // Browse button - Button(action: { - currentDriveBinding = $drive - // MARK: SwiftUI FileImporter modal bug - /// At this point in the execution, `diskImageFileImportPresented` must be `false`. - /// However there is a SwiftUI FileImporter modal bug: - /// if the user taps outside the import modal to cancel instead of tapping the actual cancel button, - /// the `.fileImporter` doesn't actually set the isPresented Binding to `false`. - if (diskImageFileImportPresented) { - /// bug! Let's set the bool to false ourselves. - diskImageFileImportPresented = false - /// One more thing: we can't immediately set it to `true` again because then the state won't have changed. - /// So we have to use the workaround, which is caught in the `.onChange` below. - workaroundFileImporterBug = true - } else { + ForEach(config.drives.filter { $0.isExternal }) { drive in + HStack { + // Drive menu + Menu { + // Browse button + Button(action: { + currentDrive = drive + // MARK: SwiftUI FileImporter modal bug + /// At this point in the execution, `diskImageFileImportPresented` must be `false`. + /// However there is a SwiftUI FileImporter modal bug: + /// if the user taps outside the import modal to cancel instead of tapping the actual cancel button, + /// the `.fileImporter` doesn't actually set the isPresented Binding to `false`. + if (diskImageFileImportPresented) { + /// bug! Let's set the bool to false ourselves. + diskImageFileImportPresented = false + /// One more thing: we can't immediately set it to `true` again because then the state won't have changed. + /// So we have to use the workaround, which is caught in the `.onChange` below. + workaroundFileImporterBug = true + } else { + diskImageFileImportPresented = true + } + }, label: { + Label("Browse…", systemImage: "doc.badge.plus") + }) + .onChange(of: workaroundFileImporterBug) { doWorkaround in + /// Explanation see "SwiftUI FileImporter modal bug" above + if doWorkaround { + DispatchQueue.main.async { + workaroundFileImporterBug = false diskImageFileImportPresented = true } - }, label: { - Label("Browse…", systemImage: "doc.badge.plus") - }) - .onChange(of: workaroundFileImporterBug) { doWorkaround in - /// Explanation see "SwiftUI FileImporter modal bug" above - if doWorkaround { - DispatchQueue.main.async { - workaroundFileImporterBug = false - diskImageFileImportPresented = true - } - } - } - // Eject button - if drive.imageURL != nil { - Button(action: { clearRemovableImage(forDrive: $drive) }, label: { - Label("Clear", systemImage: "eject") - }) } - } label: { - DriveLabel(drive: drive) - }.disabled(vm.viewState.hasSaveState) - Spacer() - // Disk image path, or (empty) - Text(pathFor(drive)) - .lineLimit(1) - .truncationMode(.tail) - .foregroundColor(.secondary) - }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [.data]) { result in - if let currentDrive = self.currentDriveBinding { - selectRemovableImage(forDrive: currentDrive, result: result) - self.currentDriveBinding = nil } + // Eject button + if vm.externalImageURL(for: drive) != nil { + Button(action: { clearRemovableImage(forDrive: drive) }, label: { + Label("Clear", systemImage: "eject") + }) + } + } label: { + DriveLabel(drive: drive, isInserted: vm.externalImageURL(for: drive) != nil) + }.disabled(vm.viewState.hasSaveState) + Spacer() + // Disk image path, or (empty) + Text(pathFor(drive)) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.secondary) + }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [.data]) { result in + if let currentDrive = self.currentDrive { + selectRemovableImage(forDrive: currentDrive, result: result) + self.currentDrive = nil } } } @@ -152,7 +150,7 @@ struct VMRemovableDrivesView: View { } private func pathFor(_ drive: UTMQemuConfigurationDrive) -> String { - if let url = drive.imageURL { + if let url = vm.externalImageURL(for: drive) { return url.lastPathComponent } else { return NSLocalizedString("(empty)", comment: "A removable drive that has no image file inserted.") @@ -161,10 +159,11 @@ struct VMRemovableDrivesView: View { private struct DriveLabel: View { let drive: UTMQemuConfigurationDrive + let isInserted: Bool var body: some View { if drive.imageType == .cd { - return Label("CD/DVD", systemImage: drive.imageURL == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") + return Label("CD/DVD", systemImage: !isInserted ? "opticaldiscdrive" : "opticaldiscdrive.fill") } else { return Label("Removable", systemImage: "externaldrive") } @@ -187,11 +186,11 @@ struct VMRemovableDrivesView: View { vm.clearSharedDirectory() } - private func selectRemovableImage(forDrive drive: Binding, result: Result) { + private func selectRemovableImage(forDrive drive: UTMQemuConfigurationDrive, result: Result) { data.busyWorkAsync { switch result { case .success(let url): - try await vm.changeMedium(&drive.wrappedValue, with: url) + try await vm.changeMedium(drive, with: url) break case .failure(let err): throw err @@ -199,9 +198,9 @@ struct VMRemovableDrivesView: View { } } - private func clearRemovableImage(forDrive drive: Binding) { + private func clearRemovableImage(forDrive drive: UTMQemuConfigurationDrive) { data.busyWorkAsync { - try await vm.eject(&drive.wrappedValue) + try await vm.eject(drive) } } } diff --git a/Platform/iOS/VMToolbarDriveMenuView.swift b/Platform/iOS/VMToolbarDriveMenuView.swift index ce33d05a5..d6d1aedb0 100644 --- a/Platform/iOS/VMToolbarDriveMenuView.swift +++ b/Platform/iOS/VMToolbarDriveMenuView.swift @@ -20,32 +20,32 @@ struct VMToolbarDriveMenuView: View { @State var config: UTMQemuConfiguration @EnvironmentObject private var session: VMSessionState @State private var isFileImporterShown: Bool = false - @State private var selectedDrive: Binding? + @State private var selectedDrive: UTMQemuConfigurationDrive? @State private var isRefreshRequired: Bool = false var body: some View { Menu { - ForEach($config.drives) { $drive in + ForEach(config.drives) { drive in if drive.isExternal { Menu { Button { - selectedDrive = $drive + selectedDrive = drive isFileImporterShown.toggle() } label: { MenuLabel("Change…", systemImage: "opticaldisc") } Button { - ejectDriveImage(for: $drive) + ejectDriveImage(for: drive) } label: { MenuLabel("Eject…", systemImage: "eject") } } label: { - MenuLabel(drive.label, systemImage: drive.imageURL == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") + MenuLabel(label(for: drive), systemImage: session.vm.externalImageURL(for: drive) == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") } } else if drive.imageType == .disk || drive.imageType == .cd { Button { } label: { - MenuLabel(drive.label, systemImage: "internaldrive") + MenuLabel(label(for: drive), systemImage: "internaldrive") }.disabled(true) } } @@ -65,10 +65,10 @@ struct VMToolbarDriveMenuView: View { } } - private func changeDriveImage(for driveBinding: Binding, with imageURL: URL) { + private func changeDriveImage(for drive: UTMQemuConfigurationDrive, with imageURL: URL) { Task.detached(priority: .background) { do { - try await session.vm.changeMedium(&driveBinding.wrappedValue, with: imageURL) + try await session.vm.changeMedium(drive, with: imageURL) Task { @MainActor in isRefreshRequired.toggle() } @@ -80,10 +80,10 @@ struct VMToolbarDriveMenuView: View { } } - private func ejectDriveImage(for driveBinding: Binding) { + private func ejectDriveImage(for drive: UTMQemuConfigurationDrive) { Task.detached(priority: .background) { do { - try await session.vm.eject(&driveBinding.wrappedValue) + try await session.vm.eject(drive) Task { @MainActor in isRefreshRequired.toggle() } @@ -94,6 +94,14 @@ struct VMToolbarDriveMenuView: View { } } } + + private func label(for drive: UTMQemuConfigurationDrive) -> String { + let imageURL = session.vm.externalImageURL(for: drive) ?? drive.imageURL + return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMToolbarDriveMenuView"), + drive.imageType.prettyValue, + drive.interface.prettyValue, + imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMToolbarDriveMenuView")) + } } struct VMToolbarDriveMenuView_Previews: PreviewProvider { diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index 1dd2f04c2..ea62061c1 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -109,7 +109,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { continue // skip non-disks } let item = NSMenuItem() - item.title = drive.label + item.title = label(for: drive) if !drive.isExternal { item.isEnabled = false } else { @@ -120,7 +120,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { keyEquivalent: "") eject.target = self eject.tag = i - eject.isEnabled = drive.imageURL != nil + eject.isEnabled = qemuVM.externalImageURL(for: drive) != nil submenu.addItem(eject) let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"), action: #selector(changeDriveImage), @@ -141,10 +141,10 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { logger.error("wrong sender for ejectDrive") return } - let config = vmQemuConfig! + let drive = vmQemuConfig.drives[menu.tag] Task.detached(priority: .background) { [self] in do { - try await qemuVM.eject(&config.drives[menu.tag]) + try await qemuVM.eject(drive) } catch { Task { @MainActor in showErrorAlert(error.localizedDescription) @@ -154,6 +154,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { } func openDriveImage(forDriveIndex index: Int) { + let drive = vmQemuConfig.drives[index] let openPanel = NSOpenPanel() openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController") openPanel.allowedContentTypes = [.data] @@ -165,10 +166,9 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { logger.debug("no file selected") return } - let config = self.vmQemuConfig! Task.detached(priority: .background) { [self] in do { - try await qemuVM.changeMedium(&config.drives[index], with: url) + try await qemuVM.changeMedium(drive, with: url) } catch { Task { @MainActor in showErrorAlert(error.localizedDescription) @@ -185,6 +185,14 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { } openDriveImage(forDriveIndex: menu.tag) } + + @nonobjc private func label(for drive: UTMQemuConfigurationDrive) -> String { + let imageURL = qemuVM.externalImageURL(for: drive) ?? drive.imageURL + return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMDisplayQemuDisplayController"), + drive.imageType.prettyValue, + drive.interface.prettyValue, + imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMDisplayQemuDisplayController")) + } } // MARK: - Shared folders From a47140c723b80fb9583cdf3c63f5ecca4559f797 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:45:32 -0700 Subject: [PATCH 08/42] registry: sync registry entry with settings --- Managers/UTMQemuVirtualMachine.swift | 31 +++++++++++++++++++++++ Managers/UTMRegistry.swift | 14 +++++++--- Managers/UTMRegistryEntry.swift | 2 +- Managers/UTMVirtualMachine.m | 1 + Managers/UTMVirtualMachineExtension.swift | 21 +++++---------- Platform/Shared/VMDetailsView.swift | 5 ++++ 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index a8ff3b188..18e0fbe21 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -100,6 +100,37 @@ extension UTMQemuVirtualMachine { } } +// MARK: - Registry syncing +extension UTMQemuVirtualMachine { + @MainActor override func updateConfigFromRegistry() { + for i in qemuConfig.drives.indices { + let drive = qemuConfig.drives[i] + if drive.isExternal { + if let file = registryEntry.externalDrives[drive.id] { + qemuConfig.drives[i].imageURL = file.url + } + } + } + // FIXME: update directory sharing + } + + override func updateRegistryPostSave() async throws { + for i in qemuConfig.drives.indices { + let drive = qemuConfig.drives[i] + if drive.isExternal, let url = drive.imageURL { + try await changeMedium(drive, with: url) + await Task { @MainActor in + // clear temporary URL + qemuConfig.drives[i].imageURL = nil + }.value + } + } + if let url = config.qemuConfig!.sharing.directoryShareUrl { + try changeSharedDirectory(url) + } + } +} + enum UTMQemuVirtualMachineError: Error { case accessDriveImageFailed case invalidVmState diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift index f714c3593..a63829b66 100644 --- a/Managers/UTMRegistry.swift +++ b/Managers/UTMRegistry.swift @@ -16,13 +16,21 @@ import Foundation -class UTMRegistry { - static let `default` = UTMRegistry() +class UTMRegistry: NSObject { + @objc static let shared = UTMRegistry() - private init() { + private override init() { } + /// Gets an existing registry entry or create a new entry + /// - Parameter vm: UTM virtual machine to locate in the registry + /// - Returns: Either an existing registry entry or a new entry + @objc func entry(for vm: UTMVirtualMachine) -> UTMRegistryEntry { + // FIXME: locate existing registry + return UTMRegistryEntry(newFrom: vm)! + } + func update(entry: UTMRegistryEntry) { } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 1b087ec71..c03878536 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -176,7 +176,7 @@ extension UTMRegistryEntry { } set { instance[keyPath: storageKeyPath].storage = newValue - UTMRegistry.default.update(entry: instance) + UTMRegistry.shared.update(entry: instance) } } diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index 0d2917253..f42fb3320 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -216,6 +216,7 @@ - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration p if (self) { self.config = configuration; self.path = packageURL; + self.registryEntry = [UTMRegistry.shared entryFor:self]; [self loadViewState]; [self loadScreenshot]; self.state = kVMStopped; diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 7fb4c952e..6baf2ee4d 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -79,7 +79,7 @@ extension UTMVirtualMachine: ObservableObject { let newPath = existingPath.deletingLastPathComponent().appendingPathComponent(config.name).appendingPathExtension("utm") do { try await config.save(to: existingPath) - try updateViewStatePostSave() + try await updateRegistryPostSave() } catch { try? reloadConfiguration() throw error @@ -93,7 +93,11 @@ extension UTMVirtualMachine: ObservableObject { } } - func updateViewStatePostSave() throws { + @MainActor func updateConfigFromRegistry() { + // do nothing by default + } + + func updateRegistryPostSave() async throws { // do nothing by default } } @@ -184,19 +188,6 @@ public extension UTMQemuVirtualMachine { return drive } - override func updateViewStatePostSave() throws { - //FIXME: remove this once we remove viewState - for drive in config.qemuConfig!.drives { - if drive.isExternal, let url = drive.imageURL { - let legacyDrive = drives.first(where: { $0.name == drive.id }) - try changeMedium(for: legacyDrive!, url: url) - } - } - if let url = config.qemuConfig!.sharing.directoryShareUrl { - try changeSharedDirectory(url) - } - } - /// Sets up values in VM configuration corrosponding to per-device data like sharing path @objc func prepareConfigurationForStart() { if config.qemuConfig!.sharing.directoryShareMode != .none { diff --git a/Platform/Shared/VMDetailsView.swift b/Platform/Shared/VMDetailsView.swift index 353f6944c..f5609d0f6 100644 --- a/Platform/Shared/VMDetailsView.swift +++ b/Platform/Shared/VMDetailsView.swift @@ -113,6 +113,11 @@ struct VMDetailsView: View { } #endif } + .onChange(of: data.showSettingsModal) { newValue in + if newValue { + vm.updateConfigFromRegistry() + } + } } } } From 7285b8a0462023f2d4d7fa8586ed103bae9bfac9 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 07:25:28 -0700 Subject: [PATCH 09/42] vm(qemu): clear out some legacy SPICE manager code --- Managers/UTMQemuVirtualMachine+SPICE.m | 28 +++------------------- Managers/UTMQemuVirtualMachine-Protected.h | 2 ++ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine+SPICE.m b/Managers/UTMQemuVirtualMachine+SPICE.m index c5c535d00..8c029f825 100644 --- a/Managers/UTMQemuVirtualMachine+SPICE.m +++ b/Managers/UTMQemuVirtualMachine+SPICE.m @@ -44,16 +44,6 @@ - (void)saveViewState; @implementation UTMQemuVirtualMachine (SPICE) -- (UTMSpiceIO *)spiceIoWithError:(NSError * _Nullable __autoreleasing *)error { - if (![self.ioService isKindOfClass:[UTMSpiceIO class]]) { - if (error) { - *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"VM frontend does not support shared directories.", "UTMVirtualMachine+Sharing")}]; - } - return nil; - } - return (UTMSpiceIO *)self.ioService; -} - #pragma mark - Shared Directory - (BOOL)saveSharedDirectory:(NSURL *)url error:(NSError * _Nullable __autoreleasing *)error { @@ -79,11 +69,7 @@ - (BOOL)changeSharedDirectory:(NSURL *)url error:(NSError * _Nullable __autorele return [self saveSharedDirectory:url error:error]; } if (self.config.qemuHasWebdavSharing) { - UTMSpiceIO *spiceIO = [self spiceIoWithError:error]; - if (!spiceIO) { - return NO; - } - [spiceIO changeSharedDirectory:url]; + [self.ioService changeSharedDirectory:url]; } return [self saveSharedDirectory:url error:error]; } @@ -104,13 +90,8 @@ - (BOOL)startSharedDirectoryWithError:(NSError * _Nullable __autoreleasing *)err if (!self.config.qemuHasWebdavSharing) { return YES; } - UTMSpiceIO *spiceIO = [self spiceIoWithError:error]; - if (!spiceIO) { - return NO; - } NSData *bookmark = nil; - BOOL legacy = NO; if (self.viewState.sharedDirectory) { UTMLog(@"found shared directory bookmark"); bookmark = self.viewState.sharedDirectory; @@ -129,12 +110,9 @@ - (BOOL)startSharedDirectoryWithError:(NSError * _Nullable __autoreleasing *)err success = [self saveSharedDirectory:shareURL error:error]; } if (success) { - [spiceIO changeSharedDirectory:shareURL]; + [self.ioService changeSharedDirectory:shareURL]; } return success; - } else if (legacy) { // ignore errors for legacy sharing since we don't have a good way of handling it - UTMLog(@"Ignoring error on legacy shared directory"); - return YES; } else { // clear the broken bookmark [self clearSharedDirectory]; @@ -165,7 +143,7 @@ - (void)requestInputTablet:(BOOL)tablet { if (err) { UTMLog(@"input select returned error: %@", err); } else { - UTMSpiceIO *spiceIO = [self spiceIoWithError:&err]; + UTMSpiceIO *spiceIO = self.ioService; if (spiceIO) { [spiceIO.primaryInput requestMouseMode:!tablet]; } else { diff --git a/Managers/UTMQemuVirtualMachine-Protected.h b/Managers/UTMQemuVirtualMachine-Protected.h index e0bfcbd4a..95dc7f986 100644 --- a/Managers/UTMQemuVirtualMachine-Protected.h +++ b/Managers/UTMQemuVirtualMachine-Protected.h @@ -18,6 +18,7 @@ @class UTMQemu; @class UTMQemuManager; +@class UTMSpiceIO; NS_ASSUME_NONNULL_BEGIN @@ -25,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) UTMQemuManager *qemu; @property (nonatomic, readonly, nullable) UTMQemu *system; +@property (nonatomic, readonly, nullable) UTMSpiceIO *ioService; - (void)saveViewState; From 23883d82fcf9c34bf8ecea620576125921a1ed20 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 08:06:06 -0700 Subject: [PATCH 10/42] vm(qemu): implement shared directory saving in registry --- Managers/UTMQemuVirtualMachine.m | 23 +++--- Managers/UTMQemuVirtualMachine.swift | 72 ++++++++++++++++++- Managers/UTMVirtualMachineExtension.swift | 5 +- Platform/Shared/VMRemovableDrivesView.swift | 8 +-- .../VMDisplayQemuDisplayController.swift | 6 +- 5 files changed, 86 insertions(+), 28 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 2d74f241d..4b860ffa4 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -263,24 +263,19 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { } assert(self.qemu.isConnected); // set up SPICE sharing and removable drives - if (![self startSharedDirectoryWithError:&err]) { - errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to start shared directory: %@", @"UTMVirtualMachine"), err.localizedDescription]; - completion([self errorWithMessage:errMsg]); - return; - } - __block NSError *restoreExternalDrivesError = nil; - dispatch_semaphore_t restoreExternalDrivesEvent = dispatch_semaphore_create(0); - [self restoreExternalDrivesWithCompletion:^(NSError *err) { - restoreExternalDrivesError = err; - dispatch_semaphore_signal(restoreExternalDrivesEvent); + __block NSError *restoreExternalDrivesAndSharesError = nil; + dispatch_semaphore_t restoreExternalDrivesAndSharesEvent = dispatch_semaphore_create(0); + [self restoreExternalDrivesAndSharesWithCompletion:^(NSError *err) { + restoreExternalDrivesAndSharesError = err; + dispatch_semaphore_signal(restoreExternalDrivesAndSharesEvent); }]; - if (dispatch_semaphore_wait(restoreExternalDrivesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { - UTMLog(@"Timed out waiting for external drives to be restored."); + if (dispatch_semaphore_wait(restoreExternalDrivesAndSharesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { + UTMLog(@"Timed out waiting for external drives and shares to be restored."); completion([self errorGeneric]); return; } - if (restoreExternalDrivesError) { - errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore removable drives: %@", @"UTMVirtualMachine"), restoreExternalDrivesError.localizedDescription]; + if (restoreExternalDrivesAndSharesError) { + errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore external drives and shares: %@", @"UTMVirtualMachine"), restoreExternalDrivesAndSharesError.localizedDescription]; completion([self errorWithMessage:errMsg]); return; } diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 18e0fbe21..e037fcfcb 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -84,10 +84,11 @@ extension UTMQemuVirtualMachine { } } - @objc func restoreExternalDrives(completion: @escaping (Error?) -> Void) { + @objc func restoreExternalDrivesAndShares(completion: @escaping (Error?) -> Void) { Task.detached { do { try await self.restoreExternalDrives() + try await self.restoreSharedDirectory() completion(nil) } catch { completion(error) @@ -100,6 +101,67 @@ extension UTMQemuVirtualMachine { } } +// MARK: - Shared directory +extension UTMQemuVirtualMachine { + var sharedDirectoryURL: URL? { + registryEntry.sharedDirectories.first?.url + } + + func clearSharedDirectory() { + registryEntry.sharedDirectories = [] + } + + func changeSharedDirectory(to url: URL) async throws { + _ = url.startAccessingSecurityScopedResource() + defer { + url.stopAccessingSecurityScopedResource() + } + let file = try UTMRegistryEntry.File(url: url, isReadOnly: qemuConfig.sharing.isDirectoryShareReadOnly) + registryEntry.sharedDirectories = [file] + if qemuConfig.sharing.directoryShareMode == .webdav { + if let ioService = ioService { + ioService.changeSharedDirectory(url) + } + } else if qemuConfig.sharing.directoryShareMode == .virtfs { + let tempBookmark = try url.bookmarkData() + try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) + } + } + + func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws { + guard let system = system else { + return + } + let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) + guard let bookmark = bookmark, let _ = path, success else { + throw UTMQemuVirtualMachineError.accessDriveImageFailed + } + if !registryEntry.sharedDirectories.isEmpty { + registryEntry.sharedDirectories[0].remoteBookmark = bookmark + } + } + + func restoreSharedDirectory() async throws { + guard let share = registryEntry.sharedDirectories.first else { + return + } + if qemuConfig.sharing.directoryShareMode == .virtfs { + if let bookmark = share.remoteBookmark { + // a share bookmark was saved while QEMU was running + try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true) + } else if let localBookmark = registryEntry.externalDrives[id]?.bookmark { + // a share bookmark was saved while QEMU was NOT running + let url = try URL(resolvingPersistentBookmarkData: localBookmark) + try await changeSharedDirectory(to: url) + } + } else if qemuConfig.sharing.directoryShareMode == .webdav { + if let ioService = ioService { + ioService.changeSharedDirectory(share.url) + } + } + } +} + // MARK: - Registry syncing extension UTMQemuVirtualMachine { @MainActor override func updateConfigFromRegistry() { @@ -111,7 +173,9 @@ extension UTMQemuVirtualMachine { } } } - // FIXME: update directory sharing + if qemuConfig.sharing.directoryShareMode != .none { + qemuConfig.sharing.directoryShareUrl = sharedDirectoryURL + } } override func updateRegistryPostSave() async throws { @@ -126,13 +190,14 @@ extension UTMQemuVirtualMachine { } } if let url = config.qemuConfig!.sharing.directoryShareUrl { - try changeSharedDirectory(url) + try await changeSharedDirectory(to: url) } } } enum UTMQemuVirtualMachineError: Error { case accessDriveImageFailed + case accessShareFailed case invalidVmState } @@ -140,6 +205,7 @@ extension UTMQemuVirtualMachineError: LocalizedError { var errorDescription: String? { switch self { case .accessDriveImageFailed: return NSLocalizedString("Failed to access drive image path.", comment: "UTMQemuVirtualMachine") + case .accessShareFailed: return NSLocalizedString("Failed to access shared directory.", comment: "UTMQemuVirtualMachine") case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine") } } diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 6baf2ee4d..b34346489 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -191,11 +191,8 @@ public extension UTMQemuVirtualMachine { /// Sets up values in VM configuration corrosponding to per-device data like sharing path @objc func prepareConfigurationForStart() { if config.qemuConfig!.sharing.directoryShareMode != .none { - var stale: Bool = false - if let bookmark = viewState.sharedDirectory, let url = try? URL(resolvingBookmarkData: bookmark, options: kUTMBookmarkResolutionOptions, bookmarkDataIsStale: &stale) { + if let url = sharedDirectoryURL { config.qemuConfig!.sharing.directoryShareUrl = url - } else { - logger.error("Cannot resolve bookmark for share path '\(viewState.sharedDirectoryPath ?? "nil")'") } } } diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 3d552031f..209cb01a8 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -32,7 +32,7 @@ struct VMRemovableDrivesView: View { // Is a shared directory set? - private var hasSharedDir: Bool { vm.viewState.sharedDirectoryPath != nil } + private var hasSharedDir: Bool { vm.sharedDirectoryURL != nil } @ViewBuilder private var shareMenuActions: some View { Button(action: { shareDirectoryFileImportPresented.toggle() }) { @@ -63,7 +63,7 @@ struct VMRemovableDrivesView: View { Menu { shareMenuActions } label: { - SharedPath(path: vm.viewState.sharedDirectoryPath) + SharedPath(path: vm.sharedDirectoryURL?.path) }.fixedSize() } else { Button("Browse…", action: { shareDirectoryFileImportPresented.toggle() }) @@ -171,10 +171,10 @@ struct VMRemovableDrivesView: View { } private func selectShareDirectory(result: Result) { - data.busyWork { + data.busyWorkAsync { switch result { case .success(let url): - try vm.changeSharedDirectory(url) + try await vm.changeSharedDirectory(to: url) break case .failure(let err): throw err diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index ea62061c1..2ab63a621 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -211,11 +211,11 @@ extension VMDisplayQemuWindowController { logger.debug("no directory selected") return } - DispatchQueue.global(qos: .background).async { + Task.detached(priority: .background) { [self] in do { - try self.qemuVM.changeSharedDirectory(url) + try await self.qemuVM.changeSharedDirectory(to: url) } catch { - DispatchQueue.main.async { + Task { @MainActor in self.showErrorAlert(error.localizedDescription) } } From 8af5fa255ac263594d652be6287bb46c6726b3f2 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 08:06:57 -0700 Subject: [PATCH 11/42] vm(qemu): rename changeMedium() method for consistency --- Managers/UTMQemuVirtualMachine.swift | 6 +++--- Platform/Shared/VMRemovableDrivesView.swift | 2 +- Platform/iOS/VMToolbarDriveMenuView.swift | 2 +- Platform/macOS/Display/VMDisplayQemuDisplayController.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index e037fcfcb..368f4ff3b 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -36,7 +36,7 @@ extension UTMQemuVirtualMachine { try qemu.ejectDrive("drive\(drive.id)", force: isForced) } - func changeMedium(_ drive: UTMQemuConfigurationDrive, with url: URL) async throws { + func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws { _ = url.startAccessingSecurityScopedResource() defer { url.stopAccessingSecurityScopedResource() @@ -79,7 +79,7 @@ extension UTMQemuVirtualMachine { } else if let localBookmark = registryEntry.externalDrives[id]?.bookmark { // an image bookmark was saved while QEMU was NOT running let url = try URL(resolvingPersistentBookmarkData: localBookmark) - try await changeMedium(drive, with: url) + try await changeMedium(drive, to: url) } } } @@ -182,7 +182,7 @@ extension UTMQemuVirtualMachine { for i in qemuConfig.drives.indices { let drive = qemuConfig.drives[i] if drive.isExternal, let url = drive.imageURL { - try await changeMedium(drive, with: url) + try await changeMedium(drive, to: url) await Task { @MainActor in // clear temporary URL qemuConfig.drives[i].imageURL = nil diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 209cb01a8..218105174 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -190,7 +190,7 @@ struct VMRemovableDrivesView: View { data.busyWorkAsync { switch result { case .success(let url): - try await vm.changeMedium(drive, with: url) + try await vm.changeMedium(drive, to: url) break case .failure(let err): throw err diff --git a/Platform/iOS/VMToolbarDriveMenuView.swift b/Platform/iOS/VMToolbarDriveMenuView.swift index d6d1aedb0..893bdca06 100644 --- a/Platform/iOS/VMToolbarDriveMenuView.swift +++ b/Platform/iOS/VMToolbarDriveMenuView.swift @@ -68,7 +68,7 @@ struct VMToolbarDriveMenuView: View { private func changeDriveImage(for drive: UTMQemuConfigurationDrive, with imageURL: URL) { Task.detached(priority: .background) { do { - try await session.vm.changeMedium(drive, with: imageURL) + try await session.vm.changeMedium(drive, to: imageURL) Task { @MainActor in isRefreshRequired.toggle() } diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index 2ab63a621..4103085e3 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -168,7 +168,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { } Task.detached(priority: .background) { [self] in do { - try await qemuVM.changeMedium(drive, with: url) + try await qemuVM.changeMedium(drive, to: url) } catch { Task { @MainActor in showErrorAlert(error.localizedDescription) From 98ac4b4111452ab07295c9dab73d0d90b8c1b439 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 08:16:41 -0700 Subject: [PATCH 12/42] toolbar: change webdav shared directory --- Platform/iOS/VMToolbarDriveMenuView.swift | 49 +++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/Platform/iOS/VMToolbarDriveMenuView.swift b/Platform/iOS/VMToolbarDriveMenuView.swift index 893bdca06..0cc1fad6b 100644 --- a/Platform/iOS/VMToolbarDriveMenuView.swift +++ b/Platform/iOS/VMToolbarDriveMenuView.swift @@ -20,16 +20,40 @@ struct VMToolbarDriveMenuView: View { @State var config: UTMQemuConfiguration @EnvironmentObject private var session: VMSessionState @State private var isFileImporterShown: Bool = false + @State private var isSelectingShare: Bool = false @State private var selectedDrive: UTMQemuConfigurationDrive? @State private var isRefreshRequired: Bool = false + private let noneText = NSLocalizedString("none", comment: "VMToolbarDriveMenuView") + var body: some View { Menu { + if config.sharing.directoryShareMode == .webdav { + Menu { + Button { + selectedDrive = nil + isSelectingShare = true + isFileImporterShown.toggle() + } label: { + MenuLabel("Change…", systemImage: "folder.badge.person.crop") + } + Button { + session.vm.clearSharedDirectory() + } label: { + MenuLabel("Clear…", systemImage: "clear") + } + } label: { + let url = session.vm.sharedDirectoryURL + MenuLabel("Shared Directory: \(url?.lastPathComponent ?? noneText)", systemImage: url == nil ? "folder.badge.person.crop" : "folder.fill.badge.person.crop") + } + Divider() + } ForEach(config.drives) { drive in if drive.isExternal { Menu { Button { selectedDrive = drive + isSelectingShare = false isFileImporterShown.toggle() } label: { MenuLabel("Change…", systemImage: "opticaldisc") @@ -51,10 +75,14 @@ struct VMToolbarDriveMenuView: View { } } label: { Label("Disk", systemImage: "opticaldisc") - }.fileImporter(isPresented: $isFileImporterShown, allowedContentTypes: [.item]) { result in + }.fileImporter(isPresented: $isFileImporterShown, allowedContentTypes: isSelectingShare ? [.folder] : [.item]) { result in switch result { case .success(let success): - changeDriveImage(for: selectedDrive!, with: success) + if isSelectingShare { + changeSharedDirectory(to: success) + } else if let drive = selectedDrive { + changeDriveImage(for: drive, with: success) + } case .failure(let failure): session.nonfatalError = failure.localizedDescription } @@ -80,6 +108,21 @@ struct VMToolbarDriveMenuView: View { } } + private func changeSharedDirectory(to url: URL) { + Task.detached(priority: .background) { + do { + try await session.vm.changeSharedDirectory(to: url) + Task { @MainActor in + isRefreshRequired.toggle() + } + } catch { + Task { @MainActor in + session.nonfatalError = error.localizedDescription + } + } + } + } + private func ejectDriveImage(for drive: UTMQemuConfigurationDrive) { Task.detached(priority: .background) { do { @@ -100,7 +143,7 @@ struct VMToolbarDriveMenuView: View { return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMToolbarDriveMenuView"), drive.imageType.prettyValue, drive.interface.prettyValue, - imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMToolbarDriveMenuView")) + imageURL?.lastPathComponent ?? noneText) } } From 699c08d55e2b04eef951749d38184d6d3052d26b Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 09:01:29 -0700 Subject: [PATCH 13/42] registry: implement shortcut bookmark and hasSaveState --- Managers/UTMQemuVirtualMachine.m | 29 +++++------- Managers/UTMRegistryEntry.swift | 45 +++++++++++++++++++ Managers/UTMVirtualMachine.m | 4 +- Platform/Shared/VMContextMenuModifier.swift | 4 +- Platform/Shared/VMRemovableDrivesView.swift | 2 +- Platform/Shared/VMToolbarModifier.swift | 4 +- Platform/iOS/UTMDataExtension.swift | 2 +- Platform/macOS/UTMDataExtension.swift | 2 +- .../macOS/VMAppleRemovableDrivesView.swift | 4 +- 9 files changed, 67 insertions(+), 29 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 4b860ffa4..d10618405 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -92,8 +92,8 @@ - (void)accessShortcutWithCompletion:(void (^ _Nullable)(NSError * _Nullable))co if (!service) { service = [UTMQemu new]; // VM has not started yet, we create a temporary process } - NSData *bookmark = self.viewState.shortcutBookmark; - NSString *bookmarkPath = self.viewState.shortcutBookmarkPath; + NSData *bookmark = self.registryEntry.packageRemoteBookmark; + NSString *bookmarkPath = self.registryEntry.packageRemotePath; BOOL existing = bookmark != nil; if (!existing) { // create temporary bookmark @@ -113,9 +113,8 @@ - (void)accessShortcutWithCompletion:(void (^ _Nullable)(NSError * _Nullable))co [service accessDataWithBookmark:bookmark securityScoped:existing completion:^(BOOL success, NSData *newBookmark, NSString *newPath) { (void)service; // required to capture service so it is not released by ARC if (success) { - self.viewState.shortcutBookmark = newBookmark; - self.viewState.shortcutBookmarkPath = newPath; - [self saveViewState]; + self.registryEntry.packageRemoteBookmark = newBookmark; + self.registryEntry.packageRemotePath = newPath; completion(nil); } else { completion([self errorWithMessage:NSLocalizedString(@"Failed to access data from shortcut.", @"UTMQemuVirtualMachine")]); @@ -142,7 +141,7 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { self.config.qemuIsDisposable = self.isRunningAsSnapshot; } else { // Loading save states isn't possible when -snapshot is used - if (self.viewState.hasSaveState) { + if (self.registryEntry.hasSaveState) { self.config.qemuSnapshotName = kSuspendSnapshotName; } } @@ -285,7 +284,7 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { completion(err); return; } - if (self.viewState.hasSaveState) { + if (self.registryEntry.hasSaveState) { [self _vmDeleteStateWithCompletion:^(NSError *error){ // ignore error completion(nil); @@ -304,10 +303,7 @@ - (void)vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { [self changeState:kVMStarting]; [self _vmStartWithCompletion:^(NSError *err){ if (err) { // delete suspend state on error - dispatch_sync(dispatch_get_main_queue(), ^{ - self.viewState.hasSaveState = NO; - }); - [self saveViewState]; + self.registryEntry.hasSaveState = NO; [self changeState:kVMStopped]; } else { [self changeState:kVMStarted]; @@ -365,10 +361,9 @@ - (void)vmStopForce:(BOOL)force completion:(void (^)(NSError * _Nullable))comple } - (void)_vmResetWithCompletion:(void (^)(NSError * _Nullable))completion { - if (self.viewState.hasSaveState) { + if (self.registryEntry.hasSaveState) { [self _vmDeleteStateWithCompletion:^(NSError *error) {}]; } - [self saveViewState]; __block NSError *resetError = nil; dispatch_semaphore_t resetTriggeredEvent = dispatch_semaphore_create(0); [self.qemu qemuResetWithCompletion:^(NSError *err) { @@ -476,8 +471,7 @@ - (void)_vmSaveStateWithCompletion:(void (^)(NSError * _Nullable))completion { saveError = [self errorGeneric]; } else if (!saveError) { UTMLog(@"Save completed"); - self.viewState.hasSaveState = YES; - [self saveViewState]; + self.registryEntry.hasSaveState = YES; [self saveScreenshot]; } completion(saveError); @@ -515,8 +509,7 @@ - (void)_vmDeleteStateWithCompletion:(void (^)(NSError * _Nullable))completion { UTMLog(@"Delete save completed"); } } // otherwise we mark as deleted - self.viewState.hasSaveState = NO; - [self saveViewState]; + self.registryEntry.hasSaveState = NO; completion(deleteError); } @@ -541,7 +534,7 @@ - (void)_vmResumeWithCompletion:(void (^)(NSError * _Nullable))completion { UTMLog(@"Resume operation timeout"); resumeError = [self errorGeneric]; } - if (self.viewState.hasSaveState) { + if (self.registryEntry.hasSaveState) { [self _vmDeleteStateWithCompletion:^(NSError *error){ completion(nil); }]; diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index c03878536..715b76254 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -23,6 +23,8 @@ import Foundation @UTMRegistryValue var uuid: String + @UTMRegistryValue var isSuspended: Bool + @UTMRegistryValue var externalDrives: [String: File] @UTMRegistryValue var sharedDirectories: [File] @@ -33,6 +35,7 @@ import Foundation case name = "Name" case package = "Package" case uuid = "UUID" + case isSuspended = "Suspended" case externalDrives = "ExternalDrives" case sharedDirectories = "SharedDirectories" case windowSettings = "WindowSettings" @@ -49,6 +52,7 @@ import Foundation } self.package = package; uuid = vm.config.uuid.uuidString + isSuspended = false externalDrives = [:] sharedDirectories = [] windowSettings = [:] @@ -59,6 +63,7 @@ import Foundation name = try container.decode(String.self, forKey: .name) package = try container.decode(File.self, forKey: .package) uuid = try container.decode(String.self, forKey: .uuid) + isSuspended = try container.decode(Bool.self, forKey: .isSuspended) externalDrives = try container.decode([String: File].self, forKey: .externalDrives) sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings) @@ -69,12 +74,52 @@ import Foundation try container.encode(name, forKey: .name) try container.encode(package, forKey: .package) try container.encode(uuid, forKey: .uuid) + try container.encode(isSuspended, forKey: .isSuspended) try container.encode(externalDrives, forKey: .externalDrives) try container.encode(sharedDirectories, forKey: .sharedDirectories) try container.encode(windowSettings, forKey: .windowSettings) } } +// MARK: - Objective C bridging +@objc extension UTMRegistryEntry { + var hasSaveState: Bool { + get { + isSuspended + } + + set { + isSuspended = newValue + } + } + + var packageRemoteBookmark: Data? { + get { + package.remoteBookmark + } + + set { + package.remoteBookmark = newValue + } + } + + var packageRemotePath: String? { + get { + if package.remoteBookmark != nil { + return package.path + } else { + return nil + } + } + + set { + if newValue != nil { + package.path = newValue! + } + } + } +} + extension UTMRegistryEntry { struct File: Codable { var url: URL diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index f42fb3320..0b0d1ddd7 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -107,7 +107,7 @@ - (void)setIsRunningAsSnapshot:(BOOL)isNextRunAsSnapshot { - (NSString *)stateLabel { switch (_state) { case kVMStopped: - if (self.viewState.hasSaveState) { + if (self.registryEntry.hasSaveState) { return NSLocalizedString(@"Suspended", "UTMVirtualMachine"); } else { return NSLocalizedString(@"Stopped", "UTMVirtualMachine"); @@ -122,7 +122,7 @@ - (NSString *)stateLabel { } - (BOOL)hasSaveState { - return self.viewState.hasSaveState; + return self.registryEntry.hasSaveState; } // MARK: - Other properties diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 27722f2c0..4e9f3f4d2 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -38,9 +38,9 @@ struct VMContextMenuModifier: ViewModifier { data.edit(vm: vm) } label: { Label("Edit", systemImage: "slider.horizontal.3") - }.disabled(vm.viewState.hasSaveState || vm.state != .vmStopped) + }.disabled(vm.hasSaveState || vm.state != .vmStopped) .help("Modify settings for this VM.") - if vm.viewState.hasSaveState || vm.state != .vmStopped { + if vm.hasSaveState || vm.state != .vmStopped { Button { confirmAction = .confirmStopVM } label: { diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 218105174..34466a0c3 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -111,7 +111,7 @@ struct VMRemovableDrivesView: View { } } label: { DriveLabel(drive: drive, isInserted: vm.externalImageURL(for: drive) != nil) - }.disabled(vm.viewState.hasSaveState) + }.disabled(vm.hasSaveState) Spacer() // Disk image path, or (empty) Text(pathFor(drive)) diff --git a/Platform/Shared/VMToolbarModifier.swift b/Platform/Shared/VMToolbarModifier.swift index ca0e7aba2..9b5a68f1e 100644 --- a/Platform/Shared/VMToolbarModifier.swift +++ b/Platform/Shared/VMToolbarModifier.swift @@ -109,7 +109,7 @@ struct VMToolbarModifier: ViewModifier { Spacer() } #endif - if vm.viewState.hasSaveState || vm.state != .vmStopped { + if vm.hasSaveState || vm.state != .vmStopped { Button { confirmAction = .confirmStopVM } label: { @@ -138,7 +138,7 @@ struct VMToolbarModifier: ViewModifier { Label("Edit", systemImage: "slider.horizontal.3") .labelStyle(.iconOnly) }.help("Edit selected VM") - .disabled(vm.viewState.hasSaveState || vm.state != .vmStopped) + .disabled(vm.hasSaveState || vm.state != .vmStopped) .padding(.leading, padding) } } diff --git a/Platform/iOS/UTMDataExtension.swift b/Platform/iOS/UTMDataExtension.swift index 4d7de0773..24836fbc6 100644 --- a/Platform/iOS/UTMDataExtension.swift +++ b/Platform/iOS/UTMDataExtension.swift @@ -24,7 +24,7 @@ extension UTMData { } func stop(vm: UTMVirtualMachine) throws { - if vm.viewState.hasSaveState { + if vm.hasSaveState { vm.requestVmDeleteState() } } diff --git a/Platform/macOS/UTMDataExtension.swift b/Platform/macOS/UTMDataExtension.swift index 785775ce5..9acd46cc7 100644 --- a/Platform/macOS/UTMDataExtension.swift +++ b/Platform/macOS/UTMDataExtension.swift @@ -68,7 +68,7 @@ extension UTMData { } func stop(vm: UTMVirtualMachine) throws { - if vm.viewState.hasSaveState { + if vm.hasSaveState { vm.requestVmDeleteState() } vm.vmStop(force: false, completion: { _ in diff --git a/Platform/macOS/VMAppleRemovableDrivesView.swift b/Platform/macOS/VMAppleRemovableDrivesView.swift index dd225bc90..70a095f34 100644 --- a/Platform/macOS/VMAppleRemovableDrivesView.swift +++ b/Platform/macOS/VMAppleRemovableDrivesView.swift @@ -68,7 +68,7 @@ struct VMAppleRemovableDrivesView: View { } } label: { Label("Shared Directory", systemImage: hasSharedDir ? "externaldrive.fill.badge.person.crop" : "externaldrive.badge.person.crop") - }.disabled(vm.viewState.hasSaveState) + } Spacer() FilePath(url: sharedDirectory.directoryURL) } @@ -94,7 +94,7 @@ struct VMAppleRemovableDrivesView: View { } } label: { Label("External Drive", systemImage: "externaldrive") - }.disabled(vm.viewState.hasSaveState) + }.disabled(vm.hasSaveState || vm.state != .vmStopped) } else { Label("\(diskImage.sizeString) Drive", systemImage: "internaldrive") } From 4c10951c82fe33a2d34432aa766ae365a1aa1ae9 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 09:10:30 -0700 Subject: [PATCH 14/42] vm: remove legacy viewState code --- .../UTMLegacyViewState.h} | 0 .../UTMLegacyViewState.m} | 2 +- Managers/UTMDrive.h | 40 ----- Managers/UTMDrive.m | 31 ---- Managers/UTMQemuVirtualMachine+Drives.h | 31 ---- Managers/UTMQemuVirtualMachine+Drives.m | 149 ------------------ Managers/UTMQemuVirtualMachine+SPICE.h | 4 - Managers/UTMQemuVirtualMachine+SPICE.m | 81 ---------- Managers/UTMQemuVirtualMachine.m | 2 - Managers/UTMSpiceIO.m | 1 - Managers/UTMVirtualMachine-Private.h | 21 --- Managers/UTMVirtualMachine.h | 2 - Managers/UTMVirtualMachine.m | 66 -------- Managers/UTMVirtualMachineExtension.swift | 45 ------ Platform/Swift-Bridging-Header.h | 4 +- UTM.xcodeproj/project.pbxproj | 40 ++--- 16 files changed, 12 insertions(+), 507 deletions(-) rename Configuration/{UTMViewState.h => Legacy/UTMLegacyViewState.h} (100%) rename Configuration/{UTMViewState.m => Legacy/UTMLegacyViewState.m} (99%) delete mode 100644 Managers/UTMDrive.h delete mode 100644 Managers/UTMDrive.m delete mode 100644 Managers/UTMQemuVirtualMachine+Drives.h delete mode 100644 Managers/UTMQemuVirtualMachine+Drives.m diff --git a/Configuration/UTMViewState.h b/Configuration/Legacy/UTMLegacyViewState.h similarity index 100% rename from Configuration/UTMViewState.h rename to Configuration/Legacy/UTMLegacyViewState.h diff --git a/Configuration/UTMViewState.m b/Configuration/Legacy/UTMLegacyViewState.m similarity index 99% rename from Configuration/UTMViewState.m rename to Configuration/Legacy/UTMLegacyViewState.m index b0990415c..eba91d5e9 100644 --- a/Configuration/UTMViewState.m +++ b/Configuration/Legacy/UTMLegacyViewState.m @@ -14,7 +14,7 @@ // limitations under the License. // -#import "UTMViewState.h" +#import "UTMLegacyViewState.h" #import "UTM-Swift.h" const NSString *const kUTMViewStateDisplayScaleKey = @"DisplayScale"; diff --git a/Managers/UTMDrive.h b/Managers/UTMDrive.h deleted file mode 100644 index b78fc77ae..000000000 --- a/Managers/UTMDrive.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright © 2020 osy. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import "UTMLegacyQemuConfiguration+Drives.h" - -typedef NS_ENUM(NSInteger, UTMDriveStatus) { - UTMDriveStatusFixed, - UTMDriveStatusEjected, - UTMDriveStatusInserted -}; - -NS_ASSUME_NONNULL_BEGIN - -@interface UTMDrive : NSObject - -@property (nonatomic) NSInteger index; -@property (nonatomic, nullable, copy) NSString *name; -@property (nonatomic) UTMDiskImageType imageType; -@property (nonatomic, nullable, copy) NSString *interface; -@property (nonatomic) UTMDriveStatus status; -@property (nonatomic, nullable, copy) NSString *path; -@property (nonatomic, readonly) NSString *label; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Managers/UTMDrive.m b/Managers/UTMDrive.m deleted file mode 100644 index 41ebbabbe..000000000 --- a/Managers/UTMDrive.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright © 2020 osy. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "UTMDrive.h" -#import "UTMLegacyQemuConfiguration+Constants.h" - -@implementation UTMDrive - -- (NSString *)label { - NSString *imageTypeStr = [UTMLegacyQemuConfiguration supportedImageTypesPretty][self.imageType]; - NSString *filename = self.path.lastPathComponent; - if (!filename) { - filename = NSLocalizedString(@"none", @"UTMDrive"); - } - return [NSString localizedStringWithFormat:NSLocalizedString(@"%@ (%@): %@", @"UTMDrive"), imageTypeStr, self.interface, filename]; -} - -@end diff --git a/Managers/UTMQemuVirtualMachine+Drives.h b/Managers/UTMQemuVirtualMachine+Drives.h deleted file mode 100644 index fdc2b16e2..000000000 --- a/Managers/UTMQemuVirtualMachine+Drives.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright © 2020 osy. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "UTMQemuVirtualMachine.h" - -@class UTMDrive; - -NS_ASSUME_NONNULL_BEGIN - -@interface UTMQemuVirtualMachine (Drives) - -- (BOOL)ejectDrive:(UTMDrive *)drive force:(BOOL)force error:(NSError * _Nullable *)error; -- (BOOL)changeMediumForDrive:(UTMDrive *)drive url:(NSURL *)url error:(NSError * _Nullable *)error; -- (BOOL)restoreRemovableDrivesFromBookmarksWithError:(NSError * _Nullable __autoreleasing *)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Managers/UTMQemuVirtualMachine+Drives.m b/Managers/UTMQemuVirtualMachine+Drives.m deleted file mode 100644 index 64ec693df..000000000 --- a/Managers/UTMQemuVirtualMachine+Drives.m +++ /dev/null @@ -1,149 +0,0 @@ -// -// Copyright © 2020 osy. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "UTMQemuVirtualMachine+Drives.h" -#import "UTMLogging.h" -#import "UTMViewState.h" -#import "UTMDrive.h" -#import "UTMQemu.h" -#import "UTMQemuManager+BlockDevices.h" -#import "UTM-Swift.h" - -extern NSString *const kUTMErrorDomain; - -@interface UTMQemuVirtualMachine () - -@property (nonatomic, readonly, nullable) UTMQemuManager *qemu; -@property (nonatomic, readonly, nullable) UTMQemu *system; - -- (void)saveViewState; - -@end - -@implementation UTMQemuVirtualMachine (Drives) - -- (BOOL)ejectDrive:(UTMDrive *)drive force:(BOOL)force error:(NSError * _Nullable __autoreleasing *)error { - NSString *oldPath = [self.viewState pathForRemovableDrive:drive.name]; - [self.viewState removeBookmarkForRemovableDrive:drive.name]; - [self saveViewState]; - [self.system stopAccessingPath:oldPath]; - if (!self.qemu.isConnected) { - return YES; // not running - } - return [self.qemu ejectDrive:[NSString stringWithFormat:@"drive%@", drive.name] force:force error:error]; -} - -- (BOOL)changeMediumForDrive:(UTMDrive *)drive url:(NSURL *)url error:(NSError * _Nullable __autoreleasing *)error { - [url startAccessingSecurityScopedResource]; - NSData *bookmark = [url bookmarkDataWithOptions:0 - includingResourceValuesForKeys:nil - relativeToURL:nil - error:error]; - [url stopAccessingSecurityScopedResource]; - if (!bookmark) { - if (error) { - *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed create bookmark.", "UTMVirtualMachine+Drives")}]; - } - return NO; - } - if (!self.qemu.isConnected) { - // On macOS, the `bookmark` will not work for the helper process. - // This is caught and addressed in `restoreRemovableDrivesFromBookmarksWithError`. - [self.viewState setBookmark:bookmark path:url.path forRemovableDrive:drive.name persistent:YES]; - [self saveViewState]; - return YES; // not ready yet - } else { - [self.viewState setBookmark:bookmark path:url.path forRemovableDrive:drive.name persistent:NO]; - NSString *oldPath = [self.viewState pathForRemovableDrive:drive.name]; - if (oldPath) { - [self.system stopAccessingPath:oldPath]; - } - return [self changeMediumForDriveInternal:drive bookmark:bookmark securityScoped:NO error:error]; - } -} - -- (BOOL)changeMediumForDriveInternal:(UTMDrive *)drive bookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped error:(NSError * _Nullable __autoreleasing *)error { - __block BOOL ret = NO; - __block NSError *qemuError = nil; - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - [self.system accessDataWithBookmark:bookmark - securityScoped:securityScoped - completion:^(BOOL success, NSData *newBookmark, NSString *path) { - if (success) { - [self.viewState setBookmark:newBookmark path:path forRemovableDrive:drive.name persistent:YES]; - [self saveViewState]; - success = [self.qemu changeMediumForDrive:[NSString stringWithFormat:@"drive%@", drive.name] path:path error:&qemuError]; - } else { - qemuError = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to access drive image path.", "UTMVirtualMachine+Drives")}]; - } - ret = success; - dispatch_semaphore_signal(sema); - }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - *error = qemuError; - return ret; -} - -- (BOOL)restoreRemovableDrivesFromBookmarksWithError:(NSError * _Nullable __autoreleasing *)error { - NSArray *drives = self.drives; - BOOL ret = YES; - BOOL viewStateChanged = NO; - for (UTMDrive *drive in drives) { - BOOL persistent = NO; - NSData *bookmark = [self.viewState bookmarkForRemovableDrive:drive.name persistent:&persistent]; - if (bookmark) { - UTMLog(@"found bookmark for %@", drive.name); - if (drive.status == UTMDriveStatusFixed) { - UTMLog(@"%@ is no longer removable, removing bookmark", drive.name); - [self.viewState removeBookmarkForRemovableDrive:drive.name]; - viewStateChanged = YES; - continue; - } - if (![self changeMediumForDriveInternal:drive bookmark:bookmark securityScoped:persistent error:error]) { -#if TARGET_OS_IPHONE - UTMLog(@"failed to change %@ image", drive.name); - // remove the bad bookmark - [self.viewState removeBookmarkForRemovableDrive:drive.name]; - viewStateChanged = YES; - ret = NO; - break; -#else - // On macOS, at this point it is possible that a disk image was chosen while the VM was powered down. - // This results in an invalid bookmark so we need to re-run changeMediumForDrive. - // If the viewState contains the path for the drive, we can use that to create a new bookmark. - NSString* path = [self.viewState pathForRemovableDrive:drive.name]; - if (path) { - NSURL* url = [NSURL fileURLWithPath:path]; - if (![self changeMediumForDrive:drive url:url error:error]) { - UTMLog(@"failed to change %@ image to %@", drive.name, path); - // remove the bad bookmark - [self.viewState removeBookmarkForRemovableDrive:drive.name]; - viewStateChanged = YES; - ret = NO; - break; - } - } -#endif - } - } - } - if (viewStateChanged) { - [self saveViewState]; - } - return ret; -} - -@end diff --git a/Managers/UTMQemuVirtualMachine+SPICE.h b/Managers/UTMQemuVirtualMachine+SPICE.h index 9ca22c7cb..d055224c1 100644 --- a/Managers/UTMQemuVirtualMachine+SPICE.h +++ b/Managers/UTMQemuVirtualMachine+SPICE.h @@ -22,10 +22,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL hasUsbRedirection; -- (BOOL)changeSharedDirectory:(NSURL *)url error:(NSError * _Nullable *)error; -- (void)clearSharedDirectory; -- (BOOL)startSharedDirectoryWithError:(NSError * _Nullable __autoreleasing *)error; - - (void)requestInputTablet:(BOOL)tablet; @end diff --git a/Managers/UTMQemuVirtualMachine+SPICE.m b/Managers/UTMQemuVirtualMachine+SPICE.m index 8c029f825..9df5d8553 100644 --- a/Managers/UTMQemuVirtualMachine+SPICE.m +++ b/Managers/UTMQemuVirtualMachine+SPICE.m @@ -18,7 +18,6 @@ #import "UTMLogging.h" #import "UTMQemuManager.h" #import "UTMSpiceIO.h" -#import "UTMViewState.h" #import "UTMJailbreak.h" #import "UTM-Swift.h" #if defined(WITH_QEMU_TCI) @@ -38,90 +37,10 @@ @interface UTMQemuVirtualMachine () @property (nonatomic, readonly, nullable) UTMSpiceIO *ioService; @property (nonatomic) BOOL changeCursorRequestInProgress; -- (void)saveViewState; - @end @implementation UTMQemuVirtualMachine (SPICE) -#pragma mark - Shared Directory - -- (BOOL)saveSharedDirectory:(NSURL *)url error:(NSError * _Nullable __autoreleasing *)error { - [url startAccessingSecurityScopedResource]; - NSData *bookmark = [url bookmarkDataWithOptions:kUTMBookmarkCreationOptions - includingResourceValuesForKeys:nil - relativeToURL:nil - error:error]; - [url stopAccessingSecurityScopedResource]; - if (!bookmark) { - return NO; - } else { - self.viewState.sharedDirectory = bookmark; - self.viewState.sharedDirectoryPath = url.path; - [self saveViewState]; - return YES; - } -} - -- (BOOL)changeSharedDirectory:(NSURL *)url error:(NSError * _Nullable __autoreleasing *)error { - if (!self.ioService) { - // if we haven't started the VM yet, save the URL for when the VM starts - return [self saveSharedDirectory:url error:error]; - } - if (self.config.qemuHasWebdavSharing) { - [self.ioService changeSharedDirectory:url]; - } - return [self saveSharedDirectory:url error:error]; -} - -- (void)clearSharedDirectory { - self.viewState.sharedDirectory = nil; - self.viewState.sharedDirectoryPath = nil; - [self saveViewState]; -} - -- (BOOL)startSharedDirectoryWithError:(NSError * _Nullable __autoreleasing *)error { - if (!self.ioService) { - if (error) { - *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot start shared directory before SPICE starts.", "UTMVirtualMachine+Sharing")}]; - } - return NO; - } - if (!self.config.qemuHasWebdavSharing) { - return YES; - } - - NSData *bookmark = nil; - if (self.viewState.sharedDirectory) { - UTMLog(@"found shared directory bookmark"); - bookmark = self.viewState.sharedDirectory; - } - if (bookmark) { - BOOL stale; - NSURL *shareURL = [NSURL URLByResolvingBookmarkData:bookmark - options:kUTMBookmarkResolutionOptions - relativeToURL:nil - bookmarkDataIsStale:&stale - error:error]; - if (shareURL) { - BOOL success = YES; - if (stale) { - UTMLog(@"stale bookmark, attempting to recreate"); - success = [self saveSharedDirectory:shareURL error:error]; - } - if (success) { - [self.ioService changeSharedDirectory:shareURL]; - } - return success; - } else { - // clear the broken bookmark - [self clearSharedDirectory]; - return NO; - } - } - return YES; -} - #pragma mark - Input device switching - (void)requestInputTablet:(BOOL)tablet { diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index d10618405..d985abdcf 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -20,9 +20,7 @@ #import "UTMLoggingDelegate.h" #import "UTMQemuManagerDelegate.h" #import "UTMQemuVirtualMachine.h" -#import "UTMQemuVirtualMachine+Drives.h" #import "UTMQemuVirtualMachine+SPICE.h" -#import "UTMViewState.h" #import "UTMQemuManager.h" #import "UTMQemuSystem.h" #import "UTMSpiceIO.h" diff --git a/Managers/UTMSpiceIO.m b/Managers/UTMSpiceIO.m index fba6f3e2a..cea2f0528 100644 --- a/Managers/UTMSpiceIO.m +++ b/Managers/UTMSpiceIO.m @@ -18,7 +18,6 @@ #import "UTMSpiceIO.h" #import "UTMQemuManager.h" #import "UTMLogging.h" -#import "UTMViewState.h" #import "UTM-Swift.h" const int kMaxSpiceStartAttempts = 15; // qemu needs to start spice server first diff --git a/Managers/UTMVirtualMachine-Private.h b/Managers/UTMVirtualMachine-Private.h index c91b63701..0135baa95 100644 --- a/Managers/UTMVirtualMachine-Private.h +++ b/Managers/UTMVirtualMachine-Private.h @@ -20,8 +20,6 @@ NS_ASSUME_NONNULL_BEGIN @interface UTMVirtualMachine () -@property (nonatomic, readwrite) UTMViewState *viewState; - @property (nonatomic, readwrite, nullable) NSData *bookmark; /// Reference to logger for VM stdout/stderr @@ -32,25 +30,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL; -/// Load a plist into a NSDictionary representation -/// @param path Path to plist -/// @param err Error thrown if failed -/// @returns A dictionary on success, nil on failure and `err` contains the thrown error -- (NSDictionary *)loadPlist:(NSURL *)path withError:(NSError **)err; - -/// Saves a plist to disk -/// @param path Path to save to -/// @param dict Dictionary to convert to plist -/// @param err Error thrown if failed -/// @returns true if successful, otherwise `err` contains the thrown error -- (BOOL)savePlist:(NSURL *)path dict:(NSDictionary *)dict withError:(NSError **)err; - -/// (Re)loads the view state from disk -- (void)loadViewState; - -/// Saves the current view state to disk -- (void)saveViewState; - @end NS_ASSUME_NONNULL_END diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index 2c06972de..a1db94a42 100644 --- a/Managers/UTMVirtualMachine.h +++ b/Managers/UTMVirtualMachine.h @@ -19,7 +19,6 @@ @class UTMConfigurationWrapper; @class UTMLogging; -@class UTMViewState; @class UTMRegistryEntry; @class CSScreenshot; @@ -53,7 +52,6 @@ NS_ASSUME_NONNULL_BEGIN /// /// This includes display size, bookmarks to removable drives, etc. /// This property is observable and must only be accessed on the main thread. -@property (nonatomic, readonly) UTMViewState *viewState; @property (nonatomic, readonly) UTMRegistryEntry *registryEntry; /// Current VM state, can observe this property for state changes or use the delegate diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index 0b0d1ddd7..d4dff4db9 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -19,7 +19,6 @@ #import "UTMVirtualMachine-Private.h" #import "UTMQemuVirtualMachine.h" #import "UTMLogging.h" -#import "UTMViewState.h" #import "UTM-Swift.h" #if defined(WITH_QEMU_TCI) @import CocoaSpiceNoUsb; @@ -69,12 +68,6 @@ - (void)setScreenshot:(CSScreenshot *)screenshot { _screenshot = screenshot; } -- (void)setViewState:(UTMViewState *)viewState { - [self propertyWillChange]; - _viewState = viewState; - self.anyCancellable = [self subscribeToConfiguration]; -} - - (void)setConfig:(UTMConfigurationWrapper *)config { [self propertyWillChange]; _config = config; @@ -206,7 +199,6 @@ - (instancetype)init { #else self.logging = [UTMLogging new]; #endif - _viewState = [[UTMViewState alloc] init]; } return self; } @@ -217,7 +209,6 @@ - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration p self.config = configuration; self.path = packageURL; self.registryEntry = [UTMRegistry.shared entryFor:self]; - [self loadViewState]; [self loadScreenshot]; self.state = kVMStopped; } @@ -393,63 +384,6 @@ - (void)vmResumeWithCompletion:(void (^)(NSError * _Nullable))completion { notImplemented; } -#pragma mark - Plist Handling - -- (NSDictionary *)loadPlist:(NSURL *)path withError:(NSError **)err { - NSData *data = [NSData dataWithContentsOfURL:path]; - if (!data) { - if (err) { - *err = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to load plist", @"UTMVirtualMachine")}]; - } - return nil; - } - id plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:err]; - if (!plist) { - return nil; - } - if (![plist isKindOfClass:[NSDictionary class]]) { - if (err) { - *err = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Config format incorrect.", @"UTMVirtualMachine")}]; - } - return nil; - } - return plist; -} - -- (BOOL)savePlist:(NSURL *)path dict:(NSDictionary *)dict withError:(NSError **)err { - NSError *_err; - // serialize plist - NSData *data = [NSPropertyListSerialization dataWithPropertyList:dict format:NSPropertyListXMLFormat_v1_0 options:0 error:&_err]; - if (_err && err) { - *err = _err; - return NO; - } - // write plist - [data writeToURL:path options:NSDataWritingAtomic error:&_err]; - if (_err && err) { - *err = _err; - return NO; - } - return YES; -} - -#pragma mark - View State - -- (void)loadViewState { - NSDictionary *plist = [self loadPlist:[self.path URLByAppendingPathComponent:kUTMBundleViewFilename] withError:nil]; - if (plist) { - self.viewState = [[UTMViewState alloc] initWithDictionary:plist]; - } else { - self.viewState = [[UTMViewState alloc] init]; - } -} - -- (void)saveViewState { - [self savePlist:[self.path URLByAppendingPathComponent:kUTMBundleViewFilename] - dict:self.viewState.dictRepresentation - withError:nil]; -} - #pragma mark - Screenshot - (BOOL)isScreenshotSaveEnabled { diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index b34346489..22db59df3 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -42,9 +42,6 @@ extension UTMVirtualMachine: ObservableObject { fileprivate static let gibInMib = 1024 func subscribeToConfiguration() -> [AnyObject] { var s: [AnyObject] = [] - s.append(viewState.objectWillChange.sink { [weak self] in - self?.objectWillChange.send() - }) if let config = config.qemuConfig { s.append(config.objectWillChange.sink { [weak self] in self?.objectWillChange.send() @@ -152,42 +149,6 @@ public extension UTMQemuVirtualMachine { return UTMQemuVirtualMachine.isSupported(systemArchitecture: config.qemuConfig!.system.architecture) } - @objc var drives: [UTMDrive] { - var drives: [UTMDrive] = [] - for i in 0.. UTMDrive { - let qemuDrive = config.qemuConfig!.drives[index] - let drive = UTMDrive() - drive.index = index - switch qemuDrive.imageType { - case .disk: drive.imageType = .disk - case .cd: drive.imageType = .CD - default: drive.imageType = .none // skip other types - } - drive.interface = qemuDrive.interface.rawValue - drive.name = qemuDrive.id - if qemuDrive.isExternal { - // removable drive -> path stored only in viewState - if let path = viewState.path(forRemovableDrive: qemuDrive.id) { - drive.status = .inserted - drive.path = path - } else { - drive.status = .ejected - drive.path = nil - } - } else { - // fixed drive -> path stored in configuration - drive.status = .fixed - drive.path = qemuDrive.imageURL?.lastPathComponent - } - return drive - } - /// Sets up values in VM configuration corrosponding to per-device data like sharing path @objc func prepareConfigurationForStart() { if config.qemuConfig!.sharing.directoryShareMode != .none { @@ -235,9 +196,3 @@ extension URL { bookmarkDataIsStale: &stale) } } - -extension UTMDrive: Identifiable { - public var id: Int { - self.index - } -} diff --git a/Platform/Swift-Bridging-Header.h b/Platform/Swift-Bridging-Header.h index 91448f283..02fe9436c 100644 --- a/Platform/Swift-Bridging-Header.h +++ b/Platform/Swift-Bridging-Header.h @@ -25,7 +25,6 @@ #include "UTMLegacyQemuConfiguration+Sharing.h" #include "UTMLegacyQemuConfiguration+System.h" #include "UTMLegacyQemuConfigurationPortForward.h" -#include "UTMDrive.h" #include "UTMQcow2.h" #include "UTMQemu.h" #include "UTMQemuManager.h" @@ -33,12 +32,11 @@ #include "UTMQemuSystem.h" #include "UTMJailbreak.h" #include "UTMLogging.h" -#include "UTMViewState.h" +#include "UTMLegacyViewState.h" #include "UTMVirtualMachine.h" #include "UTMVirtualMachine-Protected.h" #include "UTMQemuVirtualMachine.h" #include "UTMQemuVirtualMachine-Protected.h" -#include "UTMQemuVirtualMachine+Drives.h" #include "UTMQemuVirtualMachine+SPICE.h" #include "UTMSpiceIO.h" #if TARGET_OS_IPHONE diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 4e2680d29..5c090200c 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -300,7 +300,7 @@ CE0B6CF524AD568400FE012D /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */; }; CE0B6CF624AD568400FE012D /* UTMLegacyQemuConfiguration+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425302437C09C00E520F7 /* UTMLegacyQemuConfiguration+Drives.m */; }; CE0B6CF724AD568400FE012D /* UTMLegacyQemuConfiguration+Display.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */; }; - CE0B6CF824AD568400FE012D /* UTMViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMViewState.m */; }; + CE0B6CF824AD568400FE012D /* UTMLegacyViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */; }; CE0B6CF924AD568400FE012D /* UTMLegacyQemuConfiguration+Sharing.m in Sources */ = {isa = PBXBuildFile; fileRef = CE059DC4243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.m */; }; CE0B6CFA24AD568400FE012D /* UTMLegacyQemuConfiguration+System.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425332437C22A00E520F7 /* UTMLegacyQemuConfiguration+System.m */; }; CE0B6CFB24AD568400FE012D /* UTMLegacyQemuConfiguration+Networking.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA02A982436C7A30087E45F /* UTMLegacyQemuConfiguration+Networking.m */; }; @@ -561,7 +561,7 @@ CE2D929724AD46670059923A /* qapi-commands-error.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0FC23FCEC05001177D6 /* qapi-commands-error.c */; }; CE2D929924AD46670059923A /* UTMJSONStream.m in Sources */ = {isa = PBXBuildFile; fileRef = CE36B26922763F28004A1435 /* UTMJSONStream.m */; }; CE2D929A24AD46670059923A /* qapi-commands-sockets.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0BD23FCEC02001177D6 /* qapi-commands-sockets.c */; }; - CE2D929C24AD46670059923A /* UTMViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMViewState.m */; }; + CE2D929C24AD46670059923A /* UTMLegacyViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */; }; CE2D92A024AD46670059923A /* VMDisplayMetalViewController+Touch.m in Sources */ = {isa = PBXBuildFile; fileRef = CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */; }; CE2D92A124AD46670059923A /* UTMLegacyQemuConfiguration+Display.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */; }; CE2D92A224AD46670059923A /* qapi-visit-block-core.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0E023FCEC04001177D6 /* qapi-visit-block-core.c */; }; @@ -905,7 +905,7 @@ CEA45E60263519B5002FA97D /* UTMJSONStream.m in Sources */ = {isa = PBXBuildFile; fileRef = CE36B26922763F28004A1435 /* UTMJSONStream.m */; }; CEA45E61263519B5002FA97D /* VMConfigNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955024AD4F980059923A /* VMConfigNetworkView.swift */; }; CEA45E62263519B5002FA97D /* qapi-commands-sockets.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0BD23FCEC02001177D6 /* qapi-commands-sockets.c */; }; - CEA45E63263519B5002FA97D /* UTMViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMViewState.m */; }; + CEA45E63263519B5002FA97D /* UTMLegacyViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */; }; CEA45E64263519B5002FA97D /* UTMLoggingSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */; }; CEA45E66263519B5002FA97D /* qapi-visit-acpi.c in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EB032572E166000E2EBB /* qapi-visit-acpi.c */; }; CEA45E69263519B5002FA97D /* VMConfigQEMUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953924AD4F980059923A /* VMConfigQEMUView.swift */; }; @@ -932,7 +932,6 @@ CEA45E86263519B5002FA97D /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; CEA45E87263519B5002FA97D /* qapi-commands-tpm.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0BF23FCEC02001177D6 /* qapi-commands-tpm.c */; }; CEA45E88263519B5002FA97D /* qapi-visit-common.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C10223FCEC05001177D6 /* qapi-visit-common.c */; }; - CEA45E89263519B5002FA97D /* UTMQemuVirtualMachine+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */; }; CEA45E8A263519B5002FA97D /* qapi-events-trace.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C08923FCEBFF001177D6 /* qapi-events-trace.c */; }; CEA45E8B263519B5002FA97D /* UTMQcow2.c in Sources */ = {isa = PBXBuildFile; fileRef = 2C6D9E132571AFE5003298E6 /* UTMQcow2.c */; }; CEA45E8C263519B5002FA97D /* qapi-types-block-export.c in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EAFC2572E14C000E2EBB /* qapi-types-block-export.c */; }; @@ -955,7 +954,6 @@ CEA45EA3263519B5002FA97D /* VMConfigSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954724AD4F980059923A /* VMConfigSharingView.swift */; }; CEA45EA4263519B5002FA97D /* qapi-commands-misc-target.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C13C23FCEC08001177D6 /* qapi-commands-misc-target.c */; }; CEA45EA5263519B5002FA97D /* VMConfigInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954824AD4F980059923A /* VMConfigInputView.swift */; }; - CEA45EA6263519B5002FA97D /* UTMDrive.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EC124F9C9E100557D15 /* UTMDrive.m */; }; CEA45EA7263519B5002FA97D /* qapi-events-block-export.c in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EAEC2572E0C2000E2EBB /* qapi-events-block-export.c */; }; CEA45EA8263519B5002FA97D /* VMDisplayMetalViewController+Gamepad.m in Sources */ = {isa = PBXBuildFile; fileRef = 5286EC8F2437488E007E6CBC /* VMDisplayMetalViewController+Gamepad.m */; }; CEA45EA9263519B5002FA97D /* qapi-visit-introspect.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0E223FCEC04001177D6 /* qapi-visit-introspect.c */; }; @@ -1250,10 +1248,6 @@ CEF78EF026B9B7910022CAF4 /* virglrenderer.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A126AF5F0F008594E5 /* virglrenderer.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEF83EBA24F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EB924F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m */; }; CEF83EBB24F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EB924F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m */; }; - CEF83EBE24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */; }; - CEF83EBF24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */; }; - CEF83EC224F9C9E100557D15 /* UTMDrive.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EC124F9C9E100557D15 /* UTMDrive.m */; }; - CEF83EC324F9C9E100557D15 /* UTMDrive.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EC124F9C9E100557D15 /* UTMDrive.m */; }; CEF83EC724FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EC624FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m */; }; CEF83EC824FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF83EC624FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m */; }; CEF83F262500901300557D15 /* qemu in Resources */ = {isa = PBXBuildFile; fileRef = CE9D18F72265410E00355E14 /* qemu */; }; @@ -2094,8 +2088,8 @@ CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = ""; }; CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfirmActionModifier.swift; sourceTree = ""; }; - CE6EDCDD241C4A6800A719DC /* UTMViewState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMViewState.h; sourceTree = ""; }; - CE6EDCDE241C4A6800A719DC /* UTMViewState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMViewState.m; sourceTree = ""; }; + CE6EDCDD241C4A6800A719DC /* UTMLegacyViewState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyViewState.h; sourceTree = ""; }; + CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyViewState.m; sourceTree = ""; }; CE6EDCE0241DA0E900A719DC /* UTMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLogging.h; sourceTree = ""; }; CE6EDCE1241DA0E900A719DC /* UTMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLogging.m; sourceTree = ""; }; CE72B4AB2463579D00716A11 /* VMDisplayViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMDisplayViewController.h; sourceTree = ""; }; @@ -2200,10 +2194,6 @@ CEF6F5EC26DDD65700BC434D /* QEMULauncher-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMULauncher-unsigned.entitlements"; sourceTree = ""; }; CEF83EB824F9ABEA00557D15 /* UTMQemuManager+BlockDevices.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMQemuManager+BlockDevices.h"; sourceTree = ""; }; CEF83EB924F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMQemuManager+BlockDevices.m"; sourceTree = ""; }; - CEF83EBC24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMQemuVirtualMachine+Drives.h"; sourceTree = ""; }; - CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMQemuVirtualMachine+Drives.m"; sourceTree = ""; }; - CEF83EC024F9C9E100557D15 /* UTMDrive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMDrive.h; sourceTree = ""; }; - CEF83EC124F9C9E100557D15 /* UTMDrive.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMDrive.m; sourceTree = ""; }; CEF83EC624FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMQemuVirtualMachine+SPICE.m"; sourceTree = ""; }; CEF83EC924FB1BB200557D15 /* UTMQemuVirtualMachine+SPICE.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMQemuVirtualMachine+SPICE.h"; sourceTree = ""; }; CEF84ADA2887D7D300578F41 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -2484,6 +2474,8 @@ CE5425332437C22A00E520F7 /* UTMLegacyQemuConfiguration+System.m */, CE54252C2436E48D00E520F7 /* UTMLegacyQemuConfigurationPortForward.h */, CE54252D2436E48D00E520F7 /* UTMLegacyQemuConfigurationPortForward.m */, + CE6EDCDD241C4A6800A719DC /* UTMLegacyViewState.h */, + CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */, ); path = Legacy; sourceTree = ""; @@ -2702,8 +2694,6 @@ isa = PBXGroup; children = ( 841619A42843150E000034B2 /* Legacy */, - CE6EDCDD241C4A6800A719DC /* UTMViewState.h */, - CE6EDCDE241C4A6800A719DC /* UTMViewState.m */, 848A98C3286F332D006F0550 /* UTMConfiguration.swift */, 841619A9284315F9000034B2 /* UTMConfigurationInfo.swift */, 843BF83728451B380029D60D /* UTMConfigurationTerminal.swift */, @@ -3029,8 +3019,6 @@ CE5F1659226138AB00F3D56B /* Managers */ = { isa = PBXGroup; children = ( - CEF83EC024F9C9E100557D15 /* UTMDrive.h */, - CEF83EC124F9C9E100557D15 /* UTMDrive.m */, CE36B26822763F28004A1435 /* UTMJSONStream.h */, CE36B26922763F28004A1435 /* UTMJSONStream.m */, CE36B27E227665B7004A1435 /* UTMJSONStreamDelegate.h */, @@ -3054,8 +3042,6 @@ 84FCABB8268CE05E0036196C /* UTMQemuVirtualMachine.h */, 84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */, 841E999628AC80CA003C6CB6 /* UTMQemuVirtualMachine-Protected.h */, - CEF83EBC24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.h */, - CEF83EBD24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m */, CEF83EC924FB1BB200557D15 /* UTMQemuVirtualMachine+SPICE.h */, CEF83EC624FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m */, CE2B89352262B2F600C6D9D8 /* UTMVirtualMachineDelegate.h */, @@ -3606,7 +3592,7 @@ CE2D929924AD46670059923A /* UTMJSONStream.m in Sources */, CE2D958324AD4F990059923A /* VMConfigNetworkView.swift in Sources */, CE2D929A24AD46670059923A /* qapi-commands-sockets.c in Sources */, - CE2D929C24AD46670059923A /* UTMViewState.m in Sources */, + CE2D929C24AD46670059923A /* UTMLegacyViewState.m in Sources */, CE020BAB24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */, CEF0306426A2AFDF00667B63 /* VMWizardOSLinuxView.swift in Sources */, CEBE820B26A4C8E0007AAB12 /* VMWizardSummaryView.swift in Sources */, @@ -3648,7 +3634,6 @@ CE2D92B524AD46670059923A /* qapi-commands-tpm.c in Sources */, CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */, CE2D92B724AD46670059923A /* qapi-visit-common.c in Sources */, - CEF83EBE24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m in Sources */, CE2D92B824AD46670059923A /* qapi-events-trace.c in Sources */, 84909A8927CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */, CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */, @@ -3679,7 +3664,6 @@ CE2D957324AD4F990059923A /* VMConfigSharingView.swift in Sources */, CE2D92C924AD46670059923A /* qapi-commands-misc-target.c in Sources */, CE2D957524AD4F990059923A /* VMConfigInputView.swift in Sources */, - CEF83EC224F9C9E100557D15 /* UTMDrive.m in Sources */, CEF0305B26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */, 84C60FB72681A41B00B58C00 /* VMToolbarView.swift in Sources */, 2CE8EAEE2572E0C2000E2EBB /* qapi-events-block-export.c in Sources */, @@ -3874,7 +3858,6 @@ CE0B6D7E24AD584D00FE012D /* qapi-events-misc.c in Sources */, 848D99AA285DB5550055C215 /* VMConfigConstantPicker.swift in Sources */, CE0B6D4124AD584C00FE012D /* qapi-visit-transaction.c in Sources */, - CEF83EBF24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m in Sources */, CE0B6D0B24AD56C300FE012D /* qapi-dealloc-visitor.c in Sources */, 848A98C2286A2257006F0550 /* UTMAppleConfigurationMacPlatform.swift in Sources */, 84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */, @@ -3885,7 +3868,6 @@ CE0B6D7C24AD584D00FE012D /* qapi-visit-char.c in Sources */, CE9375A224BBDDD10074066F /* VMConfigDriveDetailsView.swift in Sources */, CE2D955824AD4F980059923A /* VMConfigDisplayView.swift in Sources */, - CEF83EC324F9C9E100557D15 /* UTMDrive.m in Sources */, CE0B6D5724AD584C00FE012D /* qapi-types-net.c in Sources */, CE03D05324D90B4E00F76B84 /* UTMQemuSystem.m in Sources */, 841619B82843226B000034B2 /* QEMUConstant.swift in Sources */, @@ -4051,7 +4033,7 @@ CE0B6D7824AD584D00FE012D /* qapi-visit-introspect.c in Sources */, CE0B6D6024AD584D00FE012D /* qapi-types-sockets.c in Sources */, 843BF82E284482C10029D60D /* UTMQemuConfigurationInput.swift in Sources */, - CE0B6CF824AD568400FE012D /* UTMViewState.m in Sources */, + CE0B6CF824AD568400FE012D /* UTMLegacyViewState.m in Sources */, 84C584E1268E95B3000FCABF /* UTMLegacyAppleConfiguration.swift in Sources */, 8401FDA6269D44E400265F0D /* VMConfigDisplayConsoleView.swift in Sources */, CE0B6D1524AD57FC00FE012D /* qapi-commands-misc-target.c in Sources */, @@ -4193,7 +4175,7 @@ CEA45E61263519B5002FA97D /* VMConfigNetworkView.swift in Sources */, CEA45E62263519B5002FA97D /* qapi-commands-sockets.c in Sources */, 84018698288B71BF0050AC51 /* BusyIndicator.swift in Sources */, - CEA45E63263519B5002FA97D /* UTMViewState.m in Sources */, + CEA45E63263519B5002FA97D /* UTMLegacyViewState.m in Sources */, CEA45E64263519B5002FA97D /* UTMLoggingSwift.swift in Sources */, 841619A7284315C1000034B2 /* UTMQemuConfiguration.swift in Sources */, CEA45E66263519B5002FA97D /* qapi-visit-acpi.c in Sources */, @@ -4231,7 +4213,6 @@ CEA45E86263519B5002FA97D /* VMConfigDriveCreateView.swift in Sources */, CEA45E87263519B5002FA97D /* qapi-commands-tpm.c in Sources */, CEA45E88263519B5002FA97D /* qapi-visit-common.c in Sources */, - CEA45E89263519B5002FA97D /* UTMQemuVirtualMachine+Drives.m in Sources */, CEA45E8A263519B5002FA97D /* qapi-events-trace.c in Sources */, CEA45E8B263519B5002FA97D /* UTMQcow2.c in Sources */, 84018684288A3B2E0050AC51 /* VMWindowView.swift in Sources */, @@ -4263,7 +4244,6 @@ CEA45EA3263519B5002FA97D /* VMConfigSharingView.swift in Sources */, CEA45EA4263519B5002FA97D /* qapi-commands-misc-target.c in Sources */, CEA45EA5263519B5002FA97D /* VMConfigInputView.swift in Sources */, - CEA45EA6263519B5002FA97D /* UTMDrive.m in Sources */, 841E58CF28937FED00137A20 /* UTMMainView.swift in Sources */, CEA45EA7263519B5002FA97D /* qapi-events-block-export.c in Sources */, CEA45EA8263519B5002FA97D /* VMDisplayMetalViewController+Gamepad.m in Sources */, From 6a7e26898b6d4e83a39f1f499b00308236acf3fd Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 21 Aug 2022 09:20:48 -0700 Subject: [PATCH 15/42] viewstate: remove mutating code --- Configuration/Legacy/UTMLegacyViewState.h | 28 ++--- Configuration/Legacy/UTMLegacyViewState.m | 137 +++------------------- Managers/UTMVirtualMachineExtension.swift | 8 -- 3 files changed, 28 insertions(+), 145 deletions(-) diff --git a/Configuration/Legacy/UTMLegacyViewState.h b/Configuration/Legacy/UTMLegacyViewState.h index 67ea726d1..9d05f80bd 100644 --- a/Configuration/Legacy/UTMLegacyViewState.h +++ b/Configuration/Legacy/UTMLegacyViewState.h @@ -18,25 +18,25 @@ NS_ASSUME_NONNULL_BEGIN -@interface UTMViewState : NSObject +@interface UTMLegacyViewState : NSObject @property (nonatomic, weak, readonly) NSDictionary *dictRepresentation; -@property (nonatomic, assign) double displayScale; -@property (nonatomic, assign) double displayOriginX; -@property (nonatomic, assign) double displayOriginY; -@property (nonatomic, assign) BOOL hasSaveState; -@property (nonatomic, copy, nullable) NSData *sharedDirectory; -@property (nonatomic, copy, nullable) NSString *sharedDirectoryPath; -@property (nonatomic, copy, nullable) NSData *shortcutBookmark; -@property (nonatomic, copy, nullable) NSString *shortcutBookmarkPath; - -- (instancetype)init NS_DESIGNATED_INITIALIZER; +@property (nonatomic, readonly) CGFloat displayScale; +@property (nonatomic, readonly) CGFloat displayOriginX; +@property (nonatomic, readonly) CGFloat displayOriginY; +@property (nonatomic, readonly) BOOL isKeyboardShown; +@property (nonatomic, readonly) BOOL isToolbarShown; +@property (nonatomic, readonly) BOOL hasSaveState; +@property (nonatomic, readonly, nullable) NSData *sharedDirectory; +@property (nonatomic, readonly, nullable) NSString *sharedDirectoryPath; +@property (nonatomic, readonly, nullable) NSData *shortcutBookmark; +@property (nonatomic, readonly, nullable) NSString *shortcutBookmarkPath; + +- (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER; -- (void)setBookmark:(NSData *)bookmark path:(NSString *)path forRemovableDrive:(NSString *)drive persistent:(BOOL)persistent; -- (void)removeBookmarkForRemovableDrive:(NSString *)drive; -- (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive persistent:(out BOOL *)persistent; +- (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive; - (nullable NSString *)pathForRemovableDrive:(NSString *)drive; @end diff --git a/Configuration/Legacy/UTMLegacyViewState.m b/Configuration/Legacy/UTMLegacyViewState.m index eba91d5e9..e36c3acfc 100644 --- a/Configuration/Legacy/UTMLegacyViewState.m +++ b/Configuration/Legacy/UTMLegacyViewState.m @@ -15,7 +15,6 @@ // #import "UTMLegacyViewState.h" -#import "UTM-Swift.h" const NSString *const kUTMViewStateDisplayScaleKey = @"DisplayScale"; const NSString *const kUTMViewStateDisplayOriginXKey = @"DisplayOriginX"; @@ -30,18 +29,10 @@ const NSString *const kUTMViewStateRemovableDrivesKey = @"RemovableDrives"; const NSString *const kUTMViewStateRemovableDrivesPathKey = @"RemovableDrivesPath"; -@interface UTMViewState () - -@property (nonatomic, nullable) NSURL *path; - -@end - -@implementation UTMViewState { +@implementation UTMLegacyViewState { NSMutableDictionary *_rootDict; NSMutableDictionary *_removableDrives; - NSMutableDictionary *_removableDrivesTemp; NSMutableDictionary *_removableDrivesPath; - NSMutableDictionary *_removableDrivesPathTemp; } #pragma mark - Properties @@ -50,164 +41,64 @@ - (NSDictionary *)dictRepresentation { return (NSDictionary *)_rootDict; } -- (double)displayScale { - return [_rootDict[kUTMViewStateDisplayScaleKey] doubleValue]; +- (CGFloat)displayScale { + return [_rootDict[kUTMViewStateDisplayScaleKey] floatValue]; } -- (void)setDisplayScale:(double)displayScale { - [self propertyWillChange]; - _rootDict[kUTMViewStateDisplayScaleKey] = @(displayScale); +- (CGFloat)displayOriginX { + return [_rootDict[kUTMViewStateDisplayOriginXKey] floatValue]; } -- (double)displayOriginX { - return [_rootDict[kUTMViewStateDisplayOriginXKey] doubleValue]; +- (CGFloat)displayOriginY { + return [_rootDict[kUTMViewStateDisplayOriginYKey] floatValue]; } -- (void)setDisplayOriginX:(double)displayOriginX { - [self propertyWillChange]; - _rootDict[kUTMViewStateDisplayOriginXKey] = @(displayOriginX); +- (BOOL)isKeyboardShown { + return [_rootDict[kUTMViewStateShowToolbarKey] boolValue]; } -- (double)displayOriginY { - return [_rootDict[kUTMViewStateDisplayOriginYKey] doubleValue]; -} - -- (void)setDisplayOriginY:(double)displayOriginY { - [self propertyWillChange]; - _rootDict[kUTMViewStateDisplayOriginYKey] = @(displayOriginY); +- (BOOL)isToolbarShown { + return [_rootDict[kUTMViewStateShowToolbarKey] boolValue]; } - (BOOL)hasSaveState { return [_rootDict[kUTMViewStateSuspendedKey] boolValue]; } -- (void)setHasSaveState:(BOOL)hasSaveState { - [self propertyWillChange]; - _rootDict[kUTMViewStateSuspendedKey] = @(hasSaveState); -} - - (NSData *)sharedDirectory { return _rootDict[kUTMViewStateSharedDirectoryKey]; } -- (void)setSharedDirectory:(NSData *)sharedDirectory { - [self propertyWillChange]; - if (sharedDirectory) { - _rootDict[kUTMViewStateSharedDirectoryKey] = sharedDirectory; - } else { - [_rootDict removeObjectForKey:kUTMViewStateSharedDirectoryKey]; - } -} - - (NSString *)sharedDirectoryPath { return _rootDict[kUTMViewStateSharedDirectoryPathKey]; } -- (void)setSharedDirectoryPath:(NSString *)sharedDirectoryPath { - [self propertyWillChange]; - if (sharedDirectoryPath) { - _rootDict[kUTMViewStateSharedDirectoryPathKey] = sharedDirectoryPath; - } else { - [_rootDict removeObjectForKey:kUTMViewStateSharedDirectoryPathKey]; - } -} - - (NSData *)shortcutBookmark { return _rootDict[kUTMViewStateShortcutBookmarkKey]; } -- (void)setShortcutBookmark:(NSData *)shortcutBookmark { - [self propertyWillChange]; - if (shortcutBookmark) { - _rootDict[kUTMViewStateShortcutBookmarkKey] = shortcutBookmark; - } else { - [_rootDict removeObjectForKey:kUTMViewStateShortcutBookmarkKey]; - } -} - - (NSString *)shortcutBookmarkPath { return _rootDict[kUTMViewStateShortcutBookmarkPathKey]; } -- (void)setShortcutBookmarkPath:(NSString *)shortcutBookmarkPath { - [self propertyWillChange]; - if (shortcutBookmarkPath) { - _rootDict[kUTMViewStateShortcutBookmarkPathKey] = shortcutBookmarkPath; - } else { - [_rootDict removeObjectForKey:kUTMViewStateShortcutBookmarkPathKey]; - } -} - #pragma mark - Removable drives -- (void)setBookmark:(NSData *)bookmark path:(NSString *)path forRemovableDrive:(NSString *)drive persistent:(BOOL)persistent { - [self propertyWillChange]; - if (persistent) { - _removableDrives[drive] = bookmark; - _removableDrivesPath[drive] = path; - [_removableDrivesTemp removeObjectForKey:drive]; - [_removableDrivesPathTemp removeObjectForKey:drive]; - } else { - _removableDrivesTemp[drive] = bookmark; - _removableDrivesPathTemp[drive] = path; - } -} - -- (void)removeBookmarkForRemovableDrive:(NSString *)drive { - [self propertyWillChange]; - [_removableDrives removeObjectForKey:drive]; - [_removableDrivesTemp removeObjectForKey:drive]; - [_removableDrivesPath removeObjectForKey:drive]; - [_removableDrivesPathTemp removeObjectForKey:drive]; -} - -- (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive persistent:(out BOOL *)persistent { - NSData *temp = _removableDrivesTemp[drive]; - if (temp) { - *persistent = NO; - return temp; - } else { - *persistent = YES; - return _removableDrives[drive]; - } +- (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive { + return _removableDrives[drive]; } - (nullable NSString *)pathForRemovableDrive:(NSString *)drive { - NSString *temp = _removableDrivesPathTemp[drive]; - if (temp) { - return temp; - } else { - return _removableDrivesPath[drive]; - } + return _removableDrivesPath[drive]; } #pragma mark - Init -- (instancetype)init { - self = [super init]; - if (self) { - _rootDict = [NSMutableDictionary dictionary]; - _removableDrives = [NSMutableDictionary dictionary]; - _removableDrivesTemp = [NSMutableDictionary dictionary]; - _removableDrivesPath = [NSMutableDictionary dictionary]; - _removableDrivesPathTemp = [NSMutableDictionary dictionary]; - _rootDict[kUTMViewStateRemovableDrivesKey] = _removableDrives; - _rootDict[kUTMViewStateRemovableDrivesPathKey] = _removableDrivesPath; - self.displayScale = 1.0; - self.displayOriginX = 0; - self.displayOriginY = 0; - } - return self; -} - - (instancetype)initWithDictionary:(NSDictionary *)dictionary { self = [super init]; if (self) { _rootDict = CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (__bridge CFDictionaryRef)dictionary, kCFPropertyListMutableContainers)); _removableDrives = _rootDict[kUTMViewStateRemovableDrivesKey]; _removableDrivesPath = _rootDict[kUTMViewStateRemovableDrivesPathKey]; - _removableDrivesTemp = [NSMutableDictionary dictionary]; - _removableDrivesPathTemp = [NSMutableDictionary dictionary]; if (!_removableDrives) { _removableDrives = [NSMutableDictionary dictionary]; _rootDict[kUTMViewStateRemovableDrivesKey] = _removableDrives; diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 22db59df3..1e63c6d1b 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -30,14 +30,6 @@ extension UTMVirtualMachine: ObservableObject { } -@objc extension UTMViewState: ObservableObject { - func propertyWillChange() -> Void { - if #available(iOS 13, macOS 11, *) { - DispatchQueue.main.async { self.objectWillChange.send() } - } - } -} - @objc extension UTMVirtualMachine { fileprivate static let gibInMib = 1024 func subscribeToConfiguration() -> [AnyObject] { From 88709a9c40c23ed1de949ca0dcf2496a51ceef30 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 27 Aug 2022 17:09:44 -0700 Subject: [PATCH 16/42] registry: conform to MainActor Enforce actor usage with escape valve for Obj-C code. Sync config and registry and also support observing registry changes. --- Managers/UTMQemuVirtualMachine.m | 49 ++--- Managers/UTMQemuVirtualMachine.swift | 72 ++++--- Managers/UTMRegistry.swift | 58 +++++- Managers/UTMRegistryEntry.swift | 218 +++++++++++++++------- Managers/UTMVirtualMachine-Protected.h | 3 - Managers/UTMVirtualMachine.m | 21 +-- Managers/UTMVirtualMachineExtension.swift | 22 +-- Platform/Shared/VMDetailsView.swift | 5 - Platform/UTMData.swift | 20 +- 9 files changed, 296 insertions(+), 172 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index d985abdcf..0ba92bca7 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -133,7 +133,24 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { [self.logging logToFile:self.config.qemuDebugLogURL]; } - [self prepareConfigurationForStart]; + // set up SPICE sharing and removable drives + NSString *errMsg; + __block NSError *restoreExternalDrivesAndSharesError = nil; + dispatch_semaphore_t restoreExternalDrivesAndSharesEvent = dispatch_semaphore_create(0); + [self restoreExternalDrivesAndSharesWithCompletion:^(NSError *err) { + restoreExternalDrivesAndSharesError = err; + dispatch_semaphore_signal(restoreExternalDrivesAndSharesEvent); + }]; + if (dispatch_semaphore_wait(restoreExternalDrivesAndSharesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { + UTMLog(@"Timed out waiting for external drives and shares to be restored."); + completion([self errorGeneric]); + return; + } + if (restoreExternalDrivesAndSharesError) { + errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore external drives and shares: %@", @"UTMVirtualMachine"), restoreExternalDrivesAndSharesError.localizedDescription]; + completion([self errorWithMessage:errMsg]); + return; + } if (self.isRunningAsSnapshot) { self.config.qemuIsDisposable = self.isRunningAsSnapshot; @@ -218,7 +235,6 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { __block BOOL spiceConnectFailed = NO; // failure could have no error message __block NSError *spiceConnectError = nil; NSError *err; - NSString *errMsg; // start SPICE client [self.ioService connectWithCompletion:^(UTMQemuManager *manager, NSError *error) { if (manager) { // success @@ -259,23 +275,6 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { return; } assert(self.qemu.isConnected); - // set up SPICE sharing and removable drives - __block NSError *restoreExternalDrivesAndSharesError = nil; - dispatch_semaphore_t restoreExternalDrivesAndSharesEvent = dispatch_semaphore_create(0); - [self restoreExternalDrivesAndSharesWithCompletion:^(NSError *err) { - restoreExternalDrivesAndSharesError = err; - dispatch_semaphore_signal(restoreExternalDrivesAndSharesEvent); - }]; - if (dispatch_semaphore_wait(restoreExternalDrivesAndSharesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { - UTMLog(@"Timed out waiting for external drives and shares to be restored."); - completion([self errorGeneric]); - return; - } - if (restoreExternalDrivesAndSharesError) { - errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore external drives and shares: %@", @"UTMVirtualMachine"), restoreExternalDrivesAndSharesError.localizedDescription]; - completion([self errorWithMessage:errMsg]); - return; - } // continue VM boot if (![self.qemu continueBootWithError:&err]) { UTMLog(@"Failed to boot: %@", err); @@ -301,7 +300,9 @@ - (void)vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { [self changeState:kVMStarting]; [self _vmStartWithCompletion:^(NSError *err){ if (err) { // delete suspend state on error - self.registryEntry.hasSaveState = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + self.registryEntry.hasSaveState = YES; + }); [self changeState:kVMStopped]; } else { [self changeState:kVMStarted]; @@ -469,7 +470,9 @@ - (void)_vmSaveStateWithCompletion:(void (^)(NSError * _Nullable))completion { saveError = [self errorGeneric]; } else if (!saveError) { UTMLog(@"Save completed"); - self.registryEntry.hasSaveState = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + self.registryEntry.hasSaveState = YES; + }); [self saveScreenshot]; } completion(saveError); @@ -507,7 +510,9 @@ - (void)_vmDeleteStateWithCompletion:(void (^)(NSError * _Nullable))completion { UTMLog(@"Delete save completed"); } } // otherwise we mark as deleted - self.registryEntry.hasSaveState = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + self.registryEntry.hasSaveState = NO; + }); completion(deleteError); } diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 368f4ff3b..908a1758c 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -22,14 +22,19 @@ extension UTMQemuVirtualMachine { config.qemuConfig! } - func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) throws { + func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) async throws { guard drive.isExternal else { return } - if let oldPath = registryEntry.externalDrives[drive.id]?.path { + if let oldPath = await registryEntry.externalDrives[drive.id]?.path { system?.stopAccessingPath(oldPath) } - registryEntry.externalDrives.removeValue(forKey: drive.id) + for i in qemuConfig.drives.indices { + if qemuConfig.drives[i].id == drive.id { + qemuConfig.drives[i].imageURL = nil + } + } + await registryEntry.removeExternalDrive(forId: drive.id) guard let qemu = qemu, qemu.isConnected else { return } @@ -42,13 +47,13 @@ extension UTMQemuVirtualMachine { url.stopAccessingSecurityScopedResource() } let tempBookmark = try url.bookmarkData() - try eject(drive, isForced: true) + try await eject(drive, isForced: true) let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly) - registryEntry.externalDrives[drive.id] = file - try await changeMedium(drive, with: tempBookmark, isSecurityScoped: false) + await registryEntry.setExternalDrive(file, forId: drive.id) + try await changeMedium(drive, with: tempBookmark, url: url, isSecurityScoped: false) } - private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, isSecurityScoped: Bool) async throws { + private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool) async throws { guard let system = system else { return } @@ -56,8 +61,12 @@ extension UTMQemuVirtualMachine { guard let bookmark = bookmark, let path = path, success else { throw UTMQemuVirtualMachineError.accessDriveImageFailed } - if registryEntry.externalDrives[drive.id] != nil { - registryEntry.externalDrives[drive.id]!.remoteBookmark = bookmark + await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) + let newUrl = url ?? URL(fileURLWithPath: path) + for i in qemuConfig.drives.indices { + if qemuConfig.drives[i].id == drive.id { + qemuConfig.drives[i].imageURL = newUrl + } } if let qemu = qemu, qemu.isConnected { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) @@ -73,10 +82,10 @@ extension UTMQemuVirtualMachine { continue } let id = drive.id - if let bookmark = registryEntry.externalDrives[id]?.remoteBookmark { + if let bookmark = await registryEntry.externalDrives[id]?.remoteBookmark { // an image bookmark was saved while QEMU was running - try await changeMedium(drive, with: bookmark, isSecurityScoped: true) - } else if let localBookmark = registryEntry.externalDrives[id]?.bookmark { + try await changeMedium(drive, with: bookmark, url: nil, isSecurityScoped: true) + } else if let localBookmark = await registryEntry.externalDrives[id]?.bookmark { // an image bookmark was saved while QEMU was NOT running let url = try URL(resolvingPersistentBookmarkData: localBookmark) try await changeMedium(drive, to: url) @@ -96,19 +105,20 @@ extension UTMQemuVirtualMachine { } } - func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? { + @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? { registryEntry.externalDrives[drive.id]?.url } } // MARK: - Shared directory extension UTMQemuVirtualMachine { - var sharedDirectoryURL: URL? { + @MainActor var sharedDirectoryURL: URL? { registryEntry.sharedDirectories.first?.url } - func clearSharedDirectory() { - registryEntry.sharedDirectories = [] + @MainActor func clearSharedDirectory() { + qemuConfig.sharing.directoryShareUrl = nil + registryEntry.removeAllSharedDirectories() } func changeSharedDirectory(to url: URL) async throws { @@ -117,11 +127,12 @@ extension UTMQemuVirtualMachine { url.stopAccessingSecurityScopedResource() } let file = try UTMRegistryEntry.File(url: url, isReadOnly: qemuConfig.sharing.isDirectoryShareReadOnly) - registryEntry.sharedDirectories = [file] + await registryEntry.setSingleSharedDirectory(file) if qemuConfig.sharing.directoryShareMode == .webdav { if let ioService = ioService { ioService.changeSharedDirectory(url) } + qemuConfig.sharing.directoryShareUrl = url } else if qemuConfig.sharing.directoryShareMode == .virtfs { let tempBookmark = try url.bookmarkData() try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) @@ -133,25 +144,24 @@ extension UTMQemuVirtualMachine { return } let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) - guard let bookmark = bookmark, let _ = path, success else { + guard let bookmark = bookmark, let path = path, success else { throw UTMQemuVirtualMachineError.accessDriveImageFailed } - if !registryEntry.sharedDirectories.isEmpty { - registryEntry.sharedDirectories[0].remoteBookmark = bookmark - } + await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark) + qemuConfig.sharing.directoryShareUrl = URL(fileURLWithPath: path) } func restoreSharedDirectory() async throws { - guard let share = registryEntry.sharedDirectories.first else { + guard let share = await registryEntry.sharedDirectories.first else { return } if qemuConfig.sharing.directoryShareMode == .virtfs { if let bookmark = share.remoteBookmark { // a share bookmark was saved while QEMU was running try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true) - } else if let localBookmark = registryEntry.externalDrives[id]?.bookmark { + } else { // a share bookmark was saved while QEMU was NOT running - let url = try URL(resolvingPersistentBookmarkData: localBookmark) + let url = try URL(resolvingPersistentBookmarkData: share.bookmark) try await changeSharedDirectory(to: url) } } else if qemuConfig.sharing.directoryShareMode == .webdav { @@ -164,20 +174,6 @@ extension UTMQemuVirtualMachine { // MARK: - Registry syncing extension UTMQemuVirtualMachine { - @MainActor override func updateConfigFromRegistry() { - for i in qemuConfig.drives.indices { - let drive = qemuConfig.drives[i] - if drive.isExternal { - if let file = registryEntry.externalDrives[drive.id] { - qemuConfig.drives[i].imageURL = file.url - } - } - } - if qemuConfig.sharing.directoryShareMode != .none { - qemuConfig.sharing.directoryShareUrl = sharedDirectoryURL - } - } - override func updateRegistryPostSave() async throws { for i in qemuConfig.drives.indices { let drive = qemuConfig.drives[i] diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift index a63829b66..76cdac061 100644 --- a/Managers/UTMRegistry.swift +++ b/Managers/UTMRegistry.swift @@ -14,24 +14,74 @@ // limitations under the License. // +import Combine import Foundation class UTMRegistry: NSObject { @objc static let shared = UTMRegistry() - private override init() { + private var lastUpdateTasks: [UTMRegistryEntry: Task] = [:] + + private var serializedEntries: [String: Any] { + get { + UserDefaults.standard.dictionary(forKey: "Registry") ?? [:] + } + set { + UserDefaults.standard.setValue(newValue, forKey: "Registry") + } + } + + private var changeListeners: [String: AnyCancellable] = [:] + + private var entries: [String: UTMRegistryEntry] { + didSet { + let toAdd = entries.keys.filter({ !changeListeners.keys.contains($0) }) + for key in toAdd { + let entry = entries[key]! + changeListeners[key] = entry.objectWillChange.sink { [weak self, weak entry] in + if let entry = entry { + self?.update(entry: entry) + } + } + } + let toRemove = changeListeners.keys.filter({ !entries.keys.contains($0) }) + for key in toRemove { + changeListeners.removeValue(forKey: key) + } + } + } + + private override init() { + entries = [:] + super.init() + if let newEntries = try? serializedEntries.mapValues({ value in + let dict = value as! [String: Any] + return try UTMRegistryEntry(from: dict) + }) { + entries = newEntries + } } /// Gets an existing registry entry or create a new entry /// - Parameter vm: UTM virtual machine to locate in the registry /// - Returns: Either an existing registry entry or a new entry @objc func entry(for vm: UTMVirtualMachine) -> UTMRegistryEntry { - // FIXME: locate existing registry + if let entry = entries[vm.id] { + return entry + } return UTMRegistryEntry(newFrom: vm)! } - func update(entry: UTMRegistryEntry) { - + /// Coalesce multiple updates in quick succession into one + /// - Parameter entry: Entry to update + private func update(entry: UTMRegistryEntry) { + lastUpdateTasks[entry]?.cancel() + lastUpdateTasks[entry] = Task(priority: .background) { + let uuid = await entry.uuid + let dict = try await entry.asDictionary() + serializedEntries[uuid] = dict + lastUpdateTasks.removeValue(forKey: entry) + } } } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 715b76254..92594af35 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -17,19 +17,19 @@ import Foundation @objc class UTMRegistryEntry: NSObject, Codable, ObservableObject { - @UTMRegistryValue var name: String + @Published private var _name: String - @UTMRegistryValue var package: File + @Published private var _package: File - @UTMRegistryValue var uuid: String + @Published private var _uuid: String - @UTMRegistryValue var isSuspended: Bool + @Published private var _isSuspended: Bool - @UTMRegistryValue var externalDrives: [String: File] + @Published private var _externalDrives: [String: File] - @UTMRegistryValue var sharedDirectories: [File] + @Published private var _sharedDirectories: [File] - @UTMRegistryValue var windowSettings: [Int: Window] + @Published private var _windowSettings: [Int: Window] private enum CodingKeys: String, CodingKey { case name = "Name" @@ -46,67 +46,185 @@ import Foundation return nil } let path = vm.path.path - name = vm.detailsTitleLabel + _name = vm.detailsTitleLabel guard let package = try? File(path: path, bookmark: bookmark, isReadOnly: false) else { return nil } - self.package = package; - uuid = vm.config.uuid.uuidString - isSuspended = false - externalDrives = [:] - sharedDirectories = [] - windowSettings = [:] + _package = package; + _uuid = vm.config.uuid.uuidString + _isSuspended = false + _externalDrives = [:] + _sharedDirectories = [] + _windowSettings = [:] } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - name = try container.decode(String.self, forKey: .name) - package = try container.decode(File.self, forKey: .package) - uuid = try container.decode(String.self, forKey: .uuid) - isSuspended = try container.decode(Bool.self, forKey: .isSuspended) - externalDrives = try container.decode([String: File].self, forKey: .externalDrives) - sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) - windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings) + _name = try container.decode(String.self, forKey: .name) + _package = try container.decode(File.self, forKey: .package) + _uuid = try container.decode(String.self, forKey: .uuid) + _isSuspended = try container.decode(Bool.self, forKey: .isSuspended) + _externalDrives = try container.decode([String: File].self, forKey: .externalDrives) + _sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) + _windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(name, forKey: .name) - try container.encode(package, forKey: .package) - try container.encode(uuid, forKey: .uuid) - try container.encode(isSuspended, forKey: .isSuspended) - try container.encode(externalDrives, forKey: .externalDrives) - try container.encode(sharedDirectories, forKey: .sharedDirectories) - try container.encode(windowSettings, forKey: .windowSettings) + try container.encode(_name, forKey: .name) + try container.encode(_package, forKey: .package) + try container.encode(_uuid, forKey: .uuid) + try container.encode(_isSuspended, forKey: .isSuspended) + try container.encode(_externalDrives, forKey: .externalDrives) + try container.encode(_sharedDirectories, forKey: .sharedDirectories) + try container.encode(_windowSettings, forKey: .windowSettings) + } + + @MainActor func asDictionary() throws -> [String: Any] { + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let xml = try encoder.encode(self) + let dict = try PropertyListSerialization.propertyList(from: xml, format: nil) + return dict as! [String: Any] + } +} + +protocol UTMRegistryEntryDecodable: Decodable {} +extension UTMRegistryEntry: UTMRegistryEntryDecodable {} +extension UTMRegistryEntryDecodable { + init(from dictionary: [String: Any]) throws { + let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0) + let decoder = PropertyListDecoder() + self = try decoder.decode(Self.self, from: data) + } +} + +// MARK: - Accessors +@MainActor extension UTMRegistryEntry { + var name: String { + get { + _name + } + + set { + _name = newValue + } + } + + var package: File { + get { + _package + } + + set { + _package = newValue + } + } + + var uuid: String { + get { + _uuid + } + + set { + _uuid = newValue + } + } + + var isSuspended: Bool { + get { + _isSuspended + } + + set { + _isSuspended = newValue + } + } + + var externalDrives: [String: File] { + get { + _externalDrives + } + + set { + _externalDrives = newValue + } + } + + var sharedDirectories: [File] { + get { + _sharedDirectories + } + + set { + _sharedDirectories = newValue + } + } + + var windowSettings: [Int: Window] { + get { + _windowSettings + } + + set { + _windowSettings = newValue + } + } + + func setExternalDrive(_ file: File, forId id: String) { + externalDrives[id] = file + } + + func updateExternalDriveRemoteBookmark(_ bookmark: Data, forId id: String) { + externalDrives[id]?.remoteBookmark = bookmark + } + + func removeExternalDrive(forId id: String) { + externalDrives.removeValue(forKey: id) + } + + func setSingleSharedDirectory(_ file: File) { + sharedDirectories = [file] + } + + func updateSingleSharedDirectoryRemoteBookmark(_ bookmark: Data) { + if !sharedDirectories.isEmpty { + sharedDirectories[0].remoteBookmark = bookmark + } + } + + func removeAllSharedDirectories() { + sharedDirectories = [] } } // MARK: - Objective C bridging +// FIXME: these are NOT synchronized to the actor @objc extension UTMRegistryEntry { var hasSaveState: Bool { get { - isSuspended + _isSuspended } set { - isSuspended = newValue + _isSuspended = newValue } } var packageRemoteBookmark: Data? { get { - package.remoteBookmark + _package.remoteBookmark } set { - package.remoteBookmark = newValue + _package.remoteBookmark = newValue } } var packageRemotePath: String? { get { - if package.remoteBookmark != nil { - return package.path + if _package.remoteBookmark != nil { + return _package.path } else { return nil } @@ -114,7 +232,7 @@ import Foundation set { if newValue != nil { - package.path = newValue! + _package.path = newValue! } } } @@ -209,33 +327,3 @@ extension UTMRegistryEntry { } } } - -@propertyWrapper struct UTMRegistryValue { - static subscript( - _enclosingInstance instance: UTMRegistryEntry, - wrapped wrappedKeyPath: ReferenceWritableKeyPath, - storage storageKeyPath: ReferenceWritableKeyPath - ) -> Value { - get { - instance[keyPath: storageKeyPath].storage - } - set { - instance[keyPath: storageKeyPath].storage = newValue - UTMRegistry.shared.update(entry: instance) - } - } - - @available(*, unavailable, - message: "@UTMRegistryValue can only be applied to classes" - ) - var wrappedValue: Value { - get { fatalError() } - set { fatalError() } - } - - private var storage: Value - - init(wrappedValue: Value) { - storage = wrappedValue - } -} diff --git a/Managers/UTMVirtualMachine-Protected.h b/Managers/UTMVirtualMachine-Protected.h index c3dfee47d..99e6070ae 100644 --- a/Managers/UTMVirtualMachine-Protected.h +++ b/Managers/UTMVirtualMachine-Protected.h @@ -83,9 +83,6 @@ extern const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions; /// @returns UTM error with the localized description set to `message` - (NSError *)errorWithMessage:(nullable NSString *)message; -/// Load screenshot from disk -- (void)loadScreenshot; - /// Save screenshot to disk - (void)saveScreenshot; diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index d4dff4db9..dbdfbffc6 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -68,12 +68,6 @@ - (void)setScreenshot:(CSScreenshot *)screenshot { _screenshot = screenshot; } -- (void)setConfig:(UTMConfigurationWrapper *)config { - [self propertyWillChange]; - _config = config; - self.anyCancellable = [self subscribeToConfiguration]; -} - - (NSURL *)detailsIconUrl { return self.config.iconURL; } @@ -206,11 +200,12 @@ - (instancetype)init { - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL { self = [self init]; if (self) { + _state = kVMStopped; self.config = configuration; self.path = packageURL; self.registryEntry = [UTMRegistry.shared entryFor:self]; [self loadScreenshot]; - self.state = kVMStopped; + self.anyCancellable = [self subscribeToChildren]; } return self; } @@ -227,7 +222,9 @@ - (void)startScreenshotTimer { } // delete existing screenshot if required if (!self.isScreenshotSaveEnabled) { - [self deleteScreenshot]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self deleteScreenshot]; + }); } typeof(self) __weak weakSelf = self; self.screenshotTimerHandler = ^{ @@ -247,13 +244,13 @@ - (void)changeState:(UTMVMState)state { dispatch_sync(dispatch_get_main_queue(), ^{ self.state = state; [self.delegate virtualMachine:self didTransitionToState:state]; + if (state == kVMStopped) { + [self setIsRunningAsSnapshot:NO]; + } }); if (state == kVMStarted) { [self startScreenshotTimer]; } - if (state == kVMStopped) { - [self setIsRunningAsSnapshot:NO]; - } } - (NSError *)errorGeneric { @@ -394,7 +391,7 @@ - (BOOL)isScreenshotSaveEnabled { - (void)loadScreenshot { NSURL *url = [self.path URLByAppendingPathComponent:kUTMBundleScreenshotFilename]; if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) { - self.screenshot = [[CSScreenshot alloc] initWithContentsOfURL:url]; + _screenshot = [[CSScreenshot alloc] initWithContentsOfURL:url]; } } diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 1e63c6d1b..99c250647 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -32,7 +32,7 @@ extension UTMVirtualMachine: ObservableObject { @objc extension UTMVirtualMachine { fileprivate static let gibInMib = 1024 - func subscribeToConfiguration() -> [AnyObject] { + func subscribeToChildren() -> [AnyObject] { var s: [AnyObject] = [] if let config = config.qemuConfig { s.append(config.objectWillChange.sink { [weak self] in @@ -43,11 +43,14 @@ extension UTMVirtualMachine: ObservableObject { self?.objectWillChange.send() }) } + s.append(registryEntry.objectWillChange.sink { [weak self] in + self?.objectWillChange.send() + }) return s } - func propertyWillChange() -> Void { - DispatchQueue.main.async { self.objectWillChange.send() } + @MainActor func propertyWillChange() -> Void { + objectWillChange.send() } @nonobjc convenience init(newConfig: Config, destinationURL: URL) { @@ -82,10 +85,6 @@ extension UTMVirtualMachine: ObservableObject { } } - @MainActor func updateConfigFromRegistry() { - // do nothing by default - } - func updateRegistryPostSave() async throws { // do nothing by default } @@ -140,15 +139,6 @@ public extension UTMQemuVirtualMachine { @objc var isSupported: Bool { return UTMQemuVirtualMachine.isSupported(systemArchitecture: config.qemuConfig!.system.architecture) } - - /// Sets up values in VM configuration corrosponding to per-device data like sharing path - @objc func prepareConfigurationForStart() { - if config.qemuConfig!.sharing.directoryShareMode != .none { - if let url = sharedDirectoryURL { - config.qemuConfig!.sharing.directoryShareUrl = url - } - } - } } // MARK: - Bookmark handling diff --git a/Platform/Shared/VMDetailsView.swift b/Platform/Shared/VMDetailsView.swift index f5609d0f6..353f6944c 100644 --- a/Platform/Shared/VMDetailsView.swift +++ b/Platform/Shared/VMDetailsView.swift @@ -113,11 +113,6 @@ struct VMDetailsView: View { } #endif } - .onChange(of: data.showSettingsModal) { newValue in - if newValue { - vm.updateConfigFromRegistry() - } - } } } } diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index df6131ceb..ecb1df48e 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -420,7 +420,9 @@ class UTMData: ObservableObject { guard let newVM = UTMVirtualMachine(url: url) else { throw NSLocalizedString("Unable to add a shortcut to the new location.", comment: "UTMData") } - newVM.isShortcut = true + await Task { @MainActor in + newVM.isShortcut = true + }.value try await newVM.accessShortcut() let oldSelected = await selectedVM @@ -514,11 +516,13 @@ class UTMData: ObservableObject { logger.info("found existing vm!") if let wrappedVM = vm as? UTMWrappedVirtualMachine { logger.info("existing vm is wrapped") - if let unwrappedVM = wrappedVM.unwrap() { - let index = await listRemove(vm: wrappedVM) - await listAdd(vm: unwrappedVM, at: index) - await listSelect(vm: unwrappedVM) - } + await Task { @MainActor in + if let unwrappedVM = wrappedVM.unwrap() { + let index = listRemove(vm: wrappedVM) + listAdd(vm: unwrappedVM, at: index) + listSelect(vm: unwrappedVM) + } + }.value } else { logger.info("existing vm is not wrapped") await listSelect(vm: vm) @@ -537,7 +541,9 @@ class UTMData: ObservableObject { } else if asShortcut { logger.info("loading as a shortcut") vm = UTMVirtualMachine(url: url) - vm?.isShortcut = true + await Task { @MainActor in + vm?.isShortcut = true + }.value try await vm?.accessShortcut() } else { logger.info("copying to Documents") From 2fbbcd47f2906ee7c4731baea64c2f7308bdc1b1 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 27 Aug 2022 18:05:35 -0700 Subject: [PATCH 17/42] config: conform to @MainActor This allows Swift code to be statically checked for access only on main thread and prevent SwiftUI bugs due to background thread access of an ObservableObject. --- Configuration/UTMAppleConfiguration.swift | 188 +++++++++++---- Configuration/UTMConfigurationWrapper.swift | 74 +++--- .../UTMQemuConfiguration+Arguments.swift | 2 +- Configuration/UTMQemuConfiguration.swift | 224 +++++++++++++----- Managers/UTMAppleVirtualMachine.swift | 47 ++-- Managers/UTMQemuVirtualMachine.swift | 46 ++-- Managers/UTMVirtualMachineExtension.swift | 16 +- Platform/Shared/UTMDownloadIPSWTask.swift | 6 +- Platform/iOS/VMDisplayHostedView.swift | 14 +- 9 files changed, 416 insertions(+), 201 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 5cfe8239d..240dfc189 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -21,21 +21,21 @@ import Virtualization @available(macOS 11, *) final class UTMAppleConfiguration: UTMConfiguration { /// Basic information and icon - @Published var information: UTMConfigurationInfo = .init() + @Published var _information: UTMConfigurationInfo = .init() - @Published var system: UTMAppleConfigurationSystem = .init() + @Published private var _system: UTMAppleConfigurationSystem = .init() - @Published var virtualization: UTMAppleConfigurationVirtualization = .init() + @Published private var _virtualization: UTMAppleConfigurationVirtualization = .init() - @Published var sharedDirectories: [UTMAppleConfigurationSharedDirectory] = [] + @Published private var _sharedDirectories: [UTMAppleConfigurationSharedDirectory] = [] - @Published var displays: [UTMAppleConfigurationDisplay] = [.init()] + @Published private var _displays: [UTMAppleConfigurationDisplay] = [.init()] - @Published var drives: [UTMAppleConfigurationDrive] = [] + @Published private var _drives: [UTMAppleConfigurationDrive] = [] - @Published var networks: [UTMAppleConfigurationNetwork] = [.init()] + @Published private var _networks: [UTMAppleConfigurationNetwork] = [.init()] - @Published var serials: [UTMAppleConfigurationSerial] = [] + @Published private var _serials: [UTMAppleConfigurationSerial] = [] var backend: UTMBackend { .apple @@ -70,32 +70,32 @@ final class UTMAppleConfiguration: UTMConfiguration { guard version <= Self.currentVersion else { throw UTMConfigurationError.versionTooHigh } - information = try values.decode(UTMConfigurationInfo.self, forKey: .information) - system = try values.decode(UTMAppleConfigurationSystem.self, forKey: .system) - virtualization = try values.decode(UTMAppleConfigurationVirtualization.self, forKey: .virtualization) - sharedDirectories = try values.decode([UTMAppleConfigurationSharedDirectory].self, forKey: .sharedDirectories) - displays = try values.decode([UTMAppleConfigurationDisplay].self, forKey: .displays) - drives = try values.decode([UTMAppleConfigurationDrive].self, forKey: .drives) - networks = try values.decode([UTMAppleConfigurationNetwork].self, forKey: .networks) - serials = try values.decode([UTMAppleConfigurationSerial].self, forKey: .serials) + _information = try values.decode(UTMConfigurationInfo.self, forKey: .information) + _system = try values.decode(UTMAppleConfigurationSystem.self, forKey: .system) + _virtualization = try values.decode(UTMAppleConfigurationVirtualization.self, forKey: .virtualization) + _sharedDirectories = try values.decode([UTMAppleConfigurationSharedDirectory].self, forKey: .sharedDirectories) + _displays = try values.decode([UTMAppleConfigurationDisplay].self, forKey: .displays) + _drives = try values.decode([UTMAppleConfigurationDrive].self, forKey: .drives) + _networks = try values.decode([UTMAppleConfigurationNetwork].self, forKey: .networks) + _serials = try values.decode([UTMAppleConfigurationSerial].self, forKey: .serials) // remove incompatible configurations - if #unavailable(macOS 13), system.boot.operatingSystem != .macOS { - displays = [] + if #unavailable(macOS 13), _system.boot.operatingSystem != .macOS { + _displays = [] } else if #unavailable(macOS 12) { - displays = [] + _displays = [] } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(information, forKey: .information) - try container.encode(system, forKey: .system) - try container.encode(virtualization, forKey: .virtualization) - try container.encode(sharedDirectories, forKey: .sharedDirectories) - try container.encode(displays, forKey: .displays) - try container.encode(drives, forKey: .drives) - try container.encode(networks, forKey: .networks) - try container.encode(serials, forKey: .serials) + try container.encode(_information, forKey: .information) + try container.encode(_system, forKey: .system) + try container.encode(_virtualization, forKey: .virtualization) + try container.encode(_sharedDirectories, forKey: .sharedDirectories) + try container.encode(_displays, forKey: .displays) + try container.encode(_drives, forKey: .drives) + try container.encode(_networks, forKey: .networks) + try container.encode(_serials, forKey: .serials) try container.encode(UTMBackend.apple, forKey: .backend) try container.encode(Self.currentVersion, forKey: .configurationVersion) } @@ -126,30 +126,120 @@ extension UTMAppleConfigurationError: LocalizedError { } } +// MARK: - Public accessors + +@MainActor extension UTMAppleConfiguration { + var information: UTMConfigurationInfo { + get { + _information + } + + set { + _information = newValue + } + } + + var system: UTMAppleConfigurationSystem { + get { + _system + } + + set { + _system = newValue + } + } + + var virtualization: UTMAppleConfigurationVirtualization { + get { + _virtualization + } + + set { + _virtualization = newValue + } + } + + var sharedDirectories: [UTMAppleConfigurationSharedDirectory] { + get { + _sharedDirectories + } + + set { + _sharedDirectories = newValue + } + } + + var sharedDirectoriesPublisher: Published<[UTMAppleConfigurationSharedDirectory]>.Publisher { + get { + $_sharedDirectories + } + } + + var displays: [UTMAppleConfigurationDisplay] { + get { + _displays + } + + set { + _displays = newValue + } + } + + var drives: [UTMAppleConfigurationDrive] { + get { + _drives + } + + set { + _drives = newValue + } + } + + var networks: [UTMAppleConfigurationNetwork] { + get { + _networks + } + + set { + _networks = newValue + } + } + + var serials: [UTMAppleConfigurationSerial] { + get { + _serials + } + + set { + _serials = newValue + } + } +} + // MARK: - Conversion of old config format extension UTMAppleConfiguration { convenience init(migrating oldConfig: UTMLegacyAppleConfiguration, dataURL: URL) { self.init() - information = .init(migrating: oldConfig, dataURL: dataURL) - system = .init(migrating: oldConfig) - virtualization = .init(migrating: oldConfig) - sharedDirectories = oldConfig.sharedDirectories.map { .init(migrating: $0) } + _information = .init(migrating: oldConfig, dataURL: dataURL) + _system = .init(migrating: oldConfig) + _virtualization = .init(migrating: oldConfig) + _sharedDirectories = oldConfig.sharedDirectories.map { .init(migrating: $0) } #if arch(arm64) if #available(macOS 12, *) { - displays = oldConfig.displays.map { .init(migrating: $0) } + _displays = oldConfig.displays.map { .init(migrating: $0) } } #endif - drives = oldConfig.diskImages.map { .init(migrating: $0) } - networks = oldConfig.networkDevices.map { .init(migrating: $0) } + _drives = oldConfig.diskImages.map { .init(migrating: $0) } + _networks = oldConfig.networkDevices.map { .init(migrating: $0) } if oldConfig.isConsoleDisplay { var serial = UTMAppleConfigurationSerial() serial.terminal = .init(migrating: oldConfig) - serials = [serial] + _serials = [serial] } else if oldConfig.isSerialEnabled { var serial = UTMAppleConfigurationSerial() serial.mode = .ptty - serials = [serial] + _serials = [serial] } } } @@ -158,7 +248,7 @@ extension UTMAppleConfiguration { @available(iOS, unavailable, message: "Apple Virtualization not available on iOS") @available(macOS 11, *) -extension UTMAppleConfiguration { +@MainActor extension UTMAppleConfiguration { var appleVZConfiguration: VZVirtualMachineConfiguration { let vzconfig = VZVirtualMachineConfiguration() system.fillVZConfiguration(vzconfig) @@ -213,19 +303,19 @@ extension UTMAppleConfiguration { @available(iOS, unavailable, message: "Apple Virtualization not available on iOS") @available(macOS 11, *) -extension UTMAppleConfiguration { +@MainActor extension UTMAppleConfiguration { func prepareSave(for packageURL: URL) async throws { try await virtualization.prepareSave(for: packageURL) } func saveData(to dataURL: URL) async throws -> [URL] { var existingDataURLs = [URL]() - existingDataURLs += try await information.saveData(to: dataURL) - existingDataURLs += try await system.boot.saveData(to: dataURL) + existingDataURLs += try await _information.saveData(to: dataURL) + existingDataURLs += try await _system.boot.saveData(to: dataURL) #if arch(arm64) if #available(macOS 12, *), system.macPlatform != nil { - existingDataURLs += try await system.macPlatform!.saveData(to: dataURL) + existingDataURLs += try await _system.macPlatform!.saveData(to: dataURL) } #endif @@ -233,9 +323,23 @@ extension UTMAppleConfiguration { try appleVZConfiguration.validate() for i in 0.. Void) { - guard let qemuConfig = qemuConfig, let efiVarsURL = qemuConfig.qemu.efiVarsURL else { + guard let qemuConfig = qemuConfig, let efiVarsURL = qemuConfig._qemu.efiVarsURL else { completion(nil) return } @@ -344,7 +348,7 @@ import Foundation } Task { do { - _ = try await qemuConfig.qemu.saveData(to: efiVarsURL.deletingLastPathComponent(), for: qemuConfig.system) + _ = try await qemuConfig._qemu.saveData(to: efiVarsURL.deletingLastPathComponent(), for: qemuConfig._system) completion(nil) } catch { completion(error) diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 8d2486a03..d6773cdf4 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -17,7 +17,7 @@ import Foundation /// Build QEMU arguments from config -extension UTMQemuConfiguration { +@MainActor extension UTMQemuConfiguration { /// Helper function to generate a final argument /// - Parameter string: Argument fragment /// - Returns: Final argument fragment diff --git a/Configuration/UTMQemuConfiguration.swift b/Configuration/UTMQemuConfiguration.swift index b561ca838..a3f799f11 100644 --- a/Configuration/UTMQemuConfiguration.swift +++ b/Configuration/UTMQemuConfiguration.swift @@ -19,34 +19,34 @@ import Foundation /// Settings for a QEMU configuration final class UTMQemuConfiguration: UTMConfiguration { /// Basic information and icon - @Published var information: UTMConfigurationInfo = .init() + @Published var _information: UTMConfigurationInfo = .init() /// System settings - @Published var system: UTMQemuConfigurationSystem = .init() + @Published var _system: UTMQemuConfigurationSystem = .init() /// Additional QEMU tweaks - @Published var qemu: UTMQemuConfigurationQEMU = .init() + @Published var _qemu: UTMQemuConfigurationQEMU = .init() /// Input settings - @Published var input: UTMQemuConfigurationInput = .init() + @Published var _input: UTMQemuConfigurationInput = .init() /// Sharing settings - @Published var sharing: UTMQemuConfigurationSharing = .init() + @Published var _sharing: UTMQemuConfigurationSharing = .init() /// All displays - @Published var displays: [UTMQemuConfigurationDisplay] = [] + @Published var _displays: [UTMQemuConfigurationDisplay] = [] /// All drives - @Published var drives: [UTMQemuConfigurationDrive] = [] + @Published var _drives: [UTMQemuConfigurationDrive] = [] /// All network adapters - @Published var networks: [UTMQemuConfigurationNetwork] = [] + @Published var _networks: [UTMQemuConfigurationNetwork] = [] /// All serial ouputs - @Published var serials: [UTMQemuConfigurationSerial] = [] + @Published var _serials: [UTMQemuConfigurationSerial] = [] /// All audio devices - @Published var sound: [UTMQemuConfigurationSound] = [] + @Published var _sound: [UTMQemuConfigurationSound] = [] /// True if configuration is migrated from a legacy config. Not saved. private(set) var isLegacy: Bool = false @@ -87,30 +87,30 @@ final class UTMQemuConfiguration: UTMConfiguration { guard version <= Self.currentVersion else { throw UTMConfigurationError.versionTooHigh } - information = try values.decode(UTMConfigurationInfo.self, forKey: .information) - system = try values.decode(UTMQemuConfigurationSystem.self, forKey: .system) - qemu = try values.decode(UTMQemuConfigurationQEMU.self, forKey: .qemu) - input = try values.decode(UTMQemuConfigurationInput.self, forKey: .input) - sharing = try values.decode(UTMQemuConfigurationSharing.self, forKey: .sharing) - displays = try values.decode([UTMQemuConfigurationDisplay].self, forKey: .displays) - drives = try values.decode([UTMQemuConfigurationDrive].self, forKey: .drives) - networks = try values.decode([UTMQemuConfigurationNetwork].self, forKey: .networks) - serials = try values.decode([UTMQemuConfigurationSerial].self, forKey: .serials) - sound = try values.decode([UTMQemuConfigurationSound].self, forKey: .sound) + _information = try values.decode(UTMConfigurationInfo.self, forKey: .information) + _system = try values.decode(UTMQemuConfigurationSystem.self, forKey: .system) + _qemu = try values.decode(UTMQemuConfigurationQEMU.self, forKey: .qemu) + _input = try values.decode(UTMQemuConfigurationInput.self, forKey: .input) + _sharing = try values.decode(UTMQemuConfigurationSharing.self, forKey: .sharing) + _displays = try values.decode([UTMQemuConfigurationDisplay].self, forKey: .displays) + _drives = try values.decode([UTMQemuConfigurationDrive].self, forKey: .drives) + _networks = try values.decode([UTMQemuConfigurationNetwork].self, forKey: .networks) + _serials = try values.decode([UTMQemuConfigurationSerial].self, forKey: .serials) + _sound = try values.decode([UTMQemuConfigurationSound].self, forKey: .sound) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(information, forKey: .information) - try container.encode(system, forKey: .system) - try container.encode(qemu, forKey: .qemu) - try container.encode(input, forKey: .input) - try container.encode(sharing, forKey: .sharing) - try container.encode(displays, forKey: .displays) - try container.encode(drives, forKey: .drives) - try container.encode(networks, forKey: .networks) - try container.encode(serials, forKey: .serials) - try container.encode(sound, forKey: .sound) + try container.encode(_information, forKey: .information) + try container.encode(_system, forKey: .system) + try container.encode(_qemu, forKey: .qemu) + try container.encode(_input, forKey: .input) + try container.encode(_sharing, forKey: .sharing) + try container.encode(_displays, forKey: .displays) + try container.encode(_drives, forKey: .drives) + try container.encode(_networks, forKey: .networks) + try container.encode(_serials, forKey: .serials) + try container.encode(_sound, forKey: .sound) try container.encode(UTMBackend.qemu, forKey: .backend) try container.encode(Self.currentVersion, forKey: .configurationVersion) } @@ -132,25 +132,129 @@ extension UTMQemuConfigurationError: LocalizedError { } } +// MARK: - Public accessors + +@MainActor extension UTMQemuConfiguration { + var information: UTMConfigurationInfo { + get { + _information + } + + set { + _information = newValue + } + } + + var system: UTMQemuConfigurationSystem { + get { + _system + } + + set { + _system = newValue + } + } + + var qemu: UTMQemuConfigurationQEMU { + get { + _qemu + } + + set { + _qemu = newValue + } + } + + var input: UTMQemuConfigurationInput { + get { + _input + } + + set { + _input = newValue + } + } + + var sharing: UTMQemuConfigurationSharing { + get { + _sharing + } + + set { + _sharing = newValue + } + } + + var displays: [UTMQemuConfigurationDisplay] { + get { + _displays + } + + set { + _displays = newValue + } + } + + var drives: [UTMQemuConfigurationDrive] { + get { + _drives + } + + set { + _drives = newValue + } + } + + var networks: [UTMQemuConfigurationNetwork] { + get { + _networks + } + + set { + _networks = newValue + } + } + + var serials: [UTMQemuConfigurationSerial] { + get { + _serials + } + + set { + _serials = newValue + } + } + + var sound: [UTMQemuConfigurationSound] { + get { + _sound + } + + set { + _sound = newValue + } + } +} + // MARK: - Defaults extension UTMQemuConfiguration { - func reset(all: Bool = true) { + private func reset(all: Bool = true) { if all { - information = .init() - system = .init() - drives = [] - } - qemu = .init() - input = .init() - sharing = .init() - displays = [] - networks = [] - serials = [] - sound = [] + _information = .init() + _system = .init() + _drives = [] + } + _qemu = .init() + _input = .init() + _sharing = .init() + _displays = [] + _networks = [] + _serials = [] + _sound = [] } - func reset(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget) { + @MainActor func reset(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget) { reset(all: false) qemu = .init(forArchitecture: architecture, target: target) input = .init(forArchitecture: architecture, target: target) @@ -176,35 +280,35 @@ extension UTMQemuConfiguration { convenience init(migrating oldConfig: UTMLegacyQemuConfiguration) { self.init() isLegacy = true - information = .init(migrating: oldConfig) - system = .init(migrating: oldConfig) - qemu = .init(migrating: oldConfig) - input = .init(migrating: oldConfig) - sharing = .init(migrating: oldConfig) + _information = .init(migrating: oldConfig) + _system = .init(migrating: oldConfig) + _qemu = .init(migrating: oldConfig) + _input = .init(migrating: oldConfig) + _sharing = .init(migrating: oldConfig) if let display = UTMQemuConfigurationDisplay(migrating: oldConfig) { - displays = [display] + _displays = [display] } - drives = (0.. [URL] { var existingDataURLs = [URL]() - existingDataURLs += try await information.saveData(to: dataURL) - existingDataURLs += try await qemu.saveData(to: dataURL, for: system) + existingDataURLs += try await _information.saveData(to: dataURL) + existingDataURLs += try await _qemu.saveData(to: dataURL, for: system) for i in 0..) -> Void in vmQueue.async { #if os(macOS) && arch(arm64) - let boot = self.appleConfig.system.boot if #available(macOS 13, *), boot.operatingSystem == .macOS { let options = VZMacOSVirtualMachineStartOptions() options.startUpFromMacOSRecovery = boot.startUpFromMacOSRecovery @@ -117,12 +114,14 @@ import Virtualization try await beginAccessingResources() try await _vmStart() if #available(macOS 12, *) { - sharedDirectoriesChanged = appleConfig.$sharedDirectories.sink { [weak self] newShares in - guard let self = self else { - return - } - self.vmQueue.async { - self.updateSharedDirectories(with: newShares) + Task { @MainActor in + sharedDirectoriesChanged = appleConfig.sharedDirectoriesPublisher.sink { [weak self] newShares in + guard let self = self else { + return + } + self.vmQueue.async { + self.updateSharedDirectories(with: newShares) + } } } } @@ -269,7 +268,7 @@ import Virtualization screenshot = screenshotDelegate?.screenshot } - private func createAppleVM() throws { + @MainActor private func createAppleVM() throws { for i in appleConfig.serials.indices { let (fd, sfd, name) = try createPty() let terminalTtyHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: false) @@ -306,7 +305,7 @@ import Virtualization return } changeState(.vmStarting) - try createAppleVM() + try await createAppleVM() #if os(macOS) && arch(arm64) try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in vmQueue.async { @@ -414,11 +413,9 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate { sharedDirectoriesChanged = nil Task { @MainActor in stopAccesingResources() - } - for i in appleConfig.serials.indices { - if let serialPort = appleConfig.serials[i].interface { - serialPort.close() - Task { @MainActor in + for i in appleConfig.serials.indices { + if let serialPort = appleConfig.serials[i].interface { + serialPort.close() appleConfig.serials[i].interface = nil appleConfig.serials[i].fileHandleForReading = nil appleConfig.serials[i].fileHandleForWriting = nil diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 908a1758c..7f007a116 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -29,11 +29,13 @@ extension UTMQemuVirtualMachine { if let oldPath = await registryEntry.externalDrives[drive.id]?.path { system?.stopAccessingPath(oldPath) } - for i in qemuConfig.drives.indices { - if qemuConfig.drives[i].id == drive.id { - qemuConfig.drives[i].imageURL = nil + await Task { @MainActor in + for i in qemuConfig.drives.indices { + if qemuConfig.drives[i].id == drive.id { + qemuConfig.drives[i].imageURL = nil + } } - } + }.value await registryEntry.removeExternalDrive(forId: drive.id) guard let qemu = qemu, qemu.isConnected else { return @@ -63,11 +65,13 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) let newUrl = url ?? URL(fileURLWithPath: path) - for i in qemuConfig.drives.indices { - if qemuConfig.drives[i].id == drive.id { - qemuConfig.drives[i].imageURL = newUrl + await Task { @MainActor in + for i in qemuConfig.drives.indices { + if qemuConfig.drives[i].id == drive.id { + qemuConfig.drives[i].imageURL = newUrl + } } - } + }.value if let qemu = qemu, qemu.isConnected { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) } @@ -77,7 +81,7 @@ extension UTMQemuVirtualMachine { guard system != nil && qemu != nil && qemu!.isConnected else { throw UTMQemuVirtualMachineError.invalidVmState } - for drive in qemuConfig.drives { + for drive in await qemuConfig.drives { if !drive.isExternal { continue } @@ -126,14 +130,16 @@ extension UTMQemuVirtualMachine { defer { url.stopAccessingSecurityScopedResource() } - let file = try UTMRegistryEntry.File(url: url, isReadOnly: qemuConfig.sharing.isDirectoryShareReadOnly) + let file = try await UTMRegistryEntry.File(url: url, isReadOnly: qemuConfig.sharing.isDirectoryShareReadOnly) await registryEntry.setSingleSharedDirectory(file) - if qemuConfig.sharing.directoryShareMode == .webdav { + if await qemuConfig.sharing.directoryShareMode == .webdav { if let ioService = ioService { ioService.changeSharedDirectory(url) } - qemuConfig.sharing.directoryShareUrl = url - } else if qemuConfig.sharing.directoryShareMode == .virtfs { + await Task { @MainActor in + qemuConfig.sharing.directoryShareUrl = url + }.value + } else if await qemuConfig.sharing.directoryShareMode == .virtfs { let tempBookmark = try url.bookmarkData() try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) } @@ -148,14 +154,16 @@ extension UTMQemuVirtualMachine { throw UTMQemuVirtualMachineError.accessDriveImageFailed } await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark) - qemuConfig.sharing.directoryShareUrl = URL(fileURLWithPath: path) + await Task { @MainActor in + qemuConfig.sharing.directoryShareUrl = URL(fileURLWithPath: path) + }.value } func restoreSharedDirectory() async throws { guard let share = await registryEntry.sharedDirectories.first else { return } - if qemuConfig.sharing.directoryShareMode == .virtfs { + if await qemuConfig.sharing.directoryShareMode == .virtfs { if let bookmark = share.remoteBookmark { // a share bookmark was saved while QEMU was running try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true) @@ -164,7 +172,7 @@ extension UTMQemuVirtualMachine { let url = try URL(resolvingPersistentBookmarkData: share.bookmark) try await changeSharedDirectory(to: url) } - } else if qemuConfig.sharing.directoryShareMode == .webdav { + } else if await qemuConfig.sharing.directoryShareMode == .webdav { if let ioService = ioService { ioService.changeSharedDirectory(share.url) } @@ -174,15 +182,11 @@ extension UTMQemuVirtualMachine { // MARK: - Registry syncing extension UTMQemuVirtualMachine { - override func updateRegistryPostSave() async throws { + @MainActor override func updateRegistryPostSave() async throws { for i in qemuConfig.drives.indices { let drive = qemuConfig.drives[i] if drive.isExternal, let url = drive.imageURL { try await changeMedium(drive, to: url) - await Task { @MainActor in - // clear temporary URL - qemuConfig.drives[i].imageURL = nil - }.value } } if let url = config.qemuConfig!.sharing.directoryShareUrl { diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 99c250647..8b9c12a56 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -85,33 +85,33 @@ extension UTMVirtualMachine: ObservableObject { } } - func updateRegistryPostSave() async throws { + @MainActor func updateRegistryPostSave() async throws { // do nothing by default } } public extension UTMQemuVirtualMachine { - override var detailsTitleLabel: String { + @MainActor override var detailsTitleLabel: String { config.qemuConfig!.information.name } - override var detailsSubtitleLabel: String { + @MainActor override var detailsSubtitleLabel: String { self.detailsSystemTargetLabel } - override var detailsNotes: String? { + @MainActor override var detailsNotes: String? { config.qemuConfig!.information.notes } - override var detailsSystemTargetLabel: String { + @MainActor override var detailsSystemTargetLabel: String { config.qemuConfig!.system.target.prettyValue } - override var detailsSystemArchitectureLabel: String { + @MainActor override var detailsSystemArchitectureLabel: String { config.qemuConfig!.system.architecture.prettyValue } - override var detailsSystemMemoryLabel: String { + @MainActor override var detailsSystemMemoryLabel: String { let bytesInMib = Int64(1048576) return ByteCountFormatter.string(fromByteCount: Int64(config.qemuConfig!.system.memorySize) * bytesInMib, countStyle: .memory) } @@ -137,7 +137,7 @@ public extension UTMQemuVirtualMachine { /// Check if the current VM target is supported by the host @objc var isSupported: Bool { - return UTMQemuVirtualMachine.isSupported(systemArchitecture: config.qemuConfig!.system.architecture) + return UTMQemuVirtualMachine.isSupported(systemArchitecture: config.qemuConfig!._system.architecture) } } diff --git a/Platform/Shared/UTMDownloadIPSWTask.swift b/Platform/Shared/UTMDownloadIPSWTask.swift index 1c1080556..f98e10ea1 100644 --- a/Platform/Shared/UTMDownloadIPSWTask.swift +++ b/Platform/Shared/UTMDownloadIPSWTask.swift @@ -27,7 +27,7 @@ class UTMDownloadIPSWTask: UTMDownloadTask { fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! } - init(for config: UTMAppleConfiguration) { + @MainActor init(for config: UTMAppleConfiguration) { self.config = config super.init(for: config.system.boot.macRecoveryIpswURL!, named: config.information.name) } @@ -42,7 +42,9 @@ class UTMDownloadIPSWTask: UTMDownloadTask { try fileManager.removeItem(at: cacheIpsw) } try fileManager.moveItem(at: location, to: cacheIpsw) - config.system.boot.macRecoveryIpswURL = cacheIpsw + await Task { @MainActor in + config.system.boot.macRecoveryIpswURL = cacheIpsw + }.value return UTMVirtualMachine(newConfig: config, destinationURL: UTMData.defaultStorageUrl) } } diff --git a/Platform/iOS/VMDisplayHostedView.swift b/Platform/iOS/VMDisplayHostedView.swift index fdc5406c9..acb4dc850 100644 --- a/Platform/iOS/VMDisplayHostedView.swift +++ b/Platform/iOS/VMDisplayHostedView.swift @@ -32,31 +32,31 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { vm.config.qemuConfig } - var qemuInputLegacy: Bool { + @MainActor var qemuInputLegacy: Bool { vmConfig.input.usbBusSupport == .disabled || vmConfig.qemu.hasPS2Controller } - var qemuDisplayUpscaler: MTLSamplerMinMagFilter { + @MainActor var qemuDisplayUpscaler: MTLSamplerMinMagFilter { vmConfig.displays[state.device!.configIndex].upscalingFilter.metalSamplerMinMagFilter } - var qemuDisplayDownscaler: MTLSamplerMinMagFilter { + @MainActor var qemuDisplayDownscaler: MTLSamplerMinMagFilter { vmConfig.displays[state.device!.configIndex].downscalingFilter.metalSamplerMinMagFilter } - var qemuDisplayIsDynamicResolution: Bool { + @MainActor var qemuDisplayIsDynamicResolution: Bool { vmConfig.displays[state.device!.configIndex].isDynamicResolution } - var qemuDisplayIsNativeResolution: Bool { + @MainActor var qemuDisplayIsNativeResolution: Bool { vmConfig.displays[state.device!.configIndex].isNativeResolution } - var qemuHasClipboardSharing: Bool { + @MainActor var qemuHasClipboardSharing: Bool { vmConfig.sharing.hasClipboardSharing } - var qemuConsoleResizeCommand: String? { + @MainActor var qemuConsoleResizeCommand: String? { vmConfig.serials[state.device!.configIndex].terminal?.resizeCommand } From 0d6cd7c52b2f42b5da3eba6042a21076657e400b Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 27 Aug 2022 20:06:35 -0700 Subject: [PATCH 18/42] project: use MainActor.run instead of Task when main thread sync is needed This allows the running thread to execute immediately if already on the main thread. --- Managers/UTMAppleVirtualMachine.swift | 6 ++-- Managers/UTMQemuVirtualMachine.swift | 16 +++++----- Platform/Shared/UTMDownloadIPSWTask.swift | 4 +-- Platform/UTMData.swift | 12 ++++---- Platform/macOS/VMConfigAppleBootView.swift | 36 +++++++++++++--------- 5 files changed, 39 insertions(+), 35 deletions(-) diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index 7bf036103..ddcbaa5a9 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -275,10 +275,8 @@ import Virtualization let slaveTtyHandle = FileHandle(fileDescriptor: sfd, closeOnDealloc: false) appleConfig.serials[i].fileHandleForReading = terminalTtyHandle appleConfig.serials[i].fileHandleForWriting = terminalTtyHandle - Task { @MainActor in - let serialPort = UTMSerialPort(portNamed: name, readFileHandle: slaveTtyHandle, writeFileHandle: slaveTtyHandle, terminalFileHandle: terminalTtyHandle) - appleConfig.serials[i].interface = serialPort - } + let serialPort = UTMSerialPort(portNamed: name, readFileHandle: slaveTtyHandle, writeFileHandle: slaveTtyHandle, terminalFileHandle: terminalTtyHandle) + appleConfig.serials[i].interface = serialPort } let vzConfig = appleConfig.appleVZConfiguration apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue) diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 7f007a116..723eb8d89 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -29,13 +29,13 @@ extension UTMQemuVirtualMachine { if let oldPath = await registryEntry.externalDrives[drive.id]?.path { system?.stopAccessingPath(oldPath) } - await Task { @MainActor in + await MainActor.run { for i in qemuConfig.drives.indices { if qemuConfig.drives[i].id == drive.id { qemuConfig.drives[i].imageURL = nil } } - }.value + } await registryEntry.removeExternalDrive(forId: drive.id) guard let qemu = qemu, qemu.isConnected else { return @@ -65,13 +65,13 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) let newUrl = url ?? URL(fileURLWithPath: path) - await Task { @MainActor in + await MainActor.run { for i in qemuConfig.drives.indices { if qemuConfig.drives[i].id == drive.id { qemuConfig.drives[i].imageURL = newUrl } } - }.value + } if let qemu = qemu, qemu.isConnected { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) } @@ -136,9 +136,9 @@ extension UTMQemuVirtualMachine { if let ioService = ioService { ioService.changeSharedDirectory(url) } - await Task { @MainActor in + await MainActor.run { qemuConfig.sharing.directoryShareUrl = url - }.value + } } else if await qemuConfig.sharing.directoryShareMode == .virtfs { let tempBookmark = try url.bookmarkData() try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) @@ -154,9 +154,9 @@ extension UTMQemuVirtualMachine { throw UTMQemuVirtualMachineError.accessDriveImageFailed } await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark) - await Task { @MainActor in + await MainActor.run { qemuConfig.sharing.directoryShareUrl = URL(fileURLWithPath: path) - }.value + } } func restoreSharedDirectory() async throws { diff --git a/Platform/Shared/UTMDownloadIPSWTask.swift b/Platform/Shared/UTMDownloadIPSWTask.swift index f98e10ea1..e0af13539 100644 --- a/Platform/Shared/UTMDownloadIPSWTask.swift +++ b/Platform/Shared/UTMDownloadIPSWTask.swift @@ -42,9 +42,9 @@ class UTMDownloadIPSWTask: UTMDownloadTask { try fileManager.removeItem(at: cacheIpsw) } try fileManager.moveItem(at: location, to: cacheIpsw) - await Task { @MainActor in + await MainActor.run { config.system.boot.macRecoveryIpswURL = cacheIpsw - }.value + } return UTMVirtualMachine(newConfig: config, destinationURL: UTMData.defaultStorageUrl) } } diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index ecb1df48e..fa1c33563 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -420,9 +420,9 @@ class UTMData: ObservableObject { guard let newVM = UTMVirtualMachine(url: url) else { throw NSLocalizedString("Unable to add a shortcut to the new location.", comment: "UTMData") } - await Task { @MainActor in + await MainActor.run { newVM.isShortcut = true - }.value + } try await newVM.accessShortcut() let oldSelected = await selectedVM @@ -516,13 +516,13 @@ class UTMData: ObservableObject { logger.info("found existing vm!") if let wrappedVM = vm as? UTMWrappedVirtualMachine { logger.info("existing vm is wrapped") - await Task { @MainActor in + await MainActor.run { if let unwrappedVM = wrappedVM.unwrap() { let index = listRemove(vm: wrappedVM) listAdd(vm: unwrappedVM, at: index) listSelect(vm: unwrappedVM) } - }.value + } } else { logger.info("existing vm is not wrapped") await listSelect(vm: vm) @@ -541,9 +541,9 @@ class UTMData: ObservableObject { } else if asShortcut { logger.info("loading as a shortcut") vm = UTMVirtualMachine(url: url) - await Task { @MainActor in + await MainActor.run { vm?.isShortcut = true - }.value + } try await vm?.accessShortcut() } else { logger.info("copying to Documents") diff --git a/Platform/macOS/VMConfigAppleBootView.swift b/Platform/macOS/VMConfigAppleBootView.swift index a7582d75a..be326054f 100644 --- a/Platform/macOS/VMConfigAppleBootView.swift +++ b/Platform/macOS/VMConfigAppleBootView.swift @@ -152,30 +152,36 @@ struct VMConfigAppleBootView: View { } data.busyWorkAsync { let url = try result.get() - try await Task { @MainActor in - switch selection { - case .ipsw: - if #available(macOS 12, *) { - #if arch(arm64) - let image = try await VZMacOSRestoreImage.image(from: url) - guard let model = image.mostFeaturefulSupportedConfiguration?.hardwareModel else { - throw NSLocalizedString("Your machine does not support running this IPSW.", comment: "VMConfigAppleBootView") - } + switch selection { + case .ipsw: + if #available(macOS 12, *) { + #if arch(arm64) + let image = try await VZMacOSRestoreImage.image(from: url) + guard let model = image.mostFeaturefulSupportedConfiguration?.hardwareModel else { + throw NSLocalizedString("Your machine does not support running this IPSW.", comment: "VMConfigAppleBootView") + } + await MainActor.run { config.macPlatform = UTMAppleConfigurationMacPlatform(newHardware: model) config.boot.operatingSystem = .macOS config.boot.macRecoveryIpswURL = url - #endif } - case .kernel: + #endif + } + case .kernel: + await MainActor.run { config.boot.operatingSystem = .linux config.boot.linuxKernelURL = url - case .ramdisk: + } + case .ramdisk: + await MainActor.run { config.boot.linuxInitialRamdiskURL = url - case .unsupported: - break } + case .unsupported: + break + } + await MainActor.run { operatingSystem = currentOperatingSystem - }.value + } } } } From 4e471ef54a9e7c61febc5b414cf41d71a0373528 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 27 Aug 2022 20:30:54 -0700 Subject: [PATCH 19/42] registry: perform debouncing of update writes --- Managers/UTMRegistry.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift index 76cdac061..53f28e4de 100644 --- a/Managers/UTMRegistry.swift +++ b/Managers/UTMRegistry.swift @@ -20,8 +20,6 @@ import Foundation class UTMRegistry: NSObject { @objc static let shared = UTMRegistry() - private var lastUpdateTasks: [UTMRegistryEntry: Task] = [:] - private var serializedEntries: [String: Any] { get { UserDefaults.standard.dictionary(forKey: "Registry") ?? [:] @@ -39,9 +37,11 @@ class UTMRegistry: NSObject { let toAdd = entries.keys.filter({ !changeListeners.keys.contains($0) }) for key in toAdd { let entry = entries[key]! - changeListeners[key] = entry.objectWillChange.sink { [weak self, weak entry] in + changeListeners[key] = entry.objectWillChange + .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .utility)) + .sink { [weak self, weak entry] in if let entry = entry { - self?.update(entry: entry) + self?.commit(entry: entry) } } } @@ -73,15 +73,14 @@ class UTMRegistry: NSObject { return UTMRegistryEntry(newFrom: vm)! } - /// Coalesce multiple updates in quick succession into one - /// - Parameter entry: Entry to update - private func update(entry: UTMRegistryEntry) { - lastUpdateTasks[entry]?.cancel() - lastUpdateTasks[entry] = Task(priority: .background) { + /// Commit the entry to persistent storage + /// This runs in a background queue. + /// - Parameter entry: Entry to commit + private func commit(entry: UTMRegistryEntry) { + Task { let uuid = await entry.uuid let dict = try await entry.asDictionary() serializedEntries[uuid] = dict - lastUpdateTasks.removeValue(forKey: entry) } } } From 11c5b9a44efd30373273e7daa6e617bf5c97721f Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 27 Aug 2022 23:26:18 -0700 Subject: [PATCH 20/42] data: construct VM list from registry entries --- Configuration/UTMConfigurationWrapper.swift | 4 +- Managers/UTMQemuVirtualMachine.swift | 1 + Managers/UTMRegistry.swift | 65 +++++++++++++++++---- Managers/UTMRegistryEntry.swift | 20 ++----- Managers/UTMVirtualMachine-Protected.h | 6 +- Managers/UTMVirtualMachine.m | 11 +--- Managers/UTMVirtualMachineExtension.swift | 3 +- Managers/UTMWrappedVirtualMachine.swift | 17 ++++-- Platform/UTMData.swift | 42 +++++++++++-- 9 files changed, 121 insertions(+), 48 deletions(-) diff --git a/Configuration/UTMConfigurationWrapper.swift b/Configuration/UTMConfigurationWrapper.swift index 4437a7ec8..5c4b2bfdc 100644 --- a/Configuration/UTMConfigurationWrapper.swift +++ b/Configuration/UTMConfigurationWrapper.swift @@ -317,9 +317,9 @@ import Foundation } } - @objc init(placeholderFor name: String) { + @objc init(placeholderFor name: String, uuid: UUID? = nil) { self.placeholderName = name - self.placeholderUuid = UUID() + self.placeholderUuid = uuid ?? UUID() self.placeholderIconURL = nil } diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 723eb8d89..e0f47cd57 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -183,6 +183,7 @@ extension UTMQemuVirtualMachine { // MARK: - Registry syncing extension UTMQemuVirtualMachine { @MainActor override func updateRegistryPostSave() async throws { + try await super.updateRegistryPostSave() for i in qemuConfig.drives.indices { let drive = qemuConfig.drives[i] if drive.isExternal, let url = drive.imageURL { diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift index 53f28e4de..d65667c7c 100644 --- a/Managers/UTMRegistry.swift +++ b/Managers/UTMRegistry.swift @@ -30,9 +30,11 @@ class UTMRegistry: NSObject { } } + private var registryListener: AnyCancellable? + private var changeListeners: [String: AnyCancellable] = [:] - private var entries: [String: UTMRegistryEntry] { + @Published private var entries: [String: UTMRegistryEntry] { didSet { let toAdd = entries.keys.filter({ !changeListeners.keys.contains($0) }) for key in toAdd { @@ -40,8 +42,8 @@ class UTMRegistry: NSObject { changeListeners[key] = entry.objectWillChange .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .utility)) .sink { [weak self, weak entry] in - if let entry = entry { - self?.commit(entry: entry) + if let self = self, let entry = entry { + self.commit(entry: entry, to: &self.serializedEntries) } } } @@ -61,26 +63,69 @@ class UTMRegistry: NSObject { }) { entries = newEntries } + registryListener = $entries + .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .utility)) + .sink { [weak self] newEntries in + self?.commitAll(entries: newEntries) + } } /// Gets an existing registry entry or create a new entry /// - Parameter vm: UTM virtual machine to locate in the registry /// - Returns: Either an existing registry entry or a new entry @objc func entry(for vm: UTMVirtualMachine) -> UTMRegistryEntry { - if let entry = entries[vm.id] { + if let entry = entries[vm.config.uuid.uuidString] { return entry } - return UTMRegistryEntry(newFrom: vm)! + let newEntry = UTMRegistryEntry(newFrom: vm)! + entries[newEntry.uuid.uuidString] = newEntry + return newEntry + } + + /// Get an existing registry entry for a UUID + /// - Parameter uuidString: UUID + /// - Returns: An existing registry entry or nil if it does not exist + func entry(for uuidString: String) -> UTMRegistryEntry? { + return entries[uuidString] } /// Commit the entry to persistent storage /// This runs in a background queue. /// - Parameter entry: Entry to commit - private func commit(entry: UTMRegistryEntry) { - Task { - let uuid = await entry.uuid - let dict = try await entry.asDictionary() - serializedEntries[uuid] = dict + private func commit(entry: UTMRegistryEntry, to entries: inout [String: Any]) { + let uuid = entry.uuid + if let dict = try? entry.asDictionary() { + entries[uuid.uuidString] = dict + } else { + logger.error("Failed to commit entry for \(uuid)") + } + } + + /// Commit all entries to persistent storage + /// This runs in a background queue. + /// - Parameter entries: All entries to commit + private func commitAll(entries: [String: UTMRegistryEntry]) { + var newSerializedEntries: [String: Any] = [:] + for key in entries.keys { + let entry = entries[key]! + commit(entry: entry, to: &newSerializedEntries) + } + serializedEntries = newSerializedEntries + } + + /// Remove an entry from the registry + /// - Parameter entry: Entry to remove + func remove(entry: UTMRegistryEntry) { + entries.removeValue(forKey: entry.uuid.uuidString) + } + + /// Remove all entries from the registry except for the specified set + /// - Parameter uuidStrings: Keys to NOT remove + func prune(exceptFor uuidStrings: Set) { + for key in entries.keys { + if !uuidStrings.contains(key) { + entries.removeValue(forKey: key) + } } } } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 92594af35..110c7c45e 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -21,7 +21,7 @@ import Foundation @Published private var _package: File - @Published private var _uuid: String + var uuid: UUID @Published private var _isSuspended: Bool @@ -51,7 +51,7 @@ import Foundation return nil } _package = package; - _uuid = vm.config.uuid.uuidString + uuid = vm.config.uuid _isSuspended = false _externalDrives = [:] _sharedDirectories = [] @@ -62,7 +62,7 @@ import Foundation let container = try decoder.container(keyedBy: CodingKeys.self) _name = try container.decode(String.self, forKey: .name) _package = try container.decode(File.self, forKey: .package) - _uuid = try container.decode(String.self, forKey: .uuid) + uuid = try container.decode(UUID.self, forKey: .uuid) _isSuspended = try container.decode(Bool.self, forKey: .isSuspended) _externalDrives = try container.decode([String: File].self, forKey: .externalDrives) _sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) @@ -73,14 +73,14 @@ import Foundation var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(_name, forKey: .name) try container.encode(_package, forKey: .package) - try container.encode(_uuid, forKey: .uuid) + try container.encode(uuid, forKey: .uuid) try container.encode(_isSuspended, forKey: .isSuspended) try container.encode(_externalDrives, forKey: .externalDrives) try container.encode(_sharedDirectories, forKey: .sharedDirectories) try container.encode(_windowSettings, forKey: .windowSettings) } - @MainActor func asDictionary() throws -> [String: Any] { + func asDictionary() throws -> [String: Any] { let encoder = PropertyListEncoder() encoder.outputFormat = .xml let xml = try encoder.encode(self) @@ -121,16 +121,6 @@ extension UTMRegistryEntryDecodable { } } - var uuid: String { - get { - _uuid - } - - set { - _uuid = newValue - } - } - var isSuspended: Bool { get { _isSuspended diff --git a/Managers/UTMVirtualMachine-Protected.h b/Managers/UTMVirtualMachine-Protected.h index 99e6070ae..611527415 100644 --- a/Managers/UTMVirtualMachine-Protected.h +++ b/Managers/UTMVirtualMachine-Protected.h @@ -67,9 +67,13 @@ extern const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions; @property (nonatomic, readonly) NSString *stateLabel; @property (nonatomic, readwrite) NSURL *path; -@property (nonatomic, readwrite, copy) UTMConfigurationWrapper *config; +@property (nonatomic, readwrite) UTMConfigurationWrapper *config; @property (nonatomic, readwrite, nullable) CSScreenshot *screenshot; +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL NS_DESIGNATED_INITIALIZER; + /// Updates the internal state and view state /// @param state New state - (void)changeState:(UTMVMState)state; diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index dbdfbffc6..bdeb99afa 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -185,22 +185,15 @@ + (UTMVirtualMachine *)virtualMachineWithConfiguration:(UTMConfigurationWrapper return [[UTMQemuVirtualMachine alloc] initWithConfiguration:configuration packageURL:packageURL]; } -- (instancetype)init { +- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL { self = [super init]; if (self) { + _state = kVMStopped; #if TARGET_OS_IPHONE self.logging = [UTMLogging sharedInstance]; #else self.logging = [UTMLogging new]; #endif - } - return self; -} - -- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL { - self = [self init]; - if (self) { - _state = kVMStopped; self.config = configuration; self.path = packageURL; self.registryEntry = [UTMRegistry.shared entryFor:self]; diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 8b9c12a56..5da302ae7 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -86,7 +86,8 @@ extension UTMVirtualMachine: ObservableObject { } @MainActor func updateRegistryPostSave() async throws { - // do nothing by default + registryEntry.name = config.name + registryEntry.package = try UTMRegistryEntry.File(url: path) } } diff --git a/Managers/UTMWrappedVirtualMachine.swift b/Managers/UTMWrappedVirtualMachine.swift index 80e3539cc..c10365302 100644 --- a/Managers/UTMWrappedVirtualMachine.swift +++ b/Managers/UTMWrappedVirtualMachine.swift @@ -52,13 +52,13 @@ import Foundation /// - bookmark: Bookmark data for this VM /// - name: Name of this VM /// - path: Path where the VM is located - init(bookmark: Data, name: String, path: URL) { + /// - uuid: UUID of the VM + init(bookmark: Data, name: String, path: URL, uuid: UUID? = nil) { _bookmark = bookmark _name = name _path = path - super.init() - self.path = path - self.config = UTMConfigurationWrapper(placeholderFor: name) + let config = UTMConfigurationWrapper(placeholderFor: name, uuid: uuid) + super.init(configuration: config, packageURL: path) } /// Create a new wrapped UTM VM from an existing UTM VM @@ -70,7 +70,14 @@ import Foundation self.init(bookmark: bookmark, name: vm.detailsTitleLabel, path: vm.path) } - /// Create a new wrapped UTM VM from a dictionary + /// Create a new wrapped UTM VM from a registry entry + /// - Parameter registryEntry: Registry entry + @MainActor convenience init(from registryEntry: UTMRegistryEntry) { + let file = registryEntry.package + self.init(bookmark: file.bookmark, name: registryEntry.name, path: file.url, uuid: registryEntry.uuid) + } + + /// Create a new wrapped UTM VM from a dictionary (legacy support) /// - Parameter info: Dictionary info convenience init?(from info: [String: Any]) { guard let bookmark = info["Bookmark"] as? Data, diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index fa1c33563..3617a7c06 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -152,10 +152,39 @@ class UTMData: ObservableObject { if await virtualMachines != list { await listReplace(with: list) } + // prune the registry + let uuids = list.map({ $0.registryEntry.uuid.uuidString }) + UTMRegistry.shared.prune(exceptFor: Set(uuids)) } /// Load VM list (and order) from persistent storage @MainActor private func listLoadFromDefaults() { + let defaults = UserDefaults.standard + guard defaults.object(forKey: "VMList") == nil else { + listLegacyLoadFromDefaults() + // delete legacy + defaults.removeObject(forKey: "VMList") + return + } + // registry entry list + guard let list = defaults.stringArray(forKey: "VMEntryList") else { + return + } + virtualMachines = list.compactMap { uuidString in + guard let entry = UTMRegistry.shared.entry(for: uuidString) else { + return nil + } + let wrappedVM = UTMWrappedVirtualMachine(from: entry) + if let vm = wrappedVM.unwrap() { + return vm + } else { + return wrappedVM + } + } + } + + /// Load VM list (and order) from persistent storage (legacy) + @MainActor private func listLegacyLoadFromDefaults() { let defaults = UserDefaults.standard // legacy path list if let files = defaults.array(forKey: "VMList") as? [String] { @@ -173,7 +202,11 @@ class UTMData: ObservableObject { } else if let dict = item as? [String: Any] { wrappedVM = UTMWrappedVirtualMachine(from: dict) } - if let vm = wrappedVM?.unwrap() { + if let wrappedVM = wrappedVM, let vm = wrappedVM.unwrap() { + // legacy VMs don't have UUID stored so we made a fake UUID + if wrappedVM.registryEntry.uuid != vm.registryEntry.uuid { + UTMRegistry.shared.remove(entry: wrappedVM.registryEntry) + } return vm } else { return wrappedVM @@ -185,10 +218,8 @@ class UTMData: ObservableObject { /// Save VM list (and order) to persistent storage @MainActor private func listSaveToDefaults() { let defaults = UserDefaults.standard - let wrappedVMs = virtualMachines.compactMap { vm -> [String: Any]? in - UTMWrappedVirtualMachine(placeholderFor: vm)?.serialized - } - defaults.set(wrappedVMs, forKey: "VMList") + let wrappedVMs = virtualMachines.map { $0.registryEntry.uuid.uuidString } + defaults.set(wrappedVMs, forKey: "VMEntryList") } @MainActor private func listReplace(with vms: [UTMVirtualMachine]) { @@ -224,6 +255,7 @@ class UTMData: ObservableObject { selectedVM = nil } vm.isDeleted = true // alert views to update + UTMRegistry.shared.remove(entry: vm.registryEntry) return index } From 9a1eda5d393ced50ed4d19d3750e2418d6b3c173 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 09:26:28 -0700 Subject: [PATCH 21/42] registry: migrate from legacy viewState --- Configuration/Legacy/UTMLegacyViewState.h | 1 + Configuration/Legacy/UTMLegacyViewState.m | 4 ++ Managers/UTMRegistryEntry.swift | 69 ++++++++++++++++++++++- Managers/UTMVirtualMachine.m | 3 + Managers/UTMVirtualMachineExtension.swift | 2 + 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Configuration/Legacy/UTMLegacyViewState.h b/Configuration/Legacy/UTMLegacyViewState.h index 9d05f80bd..ce9cb6a17 100644 --- a/Configuration/Legacy/UTMLegacyViewState.h +++ b/Configuration/Legacy/UTMLegacyViewState.h @@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithDictionary:(NSDictionary *)dictionary NS_DESIGNATED_INITIALIZER; +- (NSArray *)allDrives; - (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive; - (nullable NSString *)pathForRemovableDrive:(NSString *)drive; diff --git a/Configuration/Legacy/UTMLegacyViewState.m b/Configuration/Legacy/UTMLegacyViewState.m index e36c3acfc..29cdf2433 100644 --- a/Configuration/Legacy/UTMLegacyViewState.m +++ b/Configuration/Legacy/UTMLegacyViewState.m @@ -83,6 +83,10 @@ - (NSString *)shortcutBookmarkPath { #pragma mark - Removable drives +- (NSArray *)allDrives { + return [_removableDrives allKeys]; +} + - (nullable NSData *)bookmarkForRemovableDrive:(NSString *)drive { return _removableDrives[drive]; } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 110c7c45e..aee781767 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -188,6 +188,62 @@ extension UTMRegistryEntryDecodable { } } +// MARK: - Migration from UTMViewState + +extension UTMRegistryEntry { + /// Migrate from a view state + /// - Parameter viewState: View state to migrate + private func migrate(viewState: UTMLegacyViewState) { + var primaryWindow = Window() + if viewState.displayScale != .zero { + primaryWindow.scale = viewState.displayScale + } + if viewState.displayOriginX != .zero || viewState.displayOriginY != .zero { + primaryWindow.origin = CGPoint(x: viewState.displayOriginX, + y: viewState.displayOriginY) + } + primaryWindow.isKeyboardVisible = viewState.isKeyboardShown + primaryWindow.isToolbarVisible = viewState.isToolbarShown + if primaryWindow != Window() { + _windowSettings[0] = primaryWindow + } + _isSuspended = viewState.hasSaveState + if let sharedDirectoryBookmark = viewState.sharedDirectory, let sharedDirectoryPath = viewState.sharedDirectoryPath { + if let file = try? File(path: sharedDirectoryPath, + bookmark: sharedDirectoryBookmark) { + _sharedDirectories = [file] + } else { + logger.error("Failed to migrate shared directory \(sharedDirectoryPath) because bookmark is invalid.") + } + } + if let shortcutBookmark = viewState.shortcutBookmark { + _package.remoteBookmark = shortcutBookmark + } + for drive in viewState.allDrives() { + if let bookmark = viewState.bookmark(forRemovableDrive: drive), let path = viewState.path(forRemovableDrive: drive) { + let file = File(path: path, remoteBookmark: bookmark) + _externalDrives[drive] = file + } + } + } + + /// Try to migrate from a view.plist or does nothing if it does not exist. + /// - Parameter viewStateURL: URL to view.plist + @objc func migrateUnsafe(viewStateURL: URL) { + let fileManager = FileManager.default + guard fileManager.fileExists(atPath: viewStateURL.path) else { + return + } + guard let dict = try? NSDictionary(contentsOf: viewStateURL, error: ()) as? [AnyHashable : Any] else { + logger.error("Failed to parse legacy \(viewStateURL)") + return + } + let viewState = UTMLegacyViewState(dictionary: dict) + migrate(viewState: viewState) + try? fileManager.removeItem(at: viewStateURL) // delete view.plist + } +} + // MARK: - Objective C bridging // FIXME: these are NOT synchronized to the actor @objc extension UTMRegistryEntry { @@ -261,6 +317,14 @@ extension UTMRegistryEntry { self.url = url } + fileprivate init(path: String, remoteBookmark: Data) { + self.path = path + self.bookmark = Data() + self.isReadOnly = false + self.url = URL(fileURLWithPath: path) + self.remoteBookmark = remoteBookmark + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) path = try container.decode(String.self, forKey: .path) @@ -279,7 +343,7 @@ extension UTMRegistryEntry { } } - struct Window: Codable { + struct Window: Codable, Equatable { var scale: CGFloat = 1.0 var origin: CGPoint = .zero @@ -298,6 +362,9 @@ extension UTMRegistryEntry { case isDisplayZoomLocked = "DisplayZoomLocked" } + init() { + } + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) scale = try container.decode(CGFloat.self, forKey: .scale) diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index bdeb99afa..d2a39e6d4 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -197,6 +197,9 @@ - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration p self.config = configuration; self.path = packageURL; self.registryEntry = [UTMRegistry.shared entryFor:self]; + // migrate legacy view state + NSURL *viewStateURL = [packageURL URLByAppendingPathComponent:kUTMBundleViewFilename]; + [self.registryEntry migrateUnsafeWithViewStateURL:viewStateURL]; [self loadScreenshot]; self.anyCancellable = [self subscribeToChildren]; } diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 5da302ae7..31af92208 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -87,7 +87,9 @@ extension UTMVirtualMachine: ObservableObject { @MainActor func updateRegistryPostSave() async throws { registryEntry.name = config.name + let oldRemoteBookmark = registryEntry.package.remoteBookmark registryEntry.package = try UTMRegistryEntry.File(url: path) + registryEntry.package.remoteBookmark = oldRemoteBookmark } } From da77353d2eaeadff508063f16e9190a428ea24dc Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 09:28:05 -0700 Subject: [PATCH 22/42] main: no settings bundle and entitlement check on startup for macOS --- Platform/Main.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Platform/Main.swift b/Platform/Main.swift index 43e3d44a9..3547a2b7a 100644 --- a/Platform/Main.swift +++ b/Platform/Main.swift @@ -34,6 +34,7 @@ class Main { static var jitAvailable = true static func main() { + #if os(iOS) registerDefaultsFromSettingsBundle() // check if we have jailbreak if jb_has_jit_entitlement() { @@ -48,6 +49,7 @@ class Main { logger.info("JIT: ptrace() hack failed") jitAvailable = false } + #endif UTMApp.main() } From 958fa1fef93a09eda35d00574b16f0857d1e7702 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 09:54:11 -0700 Subject: [PATCH 23/42] vm(qemu): clean up unreferenced external drives from registry --- Managers/UTMQemuVirtualMachine.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index e0f47cd57..b29ff1e0a 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -193,6 +193,12 @@ extension UTMQemuVirtualMachine { if let url = config.qemuConfig!.sharing.directoryShareUrl { try await changeSharedDirectory(to: url) } + // remove any unreferenced drives + for id in registryEntry.externalDrives.keys { + if !qemuConfig.drives.contains(where: { $0.id == id }) { + registryEntry.externalDrives.removeValue(forKey: id) + } + } } } From 96932e9289ad3c9098531caa52dbebbc1a1addde Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 14:50:38 -0700 Subject: [PATCH 24/42] registry: migrate bookmarks from Apple config.plist --- Configuration/UTMAppleConfiguration.swift | 5 +- .../UTMAppleConfigurationDrive.swift | 13 +---- ...UTMAppleConfigurationSharedDirectory.swift | 1 + Managers/UTMAppleVirtualMachine.swift | 24 ++++++++ Managers/UTMQemuVirtualMachine.swift | 8 +-- Managers/UTMRegistryEntry.swift | 56 +++++++++++++++++++ Managers/UTMVirtualMachine.m | 1 + 7 files changed, 88 insertions(+), 20 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 240dfc189..45de98eb8 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -45,7 +45,7 @@ final class UTMAppleConfiguration: UTMConfiguration { case information = "Information" case system = "System" case virtualization = "Virtualization" - case sharedDirectories = "SharedDirectory" + case sharedDirectories = "SharedDirectory" // legacy case displays = "Display" case drives = "Drive" case networks = "Network" @@ -73,7 +73,7 @@ final class UTMAppleConfiguration: UTMConfiguration { _information = try values.decode(UTMConfigurationInfo.self, forKey: .information) _system = try values.decode(UTMAppleConfigurationSystem.self, forKey: .system) _virtualization = try values.decode(UTMAppleConfigurationVirtualization.self, forKey: .virtualization) - _sharedDirectories = try values.decode([UTMAppleConfigurationSharedDirectory].self, forKey: .sharedDirectories) + _sharedDirectories = try values.decodeIfPresent([UTMAppleConfigurationSharedDirectory].self, forKey: .sharedDirectories) ?? [] _displays = try values.decode([UTMAppleConfigurationDisplay].self, forKey: .displays) _drives = try values.decode([UTMAppleConfigurationDrive].self, forKey: .drives) _networks = try values.decode([UTMAppleConfigurationNetwork].self, forKey: .networks) @@ -91,7 +91,6 @@ final class UTMAppleConfiguration: UTMConfiguration { try container.encode(_information, forKey: .information) try container.encode(_system, forKey: .system) try container.encode(_virtualization, forKey: .virtualization) - try container.encode(_sharedDirectories, forKey: .sharedDirectories) try container.encode(_displays, forKey: .displays) try container.encode(_drives, forKey: .drives) try container.encode(_networks, forKey: .networks) diff --git a/Configuration/UTMAppleConfigurationDrive.swift b/Configuration/UTMAppleConfigurationDrive.swift index 94f22e6fc..1ebd9fb30 100644 --- a/Configuration/UTMAppleConfigurationDrive.swift +++ b/Configuration/UTMAppleConfigurationDrive.swift @@ -37,7 +37,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { private enum CodingKeys: String, CodingKey { case isReadOnly = "ReadOnly" case imageName = "ImageName" - case bookmark = "Bookmark" + case bookmark = "Bookmark" // legacy only case identifier = "Identifier" } @@ -91,17 +91,6 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { try container.encode(isReadOnly, forKey: .isReadOnly) if !isExternal { try container.encodeIfPresent(imageName, forKey: .imageName) - } else { - var options = NSURL.BookmarkCreationOptions.withSecurityScope - if isReadOnly { - options.insert(.securityScopeAllowOnlyReadAccess) - } - _ = imageURL?.startAccessingSecurityScopedResource() - defer { - imageURL?.stopAccessingSecurityScopedResource() - } - let bookmark = try imageURL?.bookmarkData(options: options) - try container.encodeIfPresent(bookmark, forKey: .bookmark) } try container.encode(id, forKey: .identifier) } diff --git a/Configuration/UTMAppleConfigurationSharedDirectory.swift b/Configuration/UTMAppleConfigurationSharedDirectory.swift index 4633a10ab..b45622d60 100644 --- a/Configuration/UTMAppleConfigurationSharedDirectory.swift +++ b/Configuration/UTMAppleConfigurationSharedDirectory.swift @@ -19,6 +19,7 @@ import Virtualization @available(iOS, unavailable, message: "Apple Virtualization not available on iOS") @available(macOS 11, *) +/// Represent a shared directory. This is no longer saved to config.plist in latest versions. struct UTMAppleConfigurationSharedDirectory: Codable, Hashable, Identifiable { var directoryURL: URL? var isReadOnly: Bool diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index ddcbaa5a9..6a31edb53 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -447,3 +447,27 @@ extension UTMAppleVirtualMachineError: LocalizedError { } } } + +// MARK: - Registry access +extension UTMAppleVirtualMachine { + @MainActor override func updateRegistryPostSave() async throws { + try await super.updateRegistryPostSave() + registryEntry.sharedDirectories.removeAll(keepingCapacity: true) + for sharedDirectory in appleConfig.sharedDirectories { + if let url = sharedDirectory.directoryURL { + let file = try UTMRegistryEntry.File(url: url, isReadOnly: sharedDirectory.isReadOnly) + registryEntry.sharedDirectories.append(file) + } + } + for drive in appleConfig.drives { + if drive.isExternal, let url = drive.imageURL { + let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly) + registryEntry.externalDrives[drive.id] = file + } + } + // remove any unreferenced drives + registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in + appleConfig.drives.contains(where: { $0.id == element.key }) + }) + } +} diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index b29ff1e0a..b23cee461 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -194,11 +194,9 @@ extension UTMQemuVirtualMachine { try await changeSharedDirectory(to: url) } // remove any unreferenced drives - for id in registryEntry.externalDrives.keys { - if !qemuConfig.drives.contains(where: { $0.id == id }) { - registryEntry.externalDrives.removeValue(forKey: id) - } - } + registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in + qemuConfig.drives.contains(where: { $0.id == element.key }) + }) } } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index aee781767..ae2b8d6f5 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -31,6 +31,8 @@ import Foundation @Published private var _windowSettings: [Int: Window] + @Published private var _hasMigratedConfig: Bool + private enum CodingKeys: String, CodingKey { case name = "Name" case package = "Package" @@ -39,6 +41,7 @@ import Foundation case externalDrives = "ExternalDrives" case sharedDirectories = "SharedDirectories" case windowSettings = "WindowSettings" + case hasMigratedConfig = "MigratedConfig" } init?(newFrom vm: UTMVirtualMachine) { @@ -56,6 +59,7 @@ import Foundation _externalDrives = [:] _sharedDirectories = [] _windowSettings = [:] + _hasMigratedConfig = false } required init(from decoder: Decoder) throws { @@ -67,6 +71,7 @@ import Foundation _externalDrives = try container.decode([String: File].self, forKey: .externalDrives) _sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories) _windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings) + _hasMigratedConfig = try container.decodeIfPresent(Bool.self, forKey: .hasMigratedConfig) ?? false } func encode(to encoder: Encoder) throws { @@ -78,6 +83,9 @@ import Foundation try container.encode(_externalDrives, forKey: .externalDrives) try container.encode(_sharedDirectories, forKey: .sharedDirectories) try container.encode(_windowSettings, forKey: .windowSettings) + if _hasMigratedConfig { + try container.encode(_hasMigratedConfig, forKey: .hasMigratedConfig) + } } func asDictionary() throws -> [String: Any] { @@ -161,6 +169,16 @@ extension UTMRegistryEntryDecodable { } } + var hasMigratedConfig: Bool { + get { + _hasMigratedConfig + } + + set { + _hasMigratedConfig = newValue + } + } + func setExternalDrive(_ file: File, forId id: String) { externalDrives[id] = file } @@ -242,6 +260,44 @@ extension UTMRegistryEntry { migrate(viewState: viewState) try? fileManager.removeItem(at: viewStateURL) // delete view.plist } + + /// Try to migrate old bookmarks stored in config.plist or does nothing if not appliable. + /// - Parameter config: Config to migrate + @objc func migrate(fromConfig config: UTMConfigurationWrapper) { + #if os(macOS) + if let appleConfig = config.appleConfig { + Task { @MainActor in + if !hasMigratedConfig { + migrate(fromAppleConfig: appleConfig) + hasMigratedConfig = true + } + } + } + #endif + } + + #if os(macOS) + /// Try to migrate bookmarks from an Apple VM config. + /// - Parameter config: Apple config to migrate + @MainActor func migrate(fromAppleConfig config: UTMAppleConfiguration) { + for sharedDirectory in config.sharedDirectories { + if let url = sharedDirectory.directoryURL, + let file = try? File(url: url, isReadOnly: sharedDirectory.isReadOnly) { + sharedDirectories.append(file) + } else { + logger.error("Failed to migrate a shared directory from config.") + } + } + for drive in config.drives { + if drive.isExternal, let url = drive.imageURL, + let file = try? File(url: url, isReadOnly: drive.isReadOnly) { + externalDrives[drive.id] = file + } else { + logger.error("Failed to migrate drive \(drive.id) from config.") + } + } + } + #endif } // MARK: - Objective C bridging diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index d2a39e6d4..54c4eef8c 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -200,6 +200,7 @@ - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration p // migrate legacy view state NSURL *viewStateURL = [packageURL URLByAppendingPathComponent:kUTMBundleViewFilename]; [self.registryEntry migrateUnsafeWithViewStateURL:viewStateURL]; + [self.registryEntry migrateFromConfig:configuration]; [self loadScreenshot]; self.anyCancellable = [self subscribeToChildren]; } From bf14cf674f06e5c15300bd28ca2c6900da79758c Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:37:47 -0700 Subject: [PATCH 25/42] vm: always sync config with registry --- Managers/UTMAppleVirtualMachine.swift | 24 +++++++++--- Managers/UTMQemuVirtualMachine.m | 41 ++++++++++--------- Managers/UTMQemuVirtualMachine.swift | 48 ++++++++++------------- Managers/UTMVirtualMachineExtension.swift | 22 +++++++++-- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index 6a31edb53..b8d85f205 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -450,16 +450,19 @@ extension UTMAppleVirtualMachineError: LocalizedError { // MARK: - Registry access extension UTMAppleVirtualMachine { - @MainActor override func updateRegistryPostSave() async throws { - try await super.updateRegistryPostSave() + @MainActor override func updateRegistryFromConfig() async throws { + // save a copy to not collide with updateConfigFromRegistry() + let configShares = appleConfig.sharedDirectories + let configDrives = appleConfig.drives + try await super.updateRegistryFromConfig() registryEntry.sharedDirectories.removeAll(keepingCapacity: true) - for sharedDirectory in appleConfig.sharedDirectories { + for sharedDirectory in configShares { if let url = sharedDirectory.directoryURL { let file = try UTMRegistryEntry.File(url: url, isReadOnly: sharedDirectory.isReadOnly) registryEntry.sharedDirectories.append(file) } } - for drive in appleConfig.drives { + for drive in configDrives { if drive.isExternal, let url = drive.imageURL { let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly) registryEntry.externalDrives[drive.id] = file @@ -467,7 +470,18 @@ extension UTMAppleVirtualMachine { } // remove any unreferenced drives registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in - appleConfig.drives.contains(where: { $0.id == element.key }) + configDrives.contains(where: { $0.id == element.key && $0.isExternal }) }) } + + @MainActor override func updateConfigFromRegistry() { + super.updateConfigFromRegistry() + appleConfig.sharedDirectories = registryEntry.sharedDirectories.map({ UTMAppleConfigurationSharedDirectory(directoryURL: $0.url, isReadOnly: $0.isReadOnly )}) + for i in appleConfig.drives.indices { + let id = appleConfig.drives[i].id + if let file = registryEntry.externalDrives[id], appleConfig.drives[i].isExternal { + appleConfig.drives[i].imageURL = file.url + } + } + } } diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 0ba92bca7..374b121ed 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -63,8 +63,8 @@ - (void)setIoDelegate:(id)ioDelegate { } } -- (instancetype)init { - self = [super init]; +- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL { + self = [super initWithConfiguration:configuration packageURL:packageURL]; if (self) { self.qemuWillQuitEvent = dispatch_semaphore_create(0); self.qemuDidExitEvent = dispatch_semaphore_create(0); @@ -133,25 +133,6 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { [self.logging logToFile:self.config.qemuDebugLogURL]; } - // set up SPICE sharing and removable drives - NSString *errMsg; - __block NSError *restoreExternalDrivesAndSharesError = nil; - dispatch_semaphore_t restoreExternalDrivesAndSharesEvent = dispatch_semaphore_create(0); - [self restoreExternalDrivesAndSharesWithCompletion:^(NSError *err) { - restoreExternalDrivesAndSharesError = err; - dispatch_semaphore_signal(restoreExternalDrivesAndSharesEvent); - }]; - if (dispatch_semaphore_wait(restoreExternalDrivesAndSharesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { - UTMLog(@"Timed out waiting for external drives and shares to be restored."); - completion([self errorGeneric]); - return; - } - if (restoreExternalDrivesAndSharesError) { - errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore external drives and shares: %@", @"UTMVirtualMachine"), restoreExternalDrivesAndSharesError.localizedDescription]; - completion([self errorWithMessage:errMsg]); - return; - } - if (self.isRunningAsSnapshot) { self.config.qemuIsDisposable = self.isRunningAsSnapshot; } else { @@ -275,6 +256,24 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { return; } assert(self.qemu.isConnected); + // set up SPICE sharing and removable drives + NSString *errMsg; + __block NSError *restoreExternalDrivesAndSharesError = nil; + dispatch_semaphore_t restoreExternalDrivesAndSharesEvent = dispatch_semaphore_create(0); + [self restoreExternalDrivesAndSharesWithCompletion:^(NSError *err) { + restoreExternalDrivesAndSharesError = err; + dispatch_semaphore_signal(restoreExternalDrivesAndSharesEvent); + }]; + if (dispatch_semaphore_wait(restoreExternalDrivesAndSharesEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { + UTMLog(@"Timed out waiting for external drives and shares to be restored."); + completion([self errorGeneric]); + return; + } + if (restoreExternalDrivesAndSharesError) { + errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore external drives and shares: %@", @"UTMVirtualMachine"), restoreExternalDrivesAndSharesError.localizedDescription]; + completion([self errorWithMessage:errMsg]); + return; + } // continue VM boot if (![self.qemu continueBootWithError:&err]) { UTMLog(@"Failed to boot: %@", err); diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index b23cee461..749907bbb 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -29,13 +29,6 @@ extension UTMQemuVirtualMachine { if let oldPath = await registryEntry.externalDrives[drive.id]?.path { system?.stopAccessingPath(oldPath) } - await MainActor.run { - for i in qemuConfig.drives.indices { - if qemuConfig.drives[i].id == drive.id { - qemuConfig.drives[i].imageURL = nil - } - } - } await registryEntry.removeExternalDrive(forId: drive.id) guard let qemu = qemu, qemu.isConnected else { return @@ -65,13 +58,6 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) let newUrl = url ?? URL(fileURLWithPath: path) - await MainActor.run { - for i in qemuConfig.drives.indices { - if qemuConfig.drives[i].id == drive.id { - qemuConfig.drives[i].imageURL = newUrl - } - } - } if let qemu = qemu, qemu.isConnected { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) } @@ -121,7 +107,6 @@ extension UTMQemuVirtualMachine { } @MainActor func clearSharedDirectory() { - qemuConfig.sharing.directoryShareUrl = nil registryEntry.removeAllSharedDirectories() } @@ -136,9 +121,6 @@ extension UTMQemuVirtualMachine { if let ioService = ioService { ioService.changeSharedDirectory(url) } - await MainActor.run { - qemuConfig.sharing.directoryShareUrl = url - } } else if await qemuConfig.sharing.directoryShareMode == .virtfs { let tempBookmark = try url.bookmarkData() try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) @@ -154,9 +136,6 @@ extension UTMQemuVirtualMachine { throw UTMQemuVirtualMachineError.accessDriveImageFailed } await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark) - await MainActor.run { - qemuConfig.sharing.directoryShareUrl = URL(fileURLWithPath: path) - } } func restoreSharedDirectory() async throws { @@ -182,22 +161,37 @@ extension UTMQemuVirtualMachine { // MARK: - Registry syncing extension UTMQemuVirtualMachine { - @MainActor override func updateRegistryPostSave() async throws { - try await super.updateRegistryPostSave() - for i in qemuConfig.drives.indices { - let drive = qemuConfig.drives[i] + @MainActor override func updateRegistryFromConfig() async throws { + // save a copy to not collide with updateConfigFromRegistry() + let configShare = qemuConfig.sharing.directoryShareUrl + let configDrives = qemuConfig.drives + try await super.updateRegistryFromConfig() + for drive in configDrives { if drive.isExternal, let url = drive.imageURL { try await changeMedium(drive, to: url) } } - if let url = config.qemuConfig!.sharing.directoryShareUrl { + if let url = configShare { try await changeSharedDirectory(to: url) } // remove any unreferenced drives registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in - qemuConfig.drives.contains(where: { $0.id == element.key }) + configDrives.contains(where: { $0.id == element.key && $0.isExternal }) }) } + + @MainActor override func updateConfigFromRegistry() { + super.updateConfigFromRegistry() + if let sharedDirectoryURL = sharedDirectoryURL { + qemuConfig.sharing.directoryShareUrl = sharedDirectoryURL + } + for i in qemuConfig.drives.indices { + let id = qemuConfig.drives[i].id + if let file = registryEntry.externalDrives[id], qemuConfig.drives[i].isExternal { + qemuConfig.drives[i].imageURL = file.url + } + } + } } enum UTMQemuVirtualMachineError: Error { diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 31af92208..8385530e2 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -44,8 +44,18 @@ extension UTMVirtualMachine: ObservableObject { }) } s.append(registryEntry.objectWillChange.sink { [weak self] in - self?.objectWillChange.send() + guard let self = self else { + return + } + self.objectWillChange.send() + Task { @MainActor in + self.updateConfigFromRegistry() + } }) + // first sync on construction + Task { @MainActor in + self.updateConfigFromRegistry() + } return s } @@ -71,7 +81,7 @@ extension UTMVirtualMachine: ObservableObject { let newPath = existingPath.deletingLastPathComponent().appendingPathComponent(config.name).appendingPathExtension("utm") do { try await config.save(to: existingPath) - try await updateRegistryPostSave() + try await updateRegistryFromConfig() } catch { try? reloadConfiguration() throw error @@ -85,12 +95,18 @@ extension UTMVirtualMachine: ObservableObject { } } - @MainActor func updateRegistryPostSave() async throws { + /// Called when we save the config + @MainActor func updateRegistryFromConfig() async throws { registryEntry.name = config.name let oldRemoteBookmark = registryEntry.package.remoteBookmark registryEntry.package = try UTMRegistryEntry.File(url: path) registryEntry.package.remoteBookmark = oldRemoteBookmark } + + /// Called whenever the registry entry changes + @MainActor func updateConfigFromRegistry() { + // implement in subclass + } } public extension UTMQemuVirtualMachine { From 48314593c95087d6e2b2685f8835cca4b127a0cc Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:40:47 -0700 Subject: [PATCH 26/42] vm(qemu): reorganize some swift code --- Managers/UTMQemuVirtualMachine.swift | 58 +++++++++++++++++++++-- Managers/UTMVirtualMachineExtension.swift | 51 -------------------- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.swift b/Managers/UTMQemuVirtualMachine.swift index 749907bbb..55ddee069 100644 --- a/Managers/UTMQemuVirtualMachine.swift +++ b/Managers/UTMQemuVirtualMachine.swift @@ -16,12 +16,64 @@ import Foundation -// MARK: - External drives -extension UTMQemuVirtualMachine { - var qemuConfig: UTMQemuConfiguration { +// MARK: - Display details +public extension UTMQemuVirtualMachine { + internal var qemuConfig: UTMQemuConfiguration { config.qemuConfig! } + @MainActor override var detailsTitleLabel: String { + qemuConfig.information.name + } + + @MainActor override var detailsSubtitleLabel: String { + detailsSystemTargetLabel + } + + @MainActor override var detailsNotes: String? { + qemuConfig.information.notes + } + + @MainActor override var detailsSystemTargetLabel: String { + qemuConfig.system.target.prettyValue + } + + @MainActor override var detailsSystemArchitectureLabel: String { + qemuConfig.system.architecture.prettyValue + } + + @MainActor override var detailsSystemMemoryLabel: String { + let bytesInMib = Int64(1048576) + return ByteCountFormatter.string(fromByteCount: Int64(qemuConfig.system.memorySize) * bytesInMib, countStyle: .memory) + } + + /// Check if a QEMU target is supported + /// - Parameter systemArchitecture: QEMU architecture + /// - Returns: true if UTM is compiled with the supporting binaries + internal static func isSupported(systemArchitecture: QEMUArchitecture) -> Bool { + let arch = systemArchitecture.rawValue + let bundleURL = Bundle.main.bundleURL + #if os(macOS) + let contentsURL = bundleURL.appendingPathComponent("Contents", isDirectory: true) + let base = "Versions/A/" + #else + let contentsURL = bundleURL + let base = "" + #endif + let frameworksURL = contentsURL.appendingPathComponent("Frameworks", isDirectory: true) + let framework = frameworksURL.appendingPathComponent("qemu-" + arch + "-softmmu.framework/" + base + "qemu-" + arch + "-softmmu", isDirectory: false) + logger.error("\(framework.path)") + return FileManager.default.fileExists(atPath: framework.path) + } + + /// Check if the current VM target is supported by the host + @objc var isSupported: Bool { + return UTMQemuVirtualMachine.isSupported(systemArchitecture: qemuConfig._system.architecture) + } +} + +// MARK: - External drives +extension UTMQemuVirtualMachine { func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) async throws { guard drive.isExternal else { return diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachineExtension.swift index 8385530e2..0347db843 100644 --- a/Managers/UTMVirtualMachineExtension.swift +++ b/Managers/UTMVirtualMachineExtension.swift @@ -109,57 +109,6 @@ extension UTMVirtualMachine: ObservableObject { } } -public extension UTMQemuVirtualMachine { - @MainActor override var detailsTitleLabel: String { - config.qemuConfig!.information.name - } - - @MainActor override var detailsSubtitleLabel: String { - self.detailsSystemTargetLabel - } - - @MainActor override var detailsNotes: String? { - config.qemuConfig!.information.notes - } - - @MainActor override var detailsSystemTargetLabel: String { - config.qemuConfig!.system.target.prettyValue - } - - @MainActor override var detailsSystemArchitectureLabel: String { - config.qemuConfig!.system.architecture.prettyValue - } - - @MainActor override var detailsSystemMemoryLabel: String { - let bytesInMib = Int64(1048576) - return ByteCountFormatter.string(fromByteCount: Int64(config.qemuConfig!.system.memorySize) * bytesInMib, countStyle: .memory) - } - - /// Check if a QEMU target is supported - /// - Parameter systemArchitecture: QEMU architecture - /// - Returns: true if UTM is compiled with the supporting binaries - internal static func isSupported(systemArchitecture: QEMUArchitecture) -> Bool { - let arch = systemArchitecture.rawValue - let bundleURL = Bundle.main.bundleURL - #if os(macOS) - let contentsURL = bundleURL.appendingPathComponent("Contents", isDirectory: true) - let base = "Versions/A/" - #else - let contentsURL = bundleURL - let base = "" - #endif - let frameworksURL = contentsURL.appendingPathComponent("Frameworks", isDirectory: true) - let framework = frameworksURL.appendingPathComponent("qemu-" + arch + "-softmmu.framework/" + base + "qemu-" + arch + "-softmmu", isDirectory: false) - logger.error("\(framework.path)") - return FileManager.default.fileExists(atPath: framework.path) - } - - /// Check if the current VM target is supported by the host - @objc var isSupported: Bool { - return UTMQemuVirtualMachine.isSupported(systemArchitecture: config.qemuConfig!._system.architecture) - } -} - // MARK: - Bookmark handling extension URL { private static var defaultCreationOptions: BookmarkCreationOptions { From 82fac2cc8693c2d83dfb309c4e6016ee2eeb803f Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:45:07 -0700 Subject: [PATCH 27/42] project: renamed UTMVirtualMachineExtension.swift -> UTMVirtualMachine.swift --- ...neExtension.swift => UTMVirtualMachine.swift} | 0 UTM.xcodeproj/project.pbxproj | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) rename Managers/{UTMVirtualMachineExtension.swift => UTMVirtualMachine.swift} (100%) diff --git a/Managers/UTMVirtualMachineExtension.swift b/Managers/UTMVirtualMachine.swift similarity index 100% rename from Managers/UTMVirtualMachineExtension.swift rename to Managers/UTMVirtualMachine.swift diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 5c090200c..d294ab2d1 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -261,8 +261,8 @@ CE020BA924AEDF3000B44AB6 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CE020BA824AEDF3000B44AB6 /* Logging */; }; CE020BAB24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */; }; CE020BAC24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */; }; - CE020BB624B14F8400B44AB6 /* UTMVirtualMachineExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */; }; - CE020BB724B14F8400B44AB6 /* UTMVirtualMachineExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */; }; + CE020BB624B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */; }; + CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */; }; CE03D05224D90B4E00F76B84 /* UTMQemuSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */; }; CE03D05324D90B4E00F76B84 /* UTMQemuSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */; }; CE03D08624D90F0700F76B84 /* gmodule-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63D822653C7300FC7E63 /* gmodule-2.0.0.framework */; }; @@ -911,7 +911,7 @@ CEA45E69263519B5002FA97D /* VMConfigQEMUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953924AD4F980059923A /* VMConfigQEMUView.swift */; }; CEA45E6A263519B5002FA97D /* VMDisplayMetalViewController+Touch.m in Sources */ = {isa = PBXBuildFile; fileRef = CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */; }; CEA45E6B263519B5002FA97D /* UTMLegacyQemuConfiguration+Display.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */; }; - CEA45E6C263519B5002FA97D /* UTMVirtualMachineExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */; }; + CEA45E6C263519B5002FA97D /* UTMVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */; }; CEA45E6D263519B5002FA97D /* qapi-visit-block-core.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0E023FCEC04001177D6 /* qapi-visit-block-core.c */; }; CEA45E6F263519B5002FA97D /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; }; CEA45E70263519B5002FA97D /* qapi-util.c in Sources */ = {isa = PBXBuildFile; fileRef = CECC764E2273A7D50059B955 /* qapi-util.c */; }; @@ -1697,7 +1697,7 @@ C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = ""; }; CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = ""; }; - CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachineExtension.swift; sourceTree = ""; }; + CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachine.swift; sourceTree = ""; }; CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMQemuSystem.m; sourceTree = ""; }; CE03D05424D90BE000F76B84 /* UTMQemuSystem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMQemuSystem.h; sourceTree = ""; }; CE03D0D024D9A62B00F76B84 /* QEMUHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMUHelper.entitlements; sourceTree = ""; }; @@ -3045,6 +3045,7 @@ CEF83EC924FB1BB200557D15 /* UTMQemuVirtualMachine+SPICE.h */, CEF83EC624FB1B9300557D15 /* UTMQemuVirtualMachine+SPICE.m */, CE2B89352262B2F600C6D9D8 /* UTMVirtualMachineDelegate.h */, + CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */, CE2B89332262A21E00C6D9D8 /* UTMVirtualMachine.h */, CE5F165B2261395000F3D56B /* UTMVirtualMachine.m */, 84FCABBD268CE4080036196C /* UTMVirtualMachine-Private.h */, @@ -3057,7 +3058,6 @@ E2D64BE0241EAEBE0034E0C6 /* UTMSpiceIODelegate.h */, CE059DC6243E9E3400338317 /* UTMLocationManager.h */, CE059DC7243E9E3400338317 /* UTMLocationManager.m */, - CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */, 835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */, 84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */, 841E997428AA1191003C6CB6 /* UTMRegistry.swift */, @@ -3602,7 +3602,7 @@ CE2D92A024AD46670059923A /* VMDisplayMetalViewController+Touch.m in Sources */, CE2D92A124AD46670059923A /* UTMLegacyQemuConfiguration+Display.m in Sources */, CEF0304E26A2AFBE00667B63 /* BigButtonStyle.swift in Sources */, - CE020BB624B14F8400B44AB6 /* UTMVirtualMachineExtension.swift in Sources */, + CE020BB624B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */, 841619AE28431952000034B2 /* UTMQemuConfigurationSystem.swift in Sources */, CEBE820726A4C74E007AAB12 /* VMWizardSharingView.swift in Sources */, CE2D92A224AD46670059923A /* qapi-visit-block-core.c in Sources */, @@ -3812,7 +3812,7 @@ 2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */, CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */, 85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */, - CE020BB724B14F8400B44AB6 /* UTMVirtualMachineExtension.swift in Sources */, + CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */, CE0B6D4424AD584C00FE012D /* qapi-visit-block.c in Sources */, 845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */, CE0B6D4C24AD584C00FE012D /* qapi-types-misc.c in Sources */, @@ -4183,7 +4183,7 @@ CEA45E69263519B5002FA97D /* VMConfigQEMUView.swift in Sources */, CEA45E6A263519B5002FA97D /* VMDisplayMetalViewController+Touch.m in Sources */, CEA45E6B263519B5002FA97D /* UTMLegacyQemuConfiguration+Display.m in Sources */, - CEA45E6C263519B5002FA97D /* UTMVirtualMachineExtension.swift in Sources */, + CEA45E6C263519B5002FA97D /* UTMVirtualMachine.swift in Sources */, 84B36D2A27B790BE00C22685 /* DestructiveButton.swift in Sources */, CEA45E6D263519B5002FA97D /* qapi-visit-block-core.c in Sources */, CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */, From a54283f9052a179617e72a0df9b0d5d8e68430d2 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 16:41:00 -0700 Subject: [PATCH 28/42] vm: remove bookmark handling This has moved to UTMRegistryEntry. --- Managers/UTMQemuVirtualMachine+SPICE.m | 5 ---- Managers/UTMRegistry.swift | 2 +- Managers/UTMRegistryEntry.swift | 20 ++++++------- Managers/UTMVirtualMachine-Private.h | 4 +-- Managers/UTMVirtualMachine-Protected.h | 2 -- Managers/UTMVirtualMachine.h | 9 ------ Managers/UTMVirtualMachine.m | 37 ------------------------- Managers/UTMVirtualMachine.swift | 8 ++---- Managers/UTMWrappedVirtualMachine.swift | 16 ++--------- Platform/UTMData.swift | 7 +---- 10 files changed, 18 insertions(+), 92 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine+SPICE.m b/Managers/UTMQemuVirtualMachine+SPICE.m index 9df5d8553..3b651fa2f 100644 --- a/Managers/UTMQemuVirtualMachine+SPICE.m +++ b/Managers/UTMQemuVirtualMachine+SPICE.m @@ -26,11 +26,6 @@ @import CocoaSpice; #endif -extern NSString *const kUTMErrorDomain; - -extern const NSURLBookmarkCreationOptions kUTMBookmarkCreationOptions; -extern const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions; - @interface UTMQemuVirtualMachine () @property (nonatomic, readonly, nullable) UTMQemuManager *qemu; diff --git a/Managers/UTMRegistry.swift b/Managers/UTMRegistry.swift index d65667c7c..2bbfa4f86 100644 --- a/Managers/UTMRegistry.swift +++ b/Managers/UTMRegistry.swift @@ -77,7 +77,7 @@ class UTMRegistry: NSObject { if let entry = entries[vm.config.uuid.uuidString] { return entry } - let newEntry = UTMRegistryEntry(newFrom: vm)! + let newEntry = UTMRegistryEntry(newFrom: vm) entries[newEntry.uuid.uuidString] = newEntry return newEntry } diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index ae2b8d6f5..fa76e8c23 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -44,16 +44,16 @@ import Foundation case hasMigratedConfig = "MigratedConfig" } - init?(newFrom vm: UTMVirtualMachine) { - guard let bookmark = vm.bookmark else { - return nil + init(newFrom vm: UTMVirtualMachine) { + let package: File? + let path = vm.path + if let wrappedVM = vm as? UTMWrappedVirtualMachine { + package = try? File(path: path.path, bookmark: wrappedVM.bookmark) + } else { + package = try? File(url: path) } - let path = vm.path.path _name = vm.detailsTitleLabel - guard let package = try? File(path: path, bookmark: bookmark, isReadOnly: false) else { - return nil - } - _package = package; + _package = package ?? File(path: path.path) uuid = vm.config.uuid _isSuspended = false _externalDrives = [:] @@ -373,7 +373,7 @@ extension UTMRegistryEntry { self.url = url } - fileprivate init(path: String, remoteBookmark: Data) { + fileprivate init(path: String, remoteBookmark: Data = Data()) { self.path = path self.bookmark = Data() self.isReadOnly = false @@ -387,7 +387,7 @@ extension UTMRegistryEntry { bookmark = try container.decode(Data.self, forKey: .bookmark) isReadOnly = try container.decode(Bool.self, forKey: .isReadOnly) remoteBookmark = try container.decodeIfPresent(Data.self, forKey: .remoteBookmark) - url = try URL(resolvingPersistentBookmarkData: bookmark) + url = (try? URL(resolvingPersistentBookmarkData: bookmark)) ?? URL(fileURLWithPath: path) } func encode(to encoder: Encoder) throws { diff --git a/Managers/UTMVirtualMachine-Private.h b/Managers/UTMVirtualMachine-Private.h index 0135baa95..fdba02f6d 100644 --- a/Managers/UTMVirtualMachine-Private.h +++ b/Managers/UTMVirtualMachine-Private.h @@ -20,15 +20,13 @@ NS_ASSUME_NONNULL_BEGIN @interface UTMVirtualMachine () -@property (nonatomic, readwrite, nullable) NSData *bookmark; - /// Reference to logger for VM stdout/stderr @property (nonatomic) UTMLogging *logging; @property (nonatomic, assign, readwrite) UTMVMState state; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL; +- (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL NS_DESIGNATED_INITIALIZER; @end diff --git a/Managers/UTMVirtualMachine-Protected.h b/Managers/UTMVirtualMachine-Protected.h index 611527415..cd9d73a11 100644 --- a/Managers/UTMVirtualMachine-Protected.h +++ b/Managers/UTMVirtualMachine-Protected.h @@ -21,8 +21,6 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const kUTMBundleConfigFilename; -extern const NSURLBookmarkCreationOptions kUTMBookmarkCreationOptions; -extern const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions; @interface UTMVirtualMachine () diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index a1db94a42..e5fcb2618 100644 --- a/Managers/UTMVirtualMachine.h +++ b/Managers/UTMVirtualMachine.h @@ -35,11 +35,6 @@ NS_ASSUME_NONNULL_BEGIN /// This property is observable and must only be accessed on the main thread. @property (nonatomic) BOOL isShortcut; -/// Bookmark data of the .utm bundle (can be inside or outside storage) -/// -/// This is nil if a bookmark cannot be created for any reason (such as access denied) -@property (nonatomic, readonly, nullable) NSData *bookmark; - /// Set by caller to handle VM events @property (nonatomic, weak, nullable) id delegate; @@ -106,10 +101,6 @@ NS_ASSUME_NONNULL_BEGIN /// @param url File URL + (nullable UTMVirtualMachine *)virtualMachineWithURL:(NSURL *)url; -/// Create an existing UTM virtual machine from a file bookmark -/// @param bookmark Bookmark data -+ (nullable UTMVirtualMachine *)virtualMachineWithBookmark:(NSData *)bookmark; - /// Create a new UTM virtual machine from a configuration /// /// `-saveUTMWithCompletion:` should be called to save to disk. diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index 54c4eef8c..c4beaee96 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -32,14 +32,6 @@ NSString *const kUTMBundleViewFilename = @"view.plist"; NSString *const kUTMBundleScreenshotFilename = @"screenshot.png"; -#if TARGET_OS_IPHONE -const NSURLBookmarkCreationOptions kUTMBookmarkCreationOptions = NSURLBookmarkCreationMinimalBookmark; -const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions = 0; -#else -const NSURLBookmarkCreationOptions kUTMBookmarkCreationOptions = NSURLBookmarkCreationWithSecurityScope; -const NSURLBookmarkResolutionOptions kUTMBookmarkResolutionOptions = NSURLBookmarkResolutionWithSecurityScope; -#endif - const dispatch_time_t kScreenshotPeriodSeconds = 60 * NSEC_PER_SEC; @interface UTMVirtualMachine () @@ -54,8 +46,6 @@ @interface UTMVirtualMachine () @implementation UTMVirtualMachine -@synthesize bookmark = _bookmark; - // MARK: - Observable properties - (void)setState:(UTMVMState)state { @@ -114,16 +104,6 @@ - (BOOL)hasSaveState { // MARK: - Other properties -- (NSData *)bookmark { - if (!_bookmark) { - _bookmark = [self.path bookmarkDataWithOptions:kUTMBookmarkCreationOptions - includingResourceValuesForKeys:nil - relativeToURL:nil - error:nil]; - } - return _bookmark; -} - - (void)setPath:(NSURL *)path { if (_path && self.isScopedAccess) { [_path stopAccessingSecurityScopedResource]; @@ -157,23 +137,6 @@ + (nullable UTMVirtualMachine *)virtualMachineWithURL:(NSURL *)url { } } -+ (UTMVirtualMachine *)virtualMachineWithBookmark:(NSData *)bookmark { - BOOL stale; - NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark - options:kUTMBookmarkResolutionOptions - relativeToURL:nil - bookmarkDataIsStale:&stale - error:nil]; - if (!url) { - return nil; - } - UTMVirtualMachine *vm = [UTMVirtualMachine virtualMachineWithURL:url]; - if (!stale) { - vm.bookmark = bookmark; - } - return vm; -} - + (UTMVirtualMachine *)virtualMachineWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(nonnull NSURL *)packageURL { #if TARGET_OS_OSX if (@available(macOS 11, *)) { diff --git a/Managers/UTMVirtualMachine.swift b/Managers/UTMVirtualMachine.swift index 0347db843..6780e1b05 100644 --- a/Managers/UTMVirtualMachine.swift +++ b/Managers/UTMVirtualMachine.swift @@ -17,12 +17,8 @@ import Foundation extension UTMVirtualMachine: Identifiable { - public var id: String { - if self.bookmark != nil { - return bookmark!.base64EncodedString() - } else { - return self.path.path // path if we're an existing VM - } + public var id: (UUID, String) { + return (registryEntry.uuid, path.path) } } diff --git a/Managers/UTMWrappedVirtualMachine.swift b/Managers/UTMWrappedVirtualMachine.swift index c10365302..f8d8027ec 100644 --- a/Managers/UTMWrappedVirtualMachine.swift +++ b/Managers/UTMWrappedVirtualMachine.swift @@ -26,7 +26,7 @@ import Foundation NSLocalizedString("Unavailable", comment: "UTMUnavailableVirtualMachine") } - override var bookmark: Data? { + var bookmark: Data { _bookmark } @@ -61,15 +61,6 @@ import Foundation super.init(configuration: config, packageURL: path) } - /// Create a new wrapped UTM VM from an existing UTM VM - /// - Parameter vm: Existing VM - convenience init?(placeholderFor vm: UTMVirtualMachine) { - guard let bookmark = vm.bookmark else { - return nil - } - self.init(bookmark: bookmark, name: vm.detailsTitleLabel, path: vm.path) - } - /// Create a new wrapped UTM VM from a registry entry /// - Parameter registryEntry: Registry entry @MainActor convenience init(from registryEntry: UTMRegistryEntry) { @@ -98,9 +89,8 @@ import Foundation /// Unwrap to a fully formed UTM VM /// - Returns: New UTM VM if it is valid and can be accessed - @available(iOS 14, macOS 11, *) - public func unwrap() -> UTMVirtualMachine? { - guard let vm = UTMVirtualMachine(bookmark: _bookmark) else { + @MainActor func unwrap() -> UTMVirtualMachine? { + guard let vm = UTMVirtualMachine(url: registryEntry.package.url) else { return nil } let defaultStorageUrl = UTMData.defaultStorageUrl.standardizedFileURL diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 3617a7c06..e0802afc8 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -115,12 +115,7 @@ class UTMData: ObservableObject { for i in list.indices.reversed() { let vm = list[i] if !fileManager.fileExists(atPath: vm.path.path) { - if let wrappedVM = UTMWrappedVirtualMachine(placeholderFor: vm) { - list[i] = wrappedVM - } else { - // we cannot even make a placeholder, then remove the element - list.remove(at: i) - } + list[i] = await UTMWrappedVirtualMachine(from: vm.registryEntry) } } // now look for and add new VMs in default storage From 2d18721832f9e7479a49082f73505eb5921a3b21 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 17:15:42 -0700 Subject: [PATCH 29/42] settings(apple): set shared directories/external drives from registry --- Managers/UTMRegistryEntry.swift | 4 +- Platform/Shared/VMDetailsView.swift | 4 +- .../VMDisplayAppleWindowController.swift | 27 +++--- .../macOS/VMAppleRemovableDrivesView.swift | 83 +++++++------------ 4 files changed, 50 insertions(+), 68 deletions(-) diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index fa76e8c23..1faffd15b 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -341,7 +341,7 @@ extension UTMRegistryEntry { } extension UTMRegistryEntry { - struct File: Codable { + struct File: Codable, Identifiable { var url: URL var path: String @@ -352,6 +352,8 @@ extension UTMRegistryEntry { var isReadOnly: Bool + let id: UUID = UUID() + private enum CodingKeys: String, CodingKey { case path = "Path" case bookmark = "Bookmark" diff --git a/Platform/Shared/VMDetailsView.swift b/Platform/Shared/VMDetailsView.swift index 353f6944c..0fd06d0e3 100644 --- a/Platform/Shared/VMDetailsView.swift +++ b/Platform/Shared/VMDetailsView.swift @@ -67,7 +67,7 @@ struct VMDetailsView: View { }.padding([.leading, .trailing]) #if os(macOS) if let appleVM = vm as? UTMAppleVirtualMachine { - VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig) + VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig, registryEntry: appleVM.registryEntry) .padding([.leading, .trailing, .bottom]) } else if let qemuVM = vm as? UTMQemuVirtualMachine { VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) @@ -88,7 +88,7 @@ struct VMDetailsView: View { } #if os(macOS) if let appleVM = vm as? UTMAppleVirtualMachine { - VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig) + VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig, registryEntry: appleVM.registryEntry) } else if let qemuVM = vm as? UTMQemuVirtualMachine { VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig) } diff --git a/Platform/macOS/Display/VMDisplayAppleWindowController.swift b/Platform/macOS/Display/VMDisplayAppleWindowController.swift index 7c261481a..3eee02d45 100644 --- a/Platform/macOS/Display/VMDisplayAppleWindowController.swift +++ b/Platform/macOS/Display/VMDisplayAppleWindowController.swift @@ -148,12 +148,11 @@ class VMDisplayAppleWindowController: VMDisplayWindowController { extension VMDisplayAppleWindowController { func openShareMenu(_ sender: Any) { let menu = NSMenu() - for i in appleConfig.sharedDirectories.indices { + let entry = appleVM.registryEntry + for i in entry.sharedDirectories.indices { let item = NSMenuItem() - let sharedDirectory = appleConfig.sharedDirectories[i] - guard let name = sharedDirectory.directoryURL?.lastPathComponent else { - continue - } + let sharedDirectory = entry.sharedDirectories[i] + let name = sharedDirectory.url.lastPathComponent item.title = name let submenu = NSMenu() let ro = NSMenuItem(title: NSLocalizedString("Read Only", comment: "VMDisplayAppleController"), @@ -188,8 +187,9 @@ extension VMDisplayAppleWindowController { @objc func addShare(sender: AnyObject) { pickShare { url in - let sharedDirectory = UTMAppleConfigurationSharedDirectory(directoryURL: url) - self.appleConfig.sharedDirectories.append(sharedDirectory) + if let sharedDirectory = try? UTMRegistryEntry.File(url: url) { + self.appleVM.registryEntry.sharedDirectories.append(sharedDirectory) + } } } @@ -199,10 +199,11 @@ extension VMDisplayAppleWindowController { return } let i = menu.tag - let isReadOnly = appleConfig.sharedDirectories[i].isReadOnly + let isReadOnly = appleVM.registryEntry.sharedDirectories[i].isReadOnly pickShare { url in - let sharedDirectory = UTMAppleConfigurationSharedDirectory(directoryURL: url, isReadOnly: isReadOnly) - self.appleConfig.sharedDirectories[i] = sharedDirectory + if let sharedDirectory = try? UTMRegistryEntry.File(url: url, isReadOnly: isReadOnly) { + self.appleVM.registryEntry.sharedDirectories[i] = sharedDirectory + } } } @@ -212,8 +213,8 @@ extension VMDisplayAppleWindowController { return } let i = menu.tag - let isReadOnly = appleConfig.sharedDirectories[i].isReadOnly - appleConfig.sharedDirectories[i].isReadOnly = !isReadOnly + let isReadOnly = appleVM.registryEntry.sharedDirectories[i].isReadOnly + appleVM.registryEntry.sharedDirectories[i].isReadOnly = !isReadOnly } @objc func removeShare(sender: AnyObject) { @@ -222,7 +223,7 @@ extension VMDisplayAppleWindowController { return } let i = menu.tag - appleConfig.sharedDirectories.remove(at: i) + appleVM.registryEntry.sharedDirectories.remove(at: i) } func pickShare(_ onComplete: @escaping (URL) -> Void) { diff --git a/Platform/macOS/VMAppleRemovableDrivesView.swift b/Platform/macOS/VMAppleRemovableDrivesView.swift index 70a095f34..1ad519877 100644 --- a/Platform/macOS/VMAppleRemovableDrivesView.swift +++ b/Platform/macOS/VMAppleRemovableDrivesView.swift @@ -24,11 +24,12 @@ struct VMAppleRemovableDrivesView: View { @ObservedObject var vm: UTMAppleVirtualMachine @ObservedObject var config: UTMAppleConfiguration + @ObservedObject var registryEntry: UTMRegistryEntry @EnvironmentObject private var data: UTMData @State private var fileImportPresented: Bool = false @State private var selectType: SelectType = .sharedDirectory - @State private var selectedSharedDirectoryBinding: Binding? - @State private var selectedDiskImageBinding: Binding? + @State private var selectedSharedDirectoryBinding: Binding? + @State private var selectedDiskImage: UTMAppleConfigurationDrive? /// Explanation see "SwiftUI FileImporter modal bug" in `showFileImporter` @State private var workaroundFileImporterBug: Bool = false @@ -44,10 +45,8 @@ struct VMAppleRemovableDrivesView: View { var body: some View { Group { - ForEach($config.sharedDirectories) { $sharedDirectory in + ForEach($registryEntry.sharedDirectories) { $sharedDirectory in HStack { - // Is a shared directory set? - let hasSharedDir = sharedDirectory.directoryURL != nil // Browse/Clear menu Menu { // Browse button @@ -58,19 +57,17 @@ struct VMAppleRemovableDrivesView: View { }, label: { Label("Browse…", systemImage: "doc.badge.plus") }) - if hasSharedDir { - // Clear button - Button(action: { - deleteShareDirectory(sharedDirectory) - }, label: { - Label("Remove", systemImage: "eject") - }) - } + // Clear button + Button(action: { + deleteShareDirectory(sharedDirectory) + }, label: { + Label("Remove", systemImage: "eject") + }) } label: { - Label("Shared Directory", systemImage: hasSharedDir ? "externaldrive.fill.badge.person.crop" : "externaldrive.badge.person.crop") + Label("Shared Directory", systemImage: "externaldrive.fill.badge.person.crop") } Spacer() - FilePath(url: sharedDirectory.directoryURL) + FilePath(url: sharedDirectory.url) } } ForEach($config.drives) { $diskImage in @@ -81,15 +78,15 @@ struct VMAppleRemovableDrivesView: View { // Browse button Button(action: { selectType = .diskImage - selectedDiskImageBinding = $diskImage + selectedDiskImage = diskImage showFileImporter() }, label: { Label("Browse…", systemImage: "doc.badge.plus") }) // Eject button if diskImage.isExternal && diskImage.imageURL != nil { - Button(action: { deleteRemovableImage(diskImage) }, label: { - Label("Remove", systemImage: "eject") + Button(action: { clearRemovableImage(diskImage) }, label: { + Label("Clear", systemImage: "eject") }) } } label: { @@ -112,11 +109,6 @@ struct VMAppleRemovableDrivesView: View { showFileImporter() } } - Button("New External Drive…") { - selectType = .diskImage - selectedDiskImageBinding = nil - showFileImporter() - } }.fileImporter(isPresented: $fileImportPresented, allowedContentTypes: selectType == .sharedDirectory ? [.folder] : [.data]) { result in if selectType == .sharedDirectory { if let binding = selectedSharedDirectoryBinding { @@ -126,11 +118,9 @@ struct VMAppleRemovableDrivesView: View { createShareDirectory(result) } } else { - if let binding = selectedDiskImageBinding { - selectRemovableImage(for: binding, result: result) - selectedDiskImageBinding = nil - } else { - createRemovableImage(result) + if let diskImage = selectedDiskImage { + selectRemovableImage(for: diskImage, result: result) + selectedDiskImage = nil } } }.onChange(of: workaroundFileImporterBug) { doWorkaround in @@ -178,50 +168,39 @@ struct VMAppleRemovableDrivesView: View { } } - private func selectShareDirectory(for binding: Binding, result: Result) { + private func selectShareDirectory(for binding: Binding, result: Result) { data.busyWorkAsync { let url = try result.get() - binding.wrappedValue.directoryURL = url + binding.wrappedValue.url = url } } private func createShareDirectory(_ result: Result) { data.busyWorkAsync { let url = try result.get() - let sharedDirectory = UTMAppleConfigurationSharedDirectory(directoryURL: url) + let sharedDirectory = try UTMRegistryEntry.File(url: url) await MainActor.run { - config.sharedDirectories.append(sharedDirectory) + registryEntry.sharedDirectories.append(sharedDirectory) } } } - private func deleteShareDirectory(_ sharedDirectory: UTMAppleConfigurationSharedDirectory) { - config.sharedDirectories.removeAll { existing in - existing == sharedDirectory + private func deleteShareDirectory(_ sharedDirectory: UTMRegistryEntry.File) { + vm.registryEntry.sharedDirectories.removeAll { existing in + existing.url == sharedDirectory.url } } - private func selectRemovableImage(for binding: Binding, result: Result) { + private func selectRemovableImage(for diskImage: UTMAppleConfigurationDrive, result: Result) { data.busyWorkAsync { let url = try result.get() - binding.wrappedValue.imageURL = url + let file = try UTMRegistryEntry.File(url: url) + await registryEntry.setExternalDrive(file, forId: diskImage.id) } } - private func createRemovableImage(_ result: Result) { - data.busyWorkAsync { - let url = try result.get() - let diskImage = UTMAppleConfigurationDrive(existingURL: url, isReadOnly: false, isExternal: true) - await MainActor.run { - config.drives.append(diskImage) - } - } - } - - private func deleteRemovableImage(_ diskImage: UTMAppleConfigurationDrive) { - config.drives.removeAll { existing in - existing == diskImage - } + private func clearRemovableImage(_ diskImage: UTMAppleConfigurationDrive) { + registryEntry.removeExternalDrive(forId: diskImage.id) } } @@ -230,6 +209,6 @@ struct VMAppleRemovableDrivesView_Previews: PreviewProvider { @StateObject static var config = UTMAppleConfiguration() static var previews: some View { - VMAppleRemovableDrivesView(vm: vm, config: config) + VMAppleRemovableDrivesView(vm: vm, config: config, registryEntry: vm.registryEntry) } } From 6212edb07028c21d5967703fbbf62f7f75816a40 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:49:47 -0700 Subject: [PATCH 30/42] display(iOS): save window state to registry --- Platform/iOS/VMWindowState.swift | 35 ++++++++++++++++++++++++++------ Platform/iOS/VMWindowView.swift | 20 +++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/Platform/iOS/VMWindowState.swift b/Platform/iOS/VMWindowState.swift index a63f03676..128c55aab 100644 --- a/Platform/iOS/VMWindowState.swift +++ b/Platform/iOS/VMWindowState.swift @@ -54,12 +54,6 @@ struct VMWindowState: Identifiable { } } - var displayOriginY: Float = 0.0 { - didSet { - isViewportChanged = shouldViewportChange - } - } - var displayViewSize: CGSize = .zero var isDisplayZoomLocked: Bool = false @@ -163,3 +157,32 @@ extension VMWindowState { } } } + +// MARK: - Persist changes + +@MainActor extension VMWindowState { + func saveWindow(to registryEntry: UTMRegistryEntry, device: Device?) { + guard case let .display(_, id) = device else { + return + } + var window = UTMRegistryEntry.Window() + window.scale = displayScale + window.origin = displayOrigin + window.isDisplayZoomLocked = isDisplayZoomLocked + window.isKeyboardVisible = isKeyboardShown + registryEntry.windowSettings[id] = window + } + + mutating func restoreWindow(from registryEntry: UTMRegistryEntry, device: Device?) { + guard case let .display(display, id) = device else { + return + } + let window = registryEntry.windowSettings[id] ?? UTMRegistryEntry.Window() + display.viewportScale = window.scale + display.viewportOrigin = window.origin + displayScale = window.scale + displayOrigin = window.origin + isDisplayZoomLocked = window.isDisplayZoomLocked + isKeyboardRequested = window.isKeyboardVisible + } +} diff --git a/Platform/iOS/VMWindowView.swift b/Platform/iOS/VMWindowView.swift index 0fa5f966f..794c55dbf 100644 --- a/Platform/iOS/VMWindowView.swift +++ b/Platform/iOS/VMWindowView.swift @@ -133,10 +133,12 @@ struct VMWindowView: View { state.device = nil } } - .onChange(of: state.device) { newDevice in + .onChange(of: state.device) { [oldDevice = state.device] newDevice in if session.windowDeviceMap[state.id] != newDevice { session.windowDeviceMap[state.id] = newDevice } + state.saveWindow(to: session.vm.registryEntry, device: oldDevice) + state.restoreWindow(from: session.vm.registryEntry, device: newDevice) } #if !WITH_QEMU_TCI .onChange(of: session.mostRecentConnectedDevice) { newValue in @@ -155,8 +157,8 @@ struct VMWindowView: View { state.alert = .fatalError(message) } } - .onChange(of: session.vmState) { newValue in - vmStateUpdated(newValue) + .onChange(of: session.vmState) { [oldValue = session.vmState] newValue in + vmStateUpdated(from: oldValue, to: newValue) } .onReceive(keyboardDidShowNotification) { _ in state.isKeyboardShown = true @@ -177,13 +179,14 @@ struct VMWindowView: View { return } if newValue == .background { + saveWindow() session.didEnterBackground() } else if newValue == .active { session.didEnterForeground() } } .onAppear { - vmStateUpdated(session.vmState) + vmStateUpdated(from: nil, to: session.vmState) session.registerWindow(state.id, isExternal: !isInteractive) if !isInteractive { session.externalWindowBinding = $state @@ -197,7 +200,10 @@ struct VMWindowView: View { } } - private func vmStateUpdated(_ vmState: UTMVMState) { + private func vmStateUpdated(from oldState: UTMVMState?, to vmState: UTMVMState) { + if oldState == .vmStarted { + saveWindow() + } switch vmState { case .vmStopped, .vmPaused: withOptionalAnimation { @@ -223,6 +229,10 @@ struct VMWindowView: View { break } } + + private func saveWindow() { + state.saveWindow(to: session.vm.registryEntry, device: state.device) + } } private struct HeadlessView: View { From a842ed55984e2b891c5f317db3ca9998449525ec Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:57:51 -0700 Subject: [PATCH 31/42] settings: clarified support for sharing options --- Configuration/QEMUConstant.swift | 4 ++-- Platform/Shared/VMConfigSharingView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Configuration/QEMUConstant.swift b/Configuration/QEMUConstant.swift index c2509647b..f8aa91081 100644 --- a/Configuration/QEMUConstant.swift +++ b/Configuration/QEMUConstant.swift @@ -381,8 +381,8 @@ enum QEMUFileShareMode: String, CaseIterable, QEMUConstant { var prettyValue: String { switch self { case .none: return NSLocalizedString("None", comment: "UTMQemuConstants") - case .webdav: return NSLocalizedString("SPICE WebDAV (Legacy)", comment: "UTMQemuConstants") - case .virtfs: return NSLocalizedString("VirtFS (Recommended)", comment: "UTMQemuConstants") + case .webdav: return NSLocalizedString("SPICE WebDAV", comment: "UTMQemuConstants") + case .virtfs: return NSLocalizedString("VirtFS", comment: "UTMQemuConstants") } } } diff --git a/Platform/Shared/VMConfigSharingView.swift b/Platform/Shared/VMConfigSharingView.swift index 47630a644..9d6b11a16 100644 --- a/Platform/Shared/VMConfigSharingView.swift +++ b/Platform/Shared/VMConfigSharingView.swift @@ -30,7 +30,7 @@ struct VMConfigSharingView: View { }) } - DetailedSection("Shared Directory") { + DetailedSection("Shared Directory", description: "WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers.") { VMConfigConstantPicker("Directory Share Mode", selection: $config.directoryShareMode) if config.directoryShareMode != .none { HStack { From 86579f60df881fd02fc11196fec09e5c4aad8663 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 20:08:31 -0700 Subject: [PATCH 32/42] project: add forgotten audio entitlements to macOS signed Fixes #4342 --- Platform/macOS/macOS.entitlements | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Platform/macOS/macOS.entitlements b/Platform/macOS/macOS.entitlements index 3e49d4a88..4b3bdf542 100644 --- a/Platform/macOS/macOS.entitlements +++ b/Platform/macOS/macOS.entitlements @@ -8,6 +8,8 @@ $(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM + com.apple.security.device.audio-input + com.apple.security.device.usb com.apple.security.files.user-selected.read-write From fbf7fbc8acd88d5de5a75c2cd4a67dde1acc2f54 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 28 Aug 2022 20:11:31 -0700 Subject: [PATCH 33/42] config(qemu): fixed incorrect parsing of cpu flags Fixes #4282 --- Configuration/UTMQemuConfiguration+Arguments.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index d6773cdf4..718494aff 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -196,20 +196,13 @@ import Foundation f("cortex-a15") } } else { - var flags = "" - for flag in system.cpuFlagsAdd { - flags += ",+\(flag)" - } - for flag in system.cpuFlagsRemove { - flags += ",-\(flag)" - } f("-cpu") system.cpu for flag in system.cpuFlagsAdd { - "+\(flag)" + "+\(flag.rawValue)" } for flag in system.cpuFlagsRemove { - "-\(flag)" + "-\(flag.rawValue)" } f() } From 88ed209640b7df7c4d1b6d2bae6263733b33bfc5 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:07:32 -0700 Subject: [PATCH 34/42] vm(qemu): remove forgotten viewstate reference --- Managers/UTMQemuVirtualMachine-Protected.h | 2 -- Managers/UTMQemuVirtualMachine.m | 3 --- 2 files changed, 5 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine-Protected.h b/Managers/UTMQemuVirtualMachine-Protected.h index 95dc7f986..abc0004d5 100644 --- a/Managers/UTMQemuVirtualMachine-Protected.h +++ b/Managers/UTMQemuVirtualMachine-Protected.h @@ -28,8 +28,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) UTMQemu *system; @property (nonatomic, readonly, nullable) UTMSpiceIO *ioService; -- (void)saveViewState; - @end NS_ASSUME_NONNULL_END diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 374b121ed..901891d31 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -312,9 +312,6 @@ - (void)vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { } - (void)_vmStopForce:(BOOL)force completion:(void (^)(NSError * _Nullable))completion { - // save view settings early to win exit race - [self saveViewState]; - [self.qemu qemuQuitWithCompletion:nil]; if (force || dispatch_semaphore_wait(self.qemuWillQuitEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { UTMLog(@"Stop operation timeout or force quit"); From a2165252933522d0bf5e55afd644df3f207e9746 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:13:24 -0700 Subject: [PATCH 35/42] vm: do not save screenshot when running disposible --- Managers/UTMVirtualMachine.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index c4beaee96..ba5bb333b 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -345,7 +345,7 @@ - (void)vmResumeWithCompletion:(void (^)(NSError * _Nullable))completion { - (BOOL)isScreenshotSaveEnabled { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - return ![defaults boolForKey:@"NoSaveScreenshot"]; + return ![defaults boolForKey:@"NoSaveScreenshot"] && !self.isRunningAsSnapshot; } - (void)loadScreenshot { From 13a0a6752532f91c75e90926aed3911eec999838 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 09:25:28 -0700 Subject: [PATCH 36/42] wizard: do not change title on macOS --- Platform/Shared/VMWizardDrivesView.swift | 2 ++ Platform/Shared/VMWizardHardwareView.swift | 2 ++ Platform/Shared/VMWizardOSLinuxView.swift | 2 ++ Platform/Shared/VMWizardOSOtherView.swift | 2 ++ Platform/Shared/VMWizardOSView.swift | 2 ++ Platform/Shared/VMWizardOSWindowsView.swift | 2 ++ Platform/Shared/VMWizardSharingView.swift | 2 ++ Platform/Shared/VMWizardStartView.swift | 2 ++ 8 files changed, 16 insertions(+) diff --git a/Platform/Shared/VMWizardDrivesView.swift b/Platform/Shared/VMWizardDrivesView.swift index 0b4a88385..5fec48bc1 100644 --- a/Platform/Shared/VMWizardDrivesView.swift +++ b/Platform/Shared/VMWizardDrivesView.swift @@ -43,7 +43,9 @@ struct VMWizardDrivesView: View { } } + #if os(iOS) .navigationTitle(Text("Storage")) + #endif } } diff --git a/Platform/Shared/VMWizardHardwareView.swift b/Platform/Shared/VMWizardHardwareView.swift index 719db677d..e00810493 100644 --- a/Platform/Shared/VMWizardHardwareView.swift +++ b/Platform/Shared/VMWizardHardwareView.swift @@ -122,7 +122,9 @@ struct VMWizardHardwareView: View { } } + #if os(iOS) .navigationTitle(Text("Hardware")) + #endif .textFieldStyle(.roundedBorder) .onAppear { if wizardState.useVirtualization { diff --git a/Platform/Shared/VMWizardOSLinuxView.swift b/Platform/Shared/VMWizardOSLinuxView.swift index 28fdd7dc9..749617910 100644 --- a/Platform/Shared/VMWizardOSLinuxView.swift +++ b/Platform/Shared/VMWizardOSLinuxView.swift @@ -252,7 +252,9 @@ struct VMWizardOSLinuxView: View { } + #if os(iOS) .navigationTitle(Text("Linux")) + #endif .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage) } diff --git a/Platform/Shared/VMWizardOSOtherView.swift b/Platform/Shared/VMWizardOSOtherView.swift index 5d9849baa..2b86691ed 100644 --- a/Platform/Shared/VMWizardOSOtherView.swift +++ b/Platform/Shared/VMWizardOSOtherView.swift @@ -51,7 +51,9 @@ struct VMWizardOSOtherView: View { Text("Advanced") } } + #if os(iOS) .navigationTitle(Text("Other")) + #endif .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage) } diff --git a/Platform/Shared/VMWizardOSView.swift b/Platform/Shared/VMWizardOSView.swift index d9af4c207..05b34b561 100644 --- a/Platform/Shared/VMWizardOSView.swift +++ b/Platform/Shared/VMWizardOSView.swift @@ -73,7 +73,9 @@ struct VMWizardOSView: View { } } + #if os(iOS) .navigationTitle(Text("Operating System")) + #endif .buttonStyle(.inList) } } diff --git a/Platform/Shared/VMWizardOSWindowsView.swift b/Platform/Shared/VMWizardOSWindowsView.swift index 6bc3fba13..619d690f5 100644 --- a/Platform/Shared/VMWizardOSWindowsView.swift +++ b/Platform/Shared/VMWizardOSWindowsView.swift @@ -82,7 +82,9 @@ struct VMWizardOSWindowsView: View { } } } + #if os(iOS) .navigationTitle(Text("Windows")) + #endif .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage) } diff --git a/Platform/Shared/VMWizardSharingView.swift b/Platform/Shared/VMWizardSharingView.swift index fd92719dd..5513265c2 100644 --- a/Platform/Shared/VMWizardSharingView.swift +++ b/Platform/Shared/VMWizardSharingView.swift @@ -78,7 +78,9 @@ struct VMWizardSharingView: View { Text("Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details.") } } + #if os(iOS) .navigationTitle(Text("Shared Directory")) + #endif .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.folder], onCompletion: processDirectory) } diff --git a/Platform/Shared/VMWizardStartView.swift b/Platform/Shared/VMWizardStartView.swift index d0eaf2658..fdcb56693 100644 --- a/Platform/Shared/VMWizardStartView.swift +++ b/Platform/Shared/VMWizardStartView.swift @@ -118,7 +118,9 @@ struct VMWizardStartView: View { } } + #if os(iOS) .navigationTitle(Text("Start")) + #endif } private func processIsTranslated() -> Bool { From bb7413db03e64df945fb66fd3d8e017eb5ee6f8d Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:35:40 -0700 Subject: [PATCH 37/42] vm: reload registry config when reloading --- Configuration/UTMAppleConfiguration.swift | 14 -------------- Managers/UTMAppleVirtualMachine.swift | 8 ++++++-- Managers/UTMVirtualMachine.swift | 3 +++ 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 45de98eb8..bdd11049c 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -328,17 +328,3 @@ extension UTMAppleConfiguration { return existingDataURLs } } - -// MARK: - Copy non-persistent values - -extension UTMAppleConfiguration { - /// Unsafely access another configuration and copies values. - /// Must only be called after init() or this could break concurrent accesses. - /// - Parameter other: Other configuration to copy from - func copyNonpersistentValuesUnsafely(from other: UTMAppleConfiguration) { - _sharedDirectories = other._sharedDirectories - if #available(macOS 12, *) { - _system.boot.macRecoveryIpswURL = other._system.boot.macRecoveryIpswURL - } - } -} diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index b8d85f205..60e6eb8c4 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -70,9 +70,13 @@ import Virtualization override func reloadConfiguration() throws { let newConfig = try UTMAppleConfiguration.load(from: path) as! UTMAppleConfiguration let oldConfig = appleConfig - // copy non-persistent values over - newConfig.copyNonpersistentValuesUnsafely(from: oldConfig) config = UTMConfigurationWrapper(wrapping: newConfig) + Task { @MainActor in + updateConfigFromRegistry() + if #available(macOS 12, *) { + newConfig.system.boot.macRecoveryIpswURL = oldConfig.system.boot.macRecoveryIpswURL + } + } } override func accessShortcut() async throws { diff --git a/Managers/UTMVirtualMachine.swift b/Managers/UTMVirtualMachine.swift index 6780e1b05..4cc7fd7d1 100644 --- a/Managers/UTMVirtualMachine.swift +++ b/Managers/UTMVirtualMachine.swift @@ -69,6 +69,9 @@ extension UTMVirtualMachine: ObservableObject { @objc extension UTMVirtualMachine { func reloadConfiguration() throws { try config.reload(from: path) + Task { @MainActor in + updateConfigFromRegistry() + } } func saveUTM() async throws { From c5768c7ab3468e784aac94a5a684e43036834eea Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:55:32 -0700 Subject: [PATCH 38/42] vm: fix new vm not showing up --- Managers/UTMRegistryEntry.swift | 2 +- Platform/UTMData.swift | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 1faffd15b..8b4f92227 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -52,7 +52,7 @@ import Foundation } else { package = try? File(url: path) } - _name = vm.detailsTitleLabel + _name = vm.config.name _package = package ?? File(path: path.path) uuid = vm.config.uuid _isSuspended = false diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index e0802afc8..f88b3f965 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -389,7 +389,14 @@ class UTMData: ObservableObject { guard await !virtualMachines.contains(where: { !$0.isShortcut && $0.config.name == config.information.name }) else { throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData") } - let vm = UTMVirtualMachine(newConfig: config, destinationURL: Self.defaultStorageUrl) + let vm: UTMVirtualMachine + if config is UTMQemuConfiguration { + vm = UTMQemuVirtualMachine(newConfig: config, destinationURL: Self.defaultStorageUrl) + } else if config is UTMAppleConfiguration { + vm = UTMAppleVirtualMachine(newConfig: config, destinationURL: Self.defaultStorageUrl) + } else { + fatalError("Unknown configuration.") + } try await save(vm: vm) await listAdd(vm: vm) await listSelect(vm: vm) From ce0c2ba12f1f8d97715d1ea171c680d2674b7a29 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:55:48 -0700 Subject: [PATCH 39/42] vm: fix bad saved state detection logic --- Managers/UTMQemuVirtualMachine.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 901891d31..d78395503 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -300,7 +300,7 @@ - (void)vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { [self _vmStartWithCompletion:^(NSError *err){ if (err) { // delete suspend state on error dispatch_async(dispatch_get_main_queue(), ^{ - self.registryEntry.hasSaveState = YES; + self.registryEntry.hasSaveState = NO; }); [self changeState:kVMStopped]; } else { From d713879c022df3c7288342e26a75e5ed6c9cb08b Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 12:38:15 -0700 Subject: [PATCH 40/42] registry: handle moving, cloning, deleting VMs --- Managers/UTMQemuVirtualMachine.m | 4 +- Managers/UTMRegistryEntry.swift | 10 ++++- Managers/UTMVirtualMachine-Protected.h | 2 + Managers/UTMVirtualMachine.m | 20 ++++++--- Managers/UTMVirtualMachine.swift | 31 +++++++++++--- Platform/UTMData.swift | 59 ++++++++++++++++++++++---- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index d78395503..abec33b4f 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -87,9 +87,7 @@ - (void)accessShortcutWithCompletion:(void (^ _Nullable)(NSError * _Nullable))co return; } UTMQemu *service = self.system; - if (!service) { - service = [UTMQemu new]; // VM has not started yet, we create a temporary process - } + assert(service); NSData *bookmark = self.registryEntry.packageRemoteBookmark; NSString *bookmarkPath = self.registryEntry.packageRemotePath; BOOL existing = bookmark != nil; diff --git a/Managers/UTMRegistryEntry.swift b/Managers/UTMRegistryEntry.swift index 8b4f92227..4cdf7977b 100644 --- a/Managers/UTMRegistryEntry.swift +++ b/Managers/UTMRegistryEntry.swift @@ -21,7 +21,7 @@ import Foundation @Published private var _package: File - var uuid: UUID + private(set) var uuid: UUID @Published private var _isSuspended: Bool @@ -204,6 +204,14 @@ extension UTMRegistryEntryDecodable { func removeAllSharedDirectories() { sharedDirectories = [] } + + func update(copying other: UTMRegistryEntry) { + isSuspended = other.isSuspended + externalDrives = other.externalDrives + sharedDirectories = other.sharedDirectories + windowSettings = other.windowSettings + hasMigratedConfig = other.hasMigratedConfig + } } // MARK: - Migration from UTMViewState diff --git a/Managers/UTMVirtualMachine-Protected.h b/Managers/UTMVirtualMachine-Protected.h index cd9d73a11..a2c65ded7 100644 --- a/Managers/UTMVirtualMachine-Protected.h +++ b/Managers/UTMVirtualMachine-Protected.h @@ -67,6 +67,8 @@ extern NSString *const kUTMBundleConfigFilename; @property (nonatomic, readwrite) NSURL *path; @property (nonatomic, readwrite) UTMConfigurationWrapper *config; @property (nonatomic, readwrite, nullable) CSScreenshot *screenshot; +@property (nonatomic, readwrite) UTMRegistryEntry *registryEntry; +@property (nonatomic) NSArray *anyCancellable; - (instancetype)init NS_UNAVAILABLE; diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index ba5bb333b..82b438e59 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -36,11 +36,9 @@ @interface UTMVirtualMachine () -@property (nonatomic) NSArray *anyCancellable; @property (nonatomic, readonly) BOOL isScreenshotSaveEnabled; @property (nonatomic, nullable) void (^screenshotTimerHandler)(void); @property (nonatomic) BOOL isScopedAccess; -@property (nonatomic, readwrite) UTMRegistryEntry *registryEntry; @end @@ -48,6 +46,18 @@ @implementation UTMVirtualMachine // MARK: - Observable properties +- (void)setConfig:(UTMConfigurationWrapper *)config { + [self propertyWillChange]; + _config = config; + [self subscribeToChildren]; +} + +- (void)setRegistryEntry:(UTMRegistryEntry *)registryEntry { + [self propertyWillChange]; + _registryEntry = registryEntry; + [self subscribeToChildren]; +} + - (void)setState:(UTMVMState)state { [self propertyWillChange]; _state = state; @@ -157,15 +167,15 @@ - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration p #else self.logging = [UTMLogging new]; #endif - self.config = configuration; + _config = configuration; self.path = packageURL; - self.registryEntry = [UTMRegistry.shared entryFor:self]; + _registryEntry = [UTMRegistry.shared entryFor:self]; // migrate legacy view state NSURL *viewStateURL = [packageURL URLByAppendingPathComponent:kUTMBundleViewFilename]; [self.registryEntry migrateUnsafeWithViewStateURL:viewStateURL]; [self.registryEntry migrateFromConfig:configuration]; [self loadScreenshot]; - self.anyCancellable = [self subscribeToChildren]; + [self subscribeToChildren]; } return self; } diff --git a/Managers/UTMVirtualMachine.swift b/Managers/UTMVirtualMachine.swift index 4cc7fd7d1..7ab4cf911 100644 --- a/Managers/UTMVirtualMachine.swift +++ b/Managers/UTMVirtualMachine.swift @@ -17,8 +17,8 @@ import Foundation extension UTMVirtualMachine: Identifiable { - public var id: (UUID, String) { - return (registryEntry.uuid, path.path) + public var id: UUID { + return registryEntry.uuid } } @@ -28,7 +28,7 @@ extension UTMVirtualMachine: ObservableObject { @objc extension UTMVirtualMachine { fileprivate static let gibInMib = 1024 - func subscribeToChildren() -> [AnyObject] { + func subscribeToChildren() { var s: [AnyObject] = [] if let config = config.qemuConfig { s.append(config.objectWillChange.sink { [weak self] in @@ -52,7 +52,7 @@ extension UTMVirtualMachine: ObservableObject { Task { @MainActor in self.updateConfigFromRegistry() } - return s + anyCancellable = s } @MainActor func propertyWillChange() -> Void { @@ -82,10 +82,9 @@ extension UTMVirtualMachine: ObservableObject { try await config.save(to: existingPath) try await updateRegistryFromConfig() } catch { - try? reloadConfiguration() throw error } - if existingPath.path != newPath.path { + if !isShortcut && existingPath.path != newPath.path { try await Task.detached { try fileManager.moveItem(at: existingPath, to: newPath) }.value @@ -96,16 +95,34 @@ extension UTMVirtualMachine: ObservableObject { /// Called when we save the config @MainActor func updateRegistryFromConfig() async throws { + if registryEntry.uuid != config.uuid { + changeUuid(to: config.uuid) + } registryEntry.name = config.name + let oldPath = registryEntry.package.path let oldRemoteBookmark = registryEntry.package.remoteBookmark registryEntry.package = try UTMRegistryEntry.File(url: path) - registryEntry.package.remoteBookmark = oldRemoteBookmark + if registryEntry.package.path == oldPath { + registryEntry.package.remoteBookmark = oldRemoteBookmark + } } /// Called whenever the registry entry changes @MainActor func updateConfigFromRegistry() { // implement in subclass } + + /// Called when we have a duplicate UUID + @MainActor func changeUuid(to uuid: UUID) { + if let qemuConfig = config.qemuConfig { + qemuConfig.information.uuid = uuid + } else if let appleConfig = config.appleConfig { + appleConfig.information.uuid = uuid + } else { + fatalError("Invalid configuration.") + } + registryEntry = UTMRegistry.shared.entry(for: self) + } } // MARK: - Bookmark handling diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index f88b3f965..fe4dad42b 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -24,6 +24,10 @@ import SwiftUI #if canImport(AltKit) && !WITH_QEMU_TCI import AltKit #endif +#if !os(macOS) +typealias UTMAppleConfiguration = UTMQemuConfiguration +typealias UTMAppleVirtualMachine = UTMQemuVirtualMachine +#endif struct AlertMessage: Identifiable { var message: String @@ -157,6 +161,12 @@ class UTMData: ObservableObject { let defaults = UserDefaults.standard guard defaults.object(forKey: "VMList") == nil else { listLegacyLoadFromDefaults() + // fix collisions + for vm in virtualMachines { + if uuidHasCollision(with: vm) { + uuidRegenerate(for: vm) + } + } // delete legacy defaults.removeObject(forKey: "VMList") return @@ -171,6 +181,10 @@ class UTMData: ObservableObject { } let wrappedVM = UTMWrappedVirtualMachine(from: entry) if let vm = wrappedVM.unwrap() { + if vm.registryEntry.uuid != wrappedVM.registryEntry.uuid { + // we had a duplicate UUID so we change it + vm.changeUuid(to: wrappedVM.registryEntry.uuid) + } return vm } else { return wrappedVM @@ -225,6 +239,9 @@ class UTMData: ObservableObject { /// - Parameter vm: VM to add /// - Parameter at: Optional index to add to, otherwise will be added to the end @MainActor private func listAdd(vm: UTMVirtualMachine, at index: Int? = nil) { + if uuidHasCollision(with: vm) { + uuidRegenerate(for: vm) + } if let index = index { virtualMachines.insert(vm, at: index) } else { @@ -250,7 +267,6 @@ class UTMData: ObservableObject { selectedVM = nil } vm.isDeleted = true // alert views to update - UTMRegistry.shared.remove(entry: vm.registryEntry) return index } @@ -379,6 +395,11 @@ class UTMData: ObservableObject { func discardChanges(for vm: UTMVirtualMachine? = nil) throws { if let vm = vm { try vm.reloadConfiguration() + Task { @MainActor in + if uuidHasCollision(with: vm) { + vm.changeUuid(to: UUID()) + } + } } } @@ -406,12 +427,15 @@ class UTMData: ObservableObject { /// Delete a VM from disk /// - Parameter vm: VM to delete /// - Returns: Index of item removed in VM list or nil if not in list - @discardableResult func delete(vm: UTMVirtualMachine) async throws -> Int? { + @discardableResult func delete(vm: UTMVirtualMachine, alsoRegistry: Bool = true) async throws -> Int? { if let _ = vm as? UTMWrappedVirtualMachine { } else { try fileManager.removeItem(at: vm.path) } + if alsoRegistry { + UTMRegistry.shared.remove(entry: vm.registryEntry) + } return await listRemove(vm: vm) } @@ -425,6 +449,8 @@ class UTMData: ObservableObject { guard let newVM = UTMVirtualMachine(url: newPath) else { throw NSLocalizedString("Failed to clone VM.", comment: "UTMData") } + await vm.changeUuid(to: UUID()) + try await vm.saveUTM() var index = await virtualMachines.firstIndex(of: vm) if index != nil { index! += 1 @@ -457,10 +483,10 @@ class UTMData: ObservableObject { await MainActor.run { newVM.isShortcut = true } - try await newVM.accessShortcut() + try await newVM.updateRegistryFromConfig() let oldSelected = await selectedVM - let index = try await delete(vm: vm) + let index = try await delete(vm: vm, alsoRegistry: false) await listAdd(vm: newVM, at: index) if oldSelected == vm { await listSelect(vm: newVM) @@ -479,15 +505,14 @@ class UTMData: ObservableObject { /// - Parameter vm: Existing VM to copy configuration from @MainActor func template(vm: UTMVirtualMachine) async throws { let copy = try UTMQemuConfiguration.load(from: vm.path) - #if !os(macOS) - typealias UTMAppleConfiguration = UTMQemuConfiguration - #endif if let copy = copy as? UTMQemuConfiguration { copy.information.name = self.newDefaultVMName(base: copy.information.name) + copy.information.uuid = UUID() copy.drives = [] _ = try await create(config: copy) } else if let copy = copy as? UTMAppleConfiguration { copy.information.name = self.newDefaultVMName(base: copy.information.name) + copy.information.uuid = UUID() copy.drives = [] _ = try await create(config: copy) } else { @@ -578,7 +603,6 @@ class UTMData: ObservableObject { await MainActor.run { vm?.isShortcut = true } - try await vm?.accessShortcut() } else { logger.info("copying to Documents") try fileManager.copyItem(at: url, to: dest) @@ -670,6 +694,25 @@ class UTMData: ObservableObject { } #endif + // MARK: - UUID migration + + @MainActor private func uuidHasCollision(with vm: UTMVirtualMachine) -> Bool { + for otherVM in virtualMachines { + if otherVM == vm { + return false + } else if otherVM.registryEntry.uuid == vm.registryEntry.uuid { + return true + } + } + return false + } + + @MainActor private func uuidRegenerate(for vm: UTMVirtualMachine) { + let oldEntry = vm.registryEntry + vm.changeUuid(to: UUID()) + vm.registryEntry.update(copying: oldEntry) + } + // MARK: - Other utility functions /// In some regions, iOS will prompt the user for network access From 88e7c27e7b6daf26b9b614dabc343758ee6eefcf Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 18:43:47 -0700 Subject: [PATCH 41/42] helper: terminate child task on quit --- Managers/UTMQemu.m | 1 + QEMUHelper/QEMUHelper.m | 11 +++++++++-- QEMUHelper/QEMUHelperProtocol.h | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Managers/UTMQemu.m b/Managers/UTMQemu.m index 8a6b45616..617dfb9d1 100644 --- a/Managers/UTMQemu.m +++ b/Managers/UTMQemu.m @@ -201,6 +201,7 @@ - (void)startQemu:(nonnull NSString *)name completion:(void(^)(BOOL,NSString * _ - (void)stopQemu { if (_connection) { + [[_connection remoteObjectProxy] terminate]; [_connection invalidate]; } for (NSURL *url in _urls) { diff --git a/QEMUHelper/QEMUHelper.m b/QEMUHelper/QEMUHelper.m index f0602f235..97f71ff29 100644 --- a/QEMUHelper/QEMUHelper.m +++ b/QEMUHelper/QEMUHelper.m @@ -20,7 +20,7 @@ @interface QEMUHelper () @property NSMutableArray *urls; -@property dispatch_queue_t childWaitQueue; +@property NSTask *childTask; @end @@ -29,7 +29,6 @@ @implementation QEMUHelper - (instancetype)init { if (self = [super init]) { self.urls = [NSMutableArray array]; - self.childWaitQueue = dispatch_queue_create("childWaitQueue", DISPATCH_QUEUE_CONCURRENT); } return self; } @@ -107,6 +106,7 @@ - (void)startQemuTask:(NSString *)binName standardOutput:(NSFileHandle *)standar NSTask *task = [NSTask new]; NSMutableArray *newArgv = [argv mutableCopy]; NSString *path = [libraryPath URLByAppendingPathComponent:binName].path; + __weak typeof(self) _self = self; [newArgv insertObject:path atIndex:0]; task.executableURL = [[[NSBundle mainBundle] URLForAuxiliaryExecutable:@"QEMULauncher.app"] URLByAppendingPathComponent:@"Contents/MacOS/QEMULauncher"]; task.arguments = newArgv; @@ -116,12 +116,19 @@ - (void)startQemuTask:(NSString *)binName standardOutput:(NSFileHandle *)standar task.qualityOfService = NSQualityOfServiceUserInitiated; task.terminationHandler = ^(NSTask *task) { BOOL normalExit = task.terminationReason == NSTaskTerminationReasonExit && task.terminationStatus == 0; + _self.childTask = nil; onExit(normalExit, nil); }; if (![task launchAndReturnError:&err]) { NSLog(@"Error starting QEMU: %@", err); onExit(NO, NSLocalizedString(@"Error starting QEMU.", @"QEMUHelper")); } + self.childTask = task; +} + +- (void)terminate { + [self.childTask terminate]; + self.childTask = nil; } @end diff --git a/QEMUHelper/QEMUHelperProtocol.h b/QEMUHelper/QEMUHelperProtocol.h index ebcf035a6..af1956dc9 100644 --- a/QEMUHelper/QEMUHelperProtocol.h +++ b/QEMUHelper/QEMUHelperProtocol.h @@ -24,7 +24,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion; - (void)stopAccessingPath:(nullable NSString *)path; - (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray *)argv onExit:(void(^)(BOOL,NSString *))onExit; - +- (void)terminate; + @end NS_ASSUME_NONNULL_END From 906bae86727b20e072934386888708d9af34b8f5 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Mon, 29 Aug 2022 19:00:11 -0700 Subject: [PATCH 42/42] project: update SwiftTerm to latest commit Fixes #4297 --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 552b82657..145e18aa7 100644 --- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,7 +42,7 @@ "location" : "https://github.com/migueldeicaza/SwiftTerm.git", "state" : { "branch" : "main", - "revision" : "e92856df7b5e4e12b09a9ebeb9abbd8410c9343b" + "revision" : "72daa50253e5452d36054d52495ab93be517eb4c" } }, {