mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 09:02:48 +02:00
feat: Implement cross-platform USB writing, UI/UX improvements, and enhanced Docker interaction
This major update brings several key features and improvements: 1. **Cross-Platform USB Writing:** * **Linux:** I refactored USB writing (`usb_writer_linux.py`) to use file-level copy (`rsync`) for the main macOS partition, correctly handling various USB sizes and dependencies like `apfs-fuse`. * **macOS:** I implemented USB writing (`usb_writer_macos.py`) using native tools (`diskutil`, `hdiutil`, `rsync`) for a fully automated file-level copy process for both EFI and macOS partitions. * **Windows:** I added initial USB writing support (`usb_writer_windows.py`) automating EFI partition setup and file copy (using `diskpart`, `7z.exe`, `robocopy`). Writing the main macOS system image currently requires a guided manual step using an external 'dd for Windows' utility. 2. **Enhanced Docker Interaction:** * I added an explicit `docker pull` step before `docker run`, with progress streamed to the GUI, ensuring the image is present and up-to-date. 3. **Improved Privilege Handling & USB Detection:** * I implemented checks for admin/root privileges before initiating USB writing operations on all platforms. * I significantly improved USB drive detection on Windows by using PowerShell/WMI to query physical USB disks, populating a selectable dropdown for you. Manual disk ID input is now a fallback. 4. **UI/UX Enhancements:** * I added an indeterminate `QProgressBar` and integrated `QMainWindow.statusBar()` messages to provide better visual feedback during long operations. * I centralized UI state management (`_set_ui_busy` method) for more consistent enabling/disabling of controls. * I refactored how I handle completion and errors for cleaner UI updates. 5. **Documentation:** * I updated `README.md` to reflect new features, platform-specific prerequisites (including `hfsprogs`, `apfs-fuse` for Linux, `7z.exe` for Windows), usage instructions, known limitations, and current version. 6. **Code Structure:** * I introduced `usb_writer_macos.py` and `usb_writer_windows.py`. * I updated `main_app.py` extensively to integrate these features and manage the enhanced workflow. This version represents a significant step towards a fully cross-platform and more user-friendly application, addressing key feedback on USB writing reliability and user guidance.
This commit is contained in:
parent
5cae652266
commit
f4d5cd9daf
52
.github/workflows/docker-build.yml
vendored
52
.github/workflows/docker-build.yml
vendored
@ -1,52 +0,0 @@
|
|||||||
name: Push Docker Image to Docker Hub
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_docker_hub:
|
|
||||||
name: Push Docker Image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
id: checkout_code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
id: login_docker_hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USER_NAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Echo Docker Hub Username
|
|
||||||
run: echo ${{ secrets.DOCKER_HUB_USER_NAME }}
|
|
||||||
|
|
||||||
- name: Echo GitHub SHA
|
|
||||||
run: echo $GITHUB_SHA
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
id: build_image
|
|
||||||
run: |
|
|
||||||
docker build "$GITHUB_WORKSPACE" -t sickcodes/docker-osx:master --label dockerfile-path="Dockerfile"
|
|
||||||
|
|
||||||
- name: Label Master Docker Image as Latest
|
|
||||||
id: label_image
|
|
||||||
run: |
|
|
||||||
docker tag sickcodes/docker-osx:master sickcodes/docker-osx:latest
|
|
||||||
|
|
||||||
- name: Push Docker image master
|
|
||||||
id: push_master
|
|
||||||
run: docker push sickcodes/docker-osx:master
|
|
||||||
|
|
||||||
- name: Push Docker image latest
|
|
||||||
id: push_latest
|
|
||||||
run: docker push sickcodes/docker-osx:latest
|
|
||||||
|
|
||||||
- name: Logout from Docker Hub
|
|
||||||
run: docker logout
|
|
||||||
|
|
||||||
- name: End
|
|
||||||
run: echo "Docker image pushed to Docker Hub successfully"
|
|
24
Dockerfile
24
Dockerfile
@ -159,6 +159,13 @@ RUN yes | sudo pacman -Syu bc qemu-desktop libvirt dnsmasq virt-manager bridge-u
|
|||||||
|
|
||||||
WORKDIR /home/arch/OSX-KVM
|
WORKDIR /home/arch/OSX-KVM
|
||||||
|
|
||||||
|
# shortname default is catalina, which means :latest is catalina
|
||||||
|
ARG SHORTNAME=catalina
|
||||||
|
|
||||||
|
RUN make \
|
||||||
|
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c BaseSystem.img \
|
||||||
|
&& rm ./BaseSystem.dmg
|
||||||
|
|
||||||
# fix invalid signature on old libguestfs
|
# fix invalid signature on old libguestfs
|
||||||
ARG SIGLEVEL=Never
|
ARG SIGLEVEL=Never
|
||||||
|
|
||||||
@ -228,7 +235,7 @@ RUN grep -v InstallMedia ./Launch.sh > ./Launch-nopicker.sh \
|
|||||||
|
|
||||||
USER arch
|
USER arch
|
||||||
|
|
||||||
ENV USER=arch
|
ENV USER arch
|
||||||
|
|
||||||
# These are hardcoded serials for non-iMessage related research
|
# These are hardcoded serials for non-iMessage related research
|
||||||
# Overwritten by using GENERATE_UNIQUE=true
|
# Overwritten by using GENERATE_UNIQUE=true
|
||||||
@ -353,20 +360,7 @@ VOLUME ["/tmp/.X11-unix"]
|
|||||||
# the default serial numbers are already contained in ./OpenCore/OpenCore.qcow2
|
# the default serial numbers are already contained in ./OpenCore/OpenCore.qcow2
|
||||||
# And the default serial numbers
|
# And the default serial numbers
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is below
|
|
||||||
ENV SHORTNAME=sequoia
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
|
||||||
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
; [[ "${NOPICKER}" == true ]] && { \
|
; [[ "${NOPICKER}" == true ]] && { \
|
||||||
sed -i '/^.*InstallMedia.*/d' Launch.sh \
|
sed -i '/^.*InstallMedia.*/d' Launch.sh \
|
||||||
|
@ -206,20 +206,7 @@ ENV TERMS_OF_USE=i_agree
|
|||||||
|
|
||||||
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD echo "${BOILERPLATE}" \
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is catalina, which means :latest is catalina
|
|
||||||
ENV SHORTNAME=sonoma
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; echo "${BOILERPLATE}" \
|
|
||||||
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
||||||
; echo "Disk is being copied between layers... Please wait a minute..." \
|
; echo "Disk is being copied between layers... Please wait a minute..." \
|
||||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
|
255
Dockerfile.monterey
Normal file
255
Dockerfile.monterey
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/docker
|
||||||
|
# ____ __ ____ ______ __
|
||||||
|
# / __ \____ _____/ /_____ _____/ __ \/ ___/ |/ /
|
||||||
|
# / / / / __ \/ ___/ //_/ _ \/ ___/ / / /\__ \| /
|
||||||
|
# / /_/ / /_/ / /__/ ,< / __/ / / /_/ /___/ / |
|
||||||
|
# /_____/\____/\___/_/|_|\___/_/ \____//____/_/|_| [MONTEREY]
|
||||||
|
#
|
||||||
|
# Title: Docker-OSX (Mac on Docker)
|
||||||
|
# Author: Sick.Codes https://twitter.com/sickcodes
|
||||||
|
# Version: 6.0
|
||||||
|
# License: GPLv3+
|
||||||
|
# Repository: https://github.com/sickcodes/Docker-OSX
|
||||||
|
# Website: https://sick.codes
|
||||||
|
#
|
||||||
|
# Notes: Uses a self-hosted BaseSystem.img from a USB installer.
|
||||||
|
# If you want to DIY, use https://github.com/corpnewt/gibMacOS
|
||||||
|
# Set seed as developer, and install the Install Assistant on Big Sur
|
||||||
|
# Burn to a USB, and pull out BaseSystem.img
|
||||||
|
# Or download from https://images.sick.codes/BaseSystem_Monterey.dmg
|
||||||
|
#
|
||||||
|
|
||||||
|
FROM sickcodes/docker-osx
|
||||||
|
|
||||||
|
LABEL maintainer='https://twitter.com/sickcodes <https://sick.codes>'
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
|
||||||
|
# change disk size here or add during build, e.g. --build-arg VERSION=10.14.5 --build-arg SIZE=50G
|
||||||
|
ARG SIZE=200G
|
||||||
|
ARG BASE_SYSTEM='https://images.sick.codes/BaseSystem_Monterey.dmg'
|
||||||
|
|
||||||
|
WORKDIR /home/arch/OSX-KVM
|
||||||
|
|
||||||
|
RUN wget -O BaseSystem.dmg "${BASE_SYSTEM}" \
|
||||||
|
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c BaseSystem.img \
|
||||||
|
&& rm -f BaseSystem.dmg
|
||||||
|
|
||||||
|
RUN qemu-img create -f qcow2 /home/arch/OSX-KVM/mac_hdd_ng.img "${SIZE}"
|
||||||
|
|
||||||
|
WORKDIR /home/arch/OSX-KVM
|
||||||
|
|
||||||
|
#### libguestfs versioning
|
||||||
|
|
||||||
|
# 5.13+ problem resolved by building the qcow2 against 5.12 using libguestfs-1.44.1-6
|
||||||
|
|
||||||
|
ENV SUPERMIN_KERNEL=/boot/vmlinuz-linux
|
||||||
|
ENV SUPERMIN_MODULES=/lib/modules/5.12.14-arch1-1
|
||||||
|
ENV SUPERMIN_KERNEL_VERSION=5.12.14-arch1-1
|
||||||
|
ENV KERNEL_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||||
|
ENV KERNEL_HEADERS_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-headers-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||||
|
ENV LIBGUESTFS_PACKAGE_URL=https://archive.archlinux.org/packages/l/libguestfs/libguestfs-1.44.1-6-x86_64.pkg.tar.zst
|
||||||
|
|
||||||
|
ARG LINUX=true
|
||||||
|
|
||||||
|
# required to use libguestfs inside a docker container, to create bootdisks for docker-osx on-the-fly
|
||||||
|
RUN if [[ "${LINUX}" == true ]]; then \
|
||||||
|
sudo pacman -U "${KERNEL_PACKAGE_URL}" --noconfirm \
|
||||||
|
; sudo pacman -U "${LIBGUESTFS_PACKAGE_URL}" --noconfirm \
|
||||||
|
; sudo pacman -U "${KERNEL_HEADERS_PACKAGE_URL}" --noconfirm \
|
||||||
|
; sudo pacman -S mkinitcpio --noconfirm \
|
||||||
|
; sudo libguestfs-test-tool \
|
||||||
|
; sudo rm -rf /var/tmp/.guestfs-* \
|
||||||
|
; fi
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
|
||||||
|
# optional --build-arg to change branches for testing
|
||||||
|
ARG BRANCH=master
|
||||||
|
ARG REPO='https://github.com/sickcodes/Docker-OSX.git'
|
||||||
|
# RUN git clone --recurse-submodules --depth 1 --branch "${BRANCH}" "${REPO}"
|
||||||
|
RUN rm -rf ./Docker-OSX \
|
||||||
|
&& git clone --recurse-submodules --depth 1 --branch "${BRANCH}" "${REPO}"
|
||||||
|
|
||||||
|
RUN touch Launch.sh \
|
||||||
|
&& chmod +x ./Launch.sh \
|
||||||
|
&& tee -a Launch.sh <<< '#!/bin/bash' \
|
||||||
|
&& tee -a Launch.sh <<< 'set -eux' \
|
||||||
|
&& tee -a Launch.sh <<< 'sudo chown $(id -u):$(id -g) /dev/kvm 2>/dev/null || true' \
|
||||||
|
&& tee -a Launch.sh <<< 'sudo chown -R $(id -u):$(id -g) /dev/snd 2>/dev/null || true' \
|
||||||
|
&& tee -a Launch.sh <<< '[[ "${RAM}" = max ]] && export RAM="$(("$(head -n1 /proc/meminfo | tr -dc "[:digit:]") / 1000000"))"' \
|
||||||
|
&& tee -a Launch.sh <<< '[[ "${RAM}" = half ]] && export RAM="$(("$(head -n1 /proc/meminfo | tr -dc "[:digit:]") / 2000000"))"' \
|
||||||
|
&& tee -a Launch.sh <<< 'sudo chown -R $(id -u):$(id -g) /dev/snd 2>/dev/null || true' \
|
||||||
|
&& tee -a Launch.sh <<< 'exec qemu-system-x86_64 -m ${RAM:-2}000 \' \
|
||||||
|
&& tee -a Launch.sh <<< '-cpu ${CPU:-Penryn},${CPUID_FLAGS:-vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check,}${BOOT_ARGS} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-machine q35,${KVM-"accel=kvm:tcg"} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-smp ${CPU_STRING:-${SMP:-4},cores=${CORES:-4}} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-usb -device usb-kbd -device usb-tablet \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device isa-applesmc,osk=ourhardworkbythesewordsguardedpleasedontsteal\(c\)AppleComputerInc \' \
|
||||||
|
&& tee -a Launch.sh <<< '-drive if=pflash,format=raw,readonly=on,file=/home/arch/OSX-KVM/OVMF_CODE.fd \' \
|
||||||
|
&& tee -a Launch.sh <<< '-drive if=pflash,format=raw,file=/home/arch/OSX-KVM/OVMF_VARS-1024x768.fd \' \
|
||||||
|
&& tee -a Launch.sh <<< '-smbios type=2 \' \
|
||||||
|
&& tee -a Launch.sh <<< '-audiodev ${AUDIO_DRIVER:-alsa},id=hda -device ich9-intel-hda -device hda-duplex,audiodev=hda \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device ich9-ahci,id=sata \' \
|
||||||
|
&& tee -a Launch.sh <<< '-drive id=OpenCoreBoot,if=none,snapshot=on,format=qcow2,file=${BOOTDISK:-/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.2,drive=OpenCoreBoot \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.3,drive=InstallMedia \' \
|
||||||
|
&& tee -a Launch.sh <<< '-drive id=InstallMedia,if=none,file=/home/arch/OSX-KVM/BaseSystem.img,format=qcow2 \' \
|
||||||
|
&& tee -a Launch.sh <<< '-drive id=MacHDD,if=none,file=${IMAGE_PATH:-/home/arch/OSX-KVM/mac_hdd_ng.img},format=${IMAGE_FORMAT:-qcow2} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.4,drive=MacHDD \' \
|
||||||
|
&& tee -a Launch.sh <<< '-netdev user,id=net0,hostfwd=tcp::${INTERNAL_SSH_PORT:-10022}-:22,hostfwd=tcp::${SCREEN_SHARE_PORT:-5900}-:5900,${ADDITIONAL_PORTS} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-device ${NETWORKING:-vmxnet3},netdev=net0,id=net0,mac=${MAC_ADDRESS:-52:54:00:09:49:17} \' \
|
||||||
|
&& tee -a Launch.sh <<< '-monitor stdio \' \
|
||||||
|
&& tee -a Launch.sh <<< '-boot menu=on \' \
|
||||||
|
&& tee -a Launch.sh <<< '-vga vmware \' \
|
||||||
|
&& tee -a Launch.sh <<< '${EXTRA:-}'
|
||||||
|
|
||||||
|
# docker exec containerid mv ./Launch-nopicker.sh ./Launch.sh
|
||||||
|
# This is now a legacy command.
|
||||||
|
# You can use -e BOOTDISK=/bootdisk with -v ./bootdisk.img:/bootdisk
|
||||||
|
RUN grep -v InstallMedia ./Launch.sh > ./Launch-nopicker.sh \
|
||||||
|
&& chmod +x ./Launch-nopicker.sh \
|
||||||
|
&& sed -i -e s/OpenCore\.qcow2/OpenCore\-nopicker\.qcow2/ ./Launch-nopicker.sh
|
||||||
|
|
||||||
|
USER arch
|
||||||
|
|
||||||
|
ENV USER arch
|
||||||
|
|
||||||
|
|
||||||
|
#### libguestfs versioning
|
||||||
|
|
||||||
|
# 5.13+ problem resolved by building the qcow2 against 5.12 using libguestfs-1.44.1-6
|
||||||
|
|
||||||
|
ENV SUPERMIN_KERNEL=/boot/vmlinuz-linux
|
||||||
|
ENV SUPERMIN_MODULES=/lib/modules/5.12.14-arch1-1
|
||||||
|
ENV SUPERMIN_KERNEL_VERSION=5.12.14-arch1-1
|
||||||
|
ENV KERNEL_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||||
|
ENV KERNEL_HEADERS_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-headers-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||||
|
ENV LIBGUESTFS_PACKAGE_URL=https://archive.archlinux.org/packages/l/libguestfs/libguestfs-1.44.1-6-x86_64.pkg.tar.zst
|
||||||
|
|
||||||
|
RUN sudo pacman -Syy \
|
||||||
|
&& sudo pacman -Rns linux --noconfirm \
|
||||||
|
; sudo pacman -S mkinitcpio --noconfirm \
|
||||||
|
&& sudo pacman -U "${KERNEL_PACKAGE_URL}" --noconfirm \
|
||||||
|
&& sudo pacman -U "${LIBGUESTFS_PACKAGE_URL}" --noconfirm \
|
||||||
|
&& rm -rf /var/tmp/.guestfs-* \
|
||||||
|
; libguestfs-test-tool || exit 1
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
# symlink the old directory, for redundancy
|
||||||
|
RUN ln -s /home/arch/OSX-KVM/OpenCore /home/arch/OSX-KVM/OpenCore-Catalina || true
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
#### SPECIAL RUNTIME ARGUMENTS BELOW
|
||||||
|
|
||||||
|
# env -e ADDITIONAL_PORTS with a comma
|
||||||
|
# for example, -e ADDITIONAL_PORTS=hostfwd=tcp::23-:23,
|
||||||
|
ENV ADDITIONAL_PORTS=
|
||||||
|
|
||||||
|
# add additional QEMU boot arguments
|
||||||
|
ENV BOOT_ARGS=
|
||||||
|
|
||||||
|
ENV BOOTDISK=
|
||||||
|
|
||||||
|
# edit the CPU that is being emulated
|
||||||
|
ENV CPU=Penryn
|
||||||
|
ENV CPUID_FLAGS='vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check,'
|
||||||
|
|
||||||
|
ENV DISPLAY=:0.0
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
|
ENV ENV=/env
|
||||||
|
|
||||||
|
# Boolean for generating a bootdisk with new random serials.
|
||||||
|
ENV GENERATE_UNIQUE=false
|
||||||
|
|
||||||
|
# Boolean for generating a bootdisk with specific serials.
|
||||||
|
ENV GENERATE_SPECIFIC=false
|
||||||
|
|
||||||
|
ENV IMAGE_PATH=/home/arch/OSX-KVM/mac_hdd_ng.img
|
||||||
|
ENV IMAGE_FORMAT=qcow2
|
||||||
|
|
||||||
|
ENV KVM='accel=kvm:tcg'
|
||||||
|
|
||||||
|
ENV MASTER_PLIST_URL="https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist"
|
||||||
|
|
||||||
|
# ENV NETWORKING=e1000-82545em
|
||||||
|
ENV NETWORKING=vmxnet3
|
||||||
|
|
||||||
|
# boolean for skipping the disk selection menu at in the boot process
|
||||||
|
ENV NOPICKER=false
|
||||||
|
|
||||||
|
# dynamic RAM options for runtime
|
||||||
|
ENV RAM=3
|
||||||
|
# ENV RAM=max
|
||||||
|
# ENV RAM=half
|
||||||
|
|
||||||
|
# The x and y coordinates for resolution.
|
||||||
|
# Must be used with either -e GENERATE_UNIQUE=true or -e GENERATE_SPECIFIC=true.
|
||||||
|
ENV WIDTH=1920
|
||||||
|
ENV HEIGHT=1080
|
||||||
|
|
||||||
|
# libguestfs verbose
|
||||||
|
ENV LIBGUESTFS_DEBUG=1
|
||||||
|
ENV LIBGUESTFS_TRACE=1
|
||||||
|
|
||||||
|
VOLUME ["/tmp/.X11-unix"]
|
||||||
|
|
||||||
|
# check if /image is a disk image or a directory. This allows you to optionally use -v disk.img:/image
|
||||||
|
# NOPICKER is used to skip the disk selection screen
|
||||||
|
# GENERATE_UNIQUE is used to generate serial numbers on boot.
|
||||||
|
# /env is a file that you can generate and save using -v source.sh:/env
|
||||||
|
# the env file is a file that you can carry to the next container which will supply the serials numbers.
|
||||||
|
# GENERATE_SPECIFIC is used to either accept the env serial numbers OR you can supply using:
|
||||||
|
# -e DEVICE_MODEL="iMacPro1,1" \
|
||||||
|
# -e SERIAL="C02TW0WAHX87" \
|
||||||
|
# -e BOARD_SERIAL="C027251024NJG36UE" \
|
||||||
|
# -e UUID="5CCB366D-9118-4C61-A00A-E5BAF3BED451" \
|
||||||
|
# -e MAC_ADDRESS="A8:5C:2C:9A:46:2F" \
|
||||||
|
|
||||||
|
# the output will be /bootdisk.
|
||||||
|
# /bootdisk is a useful persistent place to store the 15Mb serial number bootdisk.
|
||||||
|
|
||||||
|
# if you don't set any of the above:
|
||||||
|
# the default serial numbers are already contained in ./OpenCore/OpenCore.qcow2
|
||||||
|
# And the default serial numbers
|
||||||
|
|
||||||
|
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
|
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
|
; [[ "${NOPICKER}" == true ]] && { \
|
||||||
|
sed -i '/^.*InstallMedia.*/d' Launch.sh \
|
||||||
|
&& export BOOTDISK="${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore-nopicker.qcow2}" \
|
||||||
|
; } \
|
||||||
|
|| export BOOTDISK="${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||||
|
; [[ "${GENERATE_UNIQUE}" == true ]] && { \
|
||||||
|
./Docker-OSX/osx-serial-generator/generate-unique-machine-values.sh \
|
||||||
|
--master-plist-url="${MASTER_PLIST_URL}" \
|
||||||
|
--count 1 \
|
||||||
|
--tsv ./serial.tsv \
|
||||||
|
--bootdisks \
|
||||||
|
--width "${WIDTH:-1920}" \
|
||||||
|
--height "${HEIGHT:-1080}" \
|
||||||
|
--output-bootdisk "${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||||
|
--output-env "${ENV:=/env}" \
|
||||||
|
|| exit 1 ; } \
|
||||||
|
; [[ "${GENERATE_SPECIFIC}" == true ]] && { \
|
||||||
|
source "${ENV:=/env}" 2>/dev/null \
|
||||||
|
; ./Docker-OSX/osx-serial-generator/generate-specific-bootdisk.sh \
|
||||||
|
--master-plist-url="${MASTER_PLIST_URL}" \
|
||||||
|
--model "${DEVICE_MODEL}" \
|
||||||
|
--serial "${SERIAL}" \
|
||||||
|
--board-serial "${BOARD_SERIAL}" \
|
||||||
|
--uuid "${UUID}" \
|
||||||
|
--mac-address "${MAC_ADDRESS}" \
|
||||||
|
--width "${WIDTH:-1920}" \
|
||||||
|
--height "${HEIGHT:-1080}" \
|
||||||
|
--output-bootdisk "${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||||
|
|| exit 1 ; } \
|
||||||
|
; ./enable-ssh.sh && /bin/bash -c ./Launch.sh
|
||||||
|
|
||||||
|
# virt-manager mode: eta son
|
||||||
|
# CMD virsh define <(envsubst < Docker-OSX.xml) && virt-manager || virt-manager
|
||||||
|
# CMD virsh define <(envsubst < macOS-libvirt-Catalina.xml) && virt-manager || virt-manager
|
@ -166,20 +166,7 @@ ENV HEIGHT=1080
|
|||||||
ENV LIBGUESTFS_DEBUG=1
|
ENV LIBGUESTFS_DEBUG=1
|
||||||
ENV LIBGUESTFS_TRACE=1
|
ENV LIBGUESTFS_TRACE=1
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is catalina, which means :latest is catalina
|
|
||||||
ENV SHORTNAME=sonoma
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
|
||||||
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
; { [[ "${DISPLAY}" = ':99' ]] || [[ "${HEADLESS}" == true ]] ; } && { \
|
; { [[ "${DISPLAY}" = ':99' ]] || [[ "${HEADLESS}" == true ]] ; } && { \
|
||||||
nohup Xvfb :99 -screen 0 1920x1080x16 \
|
nohup Xvfb :99 -screen 0 1920x1080x16 \
|
||||||
|
@ -183,20 +183,7 @@ ENV TERMS_OF_USE=i_agree
|
|||||||
|
|
||||||
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD echo "${BOILERPLATE}" \
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is catalina, which means :latest is catalina
|
|
||||||
ENV SHORTNAME=sonoma
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; echo "${BOILERPLATE}" \
|
|
||||||
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
||||||
; echo "Disk is being copied between layers... Please wait a minute..." \
|
; echo "Disk is being copied between layers... Please wait a minute..." \
|
||||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||||
|
725
main_app.py
725
main_app.py
@ -4,26 +4,26 @@ import subprocess
|
|||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import platform
|
import platform
|
||||||
|
import ctypes
|
||||||
|
import json # For parsing PowerShell JSON output
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar,
|
QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar,
|
||||||
QFileDialog, QGroupBox, QLineEdit # Added QLineEdit
|
QFileDialog, QGroupBox, QLineEdit, QProgressBar # Added QProgressBar
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, Qt # Added Qt
|
||||||
|
|
||||||
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS
|
# ... (Worker classes and other imports remain the same) ...
|
||||||
|
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS, DOCKER_IMAGE_BASE
|
||||||
from utils import (
|
from utils import (
|
||||||
build_docker_command, get_unique_container_name,
|
build_docker_command, get_unique_container_name,
|
||||||
build_docker_cp_command, CONTAINER_MACOS_IMG_PATH, CONTAINER_OPENCORE_QCOW2_PATH,
|
build_docker_cp_command, CONTAINER_MACOS_IMG_PATH, CONTAINER_OPENCORE_QCOW2_PATH,
|
||||||
build_docker_stop_command, build_docker_rm_command
|
build_docker_stop_command, build_docker_rm_command
|
||||||
)
|
)
|
||||||
|
|
||||||
USBWriterLinux = None
|
USBWriterLinux = None; USBWriterMacOS = None; USBWriterWindows = None
|
||||||
USBWriterMacOS = None
|
|
||||||
USBWriterWindows = None
|
|
||||||
|
|
||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
try: from usb_writer_linux import USBWriterLinux
|
try: from usb_writer_linux import USBWriterLinux
|
||||||
except ImportError as e: print(f"Could not import USBWriterLinux: {e}")
|
except ImportError as e: print(f"Could not import USBWriterLinux: {e}")
|
||||||
@ -34,143 +34,108 @@ elif platform.system() == "Windows":
|
|||||||
try: from usb_writer_windows import USBWriterWindows
|
try: from usb_writer_windows import USBWriterWindows
|
||||||
except ImportError as e: print(f"Could not import USBWriterWindows: {e}")
|
except ImportError as e: print(f"Could not import USBWriterWindows: {e}")
|
||||||
|
|
||||||
class WorkerSignals(QObject):
|
class WorkerSignals(QObject): progress = pyqtSignal(str); finished = pyqtSignal(str); error = pyqtSignal(str)
|
||||||
progress = pyqtSignal(str)
|
|
||||||
finished = pyqtSignal(str)
|
|
||||||
error = pyqtSignal(str)
|
|
||||||
|
|
||||||
class DockerRunWorker(QObject): # ... (same as before)
|
class DockerPullWorker(QObject): # ... ( 그대로 )
|
||||||
def __init__(self, command_list):
|
signals = WorkerSignals()
|
||||||
super().__init__()
|
def __init__(self, image_name: str): super().__init__(); self.image_name = image_name
|
||||||
self.command_list = command_list
|
@pyqtSlot()
|
||||||
self.signals = WorkerSignals()
|
def run(self):
|
||||||
self.process = None
|
try:
|
||||||
self._is_running = True
|
command = ["docker", "pull", self.image_name]; self.signals.progress.emit(f"Pulling Docker image: {self.image_name}...\n")
|
||||||
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0)
|
||||||
|
if process.stdout:
|
||||||
|
for line in iter(process.stdout.readline, ''): self.signals.progress.emit(line)
|
||||||
|
process.stdout.close()
|
||||||
|
return_code = process.wait()
|
||||||
|
if return_code == 0: self.signals.finished.emit(f"Image '{self.image_name}' pulled successfully or already exists.")
|
||||||
|
else: self.signals.error.emit(f"Failed to pull image '{self.image_name}' (exit code {return_code}).")
|
||||||
|
except FileNotFoundError: self.signals.error.emit("Error: Docker command not found.")
|
||||||
|
except Exception as e: self.signals.error.emit(f"An error occurred during docker pull: {str(e)}")
|
||||||
|
|
||||||
|
class DockerRunWorker(QObject): # ... ( 그대로 )
|
||||||
|
signals = WorkerSignals()
|
||||||
|
def __init__(self, command_list): super().__init__(); self.command_list = command_list; self.process = None; self._is_running = True
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
self.signals.progress.emit(f"Executing: {' '.join(self.command_list)}\n")
|
self.signals.progress.emit(f"Executing: {' '.join(self.command_list)}\n")
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(self.command_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0)
|
||||||
self.command_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
text=True, bufsize=1, universal_newlines=True,
|
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
||||||
)
|
|
||||||
if self.process.stdout:
|
if self.process.stdout:
|
||||||
for line in iter(self.process.stdout.readline, ''):
|
for line in iter(self.process.stdout.readline, ''):
|
||||||
if not self._is_running:
|
if not self._is_running: self.signals.progress.emit("Docker process stopping at user request.\n"); break
|
||||||
self.signals.progress.emit("Docker process stopping at user request.\n")
|
|
||||||
break
|
|
||||||
self.signals.progress.emit(line)
|
self.signals.progress.emit(line)
|
||||||
self.process.stdout.close()
|
self.process.stdout.close()
|
||||||
return_code = self.process.wait()
|
return_code = self.process.wait()
|
||||||
if not self._is_running and return_code != 0 :
|
if not self._is_running and return_code != 0 : self.signals.finished.emit(f"Docker process cancelled or stopped early (exit code {return_code})."); return
|
||||||
self.signals.finished.emit(f"Docker process cancelled or stopped early (exit code {return_code}).")
|
if return_code == 0: self.signals.finished.emit("Docker VM process (QEMU) closed by user or completed.")
|
||||||
return
|
else: self.signals.finished.emit(f"Docker VM process exited (code {return_code}). Assuming macOS setup was attempted or QEMU window closed.")
|
||||||
if return_code == 0:
|
|
||||||
self.signals.finished.emit("Docker VM process (QEMU) closed by user or completed.")
|
|
||||||
else:
|
|
||||||
self.signals.finished.emit(f"Docker VM process exited (code {return_code}). Assuming macOS setup was attempted or QEMU window closed.")
|
|
||||||
except FileNotFoundError: self.signals.error.emit("Error: Docker command not found.")
|
except FileNotFoundError: self.signals.error.emit("Error: Docker command not found.")
|
||||||
except Exception as e: self.signals.error.emit(f"An error occurred during Docker run: {str(e)}")
|
except Exception as e: self.signals.error.emit(f"An error occurred during Docker run: {str(e)}")
|
||||||
finally: self._is_running = False
|
finally: self._is_running = False
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
if self.process and self.process.poll() is None:
|
if self.process and self.process.poll() is None:
|
||||||
self.signals.progress.emit("Attempting to stop Docker process...\n")
|
self.signals.progress.emit("Attempting to stop Docker process...\n")
|
||||||
try:
|
try: self.process.terminate(); self.process.wait(timeout=5)
|
||||||
self.process.terminate()
|
except subprocess.TimeoutExpired: self.signals.progress.emit("Process did not terminate gracefully, killing.\n"); self.process.kill()
|
||||||
try: self.process.wait(timeout=5)
|
self.signals.progress.emit("Docker process stopped.\n")
|
||||||
except subprocess.TimeoutExpired:
|
elif self.process and self.process.poll() is not None: self.signals.progress.emit("Docker process already stopped.\n")
|
||||||
self.signals.progress.emit("Process did not terminate gracefully, killing.\n")
|
|
||||||
self.process.kill()
|
|
||||||
self.signals.progress.emit("Docker process stopped.\n")
|
|
||||||
except Exception as e: self.signals.error.emit(f"Error stopping process: {str(e)}\n")
|
|
||||||
|
|
||||||
class DockerCommandWorker(QObject): # ... (same as before)
|
|
||||||
def __init__(self, command_list, success_message="Command completed."):
|
|
||||||
super().__init__()
|
|
||||||
self.command_list = command_list
|
|
||||||
self.signals = WorkerSignals()
|
|
||||||
self.success_message = success_message
|
|
||||||
|
|
||||||
|
class DockerCommandWorker(QObject): # ... ( 그대로 )
|
||||||
|
signals = WorkerSignals()
|
||||||
|
def __init__(self, command_list, success_message="Command completed."): super().__init__(); self.command_list = command_list; self.signals = WorkerSignals(); self.success_message = success_message
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
self.signals.progress.emit(f"Executing: {' '.join(self.command_list)}\n")
|
self.signals.progress.emit(f"Executing: {' '.join(self.command_list)}\n"); result = subprocess.run(self.command_list, capture_output=True, text=True, check=False, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0)
|
||||||
result = subprocess.run(
|
|
||||||
self.command_list, capture_output=True, text=True, check=False,
|
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
||||||
)
|
|
||||||
if result.stdout and result.stdout.strip(): self.signals.progress.emit(result.stdout)
|
if result.stdout and result.stdout.strip(): self.signals.progress.emit(result.stdout)
|
||||||
if result.stderr and result.stderr.strip(): self.signals.progress.emit(f"STDERR: {result.stderr}")
|
if result.stderr and result.stderr.strip(): self.signals.progress.emit(f"STDERR: {result.stderr}")
|
||||||
if result.returncode == 0: self.signals.finished.emit(self.success_message)
|
if result.returncode == 0: self.signals.finished.emit(self.success_message)
|
||||||
else:
|
else: self.signals.error.emit(f"Command failed (code {result.returncode}): {result.stderr or result.stdout or 'Unknown error'}".strip())
|
||||||
err_msg = result.stderr or result.stdout or "Unknown error"
|
|
||||||
self.signals.error.emit(f"Command failed with code {result.returncode}: {err_msg.strip()}")
|
|
||||||
except FileNotFoundError: self.signals.error.emit("Error: Docker command not found.")
|
except FileNotFoundError: self.signals.error.emit("Error: Docker command not found.")
|
||||||
except Exception as e: self.signals.error.emit(f"An error occurred: {str(e)}")
|
except Exception as e: self.signals.error.emit(f"An error occurred: {str(e)}")
|
||||||
|
|
||||||
class USBWriterWorker(QObject): # ... (same as before, uses platform check)
|
class USBWriterWorker(QObject): # ... ( 그대로 )
|
||||||
signals = WorkerSignals()
|
signals = WorkerSignals()
|
||||||
def __init__(self, device, opencore_path, macos_path):
|
def __init__(self, device, opencore_path, macos_path): super().__init__(); self.device, self.opencore_path, self.macos_path = device, opencore_path, macos_path; self.writer_instance = None
|
||||||
super().__init__()
|
|
||||||
self.device = device
|
|
||||||
self.opencore_path = opencore_path
|
|
||||||
self.macos_path = macos_path
|
|
||||||
self.writer_instance = None
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self):
|
def run(self):
|
||||||
current_os = platform.system()
|
current_os = platform.system()
|
||||||
try:
|
try:
|
||||||
if current_os == "Linux":
|
writer_cls = None
|
||||||
if USBWriterLinux is None: self.signals.error.emit("USBWriterLinux module not available."); return
|
if current_os == "Linux": writer_cls = USBWriterLinux
|
||||||
self.writer_instance = USBWriterLinux(self.device, self.opencore_path, self.macos_path, lambda msg: self.signals.progress.emit(msg))
|
elif current_os == "Darwin": writer_cls = USBWriterMacOS
|
||||||
elif current_os == "Darwin":
|
elif current_os == "Windows": writer_cls = USBWriterWindows
|
||||||
if USBWriterMacOS is None: self.signals.error.emit("USBWriterMacOS module not available."); return
|
if writer_cls is None: self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return
|
||||||
self.writer_instance = USBWriterMacOS(self.device, self.opencore_path, self.macos_path, lambda msg: self.signals.progress.emit(msg))
|
self.writer_instance = writer_cls(self.device, self.opencore_path, self.macos_path, lambda msg: self.signals.progress.emit(msg))
|
||||||
elif current_os == "Windows":
|
if self.writer_instance.format_and_write(): self.signals.finished.emit("USB writing process completed successfully.")
|
||||||
if USBWriterWindows is None: self.signals.error.emit("USBWriterWindows module not available."); return
|
else: self.signals.error.emit("USB writing process failed. Check output for details.")
|
||||||
self.writer_instance = USBWriterWindows(self.device, self.opencore_path, self.macos_path, lambda msg: self.signals.progress.emit(msg))
|
except Exception as e: self.signals.error.emit(f"USB writing preparation error: {str(e)}")
|
||||||
else:
|
|
||||||
self.signals.error.emit(f"USB writing not supported on {current_os}."); return
|
|
||||||
|
|
||||||
if self.writer_instance.format_and_write():
|
|
||||||
self.signals.finished.emit("USB writing process completed successfully.")
|
|
||||||
else:
|
|
||||||
self.signals.error.emit("USB writing process failed. Check output for details.")
|
|
||||||
except Exception as e:
|
|
||||||
self.signals.error.emit(f"USB writing preparation error: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow): # ... (init and _setup_ui need changes for Windows USB input)
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self): # ... (init remains the same)
|
||||||
super().__init__()
|
super().__init__(); self.setWindowTitle(APP_NAME); self.setGeometry(100, 100, 800, 850)
|
||||||
self.setWindowTitle(APP_NAME)
|
self.current_container_name = None; self.extracted_main_image_path = None; self.extracted_opencore_image_path = None
|
||||||
self.setGeometry(100, 100, 800, 850) # Adjusted height
|
self.extraction_status = {"main": False, "opencore": False}; self.active_worker_thread = None
|
||||||
self.current_container_name = None
|
self.docker_run_worker_instance = None; self.docker_pull_worker_instance = None
|
||||||
self.extracted_main_image_path = None
|
self._current_usb_selection_text = None
|
||||||
self.extracted_opencore_image_path = None
|
self._setup_ui(); self.refresh_usb_drives()
|
||||||
self.extraction_status = {"main": False, "opencore": False}
|
|
||||||
self.active_worker_thread = None
|
|
||||||
self.docker_run_worker_instance = None
|
|
||||||
self._setup_ui()
|
|
||||||
self.refresh_usb_drives()
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self): # Updated for Windows USB detection
|
||||||
# ... (Menu bar, Step 1, 2, 3 groups - same as before) ...
|
|
||||||
menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help")
|
menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help")
|
||||||
exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action)
|
exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action)
|
||||||
about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action)
|
about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action)
|
||||||
central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
|
central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Steps 1, 2, 3 remain the same UI structure
|
||||||
vm_creation_group = QGroupBox("Step 1: Create and Install macOS VM"); vm_layout = QVBoxLayout()
|
vm_creation_group = QGroupBox("Step 1: Create and Install macOS VM"); vm_layout = QVBoxLayout()
|
||||||
selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox()
|
selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox()
|
||||||
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
|
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
|
||||||
vm_layout.addLayout(selection_layout); self.run_vm_button = QPushButton("Create VM and Start macOS Installation")
|
vm_layout.addLayout(selection_layout); self.run_vm_button = QPushButton("Create VM and Start macOS Installation")
|
||||||
self.run_vm_button.clicked.connect(self.run_macos_vm); vm_layout.addWidget(self.run_vm_button)
|
self.run_vm_button.clicked.connect(self.initiate_vm_creation_flow); vm_layout.addWidget(self.run_vm_button)
|
||||||
self.stop_vm_button = QPushButton("Stop/Cancel VM Creation"); self.stop_vm_button.clicked.connect(self.stop_docker_run_process)
|
self.stop_vm_button = QPushButton("Stop/Cancel Current Docker Operation"); self.stop_vm_button.clicked.connect(self.stop_current_docker_operation)
|
||||||
self.stop_vm_button.setEnabled(False); vm_layout.addWidget(self.stop_vm_button); vm_creation_group.setLayout(vm_layout)
|
self.stop_vm_button.setEnabled(False); vm_layout.addWidget(self.stop_vm_button); vm_creation_group.setLayout(vm_layout)
|
||||||
main_layout.addWidget(vm_creation_group)
|
main_layout.addWidget(vm_creation_group)
|
||||||
extraction_group = QGroupBox("Step 2: Extract VM Images"); ext_layout = QVBoxLayout()
|
extraction_group = QGroupBox("Step 2: Extract VM Images"); ext_layout = QVBoxLayout()
|
||||||
@ -184,12 +149,12 @@ class MainWindow(QMainWindow): # ... (init and _setup_ui need changes for Window
|
|||||||
self.remove_container_button.setEnabled(False); mgmt_layout.addWidget(self.remove_container_button); mgmt_group.setLayout(mgmt_layout)
|
self.remove_container_button.setEnabled(False); mgmt_layout.addWidget(self.remove_container_button); mgmt_group.setLayout(mgmt_layout)
|
||||||
main_layout.addWidget(mgmt_group)
|
main_layout.addWidget(mgmt_group)
|
||||||
|
|
||||||
# Step 4: USB Drive Selection - Modified for Windows
|
# Step 4: USB Drive Selection - UI now adapts to Windows
|
||||||
usb_group = QGroupBox("Step 4: Select Target USB Drive and Write")
|
usb_group = QGroupBox("Step 4: Select Target USB Drive and Write")
|
||||||
usb_layout = QVBoxLayout()
|
self.usb_layout = QVBoxLayout()
|
||||||
|
|
||||||
self.usb_drive_label = QLabel("Available USB Drives (for Linux/macOS):")
|
self.usb_drive_label = QLabel("Available USB Drives:")
|
||||||
usb_layout.addWidget(self.usb_drive_label)
|
self.usb_layout.addWidget(self.usb_drive_label)
|
||||||
|
|
||||||
usb_selection_layout = QHBoxLayout()
|
usb_selection_layout = QHBoxLayout()
|
||||||
self.usb_drive_combo = QComboBox()
|
self.usb_drive_combo = QComboBox()
|
||||||
@ -199,246 +164,322 @@ class MainWindow(QMainWindow): # ... (init and _setup_ui need changes for Window
|
|||||||
self.refresh_usb_button = QPushButton("Refresh List")
|
self.refresh_usb_button = QPushButton("Refresh List")
|
||||||
self.refresh_usb_button.clicked.connect(self.refresh_usb_drives)
|
self.refresh_usb_button.clicked.connect(self.refresh_usb_drives)
|
||||||
usb_selection_layout.addWidget(self.refresh_usb_button)
|
usb_selection_layout.addWidget(self.refresh_usb_button)
|
||||||
usb_layout.addLayout(usb_selection_layout)
|
self.usb_layout.addLayout(usb_selection_layout)
|
||||||
|
|
||||||
# Windows-specific input for disk ID
|
# Windows-specific input for disk ID - initially hidden and managed by refresh_usb_drives
|
||||||
self.windows_usb_input_label = QLabel("For Windows: Enter USB Disk Number (e.g., 1, 2). Use 'diskpart' -> 'list disk' in an Admin CMD to find it.")
|
self.windows_usb_guidance_label = QLabel("For Windows: Detected USB Disks (select from dropdown).")
|
||||||
|
self.windows_usb_input_label = QLabel("Manual Fallback: Enter USB Disk Number (e.g., 1, 2):")
|
||||||
self.windows_disk_id_input = QLineEdit()
|
self.windows_disk_id_input = QLineEdit()
|
||||||
self.windows_disk_id_input.setPlaceholderText("Enter Disk Number (e.g., 1)")
|
self.windows_disk_id_input.setPlaceholderText("Enter Disk Number if dropdown empty")
|
||||||
self.windows_disk_id_input.textChanged.connect(self.update_write_to_usb_button_state)
|
self.windows_disk_id_input.textChanged.connect(self.update_write_to_usb_button_state)
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
self.usb_layout.addWidget(self.windows_usb_guidance_label)
|
||||||
self.usb_drive_label.setText("Detected Mountable Partitions (for reference only for writing):")
|
self.usb_layout.addWidget(self.windows_usb_input_label)
|
||||||
usb_layout.addWidget(self.windows_usb_input_label)
|
self.usb_layout.addWidget(self.windows_disk_id_input)
|
||||||
usb_layout.addWidget(self.windows_disk_id_input)
|
# Visibility will be toggled in refresh_usb_drives based on OS
|
||||||
else:
|
|
||||||
self.windows_usb_input_label.setVisible(False)
|
|
||||||
self.windows_disk_id_input.setVisible(False)
|
|
||||||
|
|
||||||
warning_label = QLabel("WARNING: Selecting a drive and proceeding to write will ERASE ALL DATA on it!")
|
warning_label = QLabel("WARNING: Selecting a drive and proceeding to write will ERASE ALL DATA on it!")
|
||||||
warning_label.setStyleSheet("color: red; font-weight: bold;")
|
warning_label.setStyleSheet("color: red; font-weight: bold;")
|
||||||
usb_layout.addWidget(warning_label)
|
self.usb_layout.addWidget(warning_label)
|
||||||
|
|
||||||
self.write_to_usb_button = QPushButton("Write Images to USB Drive")
|
self.write_to_usb_button = QPushButton("Write Images to USB Drive")
|
||||||
self.write_to_usb_button.clicked.connect(self.handle_write_to_usb)
|
self.write_to_usb_button.clicked.connect(self.handle_write_to_usb)
|
||||||
self.write_to_usb_button.setEnabled(False)
|
self.write_to_usb_button.setEnabled(False)
|
||||||
usb_layout.addWidget(self.write_to_usb_button)
|
self.usb_layout.addWidget(self.write_to_usb_button)
|
||||||
|
|
||||||
usb_group.setLayout(usb_layout)
|
usb_group.setLayout(self.usb_layout)
|
||||||
main_layout.addWidget(usb_group)
|
main_layout.addWidget(usb_group)
|
||||||
|
|
||||||
self.output_area = QTextEdit()
|
self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area)
|
||||||
self.output_area.setReadOnly(True)
|
|
||||||
main_layout.addWidget(self.output_area)
|
|
||||||
|
|
||||||
def show_about_dialog(self): # ... (same as before, update version)
|
# Status Bar and Progress Bar
|
||||||
QMessageBox.about(self, f"About {APP_NAME}",
|
self.statusBar = self.statusBar()
|
||||||
f"Version: 0.6.0\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\n"
|
self.progressBar = QProgressBar(self)
|
||||||
"This tool helps create bootable macOS USB drives using Docker-OSX.")
|
self.progressBar.setRange(0, 0) # Indeterminate
|
||||||
|
self.progressBar.setVisible(False)
|
||||||
|
self.statusBar.addPermanentWidget(self.progressBar, 0)
|
||||||
|
|
||||||
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker"): # ... (same as before)
|
|
||||||
|
def _set_ui_busy(self, is_busy: bool, status_message: str = None):
|
||||||
|
"""Manages UI element states and progress indicators."""
|
||||||
|
self.general_interactive_widgets = [
|
||||||
|
self.run_vm_button, self.version_combo, self.extract_images_button,
|
||||||
|
self.stop_container_button, self.remove_container_button,
|
||||||
|
self.usb_drive_combo, self.refresh_usb_button, self.write_to_usb_button,
|
||||||
|
self.windows_disk_id_input
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_busy:
|
||||||
|
for widget in self.general_interactive_widgets:
|
||||||
|
widget.setEnabled(False)
|
||||||
|
self.progressBar.setVisible(True)
|
||||||
|
self.statusBar.showMessage(status_message or "Processing...", 0)
|
||||||
|
# stop_vm_button's state is managed specifically by the calling function if needed
|
||||||
|
else:
|
||||||
|
# Re-enable based on current application state by calling a dedicated method
|
||||||
|
self.update_button_states_after_operation() # This will set appropriate states
|
||||||
|
self.progressBar.setVisible(False)
|
||||||
|
self.statusBar.showMessage(status_message or "Ready.", 5000) # Message disappears after 5s
|
||||||
|
|
||||||
|
def update_button_states_after_operation(self):
|
||||||
|
"""Centralized method to update button states based on app's current state."""
|
||||||
|
is_worker_running = self.active_worker_thread and self.active_worker_thread.isRunning()
|
||||||
|
|
||||||
|
self.run_vm_button.setEnabled(not is_worker_running)
|
||||||
|
self.version_combo.setEnabled(not is_worker_running)
|
||||||
|
|
||||||
|
pull_worker_active = getattr(self, "docker_pull_instance", None) is not None
|
||||||
|
run_worker_active = getattr(self, "docker_run_instance", None) is not None
|
||||||
|
self.stop_vm_button.setEnabled(is_worker_running and (pull_worker_active or run_worker_active))
|
||||||
|
|
||||||
|
can_extract = self.current_container_name is not None and not is_worker_running
|
||||||
|
self.extract_images_button.setEnabled(can_extract)
|
||||||
|
|
||||||
|
can_manage_container = self.current_container_name is not None and not is_worker_running
|
||||||
|
self.stop_container_button.setEnabled(can_manage_container)
|
||||||
|
# Remove button is enabled if container exists and no worker is running (simplification)
|
||||||
|
# A more accurate state for remove_container_button would be if the container is actually stopped.
|
||||||
|
# This is typically handled by the finished slot of the stop_container worker.
|
||||||
|
# For now, this is a general enablement if not busy.
|
||||||
|
self.remove_container_button.setEnabled(can_manage_container)
|
||||||
|
|
||||||
|
|
||||||
|
self.refresh_usb_button.setEnabled(not is_worker_running)
|
||||||
|
self.update_write_to_usb_button_state() # This handles its own complex logic
|
||||||
|
|
||||||
|
def show_about_dialog(self): # Updated version
|
||||||
|
QMessageBox.about(self, f"About {APP_NAME}", f"Version: 0.8.1\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using Docker-OSX.")
|
||||||
|
|
||||||
|
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", busy_message="Processing..."):
|
||||||
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||||
QMessageBox.warning(self, "Busy", "Another operation is already in progress. Please wait.")
|
QMessageBox.warning(self, "Busy", "Another operation is in progress."); return False
|
||||||
return False
|
|
||||||
self.active_worker_thread = QThread()
|
self._set_ui_busy(True, busy_message)
|
||||||
self.active_worker_thread.setObjectName(worker_name + "_thread")
|
if worker_name in ["docker_pull", "docker_run"]:
|
||||||
setattr(self, f"{worker_name}_instance", worker_instance)
|
self.stop_vm_button.setEnabled(True) # Enable stop for these specific long ops
|
||||||
|
else: # For other workers, the main stop button for docker ops is not relevant
|
||||||
|
self.stop_vm_button.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
|
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread"); setattr(self, f"{worker_name}_instance", worker_instance)
|
||||||
worker_instance.moveToThread(self.active_worker_thread)
|
worker_instance.moveToThread(self.active_worker_thread)
|
||||||
|
|
||||||
|
# Connect to generic handlers
|
||||||
worker_instance.signals.progress.connect(self.update_output)
|
worker_instance.signals.progress.connect(self.update_output)
|
||||||
worker_instance.signals.finished.connect(on_finished_slot)
|
worker_instance.signals.finished.connect(lambda message: self._handle_worker_finished(message, on_finished_slot, worker_name))
|
||||||
worker_instance.signals.error.connect(on_error_slot)
|
worker_instance.signals.error.connect(lambda error_message: self._handle_worker_error(error_message, on_error_slot, worker_name))
|
||||||
worker_instance.signals.finished.connect(self.active_worker_thread.quit)
|
|
||||||
worker_instance.signals.error.connect(self.active_worker_thread.quit)
|
|
||||||
self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater)
|
self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater)
|
||||||
self.active_worker_thread.finished.connect(lambda: self._clear_worker_instance(worker_name)) # Use new clear method
|
# No need to call _clear_worker_instance here, _handle_worker_finished/error will do it.
|
||||||
self.active_worker_thread.started.connect(worker_instance.run)
|
self.active_worker_thread.started.connect(worker_instance.run); self.active_worker_thread.start(); return True
|
||||||
self.active_worker_thread.start()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _clear_worker_instance(self, worker_name): # New method to clean up worker instance from self
|
def _handle_worker_finished(self, message, specific_finished_slot, worker_name):
|
||||||
|
"""Generic handler for worker finished signals."""
|
||||||
|
self.output_area.append(f"\n--- Worker '{worker_name}' Finished --- \n{message}") # Generic log
|
||||||
|
self._clear_worker_instance(worker_name) # Clear the worker instance from self
|
||||||
|
self.active_worker_thread = None # Mark thread as free
|
||||||
|
if specific_finished_slot:
|
||||||
|
specific_finished_slot(message) # Call the specific logic for this worker
|
||||||
|
self._set_ui_busy(False, "Operation completed successfully.") # Reset UI
|
||||||
|
|
||||||
|
def _handle_worker_error(self, error_message, specific_error_slot, worker_name):
|
||||||
|
"""Generic handler for worker error signals."""
|
||||||
|
self.output_area.append(f"\n--- Worker '{worker_name}' Error --- \n{error_message}") # Generic log
|
||||||
|
self._clear_worker_instance(worker_name) # Clear the worker instance from self
|
||||||
|
self.active_worker_thread = None # Mark thread as free
|
||||||
|
if specific_error_slot:
|
||||||
|
specific_error_slot(error_message) # Call the specific logic for this worker
|
||||||
|
self._set_ui_busy(False, "An error occurred.") # Reset UI
|
||||||
|
|
||||||
|
def _clear_worker_instance(self, worker_name):
|
||||||
attr_name = f"{worker_name}_instance"
|
attr_name = f"{worker_name}_instance"
|
||||||
if hasattr(self, attr_name):
|
if hasattr(self, attr_name): delattr(self, attr_name)
|
||||||
delattr(self, attr_name)
|
|
||||||
|
|
||||||
def run_macos_vm(self): # ... (same as before, ensure worker_name matches for _clear_worker_instance)
|
def initiate_vm_creation_flow(self):
|
||||||
selected_version_name = self.version_combo.currentText()
|
self.output_area.clear(); selected_version_name = self.version_combo.currentText(); image_tag = MACOS_VERSIONS.get(selected_version_name)
|
||||||
self.current_container_name = get_unique_container_name()
|
if not image_tag: self.handle_error(f"Invalid macOS version: {selected_version_name}"); return # handle_error calls _set_ui_busy(False)
|
||||||
|
full_image_name = f"{DOCKER_IMAGE_BASE}:{image_tag}"
|
||||||
|
pull_worker = DockerPullWorker(full_image_name)
|
||||||
|
# Pass busy message to _start_worker
|
||||||
|
self._start_worker(pull_worker,
|
||||||
|
self.docker_pull_finished,
|
||||||
|
self.docker_pull_error,
|
||||||
|
"docker_pull",
|
||||||
|
f"Pulling image {full_image_name}...")
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def docker_pull_finished(self, message): # Specific handler
|
||||||
|
# Generic handler (_handle_worker_finished) already logged, cleared instance, and reset UI.
|
||||||
|
# This slot now only handles the next step in the sequence.
|
||||||
|
self.output_area.append(f"Step 1.2: Proceeding to run Docker container for macOS installation...")
|
||||||
|
self.run_macos_vm()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def docker_pull_error(self, error_message): # Specific handler
|
||||||
|
# Generic handler (_handle_worker_error) already logged, cleared instance, and reset UI.
|
||||||
|
QMessageBox.critical(self, "Docker Pull Error", error_message)
|
||||||
|
# No further specific action needed here, UI reset is handled by the generic error handler.
|
||||||
|
|
||||||
|
def run_macos_vm(self): # This is now part 2 of the flow
|
||||||
|
selected_version_name = self.version_combo.currentText(); self.current_container_name = get_unique_container_name()
|
||||||
try:
|
try:
|
||||||
command_list = build_docker_command(selected_version_name, self.current_container_name)
|
command_list = build_docker_command(selected_version_name, self.current_container_name)
|
||||||
self.output_area.clear()
|
run_worker = DockerRunWorker(command_list)
|
||||||
self.output_area.append(f"Starting macOS VM creation for {selected_version_name}...") # ... rest of messages
|
# Pass busy message to _start_worker
|
||||||
|
self._start_worker(run_worker,
|
||||||
docker_run_worker = DockerRunWorker(command_list) # Local var, instance stored by _start_worker
|
self.docker_run_finished,
|
||||||
if self._start_worker(docker_run_worker, self.docker_run_finished, self.docker_run_error, "docker_run"):
|
self.docker_run_error,
|
||||||
self.run_vm_button.setEnabled(False); self.version_combo.setEnabled(False)
|
"docker_run",
|
||||||
self.stop_vm_button.setEnabled(True); self.extract_images_button.setEnabled(False)
|
f"Starting container {self.current_container_name}...")
|
||||||
self.write_to_usb_button.setEnabled(False)
|
except ValueError as e: self.handle_error(f"Failed to build command: {str(e)}") # This error is before worker start
|
||||||
except ValueError as e: self.handle_error(f"Failed to build command: {str(e)}")
|
except Exception as e: self.handle_error(f"An unexpected error: {str(e)}") # This error is before worker start
|
||||||
except Exception as e: self.handle_error(f"An unexpected error: {str(e)}")
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def update_output(self, text): # ... (same as before)
|
def update_output(self, text): self.output_area.append(text.strip()); QApplication.processEvents()
|
||||||
self.output_area.append(text.strip()); QApplication.processEvents()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def docker_run_finished(self, message): # ... (same as before)
|
def docker_run_finished(self, message): # Specific handler
|
||||||
self.output_area.append(f"\n--- macOS VM Setup Process Finished ---\n{message}")
|
# Generic handler already took care of logging, instance clearing, and UI reset.
|
||||||
QMessageBox.information(self, "VM Setup Complete", f"{message}\nYou can now proceed to extract images.")
|
QMessageBox.information(self, "VM Setup Complete", f"{message}\nYou can now proceed to extract images.")
|
||||||
self.run_vm_button.setEnabled(True); self.version_combo.setEnabled(True)
|
# Specific logic after run finishes (e.g. enabling extraction) is now in update_button_states_after_operation
|
||||||
self.stop_vm_button.setEnabled(False); self.extract_images_button.setEnabled(True)
|
|
||||||
self.stop_container_button.setEnabled(True)
|
|
||||||
self.active_worker_thread = None # Cleared by _start_worker's finished connection
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def docker_run_error(self, error_message): # ... (same as before)
|
def docker_run_error(self, error_message): # Specific handler
|
||||||
self.output_area.append(f"\n--- macOS VM Setup Process Error ---\n{error_message}")
|
# Generic handler already took care of logging, instance clearing, and UI reset.
|
||||||
if "exited" in error_message.lower() and self.current_container_name:
|
if "exited" in error_message.lower() and self.current_container_name:
|
||||||
QMessageBox.warning(self, "VM Setup Ended", f"{error_message}\nAssuming macOS setup was attempted...")
|
QMessageBox.warning(self, "VM Setup Ended", f"{error_message}\nAssuming macOS setup was attempted...")
|
||||||
self.extract_images_button.setEnabled(True); self.stop_container_button.setEnabled(True)
|
# Specific logic (e.g. enabling extraction) is now in update_button_states_after_operation
|
||||||
else: QMessageBox.critical(self, "VM Setup Error", error_message)
|
else:
|
||||||
self.run_vm_button.setEnabled(True); self.version_combo.setEnabled(True); self.stop_vm_button.setEnabled(False)
|
QMessageBox.critical(self, "VM Setup Error", error_message)
|
||||||
self.active_worker_thread = None
|
|
||||||
|
|
||||||
|
def stop_current_docker_operation(self):
|
||||||
|
pull_worker = getattr(self, "docker_pull_instance", None); run_worker = getattr(self, "docker_run_instance", None)
|
||||||
|
if pull_worker: self.output_area.append("\n--- Docker pull cannot be directly stopped by this button. Close app to abort. ---")
|
||||||
|
elif run_worker: self.output_area.append("\n--- Attempting to stop macOS VM creation (docker run) ---"); run_worker.stop()
|
||||||
|
else: self.output_area.append("\n--- No stoppable Docker operation active. ---")
|
||||||
|
|
||||||
def stop_docker_run_process(self):
|
def extract_vm_images(self):
|
||||||
docker_run_worker_inst = getattr(self, "docker_run_instance", None) # Use specific name
|
|
||||||
if docker_run_worker_inst:
|
|
||||||
self.output_area.append("\n--- Attempting to stop macOS VM creation ---")
|
|
||||||
docker_run_worker_inst.stop()
|
|
||||||
self.stop_vm_button.setEnabled(False)
|
|
||||||
|
|
||||||
def extract_vm_images(self): # ... (same as before, ensure worker_names are unique)
|
|
||||||
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No active container."); return
|
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No active container."); return
|
||||||
save_dir = QFileDialog.getExistingDirectory(self, "Select Directory to Save VM Images")
|
save_dir = QFileDialog.getExistingDirectory(self, "Select Directory to Save VM Images");
|
||||||
if not save_dir: return
|
if not save_dir: return
|
||||||
self.output_area.append(f"\n--- Starting Image Extraction from {self.current_container_name} to {save_dir} ---")
|
self.output_area.append(f"\n--- Starting Image Extraction from {self.current_container_name} to {save_dir} ---"); self.extract_images_button.setEnabled(False); self.write_to_usb_button.setEnabled(False)
|
||||||
self.extract_images_button.setEnabled(False); self.write_to_usb_button.setEnabled(False)
|
self.extracted_main_image_path = os.path.join(save_dir, "mac_hdd_ng.img"); self.extracted_opencore_image_path = os.path.join(save_dir, "OpenCore.qcow2"); self.extraction_status = {"main": False, "opencore": False}
|
||||||
self.extracted_main_image_path = os.path.join(save_dir, "mac_hdd_ng.img")
|
cp_main_cmd = build_docker_cp_command(self.current_container_name, CONTAINER_MACOS_IMG_PATH, self.extracted_main_image_path); main_worker = DockerCommandWorker(cp_main_cmd, f"Main macOS image copied to {self.extracted_main_image_path}")
|
||||||
self.extracted_opencore_image_path = os.path.join(save_dir, "OpenCore.qcow2")
|
if not self._start_worker(main_worker, lambda msg: self.docker_utility_finished(msg, "main_img_extract"), lambda err: self.docker_utility_error(err, "main_img_extract_error"), "cp_main_worker"): self.extract_images_button.setEnabled(True); return
|
||||||
self.extraction_status = {"main": False, "opencore": False}
|
|
||||||
cp_main_cmd = build_docker_cp_command(self.current_container_name, CONTAINER_MACOS_IMG_PATH, self.extracted_main_image_path)
|
|
||||||
main_worker = DockerCommandWorker(cp_main_cmd, f"Main macOS image copied to {self.extracted_main_image_path}")
|
|
||||||
if not self._start_worker(main_worker, lambda msg: self.docker_utility_finished(msg, "main_img_extract"),
|
|
||||||
lambda err: self.docker_utility_error(err, "main_img_extract_error"), "cp_main"): # Unique name
|
|
||||||
self.extract_images_button.setEnabled(True); return
|
|
||||||
self.output_area.append(f"Extraction for main image started. OpenCore extraction will follow.")
|
self.output_area.append(f"Extraction for main image started. OpenCore extraction will follow.")
|
||||||
|
|
||||||
|
def _start_opencore_extraction(self):
|
||||||
def _start_opencore_extraction(self): # ... (same as before, ensure worker_name is unique)
|
|
||||||
if not self.current_container_name or not self.extracted_opencore_image_path: return
|
if not self.current_container_name or not self.extracted_opencore_image_path: return
|
||||||
cp_oc_cmd = build_docker_cp_command(self.current_container_name, CONTAINER_OPENCORE_QCOW2_PATH, self.extracted_opencore_image_path)
|
cp_oc_cmd = build_docker_cp_command(self.current_container_name, CONTAINER_OPENCORE_QCOW2_PATH, self.extracted_opencore_image_path); oc_worker = DockerCommandWorker(cp_oc_cmd, f"OpenCore image copied to {self.extracted_opencore_image_path}")
|
||||||
oc_worker = DockerCommandWorker(cp_oc_cmd, f"OpenCore image copied to {self.extracted_opencore_image_path}")
|
self._start_worker(oc_worker, lambda msg: self.docker_utility_finished(msg, "oc_img_extract"), lambda err: self.docker_utility_error(err, "oc_img_extract_error"), "cp_oc_worker")
|
||||||
self._start_worker(oc_worker, lambda msg: self.docker_utility_finished(msg, "oc_img_extract"),
|
|
||||||
lambda err: self.docker_utility_error(err, "oc_img_extract_error"), "cp_oc") # Unique name
|
|
||||||
|
|
||||||
def stop_persistent_container(self): # ... (same as before, ensure worker_name is unique)
|
def stop_persistent_container(self):
|
||||||
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No container name."); return
|
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No container name."); return
|
||||||
cmd = build_docker_stop_command(self.current_container_name)
|
cmd = build_docker_stop_command(self.current_container_name); worker = DockerCommandWorker(cmd, f"Container {self.current_container_name} stopped.")
|
||||||
worker = DockerCommandWorker(cmd, f"Container {self.current_container_name} stopped.")
|
if self._start_worker(worker, lambda msg: self.docker_utility_finished(msg, "stop_container"), lambda err: self.docker_utility_error(err, "stop_container_error"), "stop_worker"): self.stop_container_button.setEnabled(False)
|
||||||
if self._start_worker(worker, lambda msg: self.docker_utility_finished(msg, "stop_container"),
|
|
||||||
lambda err: self.docker_utility_error(err, "stop_container_error"), "stop_docker"): # Unique name
|
|
||||||
self.stop_container_button.setEnabled(False)
|
|
||||||
|
|
||||||
|
def remove_persistent_container(self):
|
||||||
def remove_persistent_container(self): # ... (same as before, ensure worker_name is unique)
|
|
||||||
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No container name."); return
|
if not self.current_container_name: QMessageBox.warning(self, "Warning", "No container name."); return
|
||||||
reply = QMessageBox.question(self, 'Confirm Remove', f"Remove container '{self.current_container_name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
reply = QMessageBox.question(self, 'Confirm Remove', f"Remove container '{self.current_container_name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
||||||
if reply == QMessageBox.StandardButton.No: return
|
if reply == QMessageBox.StandardButton.No: return
|
||||||
cmd = build_docker_rm_command(self.current_container_name)
|
cmd = build_docker_rm_command(self.current_container_name); worker = DockerCommandWorker(cmd, f"Container {self.current_container_name} removed.")
|
||||||
worker = DockerCommandWorker(cmd, f"Container {self.current_container_name} removed.")
|
if self._start_worker(worker, lambda msg: self.docker_utility_finished(msg, "rm_container"), lambda err: self.docker_utility_error(err, "rm_container_error"), "rm_worker"): self.remove_container_button.setEnabled(False)
|
||||||
if self._start_worker(worker, lambda msg: self.docker_utility_finished(msg, "rm_container"),
|
|
||||||
lambda err: self.docker_utility_error(err, "rm_container_error"), "rm_docker"): # Unique name
|
|
||||||
self.remove_container_button.setEnabled(False)
|
|
||||||
|
|
||||||
def docker_utility_finished(self, message, task_id): # ... (same as before)
|
def docker_utility_finished(self, message, task_id): # Specific handler
|
||||||
self.output_area.append(f"\n--- Task '{task_id}' Succeeded ---\n{message}"); QMessageBox.information(self, f"Task Complete", message)
|
QMessageBox.information(self, f"Task Complete", message) # Show specific popup
|
||||||
if task_id == "main_img_extract": self.extraction_status["main"] = True; self._start_opencore_extraction(); return
|
# Core logic based on task_id
|
||||||
elif task_id == "oc_img_extract": self.extraction_status["opencore"] = True
|
if task_id == "main_img_extract":
|
||||||
self.active_worker_thread = None # Cleared by _start_worker's finished connection
|
self.extraction_status["main"] = True
|
||||||
if self.extraction_status.get("main") and self.extraction_status.get("opencore"):
|
# _handle_worker_finished (generic) has already reset active_worker_thread.
|
||||||
self.output_area.append("\nBoth VM images extracted successfully."); self.update_write_to_usb_button_state(); self.extract_images_button.setEnabled(True)
|
self._start_opencore_extraction() # Start the next part of the sequence
|
||||||
elif task_id.startswith("extract"): self.extract_images_button.setEnabled(True)
|
return # Return here as active_worker_thread will be managed by _start_opencore_extraction
|
||||||
if task_id == "stop_container": self.remove_container_button.setEnabled(True)
|
elif task_id == "oc_img_extract":
|
||||||
if task_id == "rm_container":
|
self.extraction_status["opencore"] = True
|
||||||
self.current_container_name = None; self.stop_container_button.setEnabled(False)
|
|
||||||
self.extract_images_button.setEnabled(False); self.update_write_to_usb_button_state()
|
elif task_id == "rm_container": # Specific logic for after rm
|
||||||
|
self.current_container_name = None
|
||||||
|
|
||||||
|
# For other utility tasks (like stop_container), or after oc_img_extract,
|
||||||
|
# or after rm_container specific logic, the generic handler _handle_worker_finished
|
||||||
|
# (which called this) will then call _set_ui_busy(False) -> update_button_states_after_operation.
|
||||||
|
# So, no explicit call to self.update_button_states_after_operation() is needed here
|
||||||
|
# unless a state relevant to it changed *within this specific handler*.
|
||||||
|
# In case of rm_container, current_container_name changes, so a UI update is good.
|
||||||
|
if task_id == "rm_container" or (task_id == "oc_img_extract" and self.extraction_status.get("main")):
|
||||||
|
self.update_button_states_after_operation()
|
||||||
|
|
||||||
|
|
||||||
def docker_utility_error(self, error_message, task_id): # ... (same as before)
|
def docker_utility_error(self, error_message, task_id): # Specific handler
|
||||||
self.output_area.append(f"\n--- Task '{task_id}' Error ---\n{error_message}"); QMessageBox.critical(self, f"Task Error", error_message)
|
QMessageBox.critical(self, f"Task Error: {task_id}", error_message)
|
||||||
self.active_worker_thread = None
|
# UI state reset by generic _handle_worker_error -> _set_ui_busy(False) -> update_button_states_after_operation
|
||||||
if task_id.startswith("extract"): self.extract_images_button.setEnabled(True)
|
# Task-specific error UI updates if needed can be added here, but usually generic reset is enough.
|
||||||
if task_id == "stop_container": self.stop_container_button.setEnabled(True)
|
|
||||||
if task_id == "rm_container": self.remove_container_button.setEnabled(True)
|
|
||||||
|
|
||||||
|
def handle_error(self, message): # General error handler for non-worker related setup issues
|
||||||
def handle_error(self, message): # ... (same as before)
|
|
||||||
self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message)
|
self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message)
|
||||||
self.run_vm_button.setEnabled(True); self.version_combo.setEnabled(True); self.stop_vm_button.setEnabled(False)
|
self.run_vm_button.setEnabled(True); self.version_combo.setEnabled(True); self.stop_vm_button.setEnabled(False); self.extract_images_button.setEnabled(False); self.write_to_usb_button.setEnabled(False)
|
||||||
self.extract_images_button.setEnabled(False); self.write_to_usb_button.setEnabled(False)
|
self.active_worker_thread = None;
|
||||||
self.active_worker_thread = None; # Clear active thread
|
for worker_name_suffix in ["pull", "run", "cp_main_worker", "cp_oc_worker", "stop_worker", "rm_worker", "usb_write_worker"]: self._clear_worker_instance(worker_name_suffix)
|
||||||
# Clear all potential worker instances
|
|
||||||
for attr_name in list(self.__dict__.keys()):
|
|
||||||
if attr_name.endswith("_instance") and isinstance(getattr(self,attr_name,None), QObject):
|
|
||||||
setattr(self,attr_name,None)
|
|
||||||
|
|
||||||
|
def check_admin_privileges(self) -> bool:
|
||||||
|
try:
|
||||||
|
if platform.system() == "Windows": return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||||
|
else: return os.geteuid() == 0
|
||||||
|
except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False
|
||||||
|
|
||||||
def refresh_usb_drives(self): # Modified for Windows
|
def refresh_usb_drives(self): # Modified for Windows WMI
|
||||||
self.usb_drive_combo.clear()
|
self.usb_drive_combo.clear()
|
||||||
current_selection_text = getattr(self, '_current_usb_selection_text', None)
|
self._current_usb_selection_text = self.usb_drive_combo.currentText() # Store to reselect if possible
|
||||||
self.output_area.append("\nScanning for disk devices...")
|
self.output_area.append("\nScanning for disk devices...")
|
||||||
|
|
||||||
current_os = platform.system()
|
current_os = platform.system()
|
||||||
if current_os == "Windows":
|
self.windows_usb_guidance_label.setVisible(current_os == "Windows")
|
||||||
self.usb_drive_label.setText("For Windows, identify Physical Disk number (e.g., 1, 2) using Disk Management or 'diskpart > list disk'. Input below.")
|
self.windows_usb_input_label.setVisible(False) # Hide manual input by default
|
||||||
self.windows_disk_id_input.setVisible(True)
|
self.windows_disk_id_input.setVisible(False) # Hide manual input by default
|
||||||
self.windows_usb_input_label.setVisible(True)
|
self.usb_drive_combo.setVisible(True) # Always visible, populated differently
|
||||||
self.usb_drive_combo.setVisible(False) # Hide combo for windows as input is manual
|
|
||||||
self.refresh_usb_button.setText("List Partitions (Ref.)") # Change button text
|
|
||||||
try:
|
|
||||||
partitions = psutil.disk_partitions(all=True)
|
|
||||||
ref_text = "Reference - Detected partitions/mounts:\n"
|
|
||||||
for p in partitions:
|
|
||||||
try:
|
|
||||||
usage = psutil.disk_usage(p.mountpoint)
|
|
||||||
size_gb = usage.total / (1024**3)
|
|
||||||
ref_text += f" {p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)\n"
|
|
||||||
except Exception:
|
|
||||||
ref_text += f" {p.device} ({p.fstype}) - could not get usage/mountpoint\n"
|
|
||||||
self.output_area.append(ref_text)
|
|
||||||
except Exception as e:
|
|
||||||
self.output_area.append(f"Error listing partitions for reference: {e}")
|
|
||||||
else:
|
|
||||||
self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):")
|
|
||||||
self.windows_disk_id_input.setVisible(False)
|
|
||||||
self.windows_usb_input_label.setVisible(False)
|
|
||||||
self.usb_drive_combo.setVisible(True)
|
|
||||||
self.refresh_usb_button.setText("Refresh List")
|
|
||||||
try: # psutil logic for Linux/macOS
|
|
||||||
partitions = psutil.disk_partitions(all=False)
|
|
||||||
potential_usbs = []
|
|
||||||
for p in partitions:
|
|
||||||
is_removable = 'removable' in p.opts
|
|
||||||
is_likely_usb = False
|
|
||||||
if current_os == "Darwin":
|
|
||||||
if p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True
|
|
||||||
elif current_os == "Linux":
|
|
||||||
if (p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da")): is_likely_usb = True
|
|
||||||
if is_removable or is_likely_usb:
|
|
||||||
try:
|
|
||||||
usage = psutil.disk_usage(p.mountpoint)
|
|
||||||
size_gb = usage.total / (1024**3);
|
|
||||||
if size_gb < 0.1 : continue
|
|
||||||
drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)"
|
|
||||||
potential_usbs.append((drive_text, p.device))
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
|
if current_os == "Windows":
|
||||||
|
self.usb_drive_label.setText("Available USB Disks (Windows - WMI):")
|
||||||
|
self.refresh_usb_button.setText("Refresh USB List")
|
||||||
|
powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json"
|
||||||
|
try:
|
||||||
|
process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||||
|
disks_data = json.loads(process.stdout)
|
||||||
|
if not isinstance(disks_data, list): disks_data = [disks_data] # Ensure it's a list
|
||||||
|
|
||||||
|
if disks_data:
|
||||||
|
for disk in disks_data:
|
||||||
|
if disk.get('DeviceID') is None or disk.get('Index') is None: continue
|
||||||
|
disk_text = f"Disk {disk['Index']}: {disk.get('Model','N/A')} ({disk.get('SizeGB','N/A')} GB) - {disk['DeviceID']}"
|
||||||
|
self.usb_drive_combo.addItem(disk_text, userData=str(disk['Index']))
|
||||||
|
self.output_area.append(f"Found {len(disks_data)} USB disk(s) via WMI. Select from dropdown.")
|
||||||
|
if self._current_usb_selection_text:
|
||||||
|
for i in range(self.usb_drive_combo.count()):
|
||||||
|
if self.usb_drive_combo.itemText(i) == self._current_usb_selection_text: self.usb_drive_combo.setCurrentIndex(i); break
|
||||||
|
else:
|
||||||
|
self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback.")
|
||||||
|
self.windows_usb_input_label.setVisible(True); self.windows_disk_id_input.setVisible(True) # Show manual input as fallback
|
||||||
|
except Exception as e:
|
||||||
|
self.output_area.append(f"Error querying WMI for USB disks: {e}. Manual input field shown.")
|
||||||
|
self.windows_usb_input_label.setVisible(True); self.windows_disk_id_input.setVisible(True)
|
||||||
|
else: # Linux / macOS
|
||||||
|
self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):")
|
||||||
|
self.refresh_usb_button.setText("Refresh List")
|
||||||
|
try:
|
||||||
|
partitions = psutil.disk_partitions(all=False); potential_usbs = []
|
||||||
|
for p in partitions:
|
||||||
|
is_removable = 'removable' in p.opts; is_likely_usb = False
|
||||||
|
if current_os == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True
|
||||||
|
elif current_os == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True
|
||||||
|
if is_removable or is_likely_usb:
|
||||||
|
try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3)
|
||||||
|
except Exception: continue
|
||||||
|
if size_gb < 0.1 : continue
|
||||||
|
drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)"
|
||||||
|
potential_usbs.append((drive_text, p.device))
|
||||||
if potential_usbs:
|
if potential_usbs:
|
||||||
idx_to_select = -1
|
idx_to_select = -1
|
||||||
for i, (text, device_path) in enumerate(potential_usbs):
|
for i, (text, device_path) in enumerate(potential_usbs): self.usb_drive_combo.addItem(text, userData=device_path);
|
||||||
self.usb_drive_combo.addItem(text, userData=device_path)
|
if text == self._current_usb_selection_text: idx_to_select = i
|
||||||
if text == current_selection_text: idx_to_select = i
|
|
||||||
if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select)
|
if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select)
|
||||||
self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.")
|
self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.")
|
||||||
else: self.output_area.append("No suitable USB drives found for Linux/macOS.")
|
else: self.output_area.append("No suitable USB drives found for Linux/macOS.")
|
||||||
@ -447,95 +488,68 @@ class MainWindow(QMainWindow): # ... (init and _setup_ui need changes for Window
|
|||||||
|
|
||||||
self.update_write_to_usb_button_state()
|
self.update_write_to_usb_button_state()
|
||||||
|
|
||||||
|
def handle_write_to_usb(self): # Modified for Windows WMI
|
||||||
|
if not self.check_admin_privileges():
|
||||||
|
QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return
|
||||||
|
|
||||||
def handle_write_to_usb(self): # Modified for Windows
|
current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None
|
||||||
current_os = platform.system()
|
|
||||||
usb_writer_module = None
|
|
||||||
target_device_id_for_worker = None
|
|
||||||
|
|
||||||
if current_os == "Linux":
|
if current_os == "Windows":
|
||||||
usb_writer_module = USBWriterLinux
|
target_device_id_for_worker = self.usb_drive_combo.currentData() # Disk Index from WMI
|
||||||
target_device_id_for_worker = self.usb_drive_combo.currentData()
|
if not target_device_id_for_worker: # Fallback to manual input if combo is empty or user chose to use it
|
||||||
elif current_os == "Darwin":
|
target_device_id_for_worker = self.windows_disk_id_input.text().strip()
|
||||||
usb_writer_module = USBWriterMacOS
|
if not target_device_id_for_worker: QMessageBox.warning(self, "Input Required", "Please select a USB disk or enter its Disk Number."); return
|
||||||
target_device_id_for_worker = self.usb_drive_combo.currentData()
|
if not target_device_id_for_worker.isdigit(): QMessageBox.warning(self, "Input Invalid", "Windows Disk Number must be a digit."); return
|
||||||
elif current_os == "Windows":
|
# USBWriterWindows expects just the disk number string (e.g., "1")
|
||||||
usb_writer_module = USBWriterWindows
|
usb_writer_module = USBWriterWindows
|
||||||
# For Windows, device_id for USBWriterWindows is the disk number string
|
else: # Linux/macOS
|
||||||
target_device_id_for_worker = self.windows_disk_id_input.text().strip()
|
target_device_id_for_worker = self.usb_drive_combo.currentData()
|
||||||
if not target_device_id_for_worker.isdigit(): # Basic validation
|
if current_os == "Linux": usb_writer_module = USBWriterLinux
|
||||||
QMessageBox.warning(self, "Input Required", "Please enter a valid Windows Disk Number (e.g., 1, 2)."); return
|
elif current_os == "Darwin": usb_writer_module = USBWriterMacOS
|
||||||
# USBWriterWindows expects just the number, it constructs \\.\PhysicalDriveX itself.
|
|
||||||
|
|
||||||
if not usb_writer_module:
|
if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported/enabled for {current_os}."); return
|
||||||
QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported/enabled for {current_os}."); return
|
if not (self.extracted_main_image_path and self.extracted_opencore_image_path and self.extraction_status["main"] and self.extraction_status["opencore"]):
|
||||||
|
|
||||||
if not self.extracted_main_image_path or not self.extracted_opencore_image_path or not self.extraction_status["main"] or not self.extraction_status["opencore"]:
|
|
||||||
QMessageBox.warning(self, "Missing Images", "Ensure both images are extracted."); return
|
QMessageBox.warning(self, "Missing Images", "Ensure both images are extracted."); return
|
||||||
if not target_device_id_for_worker: # Should catch empty input for Windows here too
|
if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB for {current_os}."); return
|
||||||
QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify the target USB drive for {current_os}."); return
|
|
||||||
|
|
||||||
confirm_msg = (f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED PERMANENTLY.
|
confirm_msg = (f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED PERMANENTLY.
|
||||||
"
|
Proceed?");
|
||||||
"Are you absolutely sure you want to proceed?")
|
reply = QMessageBox.warning(self, "Confirm Write Operation", confirm_msg, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel)
|
||||||
reply = QMessageBox.warning(self, "Confirm Write Operation", confirm_msg,
|
if reply == QMessageBox.StandardButton.Cancel: self.output_area.append("\nUSB write cancelled."); return
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
|
||||||
QMessageBox.StandardButton.Cancel)
|
|
||||||
if reply == QMessageBox.StandardButton.Cancel:
|
|
||||||
self.output_area.append("
|
|
||||||
USB write operation cancelled by user."); return
|
|
||||||
|
|
||||||
self.output_area.append(f"
|
self.output_area.append(f"\n--- Starting USB Write for {target_device_id_for_worker} on {current_os} ---")
|
||||||
--- Starting USB Write Process for {target_device_id_for_worker} on {current_os} ---")
|
|
||||||
self.write_to_usb_button.setEnabled(False); self.refresh_usb_button.setEnabled(False)
|
self.write_to_usb_button.setEnabled(False); self.refresh_usb_button.setEnabled(False)
|
||||||
|
|
||||||
usb_worker = USBWriterWorker(target_device_id_for_worker, self.extracted_opencore_image_path, self.extracted_main_image_path)
|
usb_worker = USBWriterWorker(target_device_id_for_worker, self.extracted_opencore_image_path, self.extracted_main_image_path)
|
||||||
if not self._start_worker(usb_worker, self.usb_write_finished, self.usb_write_error, "usb_write"): # worker_name "usb_write"
|
if not self._start_worker(usb_worker, self.usb_write_finished, self.usb_write_error, "usb_write_worker"):
|
||||||
self.write_to_usb_button.setEnabled(True); self.refresh_usb_button.setEnabled(True)
|
self.write_to_usb_button.setEnabled(True); self.refresh_usb_button.setEnabled(True)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def usb_write_finished(self, message): # ... (same as before)
|
def usb_write_finished(self, message): # Specific handler
|
||||||
self.output_area.append(f"
|
QMessageBox.information(self, "USB Write Complete", message)
|
||||||
--- USB Write Process Finished ---
|
# UI state reset by generic _handle_worker_finished -> _set_ui_busy(False)
|
||||||
{message}"); QMessageBox.information(self, "USB Write Complete", message)
|
|
||||||
self.write_to_usb_button.setEnabled(True); self.refresh_usb_button.setEnabled(True)
|
|
||||||
self.active_worker_thread = None; setattr(self, "usb_write_instance", None)
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def usb_write_error(self, error_message): # ... (same as before)
|
def usb_write_error(self, error_message): # Specific handler
|
||||||
self.output_area.append(f"
|
QMessageBox.critical(self, "USB Write Error", error_message)
|
||||||
--- USB Write Process Error ---
|
# UI state reset by generic _handle_worker_error -> _set_ui_busy(False)
|
||||||
{error_message}"); QMessageBox.critical(self, "USB Write Error", error_message)
|
|
||||||
self.write_to_usb_button.setEnabled(True); self.refresh_usb_button.setEnabled(True)
|
|
||||||
self.active_worker_thread = None; setattr(self, "usb_write_instance", None)
|
|
||||||
|
|
||||||
def update_write_to_usb_button_state(self): # Modified for Windows
|
def update_write_to_usb_button_state(self):
|
||||||
images_ready = self.extraction_status.get("main", False) and self.extraction_status.get("opencore", False)
|
images_ready = self.extraction_status.get("main", False) and self.extraction_status.get("opencore", False); usb_identified = False; current_os = platform.system(); writer_module = None
|
||||||
usb_identified = False
|
if current_os == "Linux": writer_module = USBWriterLinux; usb_identified = bool(self.usb_drive_combo.currentData())
|
||||||
current_os = platform.system()
|
elif current_os == "Darwin": writer_module = USBWriterMacOS; usb_identified = bool(self.usb_drive_combo.currentData())
|
||||||
writer_module = None
|
elif current_os == "Windows":
|
||||||
|
writer_module = USBWriterWindows
|
||||||
if current_os == "Linux": writer_module = USBWriterLinux
|
usb_identified = bool(self.usb_drive_combo.currentData()) or bool(self.windows_disk_id_input.text().strip().isdigit() and self.windows_disk_id_input.isVisible())
|
||||||
elif current_os == "Darwin": writer_module = USBWriterMacOS
|
|
||||||
elif current_os == "Windows": writer_module = USBWriterWindows
|
|
||||||
|
|
||||||
if current_os == "Windows":
|
|
||||||
usb_identified = bool(self.windows_disk_id_input.text().strip().isdigit()) # Must be a digit for disk ID
|
|
||||||
else:
|
|
||||||
usb_identified = bool(self.usb_drive_combo.currentData())
|
|
||||||
|
|
||||||
self.write_to_usb_button.setEnabled(images_ready and usb_identified and writer_module is not None)
|
self.write_to_usb_button.setEnabled(images_ready and usb_identified and writer_module is not None)
|
||||||
# ... (Tooltip logic same as before) ...
|
tooltip = ""
|
||||||
if writer_module is None: self.write_to_usb_button.setToolTip(f"USB Writing not supported on {current_os} or module missing.")
|
if writer_module is None: tooltip = f"USB Writing not supported on {current_os} or module missing."
|
||||||
elif not images_ready: self.write_to_usb_button.setToolTip("Extract VM images first.")
|
elif not images_ready: tooltip = "Extract VM images first."
|
||||||
elif not usb_identified:
|
elif not usb_identified: tooltip = "Select a USB disk from dropdown (or enter Disk Number if dropdown empty on Windows)."
|
||||||
if current_os == "Windows": self.write_to_usb_button.setToolTip("Enter a valid Windows Disk Number.")
|
else: tooltip = ""
|
||||||
else: self.write_to_usb_button.setToolTip("Select a target USB drive.")
|
self.write_to_usb_button.setToolTip(tooltip)
|
||||||
else: self.write_to_usb_button.setToolTip("")
|
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
def closeEvent(self, event): # ... (same as before)
|
|
||||||
self._current_usb_selection_text = self.usb_drive_combo.currentText()
|
self._current_usb_selection_text = self.usb_drive_combo.currentText()
|
||||||
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||||
reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
||||||
@ -544,8 +558,7 @@ USB write operation cancelled by user."); return
|
|||||||
worker_to_stop = getattr(self, worker_instance_attr_name, None)
|
worker_to_stop = getattr(self, worker_instance_attr_name, None)
|
||||||
if worker_to_stop and hasattr(worker_to_stop, 'stop'): worker_to_stop.stop()
|
if worker_to_stop and hasattr(worker_to_stop, 'stop'): worker_to_stop.stop()
|
||||||
else: self.active_worker_thread.quit()
|
else: self.active_worker_thread.quit()
|
||||||
self.active_worker_thread.wait(1000)
|
self.active_worker_thread.wait(1000); event.accept()
|
||||||
event.accept()
|
|
||||||
else: event.ignore(); return
|
else: event.ignore(); return
|
||||||
elif self.current_container_name and self.stop_container_button.isEnabled():
|
elif self.current_container_name and self.stop_container_button.isEnabled():
|
||||||
reply = QMessageBox.question(self, 'Confirm Exit', f"Container '{self.current_container_name}' may still exist. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
reply = QMessageBox.question(self, 'Confirm Exit', f"Container '{self.current_container_name}' may still exist. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
||||||
|
@ -125,17 +125,4 @@ RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(<vnc
|
|||||||
|
|
||||||
WORKDIR /home/arch/OSX-KVM
|
WORKDIR /home/arch/OSX-KVM
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is catalina, which means :latest is catalina
|
|
||||||
ENV SHORTNAME=sonoma
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
|
||||||
|
@ -203,18 +203,5 @@ RUN vncpasswd -f < vncpasswd_file > ${HOME}/.vnc/passwd
|
|||||||
RUN chmod 600 ~/.vnc/passwd
|
RUN chmod 600 ~/.vnc/passwd
|
||||||
RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(<vncpasswd_file)"
|
RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(<vncpasswd_file)"
|
||||||
|
|
||||||
# DMCA compliant download process
|
CMD ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
|
||||||
|
|
||||||
# shortname default is catalina, which means :latest is catalina
|
|
||||||
ENV SHORTNAME=sonoma
|
|
||||||
|
|
||||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
|
||||||
|
|
||||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
|
||||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
|
||||||
&& make \
|
|
||||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
|
||||||
&& rm ./BaseSystem.dmg \
|
|
||||||
; ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user