#!/bin/sh echo `# <#` set -eu # The installer runs under any POSIX sh, including when piped to one. Only # function definitions appear before the final line, so a truncated download # defines functions and exits without installing anything. kenn_installer_tab="$(printf '\t')" kenn_installer_cr="$(printf '\r')" kenn_installer_nl=' ' kenn_installer_error() { printf 'kenn installer: %s\n' "$*" >&2 } kenn_installer_progress() { printf 'kenn installer: %s\n' "$*" >&2 } kenn_installer_require_command() { command -v "$1" >/dev/null 2>&1 || { kenn_installer_error "required command not found: $1" exit 1 } } kenn_installer_download() { kenn_installer_url="$1" kenn_installer_output="$2" kenn_installer_timeout="$3" curl --fail --silent --show-error --location \ --proto '=https' --proto-redir '=https' \ --connect-timeout 10 --max-time "$kenn_installer_timeout" \ --retry 2 --retry-delay 1 \ --output "$kenn_installer_output" "$kenn_installer_url" } kenn_installer_is_sha256() { case "$1" in *[!0123456789abcdef]*) return 1 ;; esac [ "${#1}" -eq 64 ] } kenn_installer_is_generation() { case "$1" in '' | 0* | *[!0123456789]*) return 1 ;; esac } kenn_installer_is_version() { case "$1" in v[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]*) ;; *) return 1 ;; esac case "${1#v}" in *[!0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.+-]*) return 1 ;; esac } kenn_installer_read_scalar() { kenn_installer_scalar_name="$1" if ! IFS= read -r kenn_installer_line <&3; then kenn_installer_error "index is missing $kenn_installer_scalar_name" exit 1 fi case "$kenn_installer_line" in "$kenn_installer_scalar_name$kenn_installer_tab"*) kenn_installer_scalar_value="${kenn_installer_line#*"$kenn_installer_tab"}" ;; *) kenn_installer_error "index field $kenn_installer_scalar_name is malformed" exit 1 ;; esac case "$kenn_installer_scalar_value" in *"$kenn_installer_tab"* | '') kenn_installer_error "index field $kenn_installer_scalar_name is malformed" exit 1 ;; esac kenn_installer_count_tabs "$kenn_installer_line" if [ "$kenn_installer_tab_count" -ne 1 ]; then kenn_installer_error "index field $kenn_installer_scalar_name is not canonical" exit 1 fi } kenn_installer_count_tabs() { kenn_installer_tab_text="$1" kenn_installer_tab_count=0 while case "$kenn_installer_tab_text" in *"$kenn_installer_tab"*) true ;; *) false ;; esac; do kenn_installer_tab_text="${kenn_installer_tab_text#*"$kenn_installer_tab"}" kenn_installer_tab_count=$((kenn_installer_tab_count + 1)) done } kenn_installer_take_field() { case "$kenn_installer_row_rest" in *"$kenn_installer_tab"*) kenn_installer_field="${kenn_installer_row_rest%%"$kenn_installer_tab"*}" kenn_installer_row_rest="${kenn_installer_row_rest#*"$kenn_installer_tab"}" ;; *) kenn_installer_field="$kenn_installer_row_rest" kenn_installer_row_rest='' ;; esac } kenn_installer_split_row() { kenn_installer_row_rest="$1" kenn_installer_take_field kenn_installer_record="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_os="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_arch="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_format="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_signature="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_sha256="$kenn_installer_field" kenn_installer_take_field kenn_installer_row_location="$kenn_installer_field" kenn_installer_row_extra="$kenn_installer_row_rest" } kenn_installer_sha256() { kenn_installer_hash_output="$(openssl dgst -sha256 -r "$1")" kenn_installer_hash="${kenn_installer_hash_output%% *}" } kenn_installer_shell_quote() { case "$1" in *[!0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_./-]*) ;; *) kenn_installer_quoted="$1" return 0 ;; esac kenn_installer_quote_rest="$1" kenn_installer_quoted='' while case "$kenn_installer_quote_rest" in *"'"*) true ;; *) false ;; esac; do kenn_installer_quoted="$kenn_installer_quoted${kenn_installer_quote_rest%%"'"*}'\\''" kenn_installer_quote_rest="${kenn_installer_quote_rest#*"'"}" done kenn_installer_quoted="'$kenn_installer_quoted$kenn_installer_quote_rest'" } kenn_installer_validate_ready_identity() { kenn_installer_ready_path="$1" kenn_installer_ready_build_id="$2" kenn_installer_ready_schema_count=0 kenn_installer_ready_build_count=0 while IFS= read -r kenn_installer_ready_line; do if [ "$kenn_installer_ready_line" = ' "schema_version": "1"' ]; then kenn_installer_ready_schema_count=$((kenn_installer_ready_schema_count + 1)) fi if [ "$kenn_installer_ready_line" = " \"build_id\": \"$kenn_installer_ready_build_id\"," ]; then kenn_installer_ready_build_count=$((kenn_installer_ready_build_count + 1)) fi done < "$kenn_installer_ready_path" if [ "$kenn_installer_ready_schema_count" -ne 1 ] || [ "$kenn_installer_ready_build_count" -ne 1 ]; then kenn_installer_error 'private staging readiness marker identity is invalid' exit 1 fi } kenn_installer_require_ready_object() { kenn_installer_ready_path="$1" kenn_installer_ready_object="$2" kenn_installer_ready_sha256="$3" kenn_installer_ready_name_line=" \"name\": \"$kenn_installer_ready_object\"," kenn_installer_ready_sha_line=" \"sha256\": \"$kenn_installer_ready_sha256\"" kenn_installer_ready_match_count=0 kenn_installer_ready_expect_sha=false while IFS= read -r kenn_installer_ready_line; do if $kenn_installer_ready_expect_sha; then if [ "$kenn_installer_ready_line" != "$kenn_installer_ready_sha_line" ]; then kenn_installer_error "private staging readiness inventory disagrees for $kenn_installer_ready_object" exit 1 fi kenn_installer_ready_match_count=$((kenn_installer_ready_match_count + 1)) kenn_installer_ready_expect_sha=false elif [ "$kenn_installer_ready_line" = "$kenn_installer_ready_name_line" ]; then kenn_installer_ready_expect_sha=true fi done < "$kenn_installer_ready_path" if $kenn_installer_ready_expect_sha || [ "$kenn_installer_ready_match_count" -ne 1 ]; then kenn_installer_error "private staging readiness inventory is missing $kenn_installer_ready_object" exit 1 fi } # shellcheck disable=SC2329 # Invoked by the EXIT trap set in kenn_installer_main. kenn_installer_cleanup() { if [ -n "$kenn_installer_destination_tmp" ]; then rm -f -- "$kenn_installer_destination_tmp" fi rm -rf -- "$kenn_installer_tmp" } kenn_installer_main() { kenn_installer_channel="${KENN_CHANNEL:-stable}" case "$kenn_installer_channel" in stable) kenn_installer_index_origin='https://dl.kenn.sh' ;; unstable) kenn_installer_index_origin='https://dl.kenn.sh' ;; staging) kenn_installer_staging_uri="${KENN_STAGING_URI:-}" case "$kenn_installer_staging_uri" in gs://*) ;; *) kenn_installer_error 'KENN_STAGING_URI must identify a gs:// candidate' exit 2 ;; esac kenn_installer_staging_location="${kenn_installer_staging_uri#gs://}" kenn_installer_staging_bucket="${kenn_installer_staging_location%%/*}" kenn_installer_staging_object="${kenn_installer_staging_location#*/}" case "$kenn_installer_staging_bucket" in '' | *[!a-z0-9._-]*) kenn_installer_error 'KENN_STAGING_URI must identify a gs:// candidate' exit 2 ;; esac if [ "$kenn_installer_staging_location" = "$kenn_installer_staging_object" ] || [ -z "$kenn_installer_staging_object" ]; then kenn_installer_error 'KENN_STAGING_URI must identify a gs:// candidate' exit 2 fi case "$kenn_installer_staging_object" in /* | */ | *//* | . | .. | ./* | */./* | */. | ../* | */../* | */.. | *"$kenn_installer_tab"* | *"$kenn_installer_cr"* | *"$kenn_installer_nl"* | *' '* | *"\\\\"*) kenn_installer_error 'KENN_STAGING_URI must identify a canonical gs:// candidate' exit 2 ;; esac ;; *) kenn_installer_error "unsupported channel $kenn_installer_channel" exit 2 ;; esac kenn_installer_require_command openssl kenn_installer_require_command tar kenn_installer_require_command mktemp if [ "$kenn_installer_channel" = staging ]; then kenn_installer_require_command gsutil else kenn_installer_require_command curl fi case "$(uname -s)" in Darwin) kenn_installer_os=darwin ;; Linux) kenn_installer_os=linux ;; *) kenn_installer_error "unsupported operating system $(uname -s)" exit 1 ;; esac case "$(uname -m)" in x86_64 | amd64) kenn_installer_arch=amd64 ;; arm64 | aarch64) kenn_installer_arch=arm64 ;; *) kenn_installer_error "unsupported architecture $(uname -m)" exit 1 ;; esac kenn_installer_progress "channel=$kenn_installer_channel platform=$kenn_installer_os/$kenn_installer_arch" kenn_installer_install_dir="${KENN_INSTALL_DIR:-${HOME:?HOME is not set}/.local/bin}" case "$kenn_installer_install_dir" in /*) ;; *) kenn_installer_error 'KENN_INSTALL_DIR must be an absolute path' exit 1 ;; esac kenn_installer_tmp="$(mktemp -d "${TMPDIR:-/tmp}/kenn-installer.XXXXXX")" kenn_installer_destination_tmp='' trap kenn_installer_cleanup EXIT trap 'exit 130' HUP INT TERM kenn_installer_index="$kenn_installer_tmp/index" kenn_installer_archive="$kenn_installer_tmp/archive" kenn_installer_candidate="$kenn_installer_tmp/kenn" kenn_installer_ready="$kenn_installer_tmp/READY.json" if [ "$kenn_installer_channel" = staging ]; then kenn_installer_progress "fetching private staging metadata from $kenn_installer_staging_uri" gsutil cp "$kenn_installer_staging_uri/READY.json" "$kenn_installer_ready" gsutil cp "$kenn_installer_staging_uri/install-index-v1" "$kenn_installer_index" exec 3< "$kenn_installer_index" if ! IFS= read -r kenn_installer_line <&3 || [ "$kenn_installer_line" != 'kenn-staging-installer-index-v1' ]; then kenn_installer_error 'unsupported private staging installer index schema' exit 1 fi kenn_installer_read_scalar build_id kenn_installer_build_id="$kenn_installer_scalar_value" kenn_installer_read_scalar version kenn_installer_version="$kenn_installer_scalar_value" if ! kenn_installer_is_sha256 "$kenn_installer_build_id" || ! kenn_installer_is_version "$kenn_installer_version" || [ "${kenn_installer_staging_uri##*/}" != "$kenn_installer_build_id" ]; then kenn_installer_error 'private staging candidate identity is invalid' exit 1 fi kenn_installer_selected_object='' kenn_installer_selected_sha256='' kenn_installer_artifact_number=0 while [ "$kenn_installer_artifact_number" -lt 6 ]; do if ! IFS= read -r kenn_installer_line <&3; then kenn_installer_error 'private staging index is missing a platform artifact' exit 1 fi kenn_installer_split_row "$kenn_installer_line" kenn_installer_row_object="$kenn_installer_row_location" kenn_installer_count_tabs "$kenn_installer_line" if [ "$kenn_installer_tab_count" -ne 6 ] || [ "$kenn_installer_record" != artifact ] || [ -n "$kenn_installer_row_extra" ]; then kenn_installer_error 'private staging index artifact row is malformed' exit 1 fi case "$kenn_installer_artifact_number" in 0) kenn_installer_expected_identity='darwin amd64 tar.gz' ;; 1) kenn_installer_expected_identity='darwin arm64 tar.gz' ;; 2) kenn_installer_expected_identity='linux amd64 tar.gz' ;; 3) kenn_installer_expected_identity='linux arm64 tar.gz' ;; 4) kenn_installer_expected_identity='windows amd64 zip' ;; 5) kenn_installer_expected_identity='windows arm64 zip' ;; esac if [ "$kenn_installer_row_os $kenn_installer_row_arch $kenn_installer_row_format" != "$kenn_installer_expected_identity" ] || [ "$kenn_installer_row_signature" != none ] || ! kenn_installer_is_sha256 "$kenn_installer_row_sha256"; then kenn_installer_error 'private staging index platform matrix is invalid' exit 1 fi case "$kenn_installer_row_object" in units/*) ;; *) kenn_installer_error 'private staging index object name is invalid'; exit 1 ;; esac case "$kenn_installer_row_object" in */ | *//* | */./* | */. | ../* | */../* | */.. | *"\\\\"*) kenn_installer_error 'private staging index object name is invalid' exit 1 ;; esac if [ "$kenn_installer_row_os" = "$kenn_installer_os" ] && [ "$kenn_installer_row_arch" = "$kenn_installer_arch" ]; then kenn_installer_selected_object="$kenn_installer_row_object" kenn_installer_selected_sha256="$kenn_installer_row_sha256" fi kenn_installer_artifact_number=$((kenn_installer_artifact_number + 1)) done kenn_installer_extra_line='' if IFS= read -r kenn_installer_extra_line <&3 || [ -n "$kenn_installer_extra_line" ]; then kenn_installer_error 'private staging index contains extra data' exit 1 fi exec 3<&- if [ -z "$kenn_installer_selected_object" ]; then kenn_installer_error 'private staging index has no artifact for this platform' exit 1 fi kenn_installer_sha256 "$kenn_installer_index" kenn_installer_validate_ready_identity "$kenn_installer_ready" "$kenn_installer_build_id" kenn_installer_require_ready_object "$kenn_installer_ready" install-index-v1 "$kenn_installer_hash" kenn_installer_require_ready_object "$kenn_installer_ready" "$kenn_installer_selected_object" "$kenn_installer_selected_sha256" else kenn_installer_channel_base="$kenn_installer_index_origin/channels/$kenn_installer_channel" kenn_installer_progress "fetching $kenn_installer_channel metadata" kenn_installer_download "$kenn_installer_channel_base/install-index-v1" "$kenn_installer_index" 60 exec 3< "$kenn_installer_index" if ! IFS= read -r kenn_installer_line <&3 || [ "$kenn_installer_line" != 'kenn-installer-index-v1' ]; then kenn_installer_error 'unsupported installer index schema' exit 1 fi kenn_installer_read_scalar channel kenn_installer_index_channel="$kenn_installer_scalar_value" kenn_installer_read_scalar generation kenn_installer_generation="$kenn_installer_scalar_value" kenn_installer_read_scalar version kenn_installer_version="$kenn_installer_scalar_value" kenn_installer_read_scalar manifest_url kenn_installer_manifest_url="$kenn_installer_scalar_value" kenn_installer_read_scalar manifest_sha256 kenn_installer_manifest_sha256="$kenn_installer_scalar_value" kenn_installer_read_scalar key_id if [ "$kenn_installer_index_channel" != "$kenn_installer_channel" ] || ! kenn_installer_is_generation "$kenn_installer_generation" || ! kenn_installer_is_version "$kenn_installer_version" || ! kenn_installer_is_sha256 "$kenn_installer_manifest_sha256"; then kenn_installer_error 'installer index identity is invalid' exit 1 fi kenn_installer_expected_manifest_url="$kenn_installer_index_origin/channels/by-generation/$kenn_installer_channel/$kenn_installer_generation/manifest.json" if [ "$kenn_installer_manifest_url" != "$kenn_installer_expected_manifest_url" ]; then kenn_installer_error 'installer index manifest URL is not canonical' exit 1 fi kenn_installer_selected_url='' kenn_installer_selected_sha256='' kenn_installer_selected_signature='' kenn_installer_windows_signature='' kenn_installer_artifact_number=0 while [ "$kenn_installer_artifact_number" -lt 6 ]; do if ! IFS= read -r kenn_installer_line <&3; then kenn_installer_error 'installer index is missing a platform artifact' exit 1 fi kenn_installer_split_row "$kenn_installer_line" kenn_installer_row_url="$kenn_installer_row_location" kenn_installer_count_tabs "$kenn_installer_line" if [ "$kenn_installer_tab_count" -ne 6 ] || [ "$kenn_installer_record" != artifact ] || [ -n "$kenn_installer_row_extra" ]; then kenn_installer_error 'installer index artifact row is malformed' exit 1 fi case "$kenn_installer_artifact_number" in 0) kenn_installer_expected_identity='darwin amd64 tar.gz' ;; 1) kenn_installer_expected_identity='darwin arm64 tar.gz' ;; 2) kenn_installer_expected_identity='linux amd64 tar.gz' ;; 3) kenn_installer_expected_identity='linux arm64 tar.gz' ;; 4) kenn_installer_expected_identity='windows amd64 zip' ;; 5) kenn_installer_expected_identity='windows arm64 zip' ;; esac if [ "$kenn_installer_row_os $kenn_installer_row_arch $kenn_installer_row_format" != "$kenn_installer_expected_identity" ] || ! kenn_installer_is_sha256 "$kenn_installer_row_sha256"; then kenn_installer_error 'installer index platform matrix is invalid' exit 1 fi if [ "$kenn_installer_row_os" = windows ]; then case "$kenn_installer_row_signature" in none | authenticode) ;; *) kenn_installer_error 'installer index application signature is invalid' exit 1 esac if [ -z "$kenn_installer_windows_signature" ]; then kenn_installer_windows_signature="$kenn_installer_row_signature" elif [ "$kenn_installer_windows_signature" != "$kenn_installer_row_signature" ]; then kenn_installer_error 'installer index Windows signature policy is mixed' exit 1 fi elif [ "$kenn_installer_row_signature" != none ]; then kenn_installer_error 'POSIX artifact has an unsupported application signature' exit 1 fi kenn_installer_version_bare="${kenn_installer_version#v}" kenn_installer_expected_url="$kenn_installer_index_origin/objects/sha256/$kenn_installer_row_sha256/kenn_${kenn_installer_version_bare}_${kenn_installer_row_os}_${kenn_installer_row_arch}.${kenn_installer_row_format}" if [ "$kenn_installer_row_url" != "$kenn_installer_expected_url" ]; then kenn_installer_error 'installer index artifact URL is not canonical' exit 1 fi if [ "$kenn_installer_row_os" = "$kenn_installer_os" ] && [ "$kenn_installer_row_arch" = "$kenn_installer_arch" ]; then kenn_installer_selected_url="$kenn_installer_row_url" kenn_installer_selected_sha256="$kenn_installer_row_sha256" kenn_installer_selected_signature="$kenn_installer_row_signature" fi kenn_installer_artifact_number=$((kenn_installer_artifact_number + 1)) done kenn_installer_extra_line='' if IFS= read -r kenn_installer_extra_line <&3 || [ -n "$kenn_installer_extra_line" ]; then kenn_installer_error 'installer index contains extra data' exit 1 fi exec 3<&- if [ -z "$kenn_installer_selected_url" ] || [ "$kenn_installer_selected_signature" != none ]; then kenn_installer_error 'installer index does not contain an installable artifact for this platform' exit 1 fi fi kenn_installer_progress "downloading Kenn $kenn_installer_version for $kenn_installer_os/$kenn_installer_arch" if [ "$kenn_installer_channel" = staging ]; then gsutil cp "$kenn_installer_staging_uri/$kenn_installer_selected_object" "$kenn_installer_archive" else kenn_installer_download "$kenn_installer_selected_url" "$kenn_installer_archive" 600 fi kenn_installer_sha256 "$kenn_installer_archive" if [ "$kenn_installer_hash" != "$kenn_installer_selected_sha256" ]; then kenn_installer_error 'archive SHA-256 verification failed' exit 1 fi kenn_installer_progress 'verified archive SHA-256' kenn_installer_members="$(tar -tzf "$kenn_installer_archive")" kenn_installer_verbose_member="$(tar -tvzf "$kenn_installer_archive")" if [ "$kenn_installer_members" != kenn ] || [ "${kenn_installer_verbose_member#-}" = "$kenn_installer_verbose_member" ]; then kenn_installer_error 'archive must contain exactly one root kenn executable' exit 1 fi tar -xOzf "$kenn_installer_archive" kenn > "$kenn_installer_candidate" chmod 0755 "$kenn_installer_candidate" kenn_installer_version_output="$kenn_installer_tmp/version.out" kenn_installer_version_error="$kenn_installer_tmp/version.err" "$kenn_installer_candidate" version --json > "$kenn_installer_version_output" 2> "$kenn_installer_version_error" & kenn_installer_version_pid=$! kenn_installer_version_waited=0 while kill -0 "$kenn_installer_version_pid" 2>/dev/null; do if [ "$kenn_installer_version_waited" -ge 5 ]; then kill "$kenn_installer_version_pid" 2>/dev/null || true sleep 1 kill -9 "$kenn_installer_version_pid" 2>/dev/null || true kenn_installer_error 'staged kenn version probe timed out' exit 1 fi sleep 1 kenn_installer_version_waited=$((kenn_installer_version_waited + 1)) done if ! wait "$kenn_installer_version_pid"; then kenn_installer_error 'staged kenn version probe failed' exit 1 fi kenn_installer_actual_version="$(cat -- "$kenn_installer_version_output")" kenn_installer_expected_version="{\"name\":\"kenn\",\"version\":\"$kenn_installer_version\"}" if [ "$kenn_installer_actual_version" != "$kenn_installer_expected_version" ]; then kenn_installer_error 'archive binary version does not match the installer index' exit 1 fi kenn_installer_progress "installing Kenn in $kenn_installer_install_dir" mkdir -p -- "$kenn_installer_install_dir" kenn_installer_destination="$kenn_installer_install_dir/kenn" if [ -d "$kenn_installer_destination" ]; then kenn_installer_error 'destination kenn path is a directory' exit 1 fi kenn_installer_destination_tmp="$(mktemp "$kenn_installer_install_dir/.kenn.XXXXXX")" cp -- "$kenn_installer_candidate" "$kenn_installer_destination_tmp" chmod 0755 "$kenn_installer_destination_tmp" mv -f -- "$kenn_installer_destination_tmp" "$kenn_installer_destination" kenn_installer_destination_tmp='' case ":${PATH:-}:" in *":$kenn_installer_install_dir:"*) ;; *) case "${SHELL:-}" in */zsh) kenn_installer_profile="$HOME/.zshrc" ;; */bash) kenn_installer_profile="$HOME/.bashrc" ;; *) kenn_installer_profile="$HOME/.profile" ;; esac kenn_installer_shell_quote "$kenn_installer_install_dir" printf "Add Kenn to PATH in %s:\n export PATH=%s:\$PATH\n" "$kenn_installer_profile" "$kenn_installer_quoted" ;; esac printf '%s\n' 'Kenn installed. Run kenn init to get started.' } kenn_installer_main; exit 0 #> > $null & { $ErrorActionPreference = 'Stop' $kennInstallerChannel = if ($env:KENN_CHANNEL) { $env:KENN_CHANNEL } else { 'stable' } switch -CaseSensitive ($kennInstallerChannel) { 'stable' { $kennInstallerIndexOrigin = 'https://dl.kenn.sh' $kennInstallerWindowsSignaturePolicy = 'authenticode' } 'unstable' { $kennInstallerIndexOrigin = 'https://dl.kenn.sh' $kennInstallerWindowsSignaturePolicy = 'none' } 'staging' { $kennInstallerStagingUri = $env:KENN_STAGING_URI if (-not $kennInstallerStagingUri -or $kennInstallerStagingUri -cnotmatch '^gs://([a-z0-9._-]+)/([^\s\\]+)$') { throw 'kenn installer: KENN_STAGING_URI must identify a gs:// candidate' } $stagingObject = $Matches[2] $unsafeParts = @($stagingObject.Split('/') | Where-Object { -not $_ -or $_ -ceq '.' -or $_ -ceq '..' }) if ($stagingObject.EndsWith('/') -or $stagingObject.Contains('//') -or $unsafeParts.Count -ne 0) { throw 'kenn installer: KENN_STAGING_URI must identify a canonical gs:// candidate' } } default { throw "kenn installer: unsupported channel $kennInstallerChannel" } } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'kenn installer: unsupported operating system for PowerShell installation' } function Get-KennInstallerLines([string] $Path) { [byte[]] $bytes = [IO.File]::ReadAllBytes($Path) if ($bytes.Length -eq 0 -or $bytes[$bytes.Length - 1] -ne 10) { throw 'kenn installer: metadata must end with LF' } foreach ($value in $bytes) { if ($value -eq 13 -or $value -gt 127) { throw 'kenn installer: metadata must be canonical ASCII with LF endings' } } $text = [Text.Encoding]::ASCII.GetString($bytes, 0, $bytes.Length - 1) return $text.Split([char] 10) } function Save-KennInstallerDownload([string] $Url, [string] $Path, [int] $Timeout) { $lastError = $null for ($attempt = 1; $attempt -le 3; $attempt++) { try { $request = [Net.HttpWebRequest]::CreateHttp($Url) $request.AllowAutoRedirect = $true $request.MaximumAutomaticRedirections = 3 $request.Timeout = 10000 $request.ReadWriteTimeout = $Timeout $response = $request.GetResponse() try { $inputStream = $response.GetResponseStream() $outputStream = [IO.File]::Create($Path) try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose() $inputStream.Dispose() } } finally { $response.Dispose() } return } catch { $lastError = $_.Exception if (Test-Path -LiteralPath $Path) { Remove-Item -Force -LiteralPath $Path } } } throw "kenn installer: download failed for $Url`: $($lastError.Message)" } function Save-KennInstallerGsutil([string] $Source, [string] $Path) { if (-not (Get-Command gsutil -ErrorAction SilentlyContinue)) { throw 'kenn installer: required command not found: gsutil' } $output = @(& gsutil cp $Source $Path 2>&1) if ($LASTEXITCODE -ne 0) { throw "kenn installer: gsutil failed for $Source`: $($output -join '; ')" } } function Get-KennInstallerSha256([string] $Path) { $sha = [Security.Cryptography.SHA256]::Create() try { return ([BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($Path)))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } } function Assert-KennInstallerReadyObject( [object] $Ready, [string] $Name, [string] $Sha256 ) { $matches = @($Ready.objects | Where-Object { $_.name -ceq $Name }) if ($matches.Count -ne 1 -or $matches[0].sha256 -cne $Sha256) { throw "kenn installer: private staging readiness inventory disagrees for $Name" } } $architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() switch -CaseSensitive ($architecture) { 'X64' { $kennInstallerArch = 'amd64' } 'Arm64' { $kennInstallerArch = 'arm64' } default { throw "kenn installer: unsupported Windows architecture $architecture" } } [Console]::Error.WriteLine("kenn installer: channel=$kennInstallerChannel platform=windows/$kennInstallerArch") $kennInstallerInstallDir = if ($env:KENN_INSTALL_DIR) { $env:KENN_INSTALL_DIR } else { Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Programs\Kenn\bin' } if (-not [IO.Path]::IsPathRooted($kennInstallerInstallDir)) { throw 'kenn installer: KENN_INSTALL_DIR must be an absolute path' } $kennInstallerTemp = Join-Path ([IO.Path]::GetTempPath()) ("kenn-installer-" + [Guid]::NewGuid()) $kennInstallerDestinationTemp = $null $kennInstallerDestinationBackup = $null [IO.Directory]::CreateDirectory($kennInstallerTemp) | Out-Null try { $indexPath = Join-Path $kennInstallerTemp 'index' $readyPath = Join-Path $kennInstallerTemp 'READY.json' $archivePath = Join-Path $kennInstallerTemp 'archive.zip' $candidatePath = Join-Path $kennInstallerTemp 'kenn.exe' if ($kennInstallerChannel -ceq 'staging') { [Console]::Error.WriteLine("kenn installer: fetching private staging metadata from $kennInstallerStagingUri") Save-KennInstallerGsutil "$kennInstallerStagingUri/READY.json" $readyPath Save-KennInstallerGsutil "$kennInstallerStagingUri/install-index-v1" $indexPath try { $ready = Get-Content -Raw -LiteralPath $readyPath | ConvertFrom-Json } catch { throw 'kenn installer: private staging readiness marker is malformed' } $lines = @(Get-KennInstallerLines $indexPath) if ($lines.Count -ne 9 -or $lines[0] -cne 'kenn-staging-installer-index-v1') { throw 'kenn installer: private staging index has the wrong schema or line count' } $scalars = @{} foreach ($scalar in @(@('build_id', 1), @('version', 2))) { $fields = @($lines[$scalar[1]].Split([char] 9)) if ($fields.Count -ne 2 -or $fields[0] -cne $scalar[0] -or -not $fields[1]) { throw "kenn installer: private staging index field $($scalar[0]) is malformed" } $scalars[$scalar[0]] = $fields[1] } $candidateBuildId = $kennInstallerStagingUri.Substring($kennInstallerStagingUri.LastIndexOf('/') + 1) if ($scalars.build_id -cnotmatch '^[0-9a-f]{64}$' -or $scalars.version -cnotmatch '^v[0-9A-Za-z][0-9A-Za-z.+-]*$' -or $candidateBuildId -cne $scalars.build_id) { throw 'kenn installer: private staging candidate identity is invalid' } if ($ready.schema_version -cne '1' -or $ready.build_id -cne $scalars.build_id) { throw 'kenn installer: private staging readiness marker identity is invalid' } Assert-KennInstallerReadyObject $ready 'install-index-v1' (Get-KennInstallerSha256 $indexPath) $expectedPlatforms = @( 'darwin amd64 tar.gz', 'darwin arm64 tar.gz', 'linux amd64 tar.gz', 'linux arm64 tar.gz', 'windows amd64 zip', 'windows arm64 zip' ) $selected = $null for ($row = 0; $row -lt 6; $row++) { $fields = @($lines[$row + 3].Split([char] 9)) if ($fields.Count -ne 7 -or $fields[0] -cne 'artifact') { throw 'kenn installer: private staging index artifact row is malformed' } $identity = "$($fields[1]) $($fields[2]) $($fields[3])" if ($identity -cne $expectedPlatforms[$row] -or $fields[4] -cne 'none' -or $fields[5] -cnotmatch '^[0-9a-f]{64}$') { throw 'kenn installer: private staging index platform matrix is invalid' } $objectParts = @($fields[6].Split('/')) $unsafeObjectParts = @($objectParts | Where-Object { -not $_ -or $_ -ceq '.' -or $_ -ceq '..' }) if (-not $fields[6].StartsWith('units/') -or $fields[6].Contains('\') -or $unsafeObjectParts.Count -ne 0) { throw 'kenn installer: private staging index object name is invalid' } if ($fields[1] -ceq 'windows' -and $fields[2] -ceq $kennInstallerArch) { $selected = $fields } } if ($null -eq $selected) { throw 'kenn installer: private staging index has no artifact for this platform' } Assert-KennInstallerReadyObject $ready $selected[6] $selected[5] $selectedSignaturePolicy = 'none' } else { $channelBase = "$kennInstallerIndexOrigin/channels/$kennInstallerChannel" [Console]::Error.WriteLine("kenn installer: fetching $kennInstallerChannel metadata") Save-KennInstallerDownload "$channelBase/install-index-v1" $indexPath 60000 $lines = @(Get-KennInstallerLines $indexPath) if ($lines.Count -ne 13 -or $lines[0] -cne 'kenn-installer-index-v1') { throw 'kenn installer: installer index has the wrong schema or line count' } $scalarNames = @('channel', 'generation', 'version', 'manifest_url', 'manifest_sha256', 'key_id') $scalars = @{} for ($lineNumber = 1; $lineNumber -le 6; $lineNumber++) { $fields = @($lines[$lineNumber].Split([char] 9)) $name = $scalarNames[$lineNumber - 1] if ($fields.Count -ne 2 -or $fields[0] -cne $name -or -not $fields[1]) { throw "kenn installer: index field $name is malformed" } $scalars[$name] = $fields[1] } if ($scalars.channel -cne $kennInstallerChannel -or $scalars.generation -cnotmatch '^[1-9][0-9]*$' -or $scalars.version -cnotmatch '^v[0-9A-Za-z][0-9A-Za-z.+-]*$' -or $scalars.manifest_sha256 -cnotmatch '^[0-9a-f]{64}$') { throw 'kenn installer: installer index identity is invalid' } $expectedManifest = "$kennInstallerIndexOrigin/channels/by-generation/$kennInstallerChannel/$($scalars.generation)/manifest.json" if ($scalars.manifest_url -cne $expectedManifest) { throw 'kenn installer: installer index manifest URL is not canonical' } $expectedPlatforms = @( 'darwin amd64 tar.gz', 'darwin arm64 tar.gz', 'linux amd64 tar.gz', 'linux arm64 tar.gz', 'windows amd64 zip', 'windows arm64 zip' ) $selected = $null $windowsSignature = $null for ($row = 0; $row -lt 6; $row++) { $fields = @($lines[$row + 7].Split([char] 9)) if ($fields.Count -ne 7 -or $fields[0] -cne 'artifact') { throw 'kenn installer: installer index artifact row is malformed' } $identity = "$($fields[1]) $($fields[2]) $($fields[3])" if ($identity -cne $expectedPlatforms[$row] -or $fields[5] -cnotmatch '^[0-9a-f]{64}$') { throw 'kenn installer: installer index platform matrix is invalid' } if ($fields[1] -ceq 'windows') { if ($fields[4] -cne 'none' -and $fields[4] -cne 'authenticode') { throw 'kenn installer: Windows signature state is invalid' } if ($null -eq $windowsSignature) { $windowsSignature = $fields[4] } elseif ($windowsSignature -cne $fields[4]) { throw 'kenn installer: Windows signature policy is mixed' } } elseif ($fields[4] -cne 'none') { throw 'kenn installer: POSIX signature state is invalid' } $bareVersion = $scalars.version.Substring(1) $expectedUrl = "$kennInstallerIndexOrigin/objects/sha256/$($fields[5])/kenn_${bareVersion}_$($fields[1])_$($fields[2]).$($fields[3])" if ($fields[6] -cne $expectedUrl) { throw 'kenn installer: artifact URL is not canonical' } if ($fields[1] -ceq 'windows' -and $fields[2] -ceq $kennInstallerArch) { $selected = $fields } } if ($null -eq $selected) { throw 'kenn installer: index has no artifact for this platform' } if ($selected[4] -cne $kennInstallerWindowsSignaturePolicy) { throw 'kenn installer: Windows application-signature policy does not match the index' } $selectedSignaturePolicy = $kennInstallerWindowsSignaturePolicy } [Console]::Error.WriteLine("kenn installer: downloading Kenn $($scalars.version) for windows/$kennInstallerArch") if ($kennInstallerChannel -ceq 'staging') { Save-KennInstallerGsutil "$kennInstallerStagingUri/$($selected[6])" $archivePath } else { Save-KennInstallerDownload $selected[6] $archivePath 600000 } $actualDigest = Get-KennInstallerSha256 $archivePath if ($actualDigest -cne $selected[5]) { throw 'kenn installer: archive SHA-256 verification failed' } [Console]::Error.WriteLine('kenn installer: verified archive SHA-256') Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($archivePath) try { if ($zip.Entries.Count -ne 1 -or $zip.Entries[0].FullName -cne 'kenn.exe' -or $zip.Entries[0].Name -cne 'kenn.exe') { throw 'kenn installer: archive must contain exactly one root kenn.exe' } $inputStream = $zip.Entries[0].Open() $outputStream = [IO.File]::Create($candidatePath) try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose() $inputStream.Dispose() } } finally { $zip.Dispose() } if ($selectedSignaturePolicy -ceq 'authenticode') { $authenticode = Get-AuthenticodeSignature -LiteralPath $candidatePath if ($authenticode.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { throw "kenn installer: Authenticode verification failed: $($authenticode.Status)" } } else { [Console]::Error.WriteLine('kenn installer: warning: archive integrity is verified, but kenn.exe is not Authenticode-signed') } if ($null -eq ('KennInstallerProbeV1' -as [type])) { Add-Type -TypeDefinition @' using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; using System.Text; using System.Threading; public sealed class KennInstallerProbeResult { public int ExitCode { get; private set; } public string Output { get; private set; } public KennInstallerProbeResult(int exitCode, string output) { ExitCode = exitCode; Output = output; } } public static class KennInstallerProbeV1 { private const uint JobObjectExtendedLimitInformation = 9; private const uint JobObjectLimitKillOnJobClose = 0x00002000; private const uint StartfUseStdHandles = 0x00000100; private const uint CreateSuspended = 0x00000004; private const uint CreateNoWindow = 0x08000000; private const uint ExtendedStartupInfoPresent = 0x00080000; private const uint HandleFlagInherit = 0x00000001; private const uint WaitObject0 = 0; private const uint WaitTimeout = 258; private const uint GenericRead = 0x80000000; private const uint FileShareRead = 0x00000001; private const uint FileShareWrite = 0x00000002; private const uint OpenExisting = 3; private const int MaxOutputCharacters = 65536; private static readonly IntPtr ProcThreadAttributeHandleList = new IntPtr(0x00020002); [StructLayout(LayoutKind.Sequential)] private struct JobObjectBasicLimitInformation { public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public uint ActiveProcessLimit; public UIntPtr Affinity; public uint PriorityClass; public uint SchedulingClass; } [StructLayout(LayoutKind.Sequential)] private struct IoCounters { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] private struct JobObjectExtendedLimitInformationValue { public JobObjectBasicLimitInformation BasicLimitInformation; public IoCounters IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } [StructLayout(LayoutKind.Sequential)] private struct SecurityAttributes { public int Length; public IntPtr SecurityDescriptor; [MarshalAs(UnmanagedType.Bool)] public bool InheritHandle; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct StartupInfo { public int Size; public string Reserved; public string Desktop; public string Title; public uint X; public uint Y; public uint XSize; public uint YSize; public uint XCountChars; public uint YCountChars; public uint FillAttribute; public uint Flags; public short ShowWindow; public short Reserved2Size; public IntPtr Reserved2; public IntPtr StandardInput; public IntPtr StandardOutput; public IntPtr StandardError; } [StructLayout(LayoutKind.Sequential)] private struct ProcessInformation { public IntPtr Process; public IntPtr Thread; public uint ProcessId; public uint ThreadId; } [StructLayout(LayoutKind.Sequential)] private struct StartupInfoEx { public StartupInfo StartupInfo; public IntPtr AttributeList; } private sealed class CapturedOutput { public string Text; public bool Exceeded; public Exception Error; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr CreateJobObject(IntPtr attributes, string name); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetInformationJobObject( IntPtr job, uint informationClass, ref JobObjectExtendedLimitInformationValue information, uint informationLength ); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreatePipe( out IntPtr readPipe, out IntPtr writePipe, ref SecurityAttributes attributes, uint size ); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr CreateFile( string filename, uint access, uint share, ref SecurityAttributes attributes, uint creationDisposition, uint flags, IntPtr template ); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreateProcess( string applicationName, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation ); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InitializeProcThreadAttributeList( IntPtr attributeList, int attributeCount, int flags, ref IntPtr size ); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UpdateProcThreadAttribute( IntPtr attributeList, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previousValue, IntPtr returnSize ); [DllImport("kernel32.dll")] private static extern void DeleteProcThreadAttributeList(IntPtr attributeList); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr thread); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool TerminateProcess(IntPtr process, uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr handle); private static void ReadBounded(StreamReader reader, CapturedOutput captured) { try { char[] buffer = new char[4096]; StringBuilder text = new StringBuilder(MaxOutputCharacters); int count; while ((count = reader.Read(buffer, 0, buffer.Length)) > 0) { int available = MaxOutputCharacters - text.Length; int accepted = Math.Min(available, count); if (accepted > 0) { text.Append(buffer, 0, accepted); } if (accepted != count) { captured.Exceeded = true; } } captured.Text = text.ToString(); } catch (Exception error) { captured.Error = error; } finally { reader.Dispose(); } } private static StreamReader OpenPipeReader(ref IntPtr handle) { SafeFileHandle safeHandle = new SafeFileHandle(handle, true); handle = IntPtr.Zero; return new StreamReader( new FileStream(safeHandle, FileAccess.Read, 4096, false), Encoding.UTF8, true, 4096 ); } private static int RemainingMilliseconds(Stopwatch elapsed, int timeoutMilliseconds) { long remaining = timeoutMilliseconds - elapsed.ElapsedMilliseconds; if (remaining <= 0) { return 0; } return remaining > Int32.MaxValue ? Int32.MaxValue : (int)remaining; } public static KennInstallerProbeResult Run(string path, int timeoutMilliseconds) { IntPtr job = CreateJobObject(IntPtr.Zero, null); if (job == IntPtr.Zero) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not create the version-probe job"); } IntPtr outputRead = IntPtr.Zero; IntPtr outputWrite = IntPtr.Zero; IntPtr errorRead = IntPtr.Zero; IntPtr errorWrite = IntPtr.Zero; IntPtr standardInput = IntPtr.Zero; IntPtr attributeList = IntPtr.Zero; IntPtr inheritedHandles = IntPtr.Zero; bool attributeListInitialized = false; ProcessInformation process = new ProcessInformation(); try { JobObjectExtendedLimitInformationValue limits = new JobObjectExtendedLimitInformationValue(); limits.BasicLimitInformation.LimitFlags = JobObjectLimitKillOnJobClose; uint limitsSize = (uint)Marshal.SizeOf(typeof(JobObjectExtendedLimitInformationValue)); if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, ref limits, limitsSize)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not configure the version-probe job"); } SecurityAttributes inheritable = new SecurityAttributes(); inheritable.Length = Marshal.SizeOf(typeof(SecurityAttributes)); inheritable.InheritHandle = true; if (!CreatePipe(out outputRead, out outputWrite, ref inheritable, 0) || !SetHandleInformation(outputRead, HandleFlagInherit, 0) || !CreatePipe(out errorRead, out errorWrite, ref inheritable, 0) || !SetHandleInformation(errorRead, HandleFlagInherit, 0)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not create version-probe pipes"); } standardInput = CreateFile( "NUL", GenericRead, FileShareRead | FileShareWrite, ref inheritable, OpenExisting, 0, IntPtr.Zero ); if (standardInput == new IntPtr(-1)) { standardInput = IntPtr.Zero; throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not open version-probe input"); } IntPtr attributeListSize = IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref attributeListSize); attributeList = Marshal.AllocHGlobal(attributeListSize); if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not initialize version-probe handle isolation"); } attributeListInitialized = true; inheritedHandles = Marshal.AllocHGlobal(IntPtr.Size * 3); Marshal.WriteIntPtr(inheritedHandles, 0, standardInput); Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size, outputWrite); Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size * 2, errorWrite); if (!UpdateProcThreadAttribute( attributeList, 0, ProcThreadAttributeHandleList, inheritedHandles, new IntPtr(IntPtr.Size * 3), IntPtr.Zero, IntPtr.Zero )) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not isolate version-probe handles"); } StartupInfoEx startup = new StartupInfoEx(); startup.StartupInfo.Size = Marshal.SizeOf(typeof(StartupInfoEx)); startup.StartupInfo.Flags = StartfUseStdHandles; startup.StartupInfo.StandardInput = standardInput; startup.StartupInfo.StandardOutput = outputWrite; startup.StartupInfo.StandardError = errorWrite; startup.AttributeList = attributeList; StringBuilder commandLine = new StringBuilder("\"" + path + "\" version --json"); if (!CreateProcess( path, commandLine, IntPtr.Zero, IntPtr.Zero, true, CreateSuspended | CreateNoWindow | ExtendedStartupInfoPresent, IntPtr.Zero, null, ref startup, out process )) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: staged kenn version probe failed"); } DeleteProcThreadAttributeList(attributeList); attributeListInitialized = false; Marshal.FreeHGlobal(attributeList); attributeList = IntPtr.Zero; Marshal.FreeHGlobal(inheritedHandles); inheritedHandles = IntPtr.Zero; CloseHandle(outputWrite); outputWrite = IntPtr.Zero; CloseHandle(errorWrite); errorWrite = IntPtr.Zero; CloseHandle(standardInput); standardInput = IntPtr.Zero; if (!AssignProcessToJobObject(job, process.Process)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not contain the version-probe process"); } CapturedOutput output = new CapturedOutput(); CapturedOutput error = new CapturedOutput(); StreamReader outputReader = OpenPipeReader(ref outputRead); StreamReader errorReader = OpenPipeReader(ref errorRead); Thread outputThread = new Thread(() => ReadBounded(outputReader, output)); Thread errorThread = new Thread(() => ReadBounded(errorReader, error)); outputThread.IsBackground = true; errorThread.IsBackground = true; outputThread.Start(); errorThread.Start(); Stopwatch elapsed = Stopwatch.StartNew(); if (ResumeThread(process.Thread) == UInt32.MaxValue) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not start the version-probe process"); } CloseHandle(process.Thread); process.Thread = IntPtr.Zero; uint wait = WaitForSingleObject(process.Process, (uint)timeoutMilliseconds); bool timedOut = wait == WaitTimeout; if (wait != WaitObject0 && !timedOut) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: staged kenn version probe wait failed"); } uint exitCode = 0; if (!timedOut && !GetExitCodeProcess(process.Process, out exitCode)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "kenn installer: could not read the version-probe result"); } CloseHandle(job); job = IntPtr.Zero; if (timedOut) { WaitForSingleObject(process.Process, 1000); outputThread.Join(1000); errorThread.Join(1000); throw new TimeoutException("kenn installer: staged kenn version probe timed out"); } if (!outputThread.Join(RemainingMilliseconds(elapsed, timeoutMilliseconds)) || !errorThread.Join(RemainingMilliseconds(elapsed, timeoutMilliseconds))) { TerminateProcess(process.Process, 1); throw new TimeoutException("kenn installer: staged kenn version probe timed out"); } if (output.Error != null) { throw output.Error; } if (error.Error != null) { throw error.Error; } if (output.Exceeded || error.Exceeded) { throw new InvalidOperationException("kenn installer: staged kenn version probe output exceeded limit"); } return new KennInstallerProbeResult((int)exitCode, output.Text); } finally { if (job != IntPtr.Zero) { CloseHandle(job); } if (process.Process != IntPtr.Zero) { if (WaitForSingleObject(process.Process, 0) != WaitObject0) { TerminateProcess(process.Process, 1); } CloseHandle(process.Process); } if (process.Thread != IntPtr.Zero) { CloseHandle(process.Thread); } if (outputRead != IntPtr.Zero) { CloseHandle(outputRead); } if (outputWrite != IntPtr.Zero) { CloseHandle(outputWrite); } if (errorRead != IntPtr.Zero) { CloseHandle(errorRead); } if (errorWrite != IntPtr.Zero) { CloseHandle(errorWrite); } if (standardInput != IntPtr.Zero) { CloseHandle(standardInput); } if (attributeList != IntPtr.Zero) { if (attributeListInitialized) { DeleteProcThreadAttributeList(attributeList); } Marshal.FreeHGlobal(attributeList); } if (inheritedHandles != IntPtr.Zero) { Marshal.FreeHGlobal(inheritedHandles); } } } } '@ } $versionProbe = [KennInstallerProbeV1]::Run($candidatePath, 5000) if ($versionProbe.ExitCode -ne 0) { throw 'kenn installer: staged kenn version probe failed' } $version = $versionProbe.Output | ConvertFrom-Json if ($version.name -cne 'kenn' -or $version.version -cne $scalars.version) { throw 'kenn installer: archive binary version does not match the installer index' } [Console]::Error.WriteLine("kenn installer: installing Kenn in $kennInstallerInstallDir") [IO.Directory]::CreateDirectory($kennInstallerInstallDir) | Out-Null $destination = Join-Path $kennInstallerInstallDir 'kenn.exe' $kennInstallerDestinationTemp = Join-Path $kennInstallerInstallDir ('.kenn.' + [Guid]::NewGuid() + '.tmp') [IO.File]::Copy($candidatePath, $kennInstallerDestinationTemp, $false) if ([IO.File]::Exists($destination)) { $kennInstallerDestinationBackup = Join-Path $kennInstallerInstallDir ('.kenn.' + [Guid]::NewGuid() + '.bak') [IO.File]::Replace( $kennInstallerDestinationTemp, $destination, $kennInstallerDestinationBackup ) $kennInstallerDestinationTemp = $null try { [IO.File]::Delete($kennInstallerDestinationBackup) } catch { [Console]::Error.WriteLine("kenn installer: warning: could not remove replacement backup: $($_.Exception.Message)") } $kennInstallerDestinationBackup = $null } else { [IO.File]::Move($kennInstallerDestinationTemp, $destination) $kennInstallerDestinationTemp = $null } $separator = [IO.Path]::PathSeparator $processEntries = @($env:PATH.Split($separator) | Where-Object { $_ }) if (-not ($processEntries | Where-Object { $_ -ieq $kennInstallerInstallDir })) { $env:PATH = "$kennInstallerInstallDir$separator$env:PATH" } try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $userEntries = if ($userPath) { @($userPath.Split($separator) | Where-Object { $_ }) } else { @() } if (-not ($userEntries | Where-Object { $_ -ieq $kennInstallerInstallDir })) { $newUserPath = if ($userPath) { "$kennInstallerInstallDir$separator$userPath" } else { $kennInstallerInstallDir } [Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User') } } catch { [Console]::Error.WriteLine("kenn installer: warning: could not persist user PATH: $($_.Exception.Message)") } [Console]::Out.WriteLine('Kenn installed. Run kenn init to get started.') } finally { if ($kennInstallerDestinationTemp -and [IO.File]::Exists($kennInstallerDestinationTemp)) { [IO.File]::Delete($kennInstallerDestinationTemp) } if ($kennInstallerDestinationBackup -and [IO.File]::Exists($kennInstallerDestinationBackup)) { [IO.File]::Delete($kennInstallerDestinationBackup) } if ([IO.Directory]::Exists($kennInstallerTemp)) { # Windows can keep the probe executable locked briefly after termination. for ($attempt = 0; ; $attempt++) { try { [IO.Directory]::Delete($kennInstallerTemp, $true) break } catch [IO.IOException], [UnauthorizedAccessException] { if ($attempt -ge 20) { throw } Start-Sleep -Milliseconds 100 } } } } }