From aa9584df906f918ad61d0059b0a75ad15505dc9e Mon Sep 17 00:00:00 2001 From: Kroese Date: Fri, 31 Jul 2026 22:05:55 +0200 Subject: [PATCH] feat: Avoid ISO rebuilding for dramatically faster preparation (#2032) --- docs/environment.md | 1 - src/answer.sh | 1029 ++++++++++++++++++++++++------------------- src/define.sh | 26 +- src/image.sh | 345 ++++++++++----- src/install.sh | 938 +++++++++++++++++++++++---------------- src/mido.sh | 2 +- src/power.sh | 217 +++++++-- src/samba.sh | 28 +- 8 files changed, 1614 insertions(+), 972 deletions(-) diff --git a/docs/environment.md b/docs/environment.md index b5c4c4e7..17a3bcbe 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -163,7 +163,6 @@ Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/d | `VERIFY` | `N` | Verifies downloaded installation media against predefined checksums. | | `REMOVE` | `Y` | Deletes the downloaded Windows ISO after installation to save space. | | `MANUAL` | `N` | Enables manual installation instead of unattended installation. | -| `REBUILD` | `Y` | Skips rebuilding the Windows ISO and uses the original image unchanged. | | `COMMAND` | | Command to be executed during the final step of automatic installation. | ## 🔌 Shutdown diff --git a/src/answer.sh b/src/answer.sh index ad65720d..b00614b4 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -1,6 +1,385 @@ #!/usr/bin/env bash set -Eeuo pipefail +hasAnswerFile() { + + local id="$1" + local file="/run/assets/$id.xml" + + [ -s "$file" ] && return 0 + + if [[ "${id,,}" == *"-eval" ]]; then + file="/run/assets/${id%-eval}.xml" + [ -s "$file" ] && return 0 + fi + + # Editions without a dedicated template can use the generic template. + case "${id,,}" in + "win7"* | "win8"* | "win10"* | "win11"* | "winvista"* | "win20"* ) + file="/run/assets/${id%%-*}.xml" + [ -s "$file" ] && return 0 + ;; + esac + + return 1 +} + +stageAnswer() { + + local asset="$1" + local language="$2" + local stage="$3" + local answer="$stage/Autounattend.xml" + local name + + if enabled "$MANUAL"; then + removeGeneratedXML "$asset" || return 1 + return 0 + fi + + if [ ! -f "$asset" ] || [ ! -s "$asset" ]; then + error "Failed to find answer file: $asset" + return 1 + fi + + name=$(basename "$asset") || return 1 + info "Adding $name for automatic installation..." + + if ! cp -L -- "$asset" "$answer"; then + error "Failed to stage answer file: $asset" + return 1 + fi + + removeGeneratedXML "$asset" || return 1 + + if [ -z "${CUSTOM_XML:-}" ]; then + if ! updateXML "$answer" "$language"; then + error "Failed to update answer file: $answer" + return 1 + fi + fi + + if ! updateDiskID "$answer" "${DISK_TYPE:-}"; then + error "Failed to adjust the Windows installation disk!" + return 1 + fi + + if ! setConfigurationXML "$answer"; then + error "Failed to enable the Windows configuration set!" + return 1 + fi + + validateGeneratedXML "$answer" || return 1 + + return 0 +} + +markGeneratedXML() { + + local file="$1" + local marker='' + + [ -s "$file" ] || return 1 + + if head -n 1 "$file" | grep -q '^.*<\/ProductKey>/d' + -e '//,/<\/ProductKey>/d' + ) + else + expressions=( + -e '/.*<\/InstallFrom>/d' + -e '/.*<\/ProductKey>/d' + -e '//,/<\/InstallFrom>/d' + -e '//,/<\/ProductKey>/d' + ) + fi + + if ! sed "${expressions[@]}" "$source" > "$tmp"; then + rm -f "$tmp" + error "Failed to generate $type answer file from $source!" + return 1 + fi + + if [ "$type" = "evaluation" ] && [ "$remove_selector" = "Y" ]; then + if ! sed -i \ + -e '/.*<\/InstallFrom>/d' \ + -e '//,/<\/InstallFrom>/d' \ + "$tmp"; then + rm -f "$tmp" + error "Failed to replace evaluation image selector!" + return 1 + fi + fi + + if [ -n "$index" ] && ! grep -q '' "$tmp"; then + if ! sed -i \ + '0,//{ //i\ + \ + \ + /IMAGE/INDEX\ + '"$index"'\ + \ + + }' "$tmp"; then + rm -f "$tmp" + error "Failed to select $type image index $index!" + return 1 + fi + fi + + if ! markGeneratedXML "$tmp"; then + rm -f "$tmp" + error "Failed to mark generated $type answer file!" + return 1 + fi + + if ! validateGeneratedXML "$tmp"; then + rm -f "$tmp" + return 1 + fi + + if ! chmod 644 "$tmp" || ! mv -f "$tmp" "$target"; then + rm -f "$tmp" + error "Failed to create $type answer file: $target" + return 1 + fi + + return 0 +} + +generateEvalXML() { + + # Evaluation templates are generated from their normal counterpart so + # both variants remain identical except for evaluation-specific selectors. + + local id="$1" + local detected_index="${2:-}" + + [[ "${id,,}" == *"-eval" ]] || return 1 + + local normal="${id::-5}" + local source="/run/assets/$normal.xml" + local target="/run/assets/$id.xml" + local index="$detected_index" + local remove_selector="N" + + removeGeneratedXML "$source" || return 1 + + if [ ! -s "$source" ]; then + source="/run/assets/${normal%%-*}.xml" + removeGeneratedXML "$source" || return 1 + fi + + [ -s "$source" ] || return 1 + + if [ -n "$detected_index" ]; then + remove_selector="Y" + else + # No WIM was inspected, so retain the known defaults for download routes. + case "${id,,}" in + *"-ltsc-eval" ) index="1" ;; + *"-iot-eval" ) index="2" ;; + esac + fi + + generateAnswerFile \ + "$id" "$source" "$target" "$index" "evaluation" "$remove_selector" || return 1 + + return 0 +} + +generateFallbackXML() { + + # Fallback templates are generated from the generic version so unsupported + # editions can use the detected WIM index without inheriting a product key. + + local id="$1" + local index="${2:-}" + local source="/run/assets/${id%%-*}.xml" + local target="/run/assets/$id.xml" + + [ "$source" != "$target" ] || return 1 + + removeGeneratedXML "$source" || return 1 + [ -s "$source" ] || return 1 + + generateAnswerFile \ + "$id" "$source" "$target" "$index" "fallback" "Y" || return 1 + + return 0 +} + +setXML() { + + local file="$1" + local index="${2:-}" + local target="/run/assets/$DETECTED.xml" + + local custom_files=( + "/custom.xml" + "$STORAGE/custom.xml" + "/run/assets/custom.xml" + ) + + CUSTOM_XML="" + + removeGeneratedXML "$target" || return 1 + + if [ -d "${custom_files[0]}" ]; then + error "The bind ${custom_files[0]} maps to a file that does not exist!" + exit 67 + fi + + for file in "${custom_files[@]}"; do + if [ -f "$file" ] && [ -s "$file" ]; then + CUSTOM_XML="Y" + XML="$file" + return 0 + fi + done + + file="$1" + + if [[ "${DETECTED,,}" == *"-eval" ]] && + { [ ! -f "$file" ] || [ ! -s "$file" ]; }; then + + generateEvalXML "$DETECTED" "$index" || return 1 + file="$target" + + elif [ ! -f "$file" ] || [ ! -s "$file" ]; then + + file="$target" + + elif [[ "$file" != "$target" ]]; then + + generateFallbackXML "$DETECTED" "$index" || return 1 + file="$target" + + fi + + [ -f "$file" ] && [ -s "$file" ] || return 1 + + XML="$file" + return 0 +} + +updateXML() { + + local asset="$1" + local language="$2" + local domain="${DOMAIN:-}" + local workgroup="${WORKGROUP:-}" + local account="" + local auth="" + + [ -z "${WIDTH:-}" ] && WIDTH="1280" + [ -z "${HEIGHT:-}" ] && HEIGHT="720" + + validateXMLSettings || return 1 + updateDisplayXML "$asset" || return 1 + updateLocaleXML "$asset" "$language" || return 1 + + if [ -n "$domain" ]; then + prepareDomainAccount "$domain" account auth || return 1 + else + updateLocalAccountXML "$asset" || return 1 + fi + + sed -i -E \ + "s|[^<]*</PlainText>|<PlainText>false</PlainText>|g" \ + "$asset" || return 1 + + updateMembershipXML \ + "$asset" \ + "$domain" \ + "$workgroup" \ + "$account" \ + "$auth" || return 1 + + updateAutologinXML "$asset" || return 1 + enableLog "$asset" || return 1 + updateEditionXML "$asset" || return 1 + updateProductKeyXML "$asset" || return 1 + removeSharedFolderXML "$asset" || return 1 + validateGeneratedXML "$asset" || return 1 + + return 0 +} + +validateGeneratedXML() { + + local asset="$1" + + if ! xmllint --nonet --noout "$asset"; then + error "The generated answer file is not valid XML!" + return 1 + fi + + return 0 +} + +validateXMLSettings() { + + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + validateMembership || return 1 + validateComputerName "${HOST:-}" || return 1 + validateProductKey "${KEY:-}" || return 1 + validatePassword "${PASSWORD:-}" || return 1 + + return 0 +} + validateResolution() { local name="$1" @@ -124,19 +503,6 @@ validatePassword() { return 0 } -escapeXMLSed() { - - local s - - s=$(escapeXML "$1") || return 1 - s=${s//\\/\\\\} - s=${s//&/\\&} - s=${s//|/\\|} - - printf '%s' "$s" - return 0 -} - validateUsername() { local value="$1" @@ -239,21 +605,6 @@ validateDomainName() { return 0 } -getXMLArchitecture() { - - local asset="$1" - local arch - - arch=$(sed -n -E \ - '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ - "$asset") || return 1 - - [ -n "$arch" ] || return 1 - - printf '%s' "$arch" - return 0 -} - updateWorkgroup() { local asset="$1" @@ -411,357 +762,6 @@ updateDomain() { return 0 } -removeLocalAccountXML() { - - local asset="$1" - - if ! sed -i -E \ - -e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \ - -e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \ - "$asset"; then - - error "Failed to remove local account configuration from answer file!" - return 1 - fi - - if ! sed -i -E ' - /<SynchronousCommand([[:space:]>])/ { - :command - N - /<\/SynchronousCommand>/!b command - /<Description>Password Never Expires<\/Description>/d - } - ' "$asset"; then - - error "Failed to remove local account commands from answer file!" - return 1 - fi - - return 0 -} - -enableLog() { - - local file="$1" - local old='C:\OEM\install.bat"</CommandLine>' - local msg="failed to enable install logging in the answer file!" - - enabled "${LOG:-}" || return 0 - [ -f "$file" ] || return 1 - - if ! grep -Fq "$old" "$file"; then - enabled "$DEBUG" && warn "$msg" - return 0 - fi - - if ! sed -i \ - 's|C:\\OEM\\install\.bat"</CommandLine>|C:\\OEM\\install.bat \&gt; C:\\OEM\\install.log 2\&gt;\&amp;1"</CommandLine>|' \ - "$file"; then - - warn "$msg" - fi - - return 0 -} - -markGeneratedXML() { - - local file="$1" - local marker='<!-- generated-answer-file: do not reuse as a template -->' - - [ -s "$file" ] || return 1 - - if head -n 1 "$file" | grep -q '^<?xml'; then - sed -i "1a$marker" "$file" || return 1 - else - sed -i "1i$marker" "$file" || return 1 - fi - - return 0 -} - -removeGeneratedXML() { - - local file="$1" - - [ -n "$file" ] || return 0 - [ -f "$file" ] || return 0 - - head -n 5 "$file" | - grep -Fqi 'generated-answer-file' || return 0 - - if ! rm -f "$file"; then - error "Failed to remove generated answer file: $file" - return 1 - fi - - return 0 -} - -generateAnswerFile() { - - local id="$1" - local source="$2" - local target="$3" - local index="$4" - local type="$5" - local remove_selector="$6" - local tmp - - if [ -n "$index" ] && [[ ! "$index" =~ ^[1-9][0-9]*$ ]]; then - error "Invalid $type image index: $index" - return 1 - fi - - if ! tmp=$(mktemp -p /run/assets ".${id}.XXXXXX"); then - error "Failed to create a temporary $type answer file!" - return 1 - fi - - local expressions - - if [ "$type" = "evaluation" ]; then - expressions=( - -e '/<ProductKey>.*<\/ProductKey>/d' - -e '/<ProductKey>/,/<\/ProductKey>/d' - ) - else - expressions=( - -e '/<InstallFrom>.*<\/InstallFrom>/d' - -e '/<ProductKey>.*<\/ProductKey>/d' - -e '/<InstallFrom>/,/<\/InstallFrom>/d' - -e '/<ProductKey>/,/<\/ProductKey>/d' - ) - fi - - if ! sed "${expressions[@]}" "$source" > "$tmp"; then - rm -f "$tmp" - error "Failed to generate $type answer file from $source!" - return 1 - fi - - if [ "$type" = "evaluation" ] && [ "$remove_selector" = "Y" ]; then - if ! sed -i \ - -e '/<InstallFrom>.*<\/InstallFrom>/d' \ - -e '/<InstallFrom>/,/<\/InstallFrom>/d' \ - "$tmp"; then - rm -f "$tmp" - error "Failed to replace evaluation image selector!" - return 1 - fi - fi - - if [ -n "$index" ] && ! grep -q '<InstallFrom>' "$tmp"; then - if ! sed -i \ - '0,/<InstallTo>/{ /<InstallTo>/i\ - <InstallFrom>\ - <MetaData wcm:action="add">\ - <Key>/IMAGE/INDEX</Key>\ - <Value>'"$index"'</Value>\ - </MetaData>\ - </InstallFrom> - }' "$tmp"; then - rm -f "$tmp" - error "Failed to select $type image index $index!" - return 1 - fi - fi - - if ! markGeneratedXML "$tmp" || - ! xmllint --nonet --noout "$tmp"; then - rm -f "$tmp" - error "Generated $type answer file is invalid!" - return 1 - fi - - if ! chmod 644 "$tmp" || ! mv -f "$tmp" "$target"; then - rm -f "$tmp" - error "Failed to create $type answer file: $target" - return 1 - fi - - return 0 -} - -generateEvalXML() { - - # Evaluation templates are generated from their normal counterpart so - # both variants remain identical except for evaluation-specific selectors. - - local id="$1" - local detected_index="${2:-}" - - [[ "${id,,}" == *"-eval" ]] || return 1 - - local normal="${id::-5}" - local source="/run/assets/$normal.xml" - local target="/run/assets/$id.xml" - local index="$detected_index" - local remove_selector="N" - - removeGeneratedXML "$source" || return 1 - - if [ ! -s "$source" ]; then - source="/run/assets/${normal%%-*}.xml" - removeGeneratedXML "$source" || return 1 - fi - - [ -s "$source" ] || return 1 - - if [ -n "$detected_index" ]; then - remove_selector="Y" - else - # No WIM was inspected, so retain the known defaults for download routes. - case "${id,,}" in - *"-ltsc-eval" ) index="1" ;; - *"-iot-eval" ) index="2" ;; - esac - fi - - generateAnswerFile \ - "$id" "$source" "$target" "$index" "evaluation" "$remove_selector" || return 1 - - return 0 -} - -generateFallbackXML() { - - # Fallback templates are generated from the generic version so unsupported - # editions can use the detected WIM index without inheriting a product key. - - local id="$1" - local index="${2:-}" - local source="/run/assets/${id%%-*}.xml" - local target="/run/assets/$id.xml" - - [ "$source" != "$target" ] || return 1 - - removeGeneratedXML "$source" || return 1 - [ -s "$source" ] || return 1 - - generateAnswerFile \ - "$id" "$source" "$target" "$index" "fallback" "Y" || return 1 - - return 0 -} - -setXML() { - - local file="$1" - local index="${2:-}" - local target="/run/assets/$DETECTED.xml" - - local custom_files=( - "/custom.xml" - "$STORAGE/custom.xml" - "/run/assets/custom.xml" - ) - - CUSTOM_XML="" - - removeGeneratedXML "$target" || return 1 - - if [ -d "${custom_files[0]}" ]; then - error "The bind ${custom_files[0]} maps to a file that does not exist!" - exit 67 - fi - - for file in "${custom_files[@]}"; do - if [ -f "$file" ] && [ -s "$file" ]; then - CUSTOM_XML="Y" - XML="$file" - return 0 - fi - done - - file="$1" - - if [[ "${DETECTED,,}" == *"-eval" ]] && - { [ ! -f "$file" ] || [ ! -s "$file" ]; }; then - - generateEvalXML "$DETECTED" "$index" || return 1 - file="$target" - - elif [ ! -f "$file" ] || [ ! -s "$file" ]; then - - file="$target" - - elif [[ "$file" != "$target" ]]; then - - generateFallbackXML "$DETECTED" "$index" || return 1 - file="$target" - - fi - - [ -f "$file" ] && [ -s "$file" ] || return 1 - - XML="$file" - return 0 -} - -validateXMLSettings() { - - validateResolution "WIDTH" "$WIDTH" 320 || return 1 - validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 - validateMembership || return 1 - validateComputerName "${HOST:-}" || return 1 - validateProductKey "${KEY:-}" || return 1 - validatePassword "${PASSWORD:-}" || return 1 - - return 0 -} - -updateDisplayXML() { - - local asset="$1" - local app host - - app=$(escapeXMLSed "$APP for $ENGINE") || return 1 - - sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 - sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1 - sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1 - - [ -n "${HOST:-}" ] || return 0 - - host=$(escapeXMLSed "$HOST") || return 1 - sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1 - - return 0 -} - -updateLocaleXML() { - - local asset="$1" - local language="$2" - local culture region keyboard value - - culture=$(getLanguage "$language" "culture") || return 1 - - if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$culture") || return 1 - sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1 - fi - - region="${REGION:-$culture}" - - if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$region") || return 1 - sed -i "s|<UserLocale>en-US</UserLocale>|<UserLocale>$value</UserLocale>|g" "$asset" || return 1 - sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1 - fi - - keyboard="${KEYBOARD:-$culture}" - - if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$keyboard") || return 1 - sed -i "s|<InputLocale>en-US</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 - sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 - fi - - return 0 -} - prepareDomainAccount() { local domain="$1" @@ -830,6 +830,57 @@ prepareDomainAccount() { return 0 } +updateDisplayXML() { + + local asset="$1" + local app host + + app=$(escapeXMLSed "$APP for $ENGINE") || return 1 + + sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 + sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1 + sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1 + + [ -n "${HOST:-}" ] || return 0 + + host=$(escapeXMLSed "$HOST") || return 1 + sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1 + + return 0 +} + +updateLocaleXML() { + + local asset="$1" + local language="$2" + local culture region keyboard value + + culture=$(getLanguage "$language" "culture") || return 1 + + if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$culture") || return 1 + sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1 + fi + + region="${REGION:-$culture}" + + if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$region") || return 1 + sed -i "s|<UserLocale>en-US</UserLocale>|<UserLocale>$value</UserLocale>|g" "$asset" || return 1 + sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1 + fi + + keyboard="${KEYBOARD:-$culture}" + + if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$keyboard") || return 1 + sed -i "s|<InputLocale>en-US</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 + sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 + fi + + return 0 +} + updateLocalAccountXML() { local asset="$1" @@ -940,6 +991,8 @@ updateProductKeyXML() { local asset="$1" local key + return 0 # TODO + [ -n "${KEY:-}" ] || return 0 key=$(escapeXMLSed "$KEY") || return 1 @@ -959,6 +1012,89 @@ updateProductKeyXML() { return 0 } +updateDiskID() { + + local asset="$1" + local disk_type="${2,,}" + + case "$disk_type" in + "" | "scsi" | "virtio-scsi" | "blk" | "virtio-blk" ) ;; + * ) return 0 ;; + esac + + [ -s "$asset" ] || return 1 + + # Only adjust files that explicitly target Disk 0. + grep -Fq '<DiskID>0</DiskID>' "$asset" || return 0 + + # Leave multi-disk configurations untouched. + if grep -Eq '<DiskID>[[:space:]]*[1-9][0-9]*[[:space:]]*</DiskID>' "$asset"; then + return 0 + fi + + sed -i 's#<DiskID>0</DiskID>#<DiskID>1</DiskID>#g' "$asset" || return 1 + + return 0 +} + +getXMLArchitecture() { + + local asset="$1" + local arch + + arch=$(sed -n -E \ + '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ + "$asset") || return 1 + + [ -n "$arch" ] || return 1 + + printf '%s' "$arch" + return 0 +} + +setConfigurationXML() { + + local asset="$1" + local section + + [ -s "$asset" ] || return 1 + + section=$(sed -n -E ' + /<settings[^>]*pass="windowsPE"[^>]*>/,/<\/settings>/ { + /<component[^>]*name="Microsoft-Windows-Setup"[^>]*>/,/<\/component>/p + } + ' "$asset") || return 1 + + [ -n "$section" ] || return 1 + + if grep -Fq '<UseConfigurationSet>' <<< "$section"; then + + sed -i -E ' + /<settings[^>]*pass="windowsPE"[^>]*>/,/<\/settings>/ { + /<component[^>]*name="Microsoft-Windows-Setup"[^>]*>/,/<\/component>/ { + s#<UseConfigurationSet>[^<]*</UseConfigurationSet>#<UseConfigurationSet>true</UseConfigurationSet>#g + } + } + ' "$asset" || return 1 + + return 0 + fi + + if ! grep -Fq '<UserData>' <<< "$section"; then + return 1 + fi + + sed -i -E ' + /<settings[^>]*pass="windowsPE"[^>]*>/,/<\/settings>/ { + /<component[^>]*name="Microsoft-Windows-Setup"[^>]*>/,/<\/component>/ { + s#^([[:space:]]*)<UserData>#\1<UseConfigurationSet>true</UseConfigurationSet>\n\1<UserData># + } + } + ' "$asset" || return 1 + + return 0 +} + removeSharedFolderXML() { local asset="$1" @@ -985,77 +1121,59 @@ removeSharedFolderXML() { return 0 } -validateGeneratedXML() { +removeLocalAccountXML() { local asset="$1" - if ! xmllint --nonet --noout "$asset"; then - error "The generated answer file is not valid XML!" + if ! sed -i -E \ + -e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \ + -e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \ + "$asset"; then + + error "Failed to remove local account configuration from answer file!" + return 1 + fi + + if ! sed -i -E ' + /<SynchronousCommand([[:space:]>])/ { + :command + N + /<\/SynchronousCommand>/!b command + /<Description>Password Never Expires<\/Description>/d + } + ' "$asset"; then + + error "Failed to remove local account commands from answer file!" return 1 fi return 0 } -updateXML() { +enableLog() { - local asset="$1" - local language="$2" - local domain="${DOMAIN:-}" - local workgroup="${WORKGROUP:-}" - local account="" - local auth="" + local file="$1" + local old='C:\OEM\install.bat"</CommandLine>' + local msg="failed to enable install logging in the answer file!" - [ -z "${WIDTH:-}" ] && WIDTH="1280" - [ -z "${HEIGHT:-}" ] && HEIGHT="720" + enabled "${LOG:-}" || return 0 + [ -f "$file" ] || return 1 - validateXMLSettings || return 1 - updateDisplayXML "$asset" || return 1 - updateLocaleXML "$asset" "$language" || return 1 - - if [ -n "$domain" ]; then - prepareDomainAccount "$domain" account auth || return 1 - else - updateLocalAccountXML "$asset" || return 1 + if ! grep -Fq "$old" "$file"; then + enabled "$DEBUG" && warn "$msg" + return 0 fi - sed -i -E \ - "s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" \ - "$asset" || return 1 + if ! sed -i \ + 's|C:\\OEM\\install\.bat"</CommandLine>|C:\\OEM\\install.bat \&gt; C:\\OEM\\install.log 2\&gt;\&amp;1"</CommandLine>|' \ + "$file"; then - updateMembershipXML \ - "$asset" \ - "$domain" \ - "$workgroup" \ - "$account" \ - "$auth" || return 1 - - updateAutologinXML "$asset" || return 1 - enableLog "$asset" || return 1 - updateEditionXML "$asset" || return 1 - updateProductKeyXML "$asset" || return 1 - removeSharedFolderXML "$asset" || return 1 - validateGeneratedXML "$asset" || return 1 + warn "$msg" + fi return 0 } -escapeSIFValue() { - - local s="$1" - - s=${s//%/%%} - s=${s//\"/\"\"} - - printf '%s' "$s" - return 0 -} - -escapeRegistryValue() { - - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -} - validateLegacyText() { local name="$1" @@ -1130,7 +1248,36 @@ validateLegacyUsername() { return 0 } -extractLegacyDrivers() { +escapeXMLSed() { + + local s + + s=$(escapeXML "$1") || return 1 + s=${s//\\/\\\\} + s=${s//&/\\&} + s=${s//|/\\|} + + printf '%s' "$s" + return 0 +} + +escapeSIFValue() { + + local s="$1" + + s=${s//%/%%} + s=${s//\"/\"\"} + + printf '%s' "$s" + return 0 +} + +escapeRegistryValue() { + + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +extractDrivers() { local drivers="$1" @@ -1145,7 +1292,7 @@ extractLegacyDrivers() { return 0 } -copyLegacyStorageDriver() { +copyStorageDriver() { local dir="$1" local target="$2" @@ -1169,7 +1316,7 @@ copyLegacyStorageDriver() { return 0 } -addLegacyNetworkDriver() { +addNetworkDriver() { local dir="$1" local driver="$2" @@ -1190,7 +1337,7 @@ addLegacyNetworkDriver() { return 0 } -patchLegacyStorageDriver() { +patchStorageDriver() { local file="$1" local arch="$2" @@ -1205,7 +1352,7 @@ patchLegacyStorageDriver() { return 0 } -addLegacySataDriver() { +addSataDriver() { local dir="$1" local target="$2" @@ -1249,9 +1396,9 @@ addLegacyDrivers() { info "$msg" && html "$msg" - extractLegacyDrivers "$drivers" || return 1 - copyLegacyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 - addLegacyNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 + extractDrivers "$drivers" || return 1 + copyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 + addNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 @@ -1260,8 +1407,8 @@ addLegacyDrivers() { return 1 fi - patchLegacyStorageDriver "$file" "$arch" || return 1 - addLegacySataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1 + patchStorageDriver "$file" "$arch" || return 1 + addSataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1 rm -rf "$drivers" || return 1 diff --git a/src/define.sh b/src/define.sh index 92146759..94d60e8b 100644 --- a/src/define.sh +++ b/src/define.sh @@ -11,7 +11,6 @@ set -Eeuo pipefail : "${EDITION:=""}" : "${MANUAL:=""}" : "${REMOVE:=""}" -: "${REBUILD:=""}" : "${VERSION:=""}" : "${COMMAND:=""}" : "${DETECTED:=""}" @@ -976,6 +975,31 @@ getVersion() { return 0 } +skipVersion() { + + local id="$1" + + case "${id,,}" in + "win9"* | "winxp"* | "win2k"* | "win2003"* | "reactos" ) + return 0 ;; + esac + + return 1 +} + +isLegacy() { + + local id="$1" + + case "${id,,}" in + "win9"* | "win2k"* | "winxp"* | "win2003"* | \ + "winvista"* | "win7"* | "win2008"* | "reactos" ) + return 0 ;; + esac + + return 1 +} + switchEdition() { local -n id="$1" diff --git a/src/image.sh b/src/image.sh index b10a127b..e5161792 100644 --- a/src/image.sh +++ b/src/image.sh @@ -84,29 +84,6 @@ getCompatibleVersions() { fi } -hasAnswerFile() { - - local id="$1" - local file="/run/assets/$id.xml" - - [ -s "$file" ] && return 0 - - if [[ "${id,,}" == *"-eval" ]]; then - file="/run/assets/${id%-eval}.xml" - [ -s "$file" ] && return 0 - fi - - # Editions without a dedicated template can use the generic template. - case "${id,,}" in - "win7"* | "win8"* | "win10"* | "win11"* | "winvista"* | "win20"* ) - file="/run/assets/${id%%-*}.xml" - [ -s "$file" ] && return 0 - ;; - esac - - return 1 -} - getVersionPriority() { local id="${1,,}" @@ -433,7 +410,7 @@ detectVersion() { local -a bases=() local -a groups=() local -a versions=() - local -a edition_order=() + local -a selection_order=() local -A image_indexes=() printf -v "$result_name" '%s' "" @@ -454,7 +431,7 @@ detectVersion() { ;; esac - getEditionOrder "${bases[0]}" edition_order || return 1 + getEditionOrder "${bases[0]}" selection_order || return 1 selectEdition \ versions \ @@ -465,7 +442,7 @@ detectVersion() { "$result_name" \ "$index_name" \ "$normalize_name" \ - edition_order && return 0 + selection_order && return 0 local result="${versions[0]}" local key="${result,,}" @@ -506,18 +483,230 @@ detectLanguage() { return 0 } -skipVersion() { +getImageSize() { + + local stage="$1" + local folder="${2:-}" + local mib=$((1024 * 1024)) + local minimum=$((64 * mib)) + local size bytes path + local required large_file + local payload=0 paths=("$stage") + + if [ ! -d "$stage" ]; then + error "Failed to find setup directory: $stage" + return 1 + fi + + [ -n "$folder" ] && paths+=("$folder") + + large_file=$(find -L "${paths[@]}" \ + -type f \ + -size +4294967295c \ + -print \ + -quit) || return 1 + + if [ -n "$large_file" ]; then + error "Setup file exceeds the FAT32 limit: $large_file" + return 1 + fi + + for path in "${paths[@]}"; do + if ! read -r bytes _ < <( + du -Llsb --apparent-size -- "$path" + ); then + error "Failed to calculate setup size!" + return 1 + fi + + payload=$((payload + bytes)) + done + + required=$((payload + ((payload + 3) / 4) + (32 * mib))) + size="$minimum" + + while ((size < required)); do + size=$((size * 2)) + done + + printf '%s\n' "$size" +} + +bootDirect() { local id="$1" case "${id,,}" in - "win9"* | "winxp"* | "win2k"* | "win2003"* | "reactos" ) - return 0 ;; + "reactos" ) return 0 ;; esac return 1 } +canUseSetupImage() { + + local id="$1" + local iso="$2" + + case "${id,,}" in + "win9"* | "winxp"* | "win2k"* | "win2003"* | "reactos" ) + return 1 ;; + esac + + [[ "${iso,,}" != *".esd" ]] && + ! enabled "${UNPACK:-}" +} + +createImageDirectory() { + + local image="$1" + local directory="$2" + + if mdir -i "$image" "$directory" >/dev/null 2>&1; then + return 0 + fi + + mmd -i "$image" "$directory" >/dev/null +} + +createSetupImage() { + + local stage="$1" + local image="$2" + local tmp="${image}.tmp" + local target="::/\$OEM\$/\$1/OEM" + local install="$stage/.overlay-install.bat" + local folder size sectors entry find_pid + local entries=() + + folder=$(getOemFolder) || return 1 + + if ! size=$(getImageSize "$stage" "$folder"); then + return 1 + fi + + sectors=$((size / 512)) + + local msg="Writing overlay image..." + info "$msg" && html "$msg" + + rm -f -- "$tmp" || return 1 + + if ! mformat \ + -i "$tmp" \ + -C \ + -F \ + -T "$sectors" \ + -v "SETUP" \ + ::; then + rm -f -- "$tmp" + error "Failed to format setup image!" + return 1 + fi + + mapfile -d '' entries < <( + find "$stage" \ + -mindepth 1 \ + -maxdepth 1 \ + ! -name '.overlay-install.bat' \ + -print0 + ) + + find_pid=$! + + if ! wait "$find_pid"; then + rm -f -- "$tmp" + error "Failed to enumerate image files!" + return 1 + fi + + for entry in "${entries[@]}"; do + if ! mcopy -Q -s -i "$tmp" "$entry" ::; then + rm -f -- "$tmp" + error "Failed to copy image file: $entry" + return 1 + fi + done + + if [ -n "$folder" ] || [ -f "$install" ]; then + if ! createImageDirectory "$tmp" "::/\$OEM\$" || + ! createImageDirectory "$tmp" "::/\$OEM\$/\$1" || + ! createImageDirectory "$tmp" "$target"; then + rm -f -- "$tmp" + error "Failed to create OEM directory in setup image!" + return 1 + fi + fi + + if [ -n "$folder" ]; then + + mapfile -d '' entries < <( + find "$folder" -mindepth 1 -maxdepth 1 -print0 + ) + + find_pid=$! + + if ! wait "$find_pid"; then + rm -f -- "$tmp" + error "Failed to enumerate OEM files!" + return 1 + fi + + for entry in "${entries[@]}"; do + if ! mcopy -Q -s -o -i "$tmp" "$entry" "$target"; then + rm -f -- "$tmp" + error "Failed to copy OEM file: $entry" + return 1 + fi + done + + fi + + if [ -f "$install" ]; then + if ! mcopy -Q -o -i "$tmp" "$install" "$target/install.bat"; then + rm -f -- "$tmp" + error "Failed to replace install.bat in setup image!" + return 1 + fi + fi + + if ! mdir -i "$tmp" :: >/dev/null; then + rm -f -- "$tmp" + error "Failed to verify image!" + return 1 + fi + + if ! enabled "$MANUAL"; then + + local answer="$stage/Autounattend.xml" + + if [ ! -f "$answer" ] || [ ! -s "$answer" ]; then + rm -f -- "$tmp" + error "Failed to find staged answer file: $answer" + return 1 + fi + + if ! mtype -i "$tmp" ::/Autounattend.xml | cmp -s - "$answer"; then + rm -f -- "$tmp" + error "Failed to verify staged answer file!" + return 1 + fi + + fi + + if ! mv -f -- "$tmp" "$image"; then + rm -f -- "$tmp" + error "Failed to save setup image: $image" + return 1 + fi + + if ! setOwner "$image"; then + warn "Failed to set the owner for \"$image\" !" + fi + + return 0 +} + detectLegacy() { local dir="$1" @@ -655,11 +844,19 @@ resolveImage() { local version="$1" + XML="" + FB="falling back to manual installation!" + [ -z "$DETECTED" ] || return 0 - [ -z "$CUSTOM" ] || return 1 [ -z "${REUSED_ISO:-}" ] || return 1 [[ "${version,,}" != "http"* ]] || return 1 + if [ -n "$CUSTOM" ]; then + bootDirect "$version" || return 1 + DETECTED="$version" + return 0 + fi + local file="/run/assets/$version.xml" if [ -s "$file" ]; then @@ -836,16 +1033,8 @@ configureImage() { detectImage() { local dir="$1" - local version="$2" local desc - XML="" - - if resolveImage "$version"; then - setImage || return 1 - return 0 - fi - info "Detecting version from ISO image..." if detectLegacy "$dir" || detectReactOS "$dir"; then @@ -1114,90 +1303,19 @@ extractBootImage() { local iso="$1" local dir="$2" local desc="$3" - - local tmp="$TMP/boot-images" - local rc size offset image="" - local msg="using legacy extraction..." - local expected_size=$((BOOT_LOAD_SIZE * 512)) - local max_extract_size=$((64 * 1024 * 1024)) - local boot_info - local -a images=() + local offset info ETFS="boot.img" + [ -s "$dir/$ETFS" ] && return 0 - rm -f "$dir/$ETFS" || return 1 - rm -rf "$tmp" || return 1 - if command -v prlimit >/dev/null 2>&1; then - LC_ALL=C prlimit \ - "--fsize=$max_extract_size:$max_extract_size" \ - xorriso \ - -no_rc \ - -osirrox on \ - -indev "$iso" \ - -extract_boot_images "$tmp" >/dev/null 2>&1 - rc=$? - - if (( rc == 0 )); then - mapfile -t images < <( - find "$tmp" \ - -maxdepth 1 \ - -type f \ - -name 'eltorito_img*_bios.img' \ - -print - ) - - if (( ${#images[@]} == 1 )); then - image="${images[0]}" - - if [ ! -s "$image" ]; then - warn "The extracted BIOS boot image is empty, $msg" - elif ! size=$(stat -c%s "$image"); then - warn "Failed to determine the BIOS boot image size, $msg" - elif (( size != expected_size )); then - info "The extracted BIOS boot image has an unexpected size, $msg" - else - if ! mv -f "$image" "$dir/$ETFS"; then - rm -rf "$tmp" || true - error "Failed to save boot image from $desc ISO!" - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 - fi - - elif (( ${#images[@]} > 1 )); then - warn "Multiple BIOS boot images were found, $msg" - else - warn "No BIOS boot image was found, $msg" - fi - - elif (( rc == 153 )); then - info "The extracted BIOS boot image exceeded the size limit, $msg" - - else - if (( rc > 128 )); then - rm -rf "$tmp" || true - exit "$rc" - fi - - warn "Failed to extract the BIOS boot image, $msg" - fi - - else - warn "The prlimit utility is unavailable, $msg" - fi - - rm -rf "$tmp" || true - - if ! boot_info=$(isoinfo -d -i "$iso"); then + if ! info=$(isoinfo -d -i "$iso"); then error "Failed to read boot image information from $desc ISO!" return 1 fi - offset=$(awk '/Bootoff / { print $NF; exit }' <<< "$boot_info") + offset=$(awk '/Bootoff / { print $NF; exit }' <<< "$info") if [ -z "$offset" ]; then error "Failed to determine boot image offset from $desc ISO!" @@ -1216,6 +1334,7 @@ extractBootImage() { "count=$BOOT_LOAD_SIZE" \ "skip=$((offset * 4))" \ status=none; then + rm -f "$dir/$ETFS" || true error "Failed to extract boot image from $desc ISO!" return 1 diff --git a/src/install.sh b/src/install.sh index 7f6aac7b..4ffcb19f 100644 --- a/src/install.sh +++ b/src/install.sh @@ -1,191 +1,126 @@ #!/usr/bin/env bash set -Eeuo pipefail -ETFS="boot/etfsboot.com" -FB="falling back to manual installation!" -EFISYS="efi/microsoft/boot/efisys_noprompt.bin" +installWindows() { -backup () { + parseVersion || return 58 + parseLanguage || return 62 + detectCustom || return 64 - local iso="$1" - local count=1 - local name="unknown" - local root="$STORAGE/backups" - local file previous failed="" - - previous=$(readState "base") || return 1 - [ -n "$previous" ] && name="${previous%.*}" - - if ! makeDir "$root"; then - error "Failed to create directory \"$root\" !" - return 1 - fi - - local folder="$name" - local dir="$root/$folder" - - while [ -d "$dir" ]; do - (( count++ )) - folder="${name}.${count}" - dir="$root/$folder" - done - - if ! makeDir "$dir"; then - error "Failed to create directory \"$dir\" !" - return 1 - fi - - if [ -f "$iso" ]; then - if ! mv -f -- "$iso" "$dir/"; then - error "Failed to move \"$iso\" to \"$dir\"." - failed="Y" - fi - fi - - while IFS= read -r -d '' file; do - if ! mv -n -- "$file" "$dir/"; then - error "Failed to move \"$file\" to \"$dir\"." - failed="Y" - fi - done < <( - find "$STORAGE" -maxdepth 1 -type f \ - \( -iname 'data.*' -or -iname 'windows.*' -or -iname '*.rom' -or -iname '*.vars' \) \ - -not -iname '*.iso' -print0 - ) - - local find_pid=$! - - if ! wait "$find_pid"; then - error "Failed to enumerate files in \"$STORAGE\"." - failed="Y" - fi - - [ -z "$(ls -A "$dir")" ] && rm -rf "$dir" - [ -z "$(ls -A "$root")" ] && rm -rf "$root" - - [ -n "$failed" ] && return 1 - - return 0 -} - -findFile() { - - local dir file base - local fname="$1" - local boot="$STORAGE/windows.boot" - - dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) - [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) - - if [ -d "$dir" ]; then - if ! hasData || [ ! -f "$boot" ]; then - error "The bind $dir maps to a file that does not exist!" && return 1 - fi - fi - - file=$(find / -maxdepth 1 -type f -iname "$fname" -print -quit) - [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) - - if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then - base=$(basename "$VERSION") - file="$STORAGE/$base" - fi - - if [ ! -f "$file" ] || [ ! -s "$file" ]; then + if ! startInstall; then + bootWindows || return 66 return 0 fi - local size - size="$(stat -c%s "$file")" - [ -z "$size" ] || [[ "$size" == "0" ]] && return 0 - - ISO="$file" - CUSTOM="$file" - BOOT="$STORAGE/windows.$size.iso" - - return 0 -} - -detectCustom() { - - CUSTOM="" - - ! findFile "custom.iso" && return 1 - [ -n "$CUSTOM" ] && return 0 - - ! findFile "boot.iso" && return 1 - [ -n "$CUSTOM" ] && return 0 - - return 0 -} - -skipInstall() { - - local iso="$1" - local method magic previous - local boot="$STORAGE/windows.boot" - - previous=$(readState "base") || return 1 - - if [ -n "$previous" ]; then - if [[ "${STORAGE,,}/${previous,,}" != "${iso,,}" ]]; then - - if ! hasDisk; then - - if ! rm -f -- "$STORAGE/$previous"; then - error "Failed to remove ISO file \"$STORAGE/$previous\" !" - exit 50 - fi - - return 1 - - fi - - if [[ "${iso,,}" == "${STORAGE,,}/windows."* ]]; then - method="your custom .iso file was changed" - else - if [[ "${previous,,}" != "windows."* ]]; then - method="the VERSION variable was changed" - else - method="your custom .iso file was removed" - - if [ -f "$boot" ] && hasData; then - info "Detected that $method, will be ignored." - return 0 - fi - - fi - fi - - info "Detected that $method, a backup of your previous installation will be saved..." - - if ! backup "$STORAGE/$previous"; then - warn "the backup was incomplete, continuing with installation..." - fi - - return 1 - + if ! hasImage "$ISO"; then + if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then + removeIso "$ISO" && return 68 fi fi - [ -f "$boot" ] && hasData && return 0 + local extracted=0 + local boot="$BOOT" + local dir="$TMP/unpack" - [ ! -f "$iso" ] && return 1 - [ ! -s "$iso" ] && return 1 + if resolveImage "$VERSION"; then - # Check if the ISO was already processed by our script - magic=$(dd if="$iso" bs=1 count=1 status=none | tr -d '\000') - magic="$(printf '%s' "$magic" | od -A n -t x1 -v | tr -d ' \n')" - local byte="16" - enabled "$MANUAL" && byte="17" + if ! setImage; then + abortInstall "$dir" "$ISO" "$boot" || return 70 + return 0 + fi - if [[ "$magic" != "$byte" ]]; then + if needsExtraction "$DETECTED" "$ISO"; then + if ! extractImage "$ISO" "$dir" "$VERSION"; then + removeIso "$ISO" && return 72 + fi - info "The ISO will be processed again because the configuration was changed..." - return 1 + extracted=1 + fi + + else + + if ! extractImage "$ISO" "$dir" "$VERSION"; then + removeIso "$ISO" && return 74 + fi + + extracted=1 + + if ! detectImage "$dir"; then + abortInstall "$dir" "$ISO" "$boot" || return 76 + return 0 + fi fi + local desc + if ! desc=$(printVariant "$DETECTED" "$DETECTED"); then + abortInstall "$dir" "$ISO" "$boot" || return 78 + return 0 + fi + + if ! setMachine "$DETECTED" "$ISO" "$dir" "$desc"; then + abortInstall "$dir" "$ISO" "$boot" || return 80 + return 0 + fi + + if ! restoreMachineState; then + abortInstall "$dir" "$ISO" "$boot" || return 82 + return 0 + fi + + if bootDirect "$DETECTED"; then + abortInstall "$dir" "$ISO" "$boot" || return 83 + return 0 + fi + + if canUseSetupImage "$DETECTED" "$ISO"; then + + if ! stageSetup "$XML" "$LANGUAGE" "$TMP/setup"; then + abortInstall "$dir" "$ISO" "$boot" || return 84 + return 0 + fi + + if ! createSetupImage "$TMP/setup" "$STORAGE/setup.img"; then + abortInstall "$dir" "$ISO" "$boot" || return 86 + return 0 + fi + + useOriginalImage "$ISO" || return 88 + + else + + if (( ! extracted )); then + if ! extractImage "$ISO" "$dir" "$VERSION"; then + removeIso "$ISO" && return 90 + fi + fi + + if ! prepareImage "$ISO" "$dir"; then + abortInstall "$dir" "$ISO" "$boot" || return 92 + return 0 + fi + + if ! updateImage "$dir" "$XML" "$LANGUAGE"; then + abortInstall "$dir" "$ISO" "$boot" || return 94 + return 0 + fi + + removeImage "$ISO" || return 96 + buildImage "$dir" || return 98 + + fi + + finishInstall "$BOOT" "N" "$boot" || return 100 + + return 0 +} + +bootWindows() { + + restoreMachineState || return 1 + restoreBootMode || return 1 + restoreMachine || return 1 + return 0 } @@ -238,9 +173,22 @@ startInstall() { exit 50 fi - skipInstall "$BOOT" && return 1 + local setup="$STORAGE/setup.img" - if hasDisk; then + if ! rm -f -- "$setup" "${setup}.tmp"; then + error "Failed to remove setup image \"$setup\" !" + exit 50 + fi + + local previousBase + if ! previousBase=$(readState "base"); then + error "Failed to read the previous installation state!" + exit 50 + fi + + skipInstall "$BOOT" "$previousBase" && return 1 + + if [ -z "$previousBase" ] && hasDisk; then if ! backup ""; then warn "the backup was incomplete, continuing with installation..." fi @@ -288,86 +236,17 @@ startInstall() { return 0 } -finishInstall() { - - local iso="$1" - local aborted="$2" - local base - - if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then - error "Failed to find ISO file: $iso" && return 1 - fi - - if [[ "$iso" == "$STORAGE/"* ]]; then - if ! setOwner "$iso"; then - warn "failed to set the owner for \"$iso\" !" - fi - fi - - if [[ "$aborted" != [Yy1]* ]]; then - # Mark ISO as prepared via magic byte - local byte="16" - enabled "$MANUAL" && byte="17" - if ! printf '%b' "\x$byte" | dd of="$iso" bs=1 seek=0 count=1 conv=notrunc status=none; then - warn "failed to set magic byte in ISO file: $iso" - fi - fi - - local file="$STORAGE/windows.ver" - cp -f /etc/version "$file" || return 1 - - if ! setOwner "$file"; then - warn "Failed to set the owner for \"$file\" !" - fi - - if [[ "$iso" == "$STORAGE/"* ]]; then - if [[ "$aborted" != [Yy1]* ]] || [ -z "$CUSTOM" ]; then - base=$(basename "$iso") - writeState "base" "$base" || return 1 - fi - fi - - if [[ "${PLATFORM,,}" == "x64" ]]; then - if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then - - writeState "mode" "$BOOT_MODE" || return 1 - - else - - local secure="" - - # Enable secure boot + TPM on manual installs as Win11 requires - if enabled "$MANUAL" || [[ "$aborted" == [Yy1]* ]]; then - [[ "${DETECTED,,}" == "win11"* ]] && secure="Y" - fi - - # Enable secure boot on multi-socket systems to workaround freeze - if [ -n "$SOCKETS" ] && [[ "$SOCKETS" != "1" ]]; then - secure="Y" - fi - - if [ -n "$secure" ]; then - BOOT_MODE="windows_secure" - writeState "mode" "$BOOT_MODE" || return 1 - fi - - fi - fi - - rm -rf "$TMP" - return 0 -} - abortInstall() { local dir="$1" local iso="$2" + local boot="$3" local efi efi32 efi64 [[ "${iso,,}" == *".esd" ]] && exit 60 enabled "${UNPACK:-}" && exit 60 - if [[ "${PLATFORM,,}" == "x64" ]]; then + if [[ "${PLATFORM,,}" == "x64" ]] && [ -d "$dir" ]; then efi=$(find "$dir" -maxdepth 1 -type d -iname efi -print -quit) efi32=$(find "$dir" -maxdepth 3 -type f \ @@ -396,10 +275,220 @@ abortInstall() { fi fi - finishInstall "$BOOT" "Y" && return 0 + finishInstall "$BOOT" "Y" "$boot" && return 0 return 1 } +useOriginalImage() { + + local iso="$1" + + if [ -n "$CUSTOM" ]; then + BOOT="$iso" + REMOVE="N" + return 0 + fi + + if [[ "$iso" != "$BOOT" ]]; then + if ! mv -f -- "$iso" "$BOOT"; then + error "Failed to move ISO file: $iso" + return 1 + fi + fi + + return 0 +} + +skipInstall() { + + local iso="$1" + local previousBase="$2" + local boot="$STORAGE/windows.boot" + + if [ -n "$previousBase" ]; then + if [[ "${STORAGE,,}/${previousBase,,}" != "${iso,,}" ]]; then + + if ! hasDisk; then + + if ! rm -f -- "$STORAGE/$previousBase"; then + error "Failed to remove ISO file \"$STORAGE/$previousBase\" !" + exit 50 + fi + + return 1 + + fi + + local method + + if [[ "${iso,,}" == "${STORAGE,,}/windows."* ]]; then + method="your custom .iso file was changed" + else + if [[ "${previousBase,,}" != "windows."* ]]; then + method="the VERSION variable was changed" + else + method="your custom .iso file was removed" + + if [ -f "$boot" ] && hasData; then + info "Detected that $method, will be ignored." + return 0 + fi + + fi + fi + + info "Detected that $method, a backup of your previous installation will be saved..." + + if ! backup "$STORAGE/$previousBase"; then + warn "the backup was incomplete, continuing with installation..." + fi + + return 1 + + fi + fi + + [ -f "$boot" ] && hasData && return 0 + + return 1 +} + +finishInstall() { + + local iso="$1" + local aborted="$2" + local boot="$3" + local base + + if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then + error "Failed to find ISO file: $iso" && return 1 + fi + + if [[ "$iso" == "$STORAGE/"* ]]; then + if ! setOwner "$iso"; then + warn "failed to set the owner for \"$iso\" !" + fi + fi + + local file="$STORAGE/windows.ver" + cp -f /etc/version "$file" || return 1 + + if ! setOwner "$file"; then + warn "Failed to set the owner for \"$file\" !" + fi + + if [[ "$boot" == "$STORAGE/"* ]]; then + if [[ "$aborted" != [Yy1]* ]] || [ -z "$CUSTOM" ]; then + base=$(basename "$boot") + writeState "base" "$base" || return 1 + fi + fi + + if [[ "${PLATFORM,,}" == "x64" ]]; then + if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then + + writeState "mode" "$BOOT_MODE" || return 1 + + else + + local secure=0 + + # Enable secure boot + TPM on manual installs as Win11 requires + if enabled "$MANUAL" || [[ "$aborted" == [Yy1]* ]]; then + [[ "${DETECTED,,}" == "win11"* ]] && secure=1 + fi + + if (( secure )); then + BOOT_MODE="windows_secure" + writeState "mode" "$BOOT_MODE" || return 1 + fi + + fi + fi + + rm -rf "$TMP" + return 0 +} + +findFile() { + + local dir file base + local fname="$1" + local boot="$STORAGE/windows.boot" + + dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) + [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) + + if [ -d "$dir" ]; then + if ! hasData || [ ! -f "$boot" ]; then + error "The bind $dir maps to a file that does not exist!" && return 1 + fi + fi + + file=$(find / -maxdepth 1 -type f -iname "$fname" -print -quit) + [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) + + if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then + base=$(basename "$VERSION") + file="$STORAGE/$base" + fi + + if [ ! -f "$file" ] || [ ! -s "$file" ]; then + return 0 + fi + + local size + size="$(stat -c%s "$file")" + [ -z "$size" ] || [[ "$size" == "0" ]] && return 0 + + ISO="$file" + CUSTOM="$file" + BOOT="$STORAGE/windows.$size.iso" + + return 0 +} + +detectCustom() { + + CUSTOM="" + + findFile "custom.iso" || return 1 + [ -n "$CUSTOM" ] && return 0 + + findFile "boot.iso" || return 1 + [ -n "$CUSTOM" ] && return 0 + + return 0 +} + +hasImage() { + + local file="$1" + + [ -f "$file" ] && [ -s "$file" ] +} + +removeIso() { + + local iso="$1" + + rm -f -- "$iso" 2>/dev/null || : + + return 0 +} + +needsExtraction() { + + local id="$1" + local iso="$2" + + bootDirect "$id" && return 1 + + skipVersion "$id" || + [[ "${iso,,}" == *".esd" ]] || + enabled "${UNPACK:-}" +} + checkFreeSpace() { local dir="$1" @@ -692,32 +781,25 @@ setMachine() { local desc="$4" local legacy="" - if ! disabled "$REBUILD"; then - - case "${id,,}" in - "win2k"* ) legacy="2k" ;; - "winxp"* ) legacy="xp" ;; - "win2003"* ) legacy="2k3" ;; - esac - - if [ -n "$legacy" ]; then - if ! legacyInstall "$iso" "$dir" "$desc" "$legacy"; then - error "Failed to prepare $desc ISO!" - return 1 - fi - fi - - fi + ETFS="boot/etfsboot.com" case "${id,,}" in - - "win9"* | "win2k"* | "winxp"* | "win2003"* | \ - "winvista"* | "win7"* | "win2008"* | "reactos" ) - - writeState "mode" "windows_legacy" || return 1 ;; - + "win2k"* ) legacy="2k" ;; + "winxp"* ) legacy="xp" ;; + "win2003"* ) legacy="2k3" ;; esac + if [ -n "$legacy" ]; then + if ! legacyInstall "$iso" "$dir" "$desc" "$legacy"; then + error "Failed to prepare $desc ISO!" + return 1 + fi + fi + + if isLegacy "$id"; then + writeState "mode" "windows_legacy" || return 1 + fi + restoreBootMode || return 1 case "${id,,}" in @@ -738,21 +820,15 @@ setMachine() { writeState "net" "rtl8139" || return 1 writeState "usb" "pci-ohci" || return 1 ;; - "winxpx86"* ) + "winxpx"* | "win2003"* ) writeState "type" "blk" || return 1 writeState "net" "rtl8139" || return 1 writeState "sound" "usb-audio" || return 1 ;; - "winxpx64"* | "win2003"* ) - - writeState "type" "blk" || return 1 - writeState "sound" "usb-audio" || return 1 ;; - "reactos" ) [ -z "${REMOVE:-}" ] && REMOVE="N" - [ -z "${REBUILD:-}" ] && REBUILD="N" writeState "old" "pc" || return 1 writeState "type" "auto" || return 1 @@ -804,12 +880,6 @@ prepareImage() { desc=$(printVariant "$DETECTED" "$DETECTED") - # Adjust QEMU machine configuration for legacy versions - setMachine "$DETECTED" "$iso" "$dir" "$desc" || return 1 - restoreMachineState || return 1 - - disabled "$REBUILD" && return 0 - if [[ "${BOOT_MODE,,}" == "windows_legacy" && "${DETECTED,,}" != "win9"* ]]; then getBootLoadSize "$iso" "$dir" "$desc" || return 1 @@ -825,6 +895,8 @@ prepareImage() { return 1 fi + EFISYS="efi/microsoft/boot/efisys_noprompt.bin" + [ -f "$dir/$ETFS" ] && [ -s "$dir/$ETFS" ] && [ -f "$dir/$EFISYS" ] && [ -s "$dir/$EFISYS" ] && return 0 @@ -837,29 +909,65 @@ prepareImage() { return 1 } -addFolder() { +getOemFolder() { - local src="$1" - local folder="/oem" file - local dest="$src/\$OEM\$/\$1/OEM" + local folder="/oem" [ ! -d "$folder" ] && folder="/OEM" [ ! -d "$folder" ] && folder="$STORAGE/oem" [ ! -d "$folder" ] && folder="$STORAGE/OEM" - [ ! -d "$folder" ] && folder="" + [ -d "$folder" ] && echo "$folder" + + return 0 +} + +addFolder() { + + local src="$1" + local target="${2:-image}" + local log="${3:-Y}" + local mode="${4:-copy}" + local folder file="" source="" + local dest="$src/\$OEM\$/\$1/OEM" + local install="$src/.overlay-install.bat" + + folder=$(getOemFolder) || return 1 [ -z "$folder" ] && [ -z "$COMMAND" ] && return 0 - local msg="Adding OEM files to image..." - info "$msg" && html "$msg" - - mkdir -p "$dest" || return 1 - - if [ -n "$folder" ]; then - cp -Lr "$folder/." "$dest" || return 1 + if enabled "$log"; then + local msg="Adding OEM files to $target..." + info "$msg" && html "$msg" fi - file=$(find "$dest" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 + if [ "$mode" = "overlay" ]; then + + rm -f -- "$install" || return 1 + + if [ -n "$folder" ]; then + source=$(find -L "$folder" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 + + if [ -n "$source" ]; then + if ! cp -L -- "$source" "$install"; then + error "Failed to create a writable copy of $source!" + return 1 + fi + + file="$install" + fi + fi + + else + + mkdir -p "$dest" || return 1 + + if [ -n "$folder" ]; then + cp -Lr "$folder/." "$dest" || return 1 + fi + + file=$(find "$dest" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 + + fi if [ -s "$file" ]; then normalizeBatch "$file" || return 1 @@ -867,7 +975,13 @@ addFolder() { if [ -n "$COMMAND" ]; then - [ -z "$file" ] && file="$dest/install.bat" + if [ -z "$file" ]; then + if [ "$mode" = "overlay" ]; then + file="$install" + else + file="$dest/install.bat" + fi + fi if [ -s "$file" ]; then printf '\n' >> "$file" || return 1 @@ -948,16 +1062,19 @@ addDrivers() { local src="$1" local tmp="$2" - local file="$3" - local index="$4" - local version="$5" + local version="$3" + local file="${4:-}" + local index="${5:-}" + local log="${6:-Y}" local drivers="$tmp/drivers" rm -rf "$drivers" mkdir -p "$drivers" - local msg="Adding drivers to image..." - info "$msg" && html "$msg" + if enabled "$log"; then + local msg="Adding drivers to image..." + info "$msg" && html "$msg" + fi if [ -z "$version" ]; then version="win11x64" @@ -972,10 +1089,36 @@ addDrivers() { local dest="$drivers/$target" mkdir -p "$dest" || return 1 - wimlib-imagex update "$file" "$index" --command "delete --force --recursive /$target" >/dev/null || true + if [ -n "$file" ]; then + + if [ -z "$index" ]; then + error "No boot image index specified!" + return 1 + fi + + wimlib-imagex update "$file" "$index" --command "delete --force --recursive /$target" >/dev/null || true + + fi local driver - local driver_list=( qxl viofs sriov smbus qxldod viorng viostor viomem NetKVM Balloon vioscsi pvpanic vioinput viogpudo vioserial qemupciserial ) + local driver_list=( + qxl + viofs + sriov + smbus + qxldod + viorng + viostor + viomem + NetKVM + Balloon + vioscsi + pvpanic + vioinput + viogpudo + vioserial + qemupciserial + ) for driver in "${driver_list[@]}"; do addDriver "$version" "$drivers" "$target" "$driver" || return 1 @@ -992,14 +1135,61 @@ addDrivers() { ;; esac - if ! wimlib-imagex update "$file" "$index" --command "add $dest /$target" >/dev/null; then - return 1 + if [ -n "$file" ]; then + + if ! wimlib-imagex update "$file" "$index" --command "add $dest /$target" >/dev/null; then + return 1 + fi + + else + + local winpe="$src/$target" + rm -rf "$winpe" || return 1 + mkdir -p "$winpe" || return 1 + cp -Lr "$dest/." "$winpe" || return 1 + fi rm -rf "$drivers" return 0 } +stageSetup() { + + local asset="$1" + local language="$2" + local stage="$3" + + skipVersion "${DETECTED,,}" && return 0 + + local msg="Creating overlay image..." + info "$msg" && html "$msg" + + if ! rm -rf -- "$stage"; then + error "Failed to remove previous image files!" + return 1 + fi + + if ! mkdir -p "$stage"; then + error "Failed to create image staging directory!" + return 1 + fi + + if ! addDrivers "$stage" "$stage" "$DETECTED"; then + error "Failed to stage Windows drivers!" + return 1 + fi + + if ! addFolder "$stage" "image" "Y" "overlay"; then + error "Failed to stage OEM folder!" + return 1 + fi + + stageAnswer "$asset" "$language" "$stage" || return 1 + + return 0 +} + updateImage() { local dir="$1" @@ -1011,11 +1201,6 @@ updateImage() { local dat="${xml//.xml/.dat}" local desc path src wim name info - if disabled "$REBUILD"; then - info "Skipping modifications to the installation image..." - return 1 - fi - skipVersion "${DETECTED,,}" && return 0 if [ ! -s "$asset" ] || [ ! -f "$asset" ]; then @@ -1055,7 +1240,7 @@ updateImage() { idx="2" fi - if ! addDrivers "$src" "$tmp" "$wim" "$idx" "$DETECTED"; then + if ! addDrivers "$src" "$tmp" "$DETECTED" "$wim" "$idx"; then error "Failed to add drivers to image!" fi @@ -1087,22 +1272,15 @@ updateImage() { removeGeneratedXML "$asset" || return 1 - if [ -n "${CUSTOM_XML:-}" ]; then - - if ! xmllint --nonet --noout "$answer"; then - error "The custom answer file is not valid XML!" - return 1 - fi - - else - + if [ -z "${CUSTOM_XML:-}" ]; then if ! updateXML "$answer" "$language"; then error "Failed to update answer file: $answer" return 1 fi - fi + validateGeneratedXML "$answer" || return 1 + if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then MANUAL="Y" warn "failed to add answer file ($name) to ISO image, $FB" @@ -1160,6 +1338,69 @@ removeImage() { return 0 } +backup () { + + local iso="$1" + local count=1 + local name="unknown" + local root="$STORAGE/backups" + local file previous failed="" + + previous=$(readState "base") || return 1 + [ -n "$previous" ] && name="${previous%.*}" + + if ! makeDir "$root"; then + error "Failed to create directory \"$root\" !" + return 1 + fi + + local folder="$name" + local dir="$root/$folder" + + while [ -d "$dir" ]; do + (( count++ )) + folder="${name}.${count}" + dir="$root/$folder" + done + + if ! makeDir "$dir"; then + error "Failed to create directory \"$dir\" !" + return 1 + fi + + if [ -f "$iso" ]; then + if ! mv -f -- "$iso" "$dir/"; then + error "Failed to move \"$iso\" to \"$dir\"." + failed="Y" + fi + fi + + while IFS= read -r -d '' file; do + if ! mv -n -- "$file" "$dir/"; then + error "Failed to move \"$file\" to \"$dir\"." + failed="Y" + fi + done < <( + find "$STORAGE" -maxdepth 1 -type f \ + \( -iname 'data.*' -or -iname 'windows.*' -or -iname '*.rom' -or -iname '*.vars' \) \ + -not -iname '*.iso' -print0 + ) + + local find_pid=$! + + if ! wait "$find_pid"; then + error "Failed to enumerate files in \"$STORAGE\"." + failed="Y" + fi + + [ -z "$(ls -A "$dir")" ] && rm -rf "$dir" + [ -z "$(ls -A "$root")" ] && rm -rf "$root" + + [ -n "$failed" ] && return 1 + + return 0 +} + restoreBootMode() { local current="${BOOT_MODE:-}" @@ -1209,65 +1450,6 @@ restoreMachineState() { return 0 } -bootWindows() { - - restoreMachineState || return 1 - restoreBootMode || return 1 - restoreMachine || return 1 - - return 0 -} - -###################################### - -! parseVersion && exit 58 -! parseLanguage && exit 56 -! detectCustom && exit 59 - -if ! startInstall; then - bootWindows && return 0 - exit 68 -fi - -if [ ! -s "$ISO" ] || [ ! -f "$ISO" ]; then - if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then - rm -f "$ISO" 2> /dev/null || true - exit 61 - fi -fi - -DIR="$TMP/unpack" - -if ! extractImage "$ISO" "$DIR" "$VERSION"; then - rm -f "$ISO" 2> /dev/null || true - exit 62 -fi - -if ! detectImage "$DIR" "$VERSION"; then - abortInstall "$DIR" "$ISO" && return 0 - exit 60 -fi - -if ! prepareImage "$ISO" "$DIR"; then - abortInstall "$DIR" "$ISO" && return 0 - exit 66 -fi - -if ! updateImage "$DIR" "$XML" "$LANGUAGE"; then - abortInstall "$DIR" "$ISO" && return 0 - exit 63 -fi - -if ! removeImage "$ISO"; then - exit 64 -fi - -if ! buildImage "$DIR"; then - exit 65 -fi - -if ! finishInstall "$BOOT" "N"; then - exit 69 -fi +installWindows return 0 diff --git a/src/mido.sh b/src/mido.sh index 458c07de..b1f98a0b 100644 --- a/src/mido.sh +++ b/src/mido.sh @@ -921,7 +921,7 @@ verifyFile() { local hash [ -z "$check" ] && return 0 - ! enabled "$VERIFY" && return 0 + enabled "$VERIFY" || return 0 [[ "${#check}" == "40" ]] && algo="SHA1" local msg="Verifying downloaded ISO..." diff --git a/src/power.sh b/src/power.sh index 24a9cc1e..782f21f3 100644 --- a/src/power.sh +++ b/src/power.sh @@ -11,6 +11,7 @@ SHUTDOWN_SIGNAL=0 QEMU_PTY="$QEMU_DIR/qemu.pty" QEMU_END="$QEMU_DIR/qemu.end" +ACPI_SOCKET="$QEMU_DIR/acpi.sock" CONSOLE_PID="$QEMU_DIR/console.pid" CONSOLE_SOCKET="$QEMU_DIR/console.sock" QEMU_START_PID="$QEMU_DIR/qemu.start.pid" @@ -28,56 +29,96 @@ bootStatus() { last="${line#*:}" recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") - if [[ "$last" == "Booting from DVD/CD"* ]]; then - return 0 - fi - grep -Fq "Loading FreeLoader..." <<< "$recent" && return 0 - grep -Fq \ + if grep -Fq \ -e "No bootable device." \ -e "BOOTMGR is missing" \ - -e "Boot failed: not a bootable disk" \ - -e "Boot failed: could not read the boot disk" \ - <<< "$recent" && return 1 + <<< "$recent"; then + return 2 + fi - return 0 + if grep -Fq \ + -e "Boot failed: not a bootable disk" \ + -e "Boot failed: Could not read from CDROM" \ + -e "Boot failed: could not read the boot disk" \ + <<< "$recent"; then + return 5 + fi + + if [[ "$last" == "Booting from Hard Disk"* ]]; then + return 3 + fi + + if [[ "$last" == "Booting from DVD/CD"* ]]; then + return 4 + fi + + return 1 fi - grep -Fq "UEFI Interactive Shell" "$QEMU_PTY" && return 2 + local line last recent - local last - last=$(grep -E \ + line=$(grep -nE \ 'BdsDxe: starting Boot[[:xdigit:]]{4} ' \ "$QEMU_PTY" | tail -1) - [ -z "$last" ] && return 1 + [ -z "$line" ] && return 1 + + last="${line#*:}" + recent=$(tail -n +"${line%%:*}" "$QEMU_PTY") + + if [[ "$last" == *'"Windows Boot Manager"'* ]]; then + return 0 + fi + + grep -Eq \ + 'BdsDxe: failed to start Boot[[:xdigit:]]{4} "UEFI QEMU .*DVD-ROM.*: Time out' \ + <<< "$recent" && return 2 + + grep -Fq "UEFI Interactive Shell" <<< "$recent" && return 2 grep -Eq \ - -e '"Windows Boot Manager"' \ -e '"UEFI QEMU .*DVD-ROM' \ -e 'CDROM\(' \ -e 'USB\(' \ - <<< "$last" + <<< "$last" && return 4 + + return 1 } waitForBoot() { local pid="$1" local timeout="${2:-30}" + local keySent=0 + local pendingType=0 + local pendingLine="" + local pendingDeadline=0 + local keyDelay status marker local deadline=$((SECONDS + timeout)) local screen="visit http://127.0.0.1:$WEB_PORT/ to view the screen..." while isAlive "$pid"; do + if (( ! keySent )) && needsBootKey; then + + if keyDelay=$(bootKeyDelay); then + if sendKey spc "$keyDelay" 500; then + keySent=1 + fi + fi + + fi + if bootStatus; then - local status=0 + status=0 else - local status=$? + status=$? fi case "$status" in - + 0) echo if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then @@ -90,9 +131,47 @@ waitForBoot() { 2) echo - error "$(app) could not boot, aborting..." - terminateQemu - return 0 ;; + error "$(app) could not boot, aborting..." + terminateQemu + return 0 ;; + + 3 | 4) + + marker=$(getBootMarker) + + if [[ "$marker" != "$pendingLine" ]] || (( status != pendingType )); then + pendingLine="$marker" + pendingType=$status + + pendingDeadline=$((SECONDS + 6)) + fi + + if (( pendingDeadline > 0 && SECONDS >= pendingDeadline )); then + echo + + if [[ "${DISPLAY,,}" == "web" ]] && ! disabled "${WEB:-Y}"; then + info "$(app) started successfully, $screen" + else + info "$(app) started successfully." + fi + + echo && return 0 + fi + ;; + + 5) + + pendingType=0 + pendingLine="" + pendingDeadline=0 + ;; + + *) + + pendingType=0 + pendingLine="" + pendingDeadline=0 + ;; esac @@ -101,7 +180,7 @@ waitForBoot() { sleep 0.25 done - ! isAlive "$pid" && return 0 + isAlive "$pid" || return 0 [ -f "$QEMU_END" ] && return 0 error "Timeout while waiting for QEMU to boot the machine, aborting..." @@ -149,12 +228,100 @@ ready() { "$QEMU_PTY" | tail -1) grep -Eq \ - 'BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from HD\(' \ + 'BdsDxe: starting Boot[[:xdigit:]]{4} "Windows Boot Manager" from .*HD\(' \ <<< "$last" && return 0 return 1 } +sendKey() { + + local key="$1" + local delay="${2:-0}" + local hold="${3:-100}" + local output + + [ ! -S "$ACPI_SOCKET" ] && return 1 + [[ "$delay" != "0" ]] && sleep "$delay" + + if ! output=$( + printf 'sendkey %s %s\n' "$key" "$hold" | + nc -q 1 -w 1 -U "$ACPI_SOCKET" 2>&1 + ); then + return 1 + fi + + if grep -Eqi \ + -e 'unknown command' \ + -e 'unknown key' \ + -e 'invalid parameter' \ + -e 'invalid key' \ + -e '^error:' \ + <<< "$output"; then + + warn "failed to send boot key through QEMU monitor!" + + if enabled "${DEBUG:-}"; then + echo "$output" + fi + + return 1 + fi + + return 0 +} + +supportsBootKey() { + + local id="$1" + + [[ "${id,,}" == "win"* ]] +} + +needsBootKey() { + + [ ! -s "$BOOT" ] && return 1 + [[ "${BOOT,,}" != *".iso" ]] && return 1 + [ -f "$STORAGE/windows.boot" ] && return 1 + + supportsBootKey "$DETECTED" +} + +bootKeyDelay() { + + [ ! -s "$QEMU_PTY" ] && return 1 + + if grep -Fq "Press any key to" "$QEMU_PTY"; then + echo 0 + return 0 + fi + + if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then + grep -Fq "Booting from DVD/CD" "$QEMU_PTY" || return 1 + else + grep -Eq \ + 'BdsDxe: starting Boot[[:xdigit:]]{4} "UEFI QEMU .*DVD-ROM' \ + "$QEMU_PTY" || return 1 + fi + + echo 0.5 + return 0 +} + +getBootMarker() { + + if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then + grep -nE '^Booting from (Hard Disk|DVD/CD)' "$QEMU_PTY" | tail -1 + return 0 + fi + + grep -nE \ + 'BdsDxe: starting Boot[[:xdigit:]]{4} ' \ + "$QEMU_PTY" | tail -1 + + return 0 +} + markWindowsBooted() { local file="$STORAGE/windows.boot" @@ -164,7 +331,7 @@ markWindowsBooted() { fi # Remove CD-ROM ISO after install - ! ready && return 0 + ready || return 0 if ! touch "$file"; then warn "failed to create Windows installation marker!" @@ -288,7 +455,7 @@ gracefulShutdown() { finish "$code" } -! enabled "$SHUTDOWN" && return 0 +enabled "$SHUTDOWN" || return 0 [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" if interactive; then diff --git a/src/samba.sh b/src/samba.sh index c1bb5d5b..f5f42b99 100644 --- a/src/samba.sh +++ b/src/samba.sh @@ -142,21 +142,25 @@ addShare() { fi - if [[ "$writable" == "Y" ]] && [[ "$empty" == "Y" ]]; then + if [[ "$writable" == "Y" ]]; then - if ! chmod 2777 "$dir"; then - error "Failed to set permissions for directory $dir" && return 1 - fi + if [[ "$empty" == "Y" ]]; then - if ! owner=$(stat -c %u "$dir"); then - error "Failed to determine ownership for directory $dir" - return 1 - fi - - if [[ "$owner" == "0" ]]; then - if ! chown "1000:1000" "$dir"; then - error "Failed to set ownership for directory $dir" && return 1 + if ! chmod 2777 "$dir"; then + error "Failed to set permissions for directory $dir" && return 1 fi + + if ! owner=$(stat -c %u "$dir"); then + error "Failed to determine ownership for directory $dir" + return 1 + fi + + if [[ "$owner" == "0" ]]; then + if ! chown "1000:1000" "$dir"; then + error "Failed to set ownership for directory $dir" && return 1 + fi + fi + fi elif [[ "$readonly" != "Y" ]]; then