feat: Improve Windows image detection and unattended setup (#2037)

This commit is contained in:
Kroese
2026-08-03 21:25:44 +02:00
committed by GitHub
parent cd8af5af51
commit 919222ef25
7 changed files with 1958 additions and 1391 deletions
+215 -154
View File
@@ -6,6 +6,7 @@ handleCurlError() {
local code="$1"
local server="$2"
local reason="${3:-}"
local signal
if [ -n "$reason" ] && (( code <= 125 )); then
@@ -29,8 +30,7 @@ handleCurlError() {
SEGV | ABRT) error "Curl crashed with signal $signal." ;;
"") error "Curl terminated with exit status $code." ;;
*) error "Curl terminated due to signal $signal." ;;
esac
;;
esac ;;
esac
return 1
@@ -38,10 +38,9 @@ handleCurlError() {
curlRequest() {
local output="$1"
local server="$2"
local agent="$3"
shift 3
local server="$1"
local agent="$2"
shift 2
local log reason response
@@ -50,6 +49,8 @@ curlRequest() {
return 1
fi
# Preserve curl's status under errexit so its stderr can be translated
# into a useful error instead of terminating the script immediately.
{
response=$(LC_ALL=C curl \
--silent \
@@ -76,10 +77,7 @@ curlRequest() {
rm -f "$log"
if [ -n "$output" ]; then
printf -v "$output" '%s' "$response"
fi
printf '%s' "$response"
return 0
}
@@ -93,13 +91,13 @@ downloadWindowsLink() {
local desc="$6"
local type="$7"
local skuId skuJson
local linkJson link
local ovData ovTime session
local ovToken="" ovTicks=""
local profile="606624d44113"
local skuId skuJson linkJson
local link ovData ovTime session
# uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs
# Prefer the Linux kernel UUID source, with uuidgen as a portable fallback
# for macOS and systems without /proc.
if ! session=$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random); then
error "Failed to generate session ID!"
return 1
@@ -112,32 +110,32 @@ downloadWindowsLink() {
return 1
fi
# Microsoft download "protection" requires the sessionId to be whitelisted through vlscppe.microsoft.com/tags
# Register the session with Microsoft's anti-abuse endpoint before
# requesting SKU or download links.
local orgId="y6jn8c31"
local vlsUrl="https://vlscppe.microsoft.com/tags?org_id=$orgId&session_id=$session"
enabled "$DEBUG" && echo "Getting Session ID: $session"
# Permit Session ID
curlRequest "" "Microsoft" "$agent" \
curlRequest "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--max-filesize 100K \
-- "$vlsUrl" || return 1
# Microsoft download "protection" also requires an ov-df.microsoft.com request/reply
# 1) Request mdt.js to get w and rticks. InstanceId is (currently) constant.
# Complete Microsoft's ov-df challenge by retrieving a token and timing
# value, then returning both with the current timestamp.
local instance="560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175"
local ovUrl="https://ov-df.microsoft.com/mdt.js?instanceId=$instance&PageId=si&session_id=$session"
enabled "$DEBUG" && echo -n "Getting OV data: "
curlRequest ovData "Microsoft" "$agent" \
ovData=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$ovUrl" || return 1
-- "$ovUrl") || return 1
if [[ $ovData =~ [\?\&]w=([A-Fa-f0-9]+) ]]; then
ovToken="${BASH_REMATCH[1]}"
@@ -156,14 +154,12 @@ downloadWindowsLink() {
sleep 0.2
# 2) Send a reply with session ID, current epoch and previously retrieved w and rticks
ovTime=$(date +%s%3N)
ovUrl="https://ov-df.microsoft.com/?session_id=$session&CustomerId=$instance&PageId=si&w=$ovToken&mdt=$ovTime&rticks=$ovTicks"
enabled "$DEBUG" && echo "Sending OV reply: $instance"
curlRequest "" "Microsoft" "$agent" \
curlRequest "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--max-filesize 100K \
@@ -173,12 +169,14 @@ downloadWindowsLink() {
local skuUrl="https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=$profile&ProductEditionId=$productId&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest skuJson "Microsoft" "$agent" \
skuJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--max-filesize 100K \
-- "$skuUrl" || return 1
-- "$skuUrl") || return 1
# Guard jq under errexit so malformed API data can be handled as a normal
# missing-result error. The same pattern is reused for the link response.
{ skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null; local rc=$?; } || :
if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]] || (( rc != 0 )); then
@@ -190,19 +188,18 @@ downloadWindowsLink() {
enabled "$DEBUG" && echo "$skuId"
enabled "$DEBUG" && echo "Getting ISO download link..."
# Get ISO download link
# If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed)
# Microsoft normally applies request or IP blocking on this final connector
# call rather than during the preceding session setup.
local linkUrl="https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=$profile&ProductEditionId=undefined&SKU=$skuId&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest linkJson "Microsoft" "$agent" \
linkJson=$(curlRequest "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--max-filesize 100K \
-- "$linkUrl" || return 1
-- "$linkUrl") || return 1
if ! [ "$linkJson" ]; then
# This should only happen if there's been some change to how this API works
error "Microsoft servers gave us an empty response to our request for an automated download."
return 1
fi
@@ -235,9 +232,8 @@ downloadWindows() {
local lang="$2"
local desc="$3"
local agent language
local page productId
local type winVer
local agent language page
local productId type winVer
agent=$(getAgent)
language=$(getLanguage "$lang" "name")
@@ -246,22 +242,18 @@ downloadWindows() {
"win10x64" )
productId="2618"
winVer="10"
type="1"
;;
type="1" ;;
"win11x64" )
productId="3321"
winVer="11"
type="1"
;;
type="1" ;;
"win11arm64" )
productId="3324"
winVer="11arm64"
type="2"
;;
type="2" ;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1
;;
return 1 ;;
esac
local url="https://www.microsoft.com/en-us/software-download/windows$winVer"
@@ -275,14 +267,16 @@ downloadWindows() {
sleep 1
# Product edition IDs can change. If the configured ID fails, recover the
# current value from Microsoft's public download page and retry once.
local msg="retrying using a different method..."
info "Microsoft download request failed, $msg"
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
page=$(curlRequest "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$url" || return 1
-- "$url") || return 1
enabled "$DEBUG" && echo -n "Getting Product edition ID: "
productId=$(printf '%s' "$page" |
@@ -310,6 +304,7 @@ downloadWindowsEval() {
local id="$1"
local lang="$2"
local desc="$3"
local culture compare type
local agent language winVer
@@ -354,13 +349,12 @@ downloadWindowsEval() {
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
page=$(curlRequest "Microsoft" "$agent" \
--location \
--max-filesize 1M \
-- "$url" || return 1
-- "$url") || return 1
if ! [ "$page" ]; then
# This should only happen if there's been some change to where this download page is located
error "Windows server download page gave us an empty response"
return 1
fi
@@ -375,7 +369,8 @@ downloadWindowsEval() {
grep -Eio "https://go\.microsoft\.com/fwlink(/p)?/\?[^\"'<>[:space:]]+" |
grep -Ei '(^|[?&])culture='"${culture,,}"'(&|$)' |
grep -Ei '(^|[?&])country='"${country,,}"'(&|$)') || {
# This should only happen if there's been some change to the download endpoint web address
# Distinguish a changed or missing English page from an unavailable
# translation for an otherwise supported product.
if [[ "${lang,,}" == "en" || "${lang,,}" == "en-"* ]]; then
error "Windows server download page gave us no download link!"
else
@@ -385,6 +380,8 @@ downloadWindowsEval() {
return 1
}
# Evaluation pages currently expose several matching fwlinks in a known
# product/platform order, so select the entry for the requested variant.
case "$type" in
"iot" )
case "${PLATFORM,,}" in
@@ -420,19 +417,21 @@ downloadWindowsEval() {
[ -z "$link" ] && error "Could not parse download link from page!" && return 1
# Follow redirect so proceeding log message is useful
# This is a request we make that Fido doesn't
# Resolve the fwlink now so later logging and platform validation use the
# actual ISO URL rather than Microsoft's generic redirect.
curlRequest link "Microsoft" "$agent" \
link=$(curlRequest "Microsoft" "$agent" \
--location \
--output /dev/null \
--write-out "%{url_effective}" \
--head \
-- "$link" || return 1
-- "$link") || return 1
local lower="${link,,}"
local separator='(^|[[:space:]_./-])'
# Guard against page-order changes resolving to the wrong architecture
# before downloading a multi-gigabyte image.
case "${PLATFORM,,}" in
"x64" )
if [[ "$lower" =~ ${separator}(arm64|a64) ]]; then
@@ -450,6 +449,8 @@ downloadWindowsEval() {
fi ;;
esac
# During debug verification, compare the resolved filename with the static
# catalog entry to expose unexpected changes on Microsoft's page.
if enabled "$DEBUG" && enabled "$VERIFY" && [[ "${lang,,}" == "en"* ]]; then
compare=$(getMido "$id" "$lang" "")
@@ -494,14 +495,11 @@ getMidoDetected() {
# Derive the normal answer-file identity from the requested download route.
case "$default" in
*"-enterprise-ltsc-eval" )
default="${default%-enterprise-ltsc-eval}-ltsc"
;;
default="${default%-enterprise-ltsc-eval}-ltsc" ;;
*"-enterprise-iot-eval" )
default="${default%-enterprise-iot-eval}-iot"
;;
default="${default%-enterprise-iot-eval}-iot" ;;
*"-eval" )
default="${default%-eval}"
;;
default="${default%-eval}" ;;
esac
# Preserve a genuinely different DETECTED override.
@@ -513,17 +511,13 @@ getMidoDetected() {
# Select the answer-file identity for the source that actually succeeded.
case "$source" in
*"-enterprise-ltsc-eval" )
detected="${source%-enterprise-ltsc-eval}-ltsc-eval"
;;
detected="${source%-enterprise-ltsc-eval}-ltsc-eval" ;;
*"-enterprise-iot-eval" )
detected="${source%-enterprise-iot-eval}-iot-eval"
;;
detected="${source%-enterprise-iot-eval}-iot-eval" ;;
*"-eval" )
detected="$source"
;;
detected="$source" ;;
* )
detected="${current:-$default}"
;;
detected="${current:-$default}" ;;
esac
echo "$detected"
@@ -535,21 +529,21 @@ downloadWindowsLtsc() {
local id="$1"
local lang="$2"
local desc="$3"
local alternate alternate_desc
case "${id,,}" in
"win11${PLATFORM,,}-enterprise-iot-eval" )
alternate="win11${PLATFORM,,}-enterprise-ltsc-eval"
;;
alternate="win11${PLATFORM,,}-enterprise-ltsc-eval" ;;
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
alternate="win11${PLATFORM,,}-enterprise-iot-eval"
;;
alternate="win11${PLATFORM,,}-enterprise-iot-eval" ;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1
;;
return 1 ;;
esac
# IoT and LTSC share related evaluation sources and may become unavailable
# independently, so use the sibling edition as a compatibility fallback.
if downloadWindowsEval "$id" "$lang" "$desc" > /dev/null 2>&1; then
MIDO_SOURCE="$id"
return 0
@@ -574,6 +568,7 @@ getWindows() {
local lang="$2"
local desc="$3"
local web_desc="$4"
local language edition
MIDO_SOURCE=""
@@ -586,6 +581,8 @@ getWindows() {
local web_msg="Requesting $web_desc from the Microsoft servers..."
info "$msg" && html "$web_msg"
# These sources are only published in English, so avoid trying download
# routes that cannot satisfy the requested language.
case "${version,,}" in
"win2008r2"* | \
"win81${PLATFORM,,}"* | \
@@ -598,6 +595,8 @@ getWindows() {
fi ;;
esac
# ARM64 downloads exist only for the explicitly supported Windows 11
# routes; all other catalog entries remain x64-only.
case "${version,,}" in
"win10x64" ) ;;
"win11${PLATFORM,,}" ) ;;
@@ -610,6 +609,8 @@ getWindows() {
fi ;;
esac
# Prefer live Microsoft download routes. Unsupported or failed live routes
# fall through to the configured static catalog below.
case "${version,,}" in
"win10x64" | "win11${PLATFORM,,}" )
@@ -621,8 +622,7 @@ getWindows() {
"win11${PLATFORM,,}-enterprise-iot-eval" | \
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0
;;
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0 ;;
"win11${PLATFORM,,}-enterprise"* )
@@ -643,10 +643,11 @@ getWindows() {
* )
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
;;
return 1 ;;
esac
# Static catalog URLs are the last resort after live Microsoft methods are
# unavailable or have failed.
MIDO_URL=$(getMido "$version" "$lang" "")
[ -z "$MIDO_URL" ] && return 1
@@ -665,11 +666,10 @@ getBuild() {
local id="$1"
local ret="$2"
local url=""
local name=""
local build="$3"
local edition=""
local file="catalog.xml"
local url="" name="" edition=""
case "${id,,}" in
"win11${PLATFORM,,}" )
@@ -695,10 +695,9 @@ getCatalog() {
local id="$1"
local ret="$2"
local url=""
local name=""
local edition=""
local file="catalog.cab"
local url="" name="" edition=""
if [[ "${id,,}" == "win11"* ]] && ! isCompatible; then
# ARMv8.0 cannot run Windows 11 builds 24H2 and up.
@@ -728,19 +727,108 @@ getCatalog() {
"url" ) echo "$url" ;;
"file" ) echo "$file" ;;
"name" ) echo "$name" ;;
"edition" ) echo '[Edition="'"${edition}"'"]' ;;
"edition" ) echo "$edition" ;;
*) echo "";;
esac
return 0
}
getXmlTag() {
parseESD() {
local tag="$1"
local file="$2"
local xml="$1"
local version="$2"
local lang="$3"
local desc="$4"
local edition="$5"
local culture="$6"
xmllint --nonet --xpath "//$tag" "$file" 2>/dev/null | sed -E -e "s/<[\/]?$tag>//g" || true
local xmlFile="${xml##*/}"
local file_path file_sum file_size file_edition
local file_culture file_match=0 language_match=0
local records architecture language separator=$'\x1f'
ESD=""
ESD_SUM=""
ESD_SIZE=""
# Microsoft catalogs have used different XML namespaces. Match elements by
# local name and flatten the catalog once so selection needs no temporary XML.
if ! records=$(xmlstarlet sel \
-T -t \
-m "//*[local-name()='File']" \
-v "normalize-space(*[local-name()='Architecture'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Edition'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='LanguageCode'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='FilePath'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Sha1'])" \
-o "$separator" \
-v "normalize-space(*[local-name()='Size'])" \
-n \
"$xml" 2>/dev/null); then
error "Failed to parse $xmlFile!"
return 1
fi
# Track product/platform and language matches separately so failures can
# distinguish an unavailable edition from an unavailable translation.
while IFS="$separator" read -r \
architecture file_edition file_culture \
file_path file_sum file_size; do
[ -n "$architecture$file_path$file_sum$file_size$file_culture$file_edition" ] || continue
[ "${architecture,,}" = "${PLATFORM,,}" ] || continue
if [ -n "$edition" ] &&
[ "${file_edition,,}" != "${edition,,}" ]; then
continue
fi
file_match=1
[ "${file_culture,,}" = "${culture,,}" ] || continue
language_match=1
ESD="$file_path"
ESD_SUM="$file_sum"
ESD_SIZE="$file_size"
break
done <<< "$records"
if (( ! file_match )); then
desc=$(printEdition "$version" "$desc" "Y")
error "No download link available for $desc!"
return 1
fi
if (( ! language_match )); then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
if [ -z "$ESD" ]; then
error "Failed to find ESD URL in $xmlFile!"
return 1
fi
if [ -z "$ESD_SUM" ]; then
error "Failed to find ESD checksum in $xmlFile!"
return 1
fi
if [ -z "$ESD_SIZE" ]; then
error "Failed to find ESD filesize in $xmlFile!"
return 1
fi
return 0
}
@@ -751,12 +839,10 @@ getESD() {
local version="$2"
local lang="$3"
local desc="$4"
local file result culture
local language edition catalog
local file culture log
local edition catalog rc=0
local xmlFile="products.xml"
local esdFile="esd_edition.xml"
local filterFile="products_filter.xml"
local log
file=$(getCatalog "$version" "file")
catalog=$(getCatalog "$version" "url")
@@ -783,10 +869,12 @@ getESD() {
return 1
fi
# Preserve wget's status under errexit so its log can provide the actual
# server or filesystem failure reason.
{
LC_ALL=C wget "$catalog" -O "$dir/$file" --no-verbose --timeout=30 \
--no-http-keep-alive --output-file="$log"
local rc=$?
rc=$?
} || :
if (( rc != 0 )); then
@@ -813,6 +901,8 @@ getESD() {
rm -f "$log"
# Normal catalogs arrive as CAB archives, while pinned build catalogs are
# already XML and only need the common filename.
if [[ "$file" == *".xml" ]]; then
if ! mv -f "$dir/$file" "$dir/$xmlFile"; then
@@ -832,61 +922,13 @@ getESD() {
fi
if [ ! -f "$dir/$xmlFile" ] || [ ! -s "$dir/$xmlFile" ]; then
if [ ! -s "$dir/$xmlFile" ]; then
error "Failed to find $xmlFile in $file!"
return 1
fi
local query='//File[Architecture="'${PLATFORM,,}'"]'"${edition}"''
result=$(xmllint --nonet --xpath "${query}" "$dir/$xmlFile" 2>/dev/null || true)
if [ -z "$result" ]; then
query='//File[Architecture="'${PLATFORM^^}'"]'"${edition}"''
result=$(xmllint --nonet --xpath "${query}" "$dir/$xmlFile" 2>/dev/null || true)
if [ -z "$result" ]; then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download link available for $desc!"
return 1
fi
fi
echo -e '<Catalog>' > "$dir/$filterFile"
echo "$result" >> "$dir/$filterFile"
echo -e '</Catalog>'>> "$dir/$filterFile"
result=$(xmllint --nonet --xpath "//File[LanguageCode=\"${culture,,}\"]" "$dir/$filterFile" 2>/dev/null || true)
if [ -z "$result" ]; then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
echo "$result" > "$dir/$esdFile"
ESD=$(getXmlTag "FilePath" "$dir/$esdFile")
if [ -z "$ESD" ]; then
error "Failed to find ESD URL in $esdFile!"
return 1
fi
ESD_SUM=$(getXmlTag "Sha1" "$dir/$esdFile")
if [ -z "$ESD_SUM" ]; then
error "Failed to find ESD checksum in $esdFile!"
return 1
fi
ESD_SIZE=$(getXmlTag "Size" "$dir/$esdFile")
if [ -z "$ESD_SIZE" ]; then
error "Failed to find ESD filesize in $esdFile!"
if ! parseESD \
"$dir/$xmlFile" "$version" "$lang" "$desc" "$edition" "$culture"; then
return 1
fi
@@ -898,11 +940,12 @@ isCompressed() {
local url="${1%%\?*}"
# The ReactOS latest-build endpoint returns an archive without a filename
# extension, so recognize its path explicitly.
case "${url,,}" in
*.7z | *.zip | *.rar | *.tar | *.cab | *.cpio | \
*.lzh | *.lha | *.xar | */latest-x86-gcc-lin-rel )
return 0
;;
return 0 ;;
esac
return 1
@@ -926,6 +969,9 @@ verifyFile() {
[ -z "$check" ] && return 0
enabled "$VERIFY" || return 0
# Microsoft ESD catalogs publish SHA1, while current mirror metadata normally
# uses SHA256; the digest length identifies which algorithm is required.
[[ "${#check}" == "40" ]] && algo="SHA1"
local msg="Verifying downloaded ISO..."
@@ -975,10 +1021,13 @@ downloadFile() {
local desc="$4"
local web_desc="$5"
local connections="${6:-1}"
local domain dots
local msg="Downloading $web_desc"
local console_msg="Downloading $desc"
local domain dots
# Keep mirror messages concise by reducing subdomains to the final two
# labels, while Microsoft downloads retain the generic description.
domain=$(echo "$url" | awk -F/ '{print $3}')
dots=$(echo "$domain" | tr -cd '.' | wc -c)
(( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
@@ -1007,8 +1056,11 @@ tryDownload() {
local desc="$6"
local seconds="$7"
local web_desc="$8"
local total minimum="104857600"
# Compressed archives can legitimately be much smaller than the ISO they
# contain, so use a lower sanity threshold until extraction.
if isCompressed "$url"; then
minimum="10485760"
fi
@@ -1059,8 +1111,8 @@ fallbackEnglish() {
local lang="$3"
local desc="$4"
local web_desc="$5"
local culture web_msg
local culture web_msg
local msg="No working download method was found for $desc, falling back to English..."
info "$msg"
@@ -1086,12 +1138,11 @@ downloadImage() {
local iso="$1"
local version="$2"
local lang="$3"
local requested="$version"
local tried="n"
local success="n"
local seconds="5"
local detected="$DETECTED"
local url sum size base desc web_desc language i
local detected="$DETECTED"
local tried="n" success="n" seconds="5"
local i url sum size base language desc web_desc
if [[ "${version,,}" == "http"* ]]; then
@@ -1127,6 +1178,8 @@ downloadImage() {
desc+=" in $language"
fi
# Prefer a live Microsoft URL and retry link generation once before moving
# on to ESD catalogs or mirrors.
if isMido "$version" "$lang"; then
tried="y"
@@ -1167,7 +1220,13 @@ downloadImage() {
fi
fi
if switchEdition version; then
# Some editions share another download route. Update the effective version
# before looking up ESD catalogs and mirrors.
if version=$(switchEdition "$version"); then
if ! enabled "${DETECTED_ORG:-}"; then
DETECTED="${SUGGEST:-$version}"
fi
desc=$(printVariant "$DETECTED" "" "Y")
web_desc=$(printVariant "$DETECTED" "")
@@ -1196,6 +1255,8 @@ downloadImage() {
if [[ "$success" == "y" ]]; then
# Standalone ESD media requires a different extraction path, so expose
# its real extension through the active ISO variable.
ISO="${ISO%.*}.esd"
if tryDownload "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$lang" "$desc" "$seconds" "$web_desc"; then