diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a06f40b97..6a8e6e298 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: rust-profile: ${{ steps.rust-profile.outputs.rust-profile }} jetsocat-build-matrix: ${{ steps.setup-matrix.outputs.jetsocat-build-matrix }} gateway-build-matrix: ${{ steps.setup-matrix.outputs.gateway-build-matrix }} + agent-build-matrix: ${{ steps.setup-matrix.outputs.agent-build-matrix }} steps: - name: Setup matrix @@ -51,6 +52,9 @@ jobs: $GatewayMatrix = ConvertTo-JSON $Jobs -Compress echo "gateway-build-matrix=$GatewayMatrix" >> $Env:GITHUB_OUTPUT + $AgentMatrix = ConvertTo-JSON $Jobs -Compress + echo "agent-build-matrix=$AgentMatrix" >> $Env:GITHUB_OUTPUT + $Jobs = @() $Platforms += 'macos' @@ -211,7 +215,7 @@ jobs: $Env:JETSOCAT_EXECUTABLE = Join-Path $TargetOutputPath $ExecutableFileName $Env:CARGO_PACKAGE = "jetsocat" - ./ci/tlk.ps1 build -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} + ./ci/tlk.ps1 build -Product jetsocat -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} echo "staging-path=$StagingPath" >> $Env:GITHUB_OUTPUT @@ -402,7 +406,7 @@ jobs: TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }} DGATEWAY_EXECUTABLE: ${{ steps.load-variables.outputs.dgateway-executable }} CARGO_PACKAGE: devolutions-gateway - run: ./ci/tlk.ps1 build -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} + run: ./ci/tlk.ps1 build -Product gateway -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} - name: Build PowerShell module if: matrix.os == 'windows' @@ -426,8 +430,8 @@ jobs: $Env:DGATEWAY_PSMODULE_PATH = "${{ steps.load-variables.outputs.dgateway-psmodule-output-path }}" } $Env:DGATEWAY_WEBCLIENT_PATH = Join-Path "webapp" "client" | Resolve-Path - - ./ci/tlk.ps1 package -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} + + ./ci/tlk.ps1 package -Product gateway -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -448,6 +452,110 @@ jobs: pattern: devolutions-gateway-* delete-merged: true + devolutions-agent: + name: devolutions-agent [${{ matrix.os }} ${{ matrix.arch }}] + runs-on: ${{ matrix.runner }} + needs: [preflight] + if: always() + strategy: + matrix: + include: ${{ fromJson(needs.preflight.outputs.agent-build-matrix) }} + + steps: + - name: Checkout ${{ github.repository }} + uses: actions/checkout@v4 + with: + ref: ${{ needs.preflight.outputs.ref }} + + - name: Load dynamic variables + id: load-variables + shell: pwsh + run: | + $PackageVersion = "${{ needs.preflight.outputs.version }}" + $StagingPath = Join-Path $Env:RUNNER_TEMP "staging" + $TargetOutputPath = Join-Path $StagingPath ${{ matrix.os }} ${{ matrix.arch }} + $ExecutableFileName = "DevolutionsAgent_${{ runner.os }}_${PackageVersion}_${{ matrix.arch }}" + + if ($Env:RUNNER_OS -eq "Windows") { + $ExecutableFileName = "$($ExecutableFileName).exe" + $PackageFileName = "DevolutionsAgent-${{ matrix.arch }}-${PackageVersion}.msi" + $DAgentPackage = Join-Path $TargetOutputPath $PackageFileName + + echo "dagent-package=$DAgentPackage" >> $Env:GITHUB_OUTPUT + } + + $DAgentExecutable = Join-Path $TargetOutputPath $ExecutableFileName + echo "staging-path=$StagingPath" >> $Env:GITHUB_OUTPUT + echo "target-output-path=$TargetOutputPath" >> $Env:GITHUB_OUTPUT + echo "dagent-executable=$DAgentExecutable" >> $Env:GITHUB_OUTPUT + + - name: Configure Linux runner + if: matrix.os == 'linux' + run: | + sudo apt-get update + sudo apt-get -o Acquire::Retries=3 install python3-wget python3-setuptools libsystemd-dev dh-make + + - name: Configure Linux (arm) runner + if: matrix.os == 'linux' && matrix.arch == 'arm64' + run: | + sudo dpkg --add-architecture arm64 + sudo apt-get -o Acquire::Retries=3 install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user + rustup target add aarch64-unknown-linux-gnu + echo "STRIP_EXECUTABLE=aarch64-linux-gnu-strip" >> $GITHUB_ENV + + # WiX is installed on Windows runners but not in the PATH + - name: Configure Windows runner + if: matrix.os == 'windows' + run: | + # https://github.com/actions/runner-images/issues/9667 + choco uninstall wixtoolset + choco install wixtoolset --version 3.14.0 --allow-downgrade --force + echo "C:\Program Files (x86)\WiX Toolset v3.14\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Build + shell: pwsh + env: + TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }} + DAGENT_EXECUTABLE: ${{ steps.load-variables.outputs.dagent-executable }} + CARGO_PACKAGE: devolutions-agent + run: ./ci/tlk.ps1 build -Product agent -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} + + - name: Add msbuild to PATH + if: matrix.os == 'windows' + uses: microsoft/setup-msbuild@v2 + + - name: Package + shell: pwsh + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + env: + TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }} + DAGENT_EXECUTABLE: ${{ steps.load-variables.outputs.dagent-executable }} + run: | + if ($Env:RUNNER_OS -eq "Windows") { + $Env:DAGENT_PACKAGE = "${{ steps.load-variables.outputs.dagent-package }}" + } + + ./ci/tlk.ps1 package -Product agent -Platform ${{ matrix.os }} -Architecture ${{ matrix.arch }} -CargoProfile ${{ needs.preflight.outputs.rust-profile }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: devolutions-agent-${{ matrix.os }}-${{ matrix.arch }} + path: ${{ steps.load-variables.outputs.staging-path }} + + devolutions-agent-merge: + name: devolutions agent merge artifacts + runs-on: ubuntu-latest + needs: [preflight, devolutions-agent] + + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: devolutions-agent + pattern: devolutions-agent-* + delete-merged: true + upload-git-log: name: Upload git-log output runs-on: ubuntu-20.04 @@ -480,6 +588,8 @@ jobs: - preflight - devolutions-gateway - devolutions-gateway-merge + - devolutions-agent + - devolutions-agent-merge - jetsocat - jetsocat-lipo - upload-git-log @@ -518,6 +628,11 @@ jobs: name: devolutions-gateway path: ${{ runner.temp }}/artifacts_raw + - uses: actions/download-artifact@v4 + with: + name: devolutions-agent + path: ${{ runner.temp }}/artifacts_raw + - uses: actions/download-artifact@v4 with: name: git-log diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 971b0df10..fe22306a9 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -122,7 +122,7 @@ jobs: needs: preflight strategy: matrix: - project: [ jetsocat, devolutions-gateway ] + project: [ jetsocat, devolutions-gateway, devolutions-agent ] os: [ windows, macos, linux ] include: - os: windows @@ -134,6 +134,8 @@ jobs: exclude: - project: devolutions-gateway os: macos + - project: devolutions-agent + os: macos steps: - name: Checkout ${{ github.repository }} @@ -198,6 +200,7 @@ jobs: run: | $IncludePattern = switch ('${{ matrix.project }}') { 'devolutions-gateway' { 'DevolutionsGateway_*.exe' } + 'devolutions-agent' { 'DevolutionsAgent_*.exe' } 'jetsocat' { 'jetsocat_*' } } $ExcludePattern = "*.pdb" @@ -238,14 +241,14 @@ jobs: gh run download ${{ needs.preflight.outputs.run }} -n webapp-client -D "$Destination" - name: Add msbuild to PATH - if: matrix.os == 'windows' && matrix.project == 'devolutions-gateway' + if: matrix.os == 'windows' && (matrix.project == 'devolutions-gateway' || matrix.project == 'devolutions-agent') uses: microsoft/setup-msbuild@v2 - name: Regenerate MSI - if: matrix.project == 'devolutions-gateway' && matrix.os == 'windows' + if: (matrix.project == 'devolutions-gateway' || matrix.project == 'devolutions-agent') && matrix.os == 'windows' shell: pwsh run: | - $PackageRoot = Join-Path ${{ runner.temp }} devolutions-gateway + $PackageRoot = Join-Path ${{ runner.temp }} ${{ matrix.project}} $Env:DGATEWAY_EXECUTABLE = Get-ChildItem -Path $PackageRoot -Recurse -Include '*DevolutionsGateway*.exe' | Select -First 1 $Env:DGATEWAY_PSMODULE_PATH = Join-Path $PackageRoot PowerShell DevolutionsGateway $Env:DGATEWAY_WEBCLIENT_PATH = Join-Path "webapp" "client" | Resolve-Path @@ -257,7 +260,7 @@ jobs: ./ci/tlk.ps1 package -PackageOption generate - name: Sign msi runtime - if: matrix.project == 'devolutions-gateway' && matrix.os == 'windows' + if: (matrix.project == 'devolutions-gateway' || matrix.project == 'devolutions-agent') && matrix.os == 'windows' shell: pwsh working-directory: package/WindowsManaged/Release run: | @@ -273,17 +276,26 @@ jobs: AzureSignTool @Params $_.FullName } - - name: Repackage + - name: Repackage gateway if: matrix.project == 'devolutions-gateway' && matrix.os == 'windows' shell: pwsh run: | $PackageRoot = Join-Path ${{ runner.temp }} devolutions-gateway $Env:DGATEWAY_PACKAGE = Get-ChildItem -Path $PackageRoot -Recurse -Include '*DevolutionsGateway*.msi' | Where-Object { $_.Name -NotLike "*legacy*"} | Select -First 1 - ./ci/tlk.ps1 package -PackageOption assemble + ./ci/tlk.ps1 package -Product gateway -PackageOption assemble + + - name: Repackage agent + if: matrix.project == 'devolutions-agent' && matrix.os == 'windows' + shell: pwsh + run: | + $PackageRoot = Join-Path ${{ runner.temp }} devolutions-agent + $Env:DAGENT_PACKAGE = Get-ChildItem -Path $PackageRoot -Recurse -Include '*DevolutionsAgent*.msi' | Where-Object { $_.Name -NotLike "*legacy*"} | Select -First 1 + + ./ci/tlk.ps1 package -Product agent -PackageOption assemble - name: Sign packages - if: matrix.project == 'devolutions-gateway' && matrix.os == 'windows' + if: (matrix.project == 'devolutions-gateway' || matrix.project == 'devolutions-agent') && matrix.os == 'windows' shell: pwsh run: | Get-ChildItem -Path ${{ runner.temp }} -Recurse -Include '*.msi' | % { @@ -331,7 +343,7 @@ jobs: retention-days: 1 devolutions-gateway-merge: - name: Merge Artifacts + name: Merge gateway artifacts runs-on: ubuntu-latest needs: [preflight, codesign] @@ -349,8 +361,27 @@ jobs: name: devolutions-gateway overwrite: true + devolutions-agent-merge: + name: Merge agent artifacts + runs-on: ubuntu-latest + needs: [preflight, codesign] + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + pattern: devolutions-agent-* + merge-multiple: true + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + path: ${{ github.workspace }}/**/* + name: devolutions-agent + overwrite: true + jetsocat-merge: - name: Merge Artifacts + name: Merge jetsocat artifacts runs-on: ubuntu-latest needs: [preflight, codesign] diff --git a/Cargo.lock b/Cargo.lock index 3400f231f..d4ef32970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,6 +550,26 @@ dependencies = [ "libc", ] +[[package]] +name = "ceviche" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1a44085e79b3b02c2a5d18a54c8cb943bc7ae85207f76679b89f1e1c3c68c9" +dependencies = [ + "cfg-if", + "chrono", + "core-foundation", + "core-foundation-sys", + "ctrlc", + "libc", + "log", + "system-configuration-sys", + "systemd-rs", + "timer", + "widestring 0.4.3", + "winapi", +] + [[package]] name = "ceviche" version = "0.6.0" @@ -761,7 +781,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" dependencies = [ - "nix", + "nix 0.27.1", "windows-sys 0.52.0", ] @@ -900,6 +920,28 @@ dependencies = [ "cipher", ] +[[package]] +name = "devolutions-agent" +version = "2024.1.2" +dependencies = [ + "anyhow", + "camino", + "ceviche 0.5.2", + "cfg-if", + "ctrlc", + "devolutions-gateway-task", + "devolutions-log", + "embed-resource", + "futures", + "parking_lot", + "serde", + "serde_derive", + "serde_json", + "tap", + "tokio", + "tracing", +] + [[package]] name = "devolutions-gateway" version = "2024.2.1" @@ -912,10 +954,11 @@ dependencies = [ "backoff", "bytes", "camino", - "ceviche", + "ceviche 0.6.0", "cfg-if", "devolutions-gateway-generators", "devolutions-gateway-task", + "devolutions-log", "dlopen", "dlopen_derive", "embed-resource", @@ -988,6 +1031,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "devolutions-log" +version = "2024.2.1" +dependencies = [ + "anyhow", + "async-trait", + "camino", + "devolutions-gateway-task", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + [[package]] name = "digest" version = "0.10.7" @@ -1147,6 +1204,16 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "epoll" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +dependencies = [ + "bitflags 2.4.2", + "libc", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2032,6 +2099,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2285,6 +2361,18 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.27.1" @@ -3333,7 +3421,7 @@ dependencies = [ "netlink-packet-utils", "netlink-proto", "netlink-sys", - "nix", + "nix 0.27.1", "thiserror", "tokio", ] @@ -3961,6 +4049,29 @@ dependencies = [ "libc", ] +[[package]] +name = "systemd-rs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba28cdec1491faf8b9820305aa6cf9df1dff3378c1adfe41d5bd1261d4a17b59" +dependencies = [ + "epoll", + "libc", + "log", + "nix 0.24.3", + "systemd-sys", +] + +[[package]] +name = "systemd-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816f770cdd05c0a0b8bd048178c3eb3048221016945469c6124a1dd1afc6400" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "tap" version = "1.0.1" diff --git a/ci/tlk.ps1 b/ci/tlk.ps1 index cb91a94ad..f109b36d7 100644 --- a/ci/tlk.ps1 +++ b/ci/tlk.ps1 @@ -46,7 +46,7 @@ function Merge-Tokens }) if ($OutputFile) { - $AsByteStream = if ($PSEdition -eq 'Core') { @{AsByteStream = $true} } else { @{'Encoding' = 'Byte'} } + $AsByteStream = if ($PSEdition -Eq 'Core') { @{AsByteStream = $true} } else { @{'Encoding' = 'Byte'} } $OutputBytes = $([System.Text.Encoding]::UTF8).GetBytes($OutputValue) Set-Content -Path $OutputFile -Value $OutputBytes @AsByteStream } @@ -144,6 +144,19 @@ function Get-TlkArchitecture { $Architecture } +function Get-TlkProduct { + param( + [Parameter(Position=0)] + [string] $Product + ) + + if (-Not $Product) { + $Product = 'gateway' + } + + $Product +} + class TlkTarget { [string] $Platform @@ -170,15 +183,15 @@ class TlkTarget } [bool] IsWindows() { - return $this.Platform -eq 'Windows' + return $this.Platform -Eq 'Windows' } [bool] IsMacOS() { - return $this.Platform -eq 'macOS' + return $this.Platform -Eq 'macOS' } [bool] IsLinux() { - return $this.Platform -eq 'Linux' + return $this.Platform -Eq 'Linux' } [string] CargoTarget() { @@ -212,7 +225,7 @@ class TlkTarget "x86_64" { "x64" } "aarch64" { "ARM64" } } - + return $WindowsArchitecture } @@ -225,14 +238,14 @@ class TlkTarget "x86_64" { "amd64" } "arm64" { "arm64" } } - + return $DebianArchitecture } } class TlkRecipe { - [string] $PackageName + [string] $Product [string] $Version [string] $SourcePath [bool] $Verbose @@ -255,10 +268,30 @@ class TlkRecipe [void] Init() { $this.SourcePath = $($PSScriptRoot | Get-Item).Parent.FullName - $this.PackageName = "DevolutionsGateway" $this.Version = $(Get-Content -Path "$($this.SourcePath)/VERSION").Trim() $this.Verbose = $true $this.Target = [TlkTarget]::new() + $this.Product = Get-TlkProduct + } + + [string] CargoPackage() { + $CargoPackage = ` + switch ($this.Product) { + "gateway" { "devolutions-gateway" } + "agent" { "devolutions-agent" } + "jetsocat" { "jetsocat" } + } + return $CargoPackage + } + + [string] PackageName() { + $PackageName = switch ($this.Product) { + "gateway" { "DevolutionsGateway" } + "agent" { "DevolutionsAgent" } + "jetsocat" { "jetsocat" } + } + + return $PackageName } [void] Cargo([string[]]$CargoArgs) { @@ -285,18 +318,18 @@ class TlkRecipe if (Test-Path Env:TARGET_OUTPUT_PATH) { $BuildStagingDirectory = $Env:TARGET_OUTPUT_PATH } - + if ($this.Target.IsWindows()) { $Env:RUSTFLAGS = "-C target-feature=+crt-static" } - + $OutputPath = "${BuildStagingDirectory}/$($this.Target.Platform)/$($this.Target.Architecture)" New-Item -Path $OutputPath -ItemType 'Directory' -Force | Out-Null - + Push-Location Set-Location $this.SourcePath - $CargoPackage = "devolutions-gateway" + $CargoPackage = $this.CargoPackage() if (Test-Path Env:CARGO_PACKAGE) { $CargoPackage = $Env:CARGO_PACKAGE } @@ -310,14 +343,31 @@ class TlkRecipe $SrcExecutableName = $CargoPackage, $this.Target.ExecutableExtension -ne '' -Join '.' $SrcExecutablePath = "$($this.SourcePath)/target/${CargoTarget}/${CargoProfile}/${SrcExecutableName}" - if (Test-Path Env:DGATEWAY_EXECUTABLE) { - $DGatewayExecutable = $Env:DGATEWAY_EXECUTABLE - $DestinationExecutable = $DGatewayExecutable - } elseif (Test-Path Env:JETSOCAT_EXECUTABLE) { - $JetsocatExecutable = $Env:JETSOCAT_EXECUTABLE - $DestinationExecutable = $JetsocatExecutable - } else { - $DestinationExecutable = $null + $DestinationExecutable = switch ($this.Product) { + "gateway" { + if (Test-Path Env:DGATEWAY_EXECUTABLE) { + $Env:DGATEWAY_EXECUTABLE + } else { + $null + } + } + "agent" { + if (Test-Path Env:DAGENT_EXECUTABLE) { + $Env:DAGENT_EXECUTABLE + } else { + $null + } + } + "jetsocat" { + if (Test-Path Env:JETSOCAT_EXECUTABLE) { + $Env:JETSOCAT_EXECUTABLE + } else { + $null + } + } + Default { + $null + } } if ($this.Target.IsWindows() -And $DestinationExecutable) { @@ -384,14 +434,14 @@ class TlkRecipe $TargetConfiguration = "Release" # Build the base (en-US) MSI - & .\$TargetConfiguration\Build_DevolutionsGateway.cmd + & ".\$TargetConfiguration\Build_$($this.PackageName()).cmd" - $BaseMsi = Join-Path $TargetConfiguration "$($this.PackageName).msi" + $BaseMsi = Join-Path $TargetConfiguration "$($this.PackageName()).msi" foreach ($PackageLanguage in $([TlkRecipe]::PackageLanguages | Select-Object -Skip 1)) { # Build the localized MSI - & .\$TargetConfiguration\$($PackageLanguage.Name)\Build_DevolutionsGateway.cmd - $LangMsi = Join-Path $TargetConfiguration $($PackageLanguage.Name) "$($this.PackageName).msi" + & ".\$TargetConfiguration\$($PackageLanguage.Name)\Build_$($this.PackageName()).cmd" + $LangMsi = Join-Path $TargetConfiguration $($PackageLanguage.Name) "$($this.PackageName()).msi" $Transform = Join-Path $TargetConfiguration "$($PackageLanguage.Name).mst" # Generate a language transform & 'torch.exe' "$BaseMsi" "$LangMsi" "-o" "$Transform" | Out-Host @@ -403,15 +453,25 @@ class TlkRecipe $LCIDs = ([TlkRecipe]::PackageLanguages | ForEach-Object { $_.LCID }) -join ',' & 'cscript.exe' "/nologo" "../Windows/WiLangId.vbs" "$BaseMsi" "Package" "$LCIDs" | Out-Host - if (Test-Path Env:DGATEWAY_PACKAGE) { - $DGatewayPackage = $Env:DGATEWAY_PACKAGE - Copy-Item -Path "$BaseMsi" -Destination $DGatewayPackage + switch ($this.Product) { + "gateway" { + if (Test-Path Env:DGATEWAY_PACKAGE) { + $DGatewayPackage = $Env:DGATEWAY_PACKAGE + Copy-Item -Path "$BaseMsi" -Destination $DGatewayPackage + } + } + "agent" { + if (Test-Path Env:DAGENT_PACKAGE) { + $DAgentPackage = $Env:DAGENT_PACKAGE + Copy-Item -Path "$BaseMsi" -Destination $DAgentPackage + } + } } Pop-Location } - [void] Package_Windows_Managed([bool] $SourceOnlyBuild) { + [void] Package_Windows_Managed_Gateway([bool] $SourceOnlyBuild) { $ShortVersion = $this.Version.Substring(2) # msi version $Env:DGATEWAY_VERSION="$ShortVersion" @@ -431,10 +491,6 @@ class TlkRecipe $TargetConfiguration = "Release" - if ((Get-Command "MSBuild.exe" -ErrorAction SilentlyContinue) -Eq $Null) { - throw 'MSBuild was not found in the PATH' - } - if ($SourceOnlyBuild) { $Env:DGATEWAY_MSI_SOURCE_ONLY_BUILD = "1" } @@ -459,20 +515,79 @@ class TlkRecipe if (!$SourceOnlyBuild -And (Test-Path Env:DGATEWAY_PACKAGE)) { $DGatewayPackage = $Env:DGATEWAY_PACKAGE - $MsiPath = Join-Path "Release" "$($this.PackageName).msi" + $MsiPath = Join-Path "Release" "$($this.PackageName()).msi" Copy-Item -Path "$MsiPath" -Destination $DGatewayPackage } Pop-Location } + [void] Package_Windows_Managed_Agent([bool] $SourceOnlyBuild) { + $ShortVersion = $this.Version.Substring(2) # msi version + + $Env:DAGENT_VERSION="$ShortVersion" + + Push-Location + Set-Location "$($this.SourcePath)/package/Agent$($this.Target.Platform)Managed" + + if (Test-Path Env:DAGENT_EXECUTABLE) { + $DGatewayExecutable = $Env:DAGENT_EXECUTABLE + } else { + throw ("Specify DAGENT_EXECUTABLE environment variable") + } + + $TargetConfiguration = "Release" + + if ($SourceOnlyBuild) { + $Env:DAGENT_MSI_SOURCE_ONLY_BUILD = "1" + } + + & 'MSBuild.exe' "DevolutionsAgent.sln" "/t:restore,build" "/p:Configuration=$TargetConfiguration" | Out-Host + + if ($SourceOnlyBuild) { + foreach ($PackageLanguage in $([TlkRecipe]::PackageLanguages | Select-Object -Skip 1)) { + $Env:DAGENT_MSI_LANG_ID = $PackageLanguage.Name + & 'MSBuild.exe' "DevolutionsAgent.sln" "/t:restore,build" "/p:Configuration=$TargetConfiguration" | Out-Host + } + } + + $Env:DAGENT_MSI_SOURCE_ONLY_BUILD = "" + $Env:DAGENT_MSI_LANG_ID = "" + + if (!$SourceOnlyBuild -And (Test-Path Env:DAGENT_PACKAGE)) { + $DAgentPackage = $Env:DAGENT_PACKAGE + $MsiPath = Join-Path "Release" "$($this.PackageName()).msi" + Copy-Item -Path "$MsiPath" -Destination $DAgentPackage + } + + Pop-Location + } + + [void] Package_Windows_Managed([bool] $SourceOnlyBuild) { + if ((Get-Command "MSBuild.exe" -ErrorAction SilentlyContinue) -Eq $Null) { + throw 'MSBuild was not found in the PATH' + } + + if ($this.Product -eq 'gateway') { + $this.Package_Windows_Managed_Gateway($SourceOnlyBuild) + } elseif ($this.Product -eq 'agent') { + $this.Package_Windows_Managed_Agent($SourceOnlyBuild) + } else { + throw "Managed packaging for $($this.Product) is not supported" + } + } + [void] Package_Windows() { + if ($this.Product -ne 'gateway') { + throw "Legacy packaging for $($this.Product) is not supported" + } + $ShortVersion = $this.Version.Substring(2) # msi version $TargetArch = $this.Target.WindowsArchitecture() Push-Location Set-Location "$($this.SourcePath)/package/$($this.Target.Platform)" - + if (Test-Path Env:DGATEWAY_EXECUTABLE) { $DGatewayExecutable = $Env:DGATEWAY_EXECUTABLE } else { @@ -484,7 +599,7 @@ class TlkRecipe $DGatewayPSModuleStagingPath = $PSModulePaths[1] $TargetConfiguration = "Release" - $ActionsProjectPath = Join-Path $(Get-Location) 'Actions' + $ActionsProjectPath = Join-Path $(Get-Location) 'Actions' if ((Get-Command "MSBuild.exe" -ErrorAction SilentlyContinue) -Eq $Null) { throw 'MSBuild was not found in the PATH' @@ -497,42 +612,42 @@ class TlkRecipe '-cg', 'CG.DGatewayPSComponentGroup', '-var', 'var.DGatewayPSSourceDir', '-nologo', '-srd', '-suid', '-scom', '-sreg', '-sfrag', '-gg') - - & 'heat.exe' $HeatArgs + @('-t', 'HeatTransform64.xslt', '-o', "$($this.PackageName)-$TargetArch.wxs") | Out-Host + + & 'heat.exe' $HeatArgs + @('-t', 'HeatTransform64.xslt', '-o', "$($this.PackageName())-$TargetArch.wxs") | Out-Host $WixExtensions = @('WixUtilExtension', 'WixUIExtension', 'WixFirewallExtension') $WixExtensions += $(Join-Path $(Get-Location) 'WixUserPrivilegesExtension.dll') - + $WixArgs = @($WixExtensions | ForEach-Object { @('-ext', $_) }) + @( "-dDGatewayPSSourceDir=$DGatewayPSModuleStagingPath", "-dDGatewayExecutable=$DGatewayExecutable", "-dVersion=$ShortVersion", "-dActionsLib=$(Join-Path $ActionsProjectPath $TargetArch $TargetConfiguration 'DevolutionsGateway.Installer.Actions.dll')", "-v") - - $WixFiles = Get-ChildItem -Include '*.wxs' -Recurse + + $WixFiles = Get-ChildItem -Include '*.wxs' -Recurse $InputFiles = $WixFiles | Foreach-Object { Resolve-Path $_.FullName -Relative } $ObjectFiles = $WixFiles | ForEach-Object { $_.BaseName + '.wixobj' } $Cultures = @('en-US', 'fr-FR') - + foreach ($Culture in $Cultures) { & 'candle.exe' '-nologo' $InputFiles $WixArgs "-dPlatform=$TargetArch" | Out-Host - $OutputFile = "$($this.PackageName)_${Culture}.msi" - - if ($Culture -eq 'en-US') { - $OutputFile = "$($this.PackageName).msi" + $OutputFile = "$($this.PackageName())_${Culture}.msi" + + if ($Culture -Eq 'en-US') { + $OutputFile = "$($this.PackageName()).msi" } - + & 'light.exe' "-nologo" $ObjectFiles "-cultures:${Culture}" "-loc" "$($this.PackageName)_${Culture}.wxl" ` "-out" $OutputFile $WixArgs "-dPlatform=$TargetArch" "-sice:ICE61" | Out-Host } - + foreach ($Culture in $($Cultures | Select-Object -Skip 1)) { - & 'torch.exe' "$($this.PackageName).msi" "$($this.PackageName)_${Culture}.msi" "-o" "${Culture}_$TargetArch.mst" | Out-Host - & 'cscript.exe' "/nologo" "WiSubStg.vbs" "$($this.PackageName).msi" "${Culture}_$TargetArch.mst" "1036" | Out-Host - & 'cscript.exe' "/nologo" "WiLangId.vbs" "$($this.PackageName).msi" "Package" "1033,1036" | Out-Host + & 'torch.exe' "$($this.PackageName()).msi" "$($this.PackageName())_${Culture}.msi" "-o" "${Culture}_$TargetArch.mst" | Out-Host + & 'cscript.exe' "/nologo" "WiSubStg.vbs" "$($this.PackageName()).msi" "${Culture}_$TargetArch.mst" "1036" | Out-Host + & 'cscript.exe' "/nologo" "WiLangId.vbs" "$($this.PackageName()).msi" "Package" "1033,1036" | Out-Host } if (Test-Path Env:DGATEWAY_PSMODULE_CLEAN) { @@ -543,7 +658,7 @@ class TlkRecipe if (Test-Path Env:DGATEWAY_PACKAGE) { $DGatewayPackage = $Env:DGATEWAY_PACKAGE - Copy-Item -Path "$($this.PackageName).msi" -Destination $DGatewayPackage + Copy-Item -Path "$($this.PackageName()).msi" -Destination $DGatewayPackage } Pop-Location @@ -567,24 +682,44 @@ class TlkRecipe $Env:DEBFULLNAME = $Packager $Env:DEBEMAIL = $Email - if (Test-Path Env:DGATEWAY_EXECUTABLE) { - $DGatewayExecutable = $Env:DGATEWAY_EXECUTABLE - } else { - throw ("Specify DGATEWAY_EXECUTABLE environment variable") + $DGatewayExecutable = $null + $DGatewayWebClient = $null + $DAgentExecutable = $null + + switch ($this.Product) { + "gateway" { + if (Test-Path Env:DGATEWAY_EXECUTABLE) { + $DGatewayExecutable = $Env:DGATEWAY_EXECUTABLE + } else { + throw ("Specify DGATEWAY_EXECUTABLE environment variable") + } + + if (Test-Path Env:DGATEWAY_WEBCLIENT_PATH) { + $DGatewayWebClient = $Env:DGATEWAY_WEBCLIENT_PATH + } else { + throw ("Specify DGATEWAY_WEBCLIENT_PATH environment variable") + } + } + "agent" { + if (Test-Path Env:DAGENT_EXECUTABLE) { + $DAgentExecutable = $Env:DAGENT_EXECUTABLE + } else { + throw ("Specify DAGENT_EXECUTABLE environment variable") + } + } } - if (Test-Path Env:DGATEWAY_WEBCLIENT_PATH) { - $DGatewayWebClient = $Env:DGATEWAY_WEBCLIENT_PATH - } else { - throw ("Specify DGATEWAY_WEBCLIENT_PATH environment variable") + $InputPackagePathPrefix = switch ($this.Product) { + "gateway" { "" } + "agent" { "Agent" } } - $InputPackagePath = Join-Path $this.SourcePath "package/Linux" + $InputPackagePath = Join-Path $this.SourcePath "package/$($InputPackagePathPrefix)Linux" $OutputPath = Join-Path $this.SourcePath "output" New-Item -Path $OutputPath -ItemType 'Directory' -Force | Out-Null - $OutputPackagePath = Join-Path $OutputPath "gateway" + $OutputPackagePath = Join-Path $OutputPath "$($this.Product)" $OutputDebianPath = Join-Path $OutputPackagePath "debian" @($OutputPath, $OutputPackagePath, $OutputDebianPath) | % { @@ -594,10 +729,10 @@ class TlkRecipe Push-Location Set-Location $OutputPackagePath - $DebPkgName = "devolutions-gateway" + $DebPkgName = "devolutions-$($this.Product)" $PkgNameVersion = "${DebPkgName}_$($this.Version).0" $PkgNameTarget = "${PkgNameVersion}_${DebianArchitecture}" - $CopyrightFile = Join-Path $InputPackagePath "gateway/copyright" + $CopyrightFile = Join-Path $InputPackagePath "$($this.Product)/copyright" # dh_make @@ -619,23 +754,34 @@ class TlkRecipe # debian/rules $RulesFile = Join-Path $OutputDebianPath "rules" - $RulesTemplate = Join-Path $InputPackagePath "gateway/template/rules" + $RulesTemplate = Join-Path $InputPackagePath "$($this.Product)/template/rules" $DhShLibDepsOverride = ""; if ($this.Target.DebianArchitecture() -Eq "amd64") { $DhShLibDepsOverride = "dh_shlibdeps" } - Merge-Tokens -TemplateFile $RulesTemplate -Tokens @{ - dgateway_executable = $DGatewayExecutable - dgateway_webclient = $DGatewayWebClient - platform_dir = $InputPackagePath - dh_shlibdeps = $DhShLibDepsOverride - } -OutputFile $RulesFile + switch ($this.Product) { + "gateway" { + Merge-Tokens -TemplateFile $RulesTemplate -Tokens @{ + dgateway_executable = $DGatewayExecutable + dgateway_webclient = $DGatewayWebClient + platform_dir = $InputPackagePath + dh_shlibdeps = $DhShLibDepsOverride + } -OutputFile $RulesFile + } + "agent" { + Merge-Tokens -TemplateFile $RulesTemplate -Tokens @{ + dagent_executable = $DAgentExecutable + platform_dir = $InputPackagePath + dh_shlibdeps = $DhShLibDepsOverride + } -OutputFile $RulesFile + } + } # debian/control $ControlFile = Join-Path $OutputDebianPath "control" - $ControlTemplate = Join-Path $InputPackagePath "gateway/template/control" + $ControlTemplate = Join-Path $InputPackagePath "$($this.Product)/template/control" Merge-Tokens -TemplateFile $ControlTemplate -Tokens @{ arch = $DebianArchitecture deps = $($Dependencies -Join ", ") @@ -669,7 +815,7 @@ class TlkRecipe } -OutputFile $ChangelogFile @('postinst', 'prerm', 'postrm') | % { - $InputFile = Join-Path $InputPackagePath "gateway/debian/$_" + $InputFile = Join-Path $InputPackagePath "$($this.Product)/debian/$_" $OutputFile = Join-Path $OutputDebianPath $_ Copy-Item $InputFile $OutputFile } @@ -693,6 +839,10 @@ class TlkRecipe } [void] Package([string]$PackageOption) { + if ($this.Product -Eq 'jetsocat') { + throw "Packaging for $($this.Product) is not supported" + } + if ($this.Target.IsWindows()) { if (-Not $PackageOption ) { $this.Package_Windows_Managed($false) @@ -755,7 +905,9 @@ function Invoke-TlkStep { [ValidateSet('x86','x86_64','arm64')] [string] $Architecture, [ValidateSet('release', 'production')] - [string] $CargoProfile + [string] $CargoProfile, + [ValidateSet('gateway', 'agent', 'jetsocat')] + [string] $Product ) if (-Not $Platform) { @@ -770,6 +922,11 @@ function Invoke-TlkStep { $CargoProfile = 'release' } + if (-Not $Product) { + Write-Warning "`[LEGACY] Product` parameter is not specified, defaulting to 'gateway'" + $Product = 'gateway' + } + $RootPath = Split-Path -Parent $PSScriptRoot $tlk = [TlkRecipe]::new() @@ -777,6 +934,7 @@ function Invoke-TlkStep { $tlk.Target.Platform = $Platform $tlk.Target.Architecture = $Architecture $tlk.Target.CargoProfile = $CargoProfile + $tlk.Product = $Product switch ($TlkVerb) { "build" { $tlk.Build() } diff --git a/crates/devolutions-log/Cargo.toml b/crates/devolutions-log/Cargo.toml new file mode 100644 index 000000000..e0f5a4649 --- /dev/null +++ b/crates/devolutions-log/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "devolutions-log" +version = "2024.2.1" +edition = "2021" +readme = "README.md" +license = "MIT/Apache-2.0" +authors = ["Devolutions Inc. "] +description = "Logging utils library for Devolutions apps" +publish = false + +[dependencies] +# In-house +devolutions-gateway-task = { path = "../devolutions-gateway-task" } + +# Error handling +anyhow = "1.0" + +# Utils, misc +camino = { version = "1.1", features = ["serde1"] } + +# Async +async-trait = "0.1" +tokio = { version = "1.37", features = ["signal", "net", "io-util", "time", "rt", "rt-multi-thread", "sync", "macros", "parking_lot", "fs"] } + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "parking_lot", "smallvec", "local-time", "tracing-log"] } +tracing-appender = "0.2" \ No newline at end of file diff --git a/crates/devolutions-log/README.md b/crates/devolutions-log/README.md new file mode 100644 index 000000000..0d97e21a5 --- /dev/null +++ b/crates/devolutions-log/README.md @@ -0,0 +1,2 @@ +# devolutions-log +Common code for logging in devolutions apps \ No newline at end of file diff --git a/crates/devolutions-log/src/lib.rs b/crates/devolutions-log/src/lib.rs new file mode 100644 index 000000000..488a5a17e --- /dev/null +++ b/crates/devolutions-log/src/lib.rs @@ -0,0 +1,215 @@ +#[macro_use] +extern crate tracing; + +use std::io; +use std::time::SystemTime; + +use anyhow::Context as _; +use async_trait::async_trait; +use camino::{Utf8Path, Utf8PathBuf}; +use devolutions_gateway_task::{ShutdownSignal, Task}; +use tokio::fs; +use tokio::time::{sleep, Duration}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_appender::rolling; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, EnvFilter}; + +pub trait StaticLogConfig { + const MAX_BYTES_PER_LOG_FILE: u64; + const MAX_LOG_FILES: usize; + const LOG_FILE_PREFIX: &'static str; +} + +pub struct LoggerGuard { + _file_guard: WorkerGuard, + _stdio_guard: WorkerGuard, +} + +struct LogPathCfg<'a, C: StaticLogConfig> { + folder: &'a Utf8Path, + prefix: &'a str, + _phantom: std::marker::PhantomData, +} + +impl<'a, C: StaticLogConfig> LogPathCfg<'a, C> { + pub fn from_path(path: &'a Utf8Path) -> anyhow::Result { + if path.is_dir() { + Ok(Self { + folder: path, + prefix: C::LOG_FILE_PREFIX, + _phantom: std::marker::PhantomData, + }) + } else { + Ok(Self { + folder: path.parent().context("invalid log path (parent)")?, + prefix: path.file_name().context("invalid log path (file_name)")?, + _phantom: std::marker::PhantomData, + }) + } + } +} + +pub fn init( + path: &Utf8Path, + log_filter: &str, + debug_filtering_directives: Option<&str>, +) -> anyhow::Result { + let log_cfg = LogPathCfg::::from_path(path)?; + let file_appender = rolling::Builder::new() + .rotation(rolling::Rotation::max_bytes(C::MAX_BYTES_PER_LOG_FILE)) + .filename_prefix(log_cfg.prefix) + .filename_suffix("log") + .max_log_files(C::MAX_LOG_FILES) + .build(log_cfg.folder) + .context("couldn’t create file appender")?; + let (file_non_blocking, file_guard) = tracing_appender::non_blocking(file_appender); + let file_layer = fmt::layer().with_writer(file_non_blocking).with_ansi(false); + + let (non_blocking_stdio, stdio_guard) = tracing_appender::non_blocking(std::io::stdout()); + let stdio_layer = fmt::layer().with_writer(non_blocking_stdio); + + let env_filter = EnvFilter::try_new(log_filter).context("invalid built-in filtering directives (this is a bug)")?; + + // Optionally add additional debugging filtering directives + let env_filter = debug_filtering_directives + .into_iter() + .flat_map(|directives| directives.split(',')) + .fold(env_filter, |env_filter, directive| { + env_filter.add_directive(directive.parse().unwrap()) + }); + + tracing_subscriber::registry() + .with(file_layer) + .with(stdio_layer) + .with(env_filter) + .init(); + + Ok(LoggerGuard { + _file_guard: file_guard, + _stdio_guard: stdio_guard, + }) +} + +/// Find latest log file (by age) +/// +/// Given path is used to filter out by file name prefix. +#[instrument] +pub async fn find_latest_log_file(prefix: &Utf8Path) -> anyhow::Result { + let cfg = LogPathCfg::::from_path(prefix)?; + + let mut read_dir = fs::read_dir(cfg.folder).await.context("couldn't read directory")?; + + let mut most_recent_time = SystemTime::UNIX_EPOCH; + let mut most_recent = None; + + while let Ok(Some(entry)) = read_dir.next_entry().await { + match entry.file_name().to_str() { + Some(file_name) if file_name.starts_with(cfg.prefix) && file_name.contains("log") => { + debug!(file_name, "Found a log file"); + match entry.metadata().await.and_then(|metadata| metadata.modified()) { + Ok(modified) if modified > most_recent_time => { + most_recent_time = modified; + most_recent = Some(entry.path()); + } + Ok(_) => {} + Err(error) => { + warn!(%error, file_name, "Couldn't retrieve metadata for file"); + } + } + } + _ => continue, + } + } + + most_recent.context("no file found") +} + +/// File deletion task (by age) +/// +/// Given path is used to filter out by file name prefix. +pub struct LogDeleterTask { + path: Utf8PathBuf, + _phantom: std::marker::PhantomData, +} + +impl LogDeleterTask { + pub fn new(path: Utf8PathBuf) -> Self { + Self { + path, + _phantom: std::marker::PhantomData, + } + } +} + +#[async_trait] +impl Task for LogDeleterTask { + type Output = anyhow::Result<()>; + + const NAME: &'static str = "log deleter"; + + async fn run(self, shutdown_signal: ShutdownSignal) -> Self::Output { + log_deleter_task::(&self.path, shutdown_signal).await + } +} + +#[instrument(skip(shutdown_signal))] +async fn log_deleter_task( + path: &Utf8Path, + mut shutdown_signal: ShutdownSignal, +) -> anyhow::Result<()> { + const TASK_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); // once per day + const MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24 * 90); // 90 days + + debug!("Task started"); + + let cfg = LogPathCfg::::from_path(path)?; + + loop { + match fs::read_dir(cfg.folder).await { + Ok(mut read_dir) => { + while let Ok(Some(entry)) = read_dir.next_entry().await { + match entry.file_name().to_str() { + Some(file_name) if file_name.starts_with(cfg.prefix) && file_name.contains("log") => { + debug!(file_name, "Found a log file"); + match entry + .metadata() + .await + .and_then(|metadata| metadata.modified()) + .and_then(|time| time.elapsed().map_err(|e| io::Error::new(io::ErrorKind::Other, e))) + { + Ok(modified) if modified > MAX_AGE => { + info!(file_name, "Delete log file"); + if let Err(error) = fs::remove_file(entry.path()).await { + warn!(%error, file_name, "Couldn't delete log file"); + } + } + Ok(_) => { + trace!(file_name, "Keep this log file"); + } + Err(error) => { + warn!(%error, file_name, "Couldn't retrieve metadata for file"); + } + } + } + _ => continue, + } + } + } + Err(error) => { + warn!(%error, "Couldn't read log folder"); + } + } + + tokio::select! { + _ = sleep(TASK_INTERVAL) => {} + _ = shutdown_signal.wait() => { + break; + } + } + } + + debug!("Task terminated"); + + Ok(()) +} diff --git a/devolutions-agent/Cargo.toml b/devolutions-agent/Cargo.toml index bb1f072c4..8685fface 100644 --- a/devolutions-agent/Cargo.toml +++ b/devolutions-agent/Cargo.toml @@ -3,12 +3,39 @@ name = "devolutions-agent" version = "2024.1.2" edition = "2021" license = "MIT/Apache-2.0" -authors = ["Marc-André Moreau "] +authors = ["Devolutions Inc. "] build = "build.rs" +publish = false [dependencies] -ctrlc = "3.1" +# In-house +devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" } +devolutions-log = { path = "../crates/devolutions-log" } + +# Lifecycle ceviche = "0.5" +ctrlc = "3.1" + +# Serialization +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" + +# Error handling +anyhow = "1.0" + +# Utils, misc +camino = { version = "1.1", features = ["serde1"] } +cfg-if = "1.0" +parking_lot = "0.12" +tap = "1.0" + +# Async +futures = "0.3" +tokio = { version = "1.37", features = ["signal", "net", "io-util", "time", "rt", "rt-multi-thread", "sync", "macros", "parking_lot", "fs"] } + +# Logging +tracing = "0.1" [target.'cfg(windows)'.build-dependencies] embed-resource = "2.4" diff --git a/devolutions-agent/src/config.rs b/devolutions-agent/src/config.rs new file mode 100644 index 000000000..64707e514 --- /dev/null +++ b/devolutions-agent/src/config.rs @@ -0,0 +1,265 @@ +use std::env; +use std::fs::File; +use std::io::BufReader; +use std::sync::Arc; + +use anyhow::{bail, Context}; +use camino::{Utf8Path, Utf8PathBuf}; +use cfg_if::cfg_if; +use serde::{Deserialize, Serialize}; +use tap::prelude::*; + +cfg_if! { + if #[cfg(target_os = "windows")] { + const COMPANY_DIR: &str = "Devolutions"; + const PROGRAM_DIR: &str = "Agent"; + const APPLICATION_DIR: &str = "Devolutions\\Agent"; + } else if #[cfg(target_os = "macos")] { + const COMPANY_DIR: &str = "Devolutions"; + const PROGRAM_DIR: &str = "Agent"; + const APPLICATION_DIR: &str = "Devolutions Agent"; + } else { + const COMPANY_DIR: &str = "devolutions"; + const PROGRAM_DIR: &str = "agent"; + const APPLICATION_DIR: &str = "devolutions-agent"; + } +} + +#[derive(Debug, Clone)] +pub struct Conf { + pub log_file: Utf8PathBuf, + pub verbosity_profile: dto::VerbosityProfile, + pub debug: dto::DebugConf, +} + +impl Conf { + pub fn from_conf_file(conf_file: &dto::ConfFile) -> anyhow::Result { + let data_dir = get_data_dir(); + + let log_file = conf_file + .log_file + .clone() + .unwrap_or_else(|| Utf8PathBuf::from("agent")) + .pipe_ref(|path| normalize_data_path(path, &data_dir)); + + Ok(Conf { + log_file, + verbosity_profile: conf_file.verbosity_profile.unwrap_or_default(), + debug: conf_file.debug.clone().unwrap_or_default(), + }) + } +} + +/// Configuration Handle, source of truth for current configuration state +#[derive(Clone)] +pub struct ConfHandle { + inner: Arc, +} + +struct ConfHandleInner { + conf: parking_lot::RwLock>, + conf_file: parking_lot::RwLock>, +} + +impl ConfHandle { + /// Initializes configuration for this instance. + /// + /// It's best to call this only once to avoid inconsistencies. + pub fn init() -> anyhow::Result { + let conf_file = load_conf_file_or_generate_new()?; + let conf = Conf::from_conf_file(&conf_file).context("invalid configuration file")?; + + Ok(Self { + inner: Arc::new(ConfHandleInner { + conf: parking_lot::RwLock::new(Arc::new(conf)), + conf_file: parking_lot::RwLock::new(Arc::new(conf_file)), + }), + }) + } + + /// Returns current configuration state (do not hold it forever as it may become outdated) + pub fn get_conf(&self) -> Arc { + self.inner.conf.read().clone() + } + + /// Returns current configuration file state (do not hold it forever as it may become outdated) + pub fn get_conf_file(&self) -> Arc { + self.inner.conf_file.read().clone() + } +} + +fn save_config(conf: &dto::ConfFile) -> anyhow::Result<()> { + let conf_file_path = get_conf_file_path(); + let json = serde_json::to_string_pretty(conf).context("failed JSON serialization of configuration")?; + std::fs::write(&conf_file_path, json).with_context(|| format!("failed to write file at {conf_file_path}"))?; + Ok(()) +} + +fn get_data_dir() -> Utf8PathBuf { + if let Ok(config_path_env) = env::var("DAGENT_CONFIG_PATH") { + Utf8PathBuf::from(config_path_env) + } else { + let mut config_path = Utf8PathBuf::new(); + + if cfg!(target_os = "windows") { + let program_data_env = env::var("ProgramData").expect("ProgramData env variable"); + config_path.push(program_data_env); + config_path.push(COMPANY_DIR); + config_path.push(PROGRAM_DIR); + } else if cfg!(target_os = "macos") { + config_path.push("/Library/Application Support"); + config_path.push(APPLICATION_DIR); + } else { + config_path.push("/etc"); + config_path.push(APPLICATION_DIR); + } + + config_path + } +} + +fn get_conf_file_path() -> Utf8PathBuf { + get_data_dir().join("agent.json") +} + +fn normalize_data_path(path: &Utf8Path, data_dir: &Utf8Path) -> Utf8PathBuf { + if path.is_absolute() { + path.to_owned() + } else { + data_dir.join(path) + } +} + +fn load_conf_file(conf_path: &Utf8Path) -> anyhow::Result> { + match File::open(conf_path) { + Ok(file) => BufReader::new(file) + .pipe(serde_json::from_reader) + .map(Some) + .with_context(|| format!("invalid config file at {conf_path}")), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(anyhow::anyhow!(e).context(format!("couldn't open config file at {conf_path}"))), + } +} + +pub fn load_conf_file_or_generate_new() -> anyhow::Result { + let conf_file_path = get_conf_file_path(); + + let conf_file = match load_conf_file(&conf_file_path).context("failed to load configuration")? { + Some(conf_file) => conf_file, + None => { + let defaults = dto::ConfFile::generate_new(); + println!("Write default configuration to disk…"); + save_config(&defaults).context("failed to save configuration")?; + defaults + } + }; + + Ok(conf_file) +} + +pub mod dto { + use super::*; + + /// Source of truth for Agent configuration + /// + /// This struct represents the JSON file used for configuration as close as possible + /// and is not trying to be too smart. + /// + /// Unstable options are subject to change + #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "PascalCase")] + pub struct ConfFile { + /// Verbosity profile + #[serde(skip_serializing_if = "Option::is_none")] + pub verbosity_profile: Option, + + /// (Unstable) Folder and prefix for log files + #[serde(skip_serializing_if = "Option::is_none")] + pub log_file: Option, + + /// (Unstable) Unsafe debug options for developers + #[serde(default, rename = "__debug__", skip_serializing_if = "Option::is_none")] + pub debug: Option, + + // Other unofficial options. + // This field is useful so that we can deserialize + // and then losslessly serialize back all root keys of the config file. + #[serde(flatten)] + pub rest: serde_json::Map, + } + + impl ConfFile { + pub fn generate_new() -> Self { + Self { + verbosity_profile: None, + log_file: None, + debug: None, + rest: serde_json::Map::new(), + } + } + } + + /// Verbosity profile (pre-defined tracing directives) + #[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub enum VerbosityProfile { + /// The default profile, mostly info records + #[default] + Default, + /// Recommended profile for developers + Debug, + /// Show all traces + All, + /// Only show warnings and errors + Quiet, + } + + impl VerbosityProfile { + pub fn to_log_filter(self) -> &'static str { + match self { + VerbosityProfile::Default => "info", + VerbosityProfile::Debug => "info,devolutions_agent=debug", + VerbosityProfile::All => "trace", + VerbosityProfile::Quiet => "warn", + } + } + } + + /// Unsafe debug options that should only ever be used at development stage + /// + /// These options might change or get removed without further notice. + /// + /// Note to developers: all options should be safe by default, never add an option + /// that needs to be overridden manually in order to be safe. + #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] + pub struct DebugConf { + /// Directives string in the same form as the RUST_LOG environment variable + pub log_directives: Option, + } + + /// Manual Default trait implementation just to make sure default values are deliberates + #[allow(clippy::derivable_impls)] + impl Default for DebugConf { + fn default() -> Self { + Self { log_directives: None } + } + } + + impl DebugConf { + pub fn is_default(&self) -> bool { + Self::default().eq(self) + } + } +} + +pub fn handle_cli(command: &str) -> Result<(), anyhow::Error> { + match command { + "init" => { + let _config = load_conf_file_or_generate_new()?; + } + _ => { + bail!("unknown config command: {}", command); + } + } + + Ok(()) +} diff --git a/devolutions-agent/src/log.rs b/devolutions-agent/src/log.rs new file mode 100644 index 000000000..1876d00e6 --- /dev/null +++ b/devolutions-agent/src/log.rs @@ -0,0 +1,9 @@ +use devolutions_log::StaticLogConfig; + +pub struct AgentLog; + +impl StaticLogConfig for AgentLog { + const MAX_BYTES_PER_LOG_FILE: u64 = 3_000_000; // 3 MB; + const MAX_LOG_FILES: usize = 10; + const LOG_FILE_PREFIX: &'static str = "agent"; +} diff --git a/devolutions-agent/src/main.rs b/devolutions-agent/src/main.rs index 7e5668db4..06da0d61a 100644 --- a/devolutions-agent/src/main.rs +++ b/devolutions-agent/src/main.rs @@ -1,51 +1,21 @@ +#[macro_use] +extern crate tracing; + +mod config; +mod log; +mod service; + use std::env; use std::sync::mpsc; use ceviche::controller::*; use ceviche::{Service, ServiceEvent}; -pub const SERVICE_NAME: &str = "devolutions-agent"; -pub const DISPLAY_NAME: &str = "Devolutions Agent"; -pub const DESCRIPTION: &str = "Devolutions Agent service"; -pub const COMPANY_NAME: &str = "Devolutions"; - -pub struct AgentService { - pub service_name: String, - pub display_name: String, - pub description: String, - pub company_name: String, -} - -impl AgentService { - pub fn load() -> Option { - Some(AgentService { - service_name: SERVICE_NAME.to_string(), - display_name: DISPLAY_NAME.to_string(), - description: DESCRIPTION.to_string(), - company_name: COMPANY_NAME.to_string(), - }) - } - - pub fn get_service_name(&self) -> &str { - self.service_name.as_str() - } - - pub fn get_display_name(&self) -> &str { - self.display_name.as_str() - } - - pub fn get_description(&self) -> &str { - self.service_name.as_str() - } - - pub fn get_company_name(&self) -> &str { - self.company_name.as_str() - } - - pub fn start(&self) {} +use config::ConfHandle; +use service::AgentService; - pub fn stop(&self) {} -} +const BAD_CONFIG_ERR_CODE: u32 = 1; +const START_FAILED_ERR_CODE: u32 = 2; enum AgentServiceEvent {} @@ -55,34 +25,48 @@ fn agent_service_main( _args: Vec, _standalone_mode: bool, ) -> u32 { - let service = AgentService::load().expect("unable to load agent"); + let Ok(conf_handle) = ConfHandle::init() else { + // At this point, the logger is not yet initialized. + return BAD_CONFIG_ERR_CODE; + }; + + let mut service = match AgentService::load(conf_handle) { + Ok(service) => service, + Err(error) => { + // At this point, the logger may or may not be initialized. + error!(error = format!("{error:#}"), "Failed to load service"); + return START_FAILED_ERR_CODE; + } + }; - service.start(); + match service.start() { + Ok(()) => info!("{} service started", service::SERVICE_NAME), + Err(error) => { + error!(error = format!("{error:#}"), "Failed to start"); + return START_FAILED_ERR_CODE; + } + } loop { if let Ok(control_code) = rx.recv() { - match control_code { - ServiceEvent::Stop => { - service.stop(); - break; - } - _ => (), + info!("Received control code: {}", control_code); + + if let ServiceEvent::Stop = control_code { + service.stop(); + break; } } } + info!("{} service stopping", service::SERVICE_NAME); + 0 } Service!("agent", agent_service_main); fn main() { - let service = AgentService::load().expect("unable to load agent service"); - let mut controller = Controller::new( - service.get_service_name(), - service.get_display_name(), - service.get_description(), - ); + let mut controller = Controller::new(service::SERVICE_NAME, service::DISPLAY_NAME, service::DESCRIPTION); if let Some(cmd) = env::args().nth(1) { match cmd.as_str() { @@ -117,8 +101,14 @@ fn main() { agent_service_main(rx, _tx, vec![], true); } + "config" => { + let subcommand = env::args().nth(2).expect("missing config subcommand"); + if let Err(e) = config::handle_cli(subcommand.as_str()) { + eprintln!("[ERROR] Agent configuration failed: {}", e); + } + } _ => { - println!("invalid command: {}", cmd); + eprintln!("invalid command: {}", cmd); } } } else { diff --git a/devolutions-agent/src/service.rs b/devolutions-agent/src/service.rs new file mode 100644 index 000000000..1303ff94b --- /dev/null +++ b/devolutions-agent/src/service.rs @@ -0,0 +1,180 @@ +use tokio::runtime::{self, Runtime}; + +use crate::{config::ConfHandle, log::AgentLog}; +use anyhow::Context; +use devolutions_gateway_task::{ChildTask, ShutdownHandle, ShutdownSignal}; +use devolutions_log::{self, LoggerGuard}; +use std::time::Duration; + +pub const SERVICE_NAME: &str = "devolutions-agent"; +pub const DISPLAY_NAME: &str = "Devolutions Agent"; +pub const DESCRIPTION: &str = "Devolutions Agent service"; + +#[allow(clippy::large_enum_variant)] // `Running` variant is bigger than `Stopped` but we don't care +enum AgentState { + Stopped, + Running { + shutdown_handle: ShutdownHandle, + runtime: Runtime, + }, +} + +pub struct AgentService { + conf_handle: ConfHandle, + state: AgentState, + _logger_guard: LoggerGuard, +} + +impl AgentService { + pub fn load(conf_handle: ConfHandle) -> anyhow::Result { + let conf = conf_handle.get_conf(); + + let logger_guard = devolutions_log::init::( + &conf.log_file, + conf.verbosity_profile.to_log_filter(), + conf.debug.log_directives.as_deref(), + ) + .context("failed to setup logger")?; + + info!(version = env!("CARGO_PKG_VERSION")); + + let conf_file = conf_handle.get_conf_file(); + trace!(?conf_file); + + if !conf.debug.is_default() { + warn!( + ?conf.debug, + "**DEBUG OPTIONS ARE ENABLED, PLEASE DO NOT USE IN PRODUCTION**", + ); + } + + Ok(AgentService { + conf_handle, + state: AgentState::Stopped, + _logger_guard: logger_guard, + }) + } + + pub fn start(&mut self) -> anyhow::Result<()> { + let runtime = runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to create runtime"); + + let config = self.conf_handle.clone(); + + // create_futures needs to be run in the runtime in order to bind the sockets. + let tasks = runtime.block_on(spawn_tasks(config))?; + + trace!("Tasks created"); + + let mut join_all = futures::future::select_all(tasks.inner.into_iter().map(|child| Box::pin(child.join()))); + + runtime.spawn(async { + loop { + let (result, _, rest) = join_all.await; + + match result { + Ok(Ok(())) => trace!("A task terminated gracefully"), + Ok(Err(error)) => error!(error = format!("{error:#}"), "A task failed"), + Err(error) => error!(%error, "Something went very wrong with a task"), + } + + if rest.is_empty() { + break; + } else { + join_all = futures::future::select_all(rest); + } + } + }); + + self.state = AgentState::Running { + shutdown_handle: tasks.shutdown_handle, + runtime, + }; + + Ok(()) + } + + pub fn stop(&mut self) { + match std::mem::replace(&mut self.state, AgentState::Stopped) { + AgentState::Stopped => { + info!("Attempted to stop agent service, but it's already stopped"); + } + AgentState::Running { + shutdown_handle, + runtime, + } => { + info!("Stopping agent service"); + + // Send shutdown signals to all tasks + shutdown_handle.signal(); + + runtime.block_on(async move { + const MAX_COUNT: usize = 3; + let mut count = 0; + + loop { + tokio::select! { + _ = shutdown_handle.all_closed() => { + debug!("All tasks are terminated"); + break; + } + _ = tokio::time::sleep(Duration::from_secs(10)) => { + count += 1; + + if count >= MAX_COUNT { + warn!("Terminate forcefully the lingering tasks"); + break; + } else { + warn!("Termination of certain tasks is experiencing significant delays"); + } + } + } + } + }); + + // Wait for 1 more second before forcefully shutting down the runtime + runtime.shutdown_timeout(Duration::from_secs(1)); + + self.state = AgentState::Stopped; + } + } + } +} + +struct Tasks { + inner: Vec>>, + shutdown_handle: ShutdownHandle, + shutdown_signal: ShutdownSignal, +} + +impl Tasks { + fn new() -> Self { + let (shutdown_handle, shutdown_signal) = devolutions_gateway_task::ShutdownHandle::new(); + + Self { + inner: Vec::new(), + shutdown_handle, + shutdown_signal, + } + } + + fn register(&mut self, task: T) + where + T: devolutions_gateway_task::Task> + 'static, + { + let child = devolutions_gateway_task::spawn_task(task, self.shutdown_signal.clone()); + self.inner.push(child); + } +} + +async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { + let conf = conf_handle.get_conf(); + + let mut tasks = Tasks::new(); + + tasks.register(devolutions_log::LogDeleterTask::::new(conf.log_file.clone())); + + Ok(tasks) +} diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 252d8ef60..d0613fe93 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -19,6 +19,7 @@ openapi = ["dep:utoipa"] transport = { path = "../crates/transport" } jmux-proxy = { path = "../crates/jmux-proxy" } devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" } +devolutions-log = { path = "../crates/devolutions-log" } ironrdp-pdu = { version = "0.1", git = "https://github.com/Devolutions/IronRDP", rev = "4844e77b7f65024d85ba74b1824013eda6eb32b2" } ironrdp-rdcleanpath = { version = "0.1", git = "https://github.com/Devolutions/IronRDP", rev = "4844e77b7f65024d85ba74b1824013eda6eb32b2" } ceviche = "0.6" diff --git a/devolutions-gateway/src/api/diagnostics.rs b/devolutions-gateway/src/api/diagnostics.rs index bdea12f02..38daa4a2b 100644 --- a/devolutions-gateway/src/api/diagnostics.rs +++ b/devolutions-gateway/src/api/diagnostics.rs @@ -9,6 +9,7 @@ use crate::config::Conf; use crate::extract::DiagnosticsReadScope; use crate::http::HttpError; use crate::listener::ListenerUrls; +use crate::log::GatewayLog; use crate::DgwState; pub fn make_router(state: DgwState) -> Router { @@ -122,7 +123,7 @@ async fn get_logs( ) -> Result { let conf = conf_handle.get_conf(); - let latest_log_file_path = crate::log::find_latest_log_file(conf.log_file.as_path()) + let latest_log_file_path = devolutions_log::find_latest_log_file::(conf.log_file.as_path()) .await .map_err(HttpError::internal().with_msg("latest log file not found").err())?; diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index ee84c2be5..ec14b7a5d 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -1035,6 +1035,22 @@ pub mod dto { Quiet, } + impl VerbosityProfile { + pub fn to_log_filter(self) -> &'static str { + match self { + VerbosityProfile::Default => "info", + VerbosityProfile::Debug => { + "info,devolutions_gateway=debug,devolutions_gateway::api=trace,jmux_proxy=debug,tower_http=trace" + } + VerbosityProfile::Tls => { + "info,devolutions_gateway=debug,devolutions_gateway::tls=trace,rustls=trace,tokio_rustls=debug" + } + VerbosityProfile::All => "trace", + VerbosityProfile::Quiet => "warn", + } + } + } + /// Unsafe debug options that should only ever be used at development stage /// /// These options might change or get removed without further notice. diff --git a/devolutions-gateway/src/log.rs b/devolutions-gateway/src/log.rs index 2d8597584..80a580e5d 100644 --- a/devolutions-gateway/src/log.rs +++ b/devolutions-gateway/src/log.rs @@ -1,210 +1,9 @@ -use std::io; -use std::time::SystemTime; +use devolutions_log::StaticLogConfig; -use anyhow::Context as _; -use async_trait::async_trait; -use camino::{Utf8Path, Utf8PathBuf}; -use devolutions_gateway_task::{ShutdownSignal, Task}; -use tokio::fs; -use tokio::time::{sleep, Duration}; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_appender::rolling; -use tracing_subscriber::prelude::*; -use tracing_subscriber::{fmt, EnvFilter}; +pub struct GatewayLog; -use crate::config::dto::VerbosityProfile; - -const MAX_BYTES_PER_LOG_FILE: u64 = 3_000_000; // 3 MB -const MAX_LOG_FILES: usize = 10; - -pub struct LoggerGuard { - _file_guard: WorkerGuard, - _stdio_guard: WorkerGuard, -} - -struct LogPathCfg<'a> { - folder: &'a Utf8Path, - prefix: &'a str, -} - -impl<'a> LogPathCfg<'a> { - pub fn from_path(path: &'a Utf8Path) -> anyhow::Result { - if path.is_dir() { - Ok(Self { - folder: path, - prefix: "gateway", - }) - } else { - Ok(Self { - folder: path.parent().context("invalid log path (parent)")?, - prefix: path.file_name().context("invalid log path (file_name)")?, - }) - } - } -} - -fn profile_to_directives(profile: VerbosityProfile) -> &'static str { - match profile { - VerbosityProfile::Default => "info", - VerbosityProfile::Debug => { - "info,devolutions_gateway=debug,devolutions_gateway::api=trace,jmux_proxy=debug,tower_http=trace" - } - VerbosityProfile::Tls => { - "info,devolutions_gateway=debug,devolutions_gateway::tls=trace,rustls=trace,tokio_rustls=debug" - } - VerbosityProfile::All => "trace", - VerbosityProfile::Quiet => "warn", - } -} - -pub fn init( - path: &Utf8Path, - profile: VerbosityProfile, - debug_filtering_directives: Option<&str>, -) -> anyhow::Result { - let log_cfg = LogPathCfg::from_path(path)?; - let file_appender = rolling::Builder::new() - .rotation(rolling::Rotation::max_bytes(MAX_BYTES_PER_LOG_FILE)) - .filename_prefix(log_cfg.prefix) - .filename_suffix("log") - .max_log_files(MAX_LOG_FILES) - .build(log_cfg.folder) - .context("couldn’t create file appender")?; - let (file_non_blocking, file_guard) = tracing_appender::non_blocking(file_appender); - let file_layer = fmt::layer().with_writer(file_non_blocking).with_ansi(false); - - let (non_blocking_stdio, stdio_guard) = tracing_appender::non_blocking(std::io::stdout()); - let stdio_layer = fmt::layer().with_writer(non_blocking_stdio); - - let env_filter = EnvFilter::try_new(profile_to_directives(profile)) - .context("invalid built-in filtering directives (this is a bug)")?; - - // Optionally add additional debugging filtering directives - let env_filter = debug_filtering_directives - .into_iter() - .flat_map(|directives| directives.split(',')) - .fold(env_filter, |env_filter, directive| { - env_filter.add_directive(directive.parse().unwrap()) - }); - - tracing_subscriber::registry() - .with(file_layer) - .with(stdio_layer) - .with(env_filter) - .init(); - - Ok(LoggerGuard { - _file_guard: file_guard, - _stdio_guard: stdio_guard, - }) -} - -/// Find latest log file (by age) -/// -/// Given path is used to filter out by file name prefix. -#[instrument] -pub async fn find_latest_log_file(prefix: &Utf8Path) -> anyhow::Result { - let cfg = LogPathCfg::from_path(prefix)?; - - let mut read_dir = fs::read_dir(cfg.folder).await.context("couldn't read directory")?; - - let mut most_recent_time = SystemTime::UNIX_EPOCH; - let mut most_recent = None; - - while let Ok(Some(entry)) = read_dir.next_entry().await { - match entry.file_name().to_str() { - Some(file_name) if file_name.starts_with(cfg.prefix) && file_name.contains("log") => { - debug!(file_name, "Found a log file"); - match entry.metadata().await.and_then(|metadata| metadata.modified()) { - Ok(modified) if modified > most_recent_time => { - most_recent_time = modified; - most_recent = Some(entry.path()); - } - Ok(_) => {} - Err(error) => { - warn!(%error, file_name, "Couldn't retrieve metadata for file"); - } - } - } - _ => continue, - } - } - - most_recent.context("no file found") -} - -/// File deletion task (by age) -/// -/// Given path is used to filter out by file name prefix. -pub struct LogDeleterTask { - pub prefix: Utf8PathBuf, -} - -#[async_trait] -impl Task for LogDeleterTask { - type Output = anyhow::Result<()>; - - const NAME: &'static str = "log deleter"; - - async fn run(self, shutdown_signal: ShutdownSignal) -> Self::Output { - log_deleter_task(&self.prefix, shutdown_signal).await - } -} - -#[instrument(skip(shutdown_signal))] -async fn log_deleter_task(prefix: &Utf8Path, mut shutdown_signal: ShutdownSignal) -> anyhow::Result<()> { - const TASK_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); // once per day - const MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24 * 90); // 90 days - - debug!("Task started"); - - let cfg = LogPathCfg::from_path(prefix)?; - - loop { - match fs::read_dir(cfg.folder).await { - Ok(mut read_dir) => { - while let Ok(Some(entry)) = read_dir.next_entry().await { - match entry.file_name().to_str() { - Some(file_name) if file_name.starts_with(cfg.prefix) && file_name.contains("log") => { - debug!(file_name, "Found a log file"); - match entry - .metadata() - .await - .and_then(|metadata| metadata.modified()) - .and_then(|time| time.elapsed().map_err(|e| io::Error::new(io::ErrorKind::Other, e))) - { - Ok(modified) if modified > MAX_AGE => { - info!(file_name, "Delete log file"); - if let Err(error) = fs::remove_file(entry.path()).await { - warn!(%error, file_name, "Couldn't delete log file"); - } - } - Ok(_) => { - trace!(file_name, "Keep this log file"); - } - Err(error) => { - warn!(%error, file_name, "Couldn't retrieve metadata for file"); - } - } - } - _ => continue, - } - } - } - Err(error) => { - warn!(%error, "Couldn't read log folder"); - } - } - - tokio::select! { - _ = sleep(TASK_INTERVAL) => {} - _ = shutdown_signal.wait() => { - break; - } - } - } - - debug!("Task terminated"); - - Ok(()) +impl StaticLogConfig for GatewayLog { + const MAX_BYTES_PER_LOG_FILE: u64 = 3_000_000; // 3 MB; + const MAX_LOG_FILES: usize = 10; + const LOG_FILE_PREFIX: &'static str = "gateway"; } diff --git a/devolutions-gateway/src/service.rs b/devolutions-gateway/src/service.rs index 289914879..dceaf4cdd 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -1,13 +1,14 @@ use anyhow::Context as _; use devolutions_gateway::config::{Conf, ConfHandle}; use devolutions_gateway::listener::GatewayListener; -use devolutions_gateway::log::{self, LoggerGuard}; +use devolutions_gateway::log::GatewayLog; use devolutions_gateway::recording::recording_message_channel; use devolutions_gateway::session::session_manager_channel; use devolutions_gateway::subscriber::subscriber_channel; use devolutions_gateway::token::{CurrentJrl, JrlTokenClaims}; use devolutions_gateway::DgwState; use devolutions_gateway_task::{ChildTask, ShutdownHandle, ShutdownSignal}; +use devolutions_log::{self, LoggerGuard}; use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; @@ -37,9 +38,9 @@ impl GatewayService { pub fn load(conf_handle: ConfHandle) -> anyhow::Result { let conf = conf_handle.get_conf(); - let logger_guard = log::init( + let logger_guard = devolutions_log::init::( &conf.log_file, - conf.verbosity_profile, + conf.verbosity_profile.to_log_filter(), conf.debug.log_directives.as_deref(), ) .context("failed to setup logger")?; @@ -237,9 +238,9 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { tasks.register(devolutions_gateway::token::CleanupTask { token_cache }); - tasks.register(devolutions_gateway::log::LogDeleterTask { - prefix: conf.log_file.clone(), - }); + tasks.register(devolutions_log::LogDeleterTask::::new( + conf.log_file.clone(), + )); tasks.register(devolutions_gateway::subscriber::SubscriberPollingTask { sessions: session_manager_handle, diff --git a/package/AgentLinux/Dockerfile b/package/AgentLinux/Dockerfile new file mode 100644 index 000000000..089b21ab2 --- /dev/null +++ b/package/AgentLinux/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/powershell:debian-bullseye-slim +LABEL maintainer "Devolutions Inc." + +WORKDIR /opt/devolutions/agent + +ENV DAGENT_EXECUTABLE_PATH="/opt/devolutions/agent/devolutions-agent" + +COPY devolutions-agent $DAGENT_EXECUTABLE_PATH + +RUN apt-get update +RUN apt-get install -y --no-install-recommends ca-certificates curl +RUN rm -rf /var/lib/apt/lists/* + +EXPOSE 8080 + +ENTRYPOINT [ "./devolutions-agent" ] diff --git a/package/AgentLinux/agent/debian/copyright b/package/AgentLinux/agent/debian/copyright new file mode 100644 index 000000000..344399144 --- /dev/null +++ b/package/AgentLinux/agent/debian/copyright @@ -0,0 +1,8 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: devolutions-agent +Source: https://devolutions.net/agent + +Files: * +Copyright: 2020-2024 Devolutions Inc. +License: non-free + . diff --git a/package/AgentLinux/agent/debian/postinst b/package/AgentLinux/agent/debian/postinst new file mode 100644 index 000000000..641dc5c54 --- /dev/null +++ b/package/AgentLinux/agent/debian/postinst @@ -0,0 +1,16 @@ +#!/bin/sh + +# System-wide package configuration. +DEFAULTS_FILE="/etc/default/devolutions-agent" + +if [ ! -d /etc/devolutions-agent ]; then + /bin/mkdir /etc/devolutions-agent + /bin/chmod 655 /etc/devolutions-agent +fi + +if [ -d /run/systemd/system ]; then + /usr/bin/devolutions-agent service register >/dev/null + /usr/bin/devolutions-agent service config init >/dev/null + systemctl restart devolutions-agent >/dev/null 2>&1 + systemctl daemon-reload +fi diff --git a/package/AgentLinux/agent/debian/postrm b/package/AgentLinux/agent/debian/postrm new file mode 100644 index 000000000..a9cb7e8be --- /dev/null +++ b/package/AgentLinux/agent/debian/postrm @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +action="$1" + +# Only do complete clean-up on purge. +if [ "$action" != "purge" ] ; then + exit 0 +fi + +# System-wide package configuration. +DEFAULTS_FILE="/etc/default/devolutions-agent" + +if [ -s "$DEFAULTS_FILE" ]; then + rm "$DEFAULTS_FILE" || exit 1 +fi diff --git a/package/AgentLinux/agent/debian/prerm b/package/AgentLinux/agent/debian/prerm new file mode 100644 index 000000000..853042f30 --- /dev/null +++ b/package/AgentLinux/agent/debian/prerm @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +action="$1" +if [ "$2" = "in-favour" ]; then + # Treat conflict remove as an upgrade. + action="upgrade" +fi + +# Don't clean-up just for an upgrade.` +if [ "$action" = "upgrade" ] ; then + exit 0 +fi + +if [ -d /run/systemd/system ]; then + systemctl stop devolutions-agent >/dev/null 2>&1 + /usr/bin/devolutions-agent service unregister >/dev/null +fi diff --git a/package/AgentLinux/agent/template/control b/package/AgentLinux/agent/template/control new file mode 100644 index 000000000..b372455dd --- /dev/null +++ b/package/AgentLinux/agent/template/control @@ -0,0 +1,13 @@ +Source: devolutions-agent +Section: non-free/net +Priority: optional +Maintainer: {{ packager }} <{{ email }}> +Build-Depends: debhelper (>= 8.0.0) +Standards-Version: 3.9.4 +Homepage: {{ website }} + +Package: devolutions-agent +Architecture: {{ arch }} +Depends: {{ deps }} +Description: Devolutions Agent + {{ website }} diff --git a/package/AgentLinux/agent/template/rules b/package/AgentLinux/agent/template/rules new file mode 100644 index 000000000..84cbef0ed --- /dev/null +++ b/package/AgentLinux/agent/template/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f +%: + dh $@ +override_dh_auto_clean: +override_dh_auto_configure: +override_dh_auto_build: +override_dh_auto_test: +override_dh_auto_install: +override_dh_usrlocal: + install -D -m 0755 {{ dagent_executable }} $$(pwd)/debian/devolutions-agent/usr/bin/devolutions-agent +override_dh_install: + dh_install +override_dh_shlibdeps: + {{ dh_shlibdeps }} \ No newline at end of file diff --git a/package/AgentLinux/template/changelog b/package/AgentLinux/template/changelog new file mode 100644 index 000000000..a7630c312 --- /dev/null +++ b/package/AgentLinux/template/changelog @@ -0,0 +1,6 @@ +{{ package }} ({{ version }}) {{ distro }}; urgency=low + + * + + -- {{ packager }} <{{ email }}> {{ date }} + diff --git a/package/AgentLinux/template/copyright b/package/AgentLinux/template/copyright new file mode 100644 index 000000000..b3eb01440 --- /dev/null +++ b/package/AgentLinux/template/copyright @@ -0,0 +1,9 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: {{ package }} +Source: {{ website }} + +Files: * +Copyright: {{ year }} {{ packager }} +License: non-free + . + diff --git a/package/AgentWindowsManaged/.editorconfig b/package/AgentWindowsManaged/.editorconfig new file mode 100644 index 000000000..690bde15e --- /dev/null +++ b/package/AgentWindowsManaged/.editorconfig @@ -0,0 +1,7 @@ +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +[*.tt] +end_of_line = crlf \ No newline at end of file diff --git a/package/AgentWindowsManaged/.gitignore b/package/AgentWindowsManaged/.gitignore new file mode 100644 index 000000000..47fa67f51 --- /dev/null +++ b/package/AgentWindowsManaged/.gitignore @@ -0,0 +1,5 @@ +Debug +Release +wix +fr-FR.* +*_missing.json \ No newline at end of file diff --git a/package/AgentWindowsManaged/Actions/Buffer.cs b/package/AgentWindowsManaged/Actions/Buffer.cs new file mode 100644 index 000000000..1e14f28f1 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/Buffer.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.InteropServices; + +namespace DevolutionsAgent.Actions; + +internal class Buffer : IDisposable +{ + public IntPtr Handle { get; } + + public Buffer(int size) + { + this.Handle = Marshal.AllocHGlobal(size); + } + + public static implicit operator IntPtr(Buffer b) => b.Handle; + + private void ReleaseUnmanagedResources() + { + Marshal.FreeHGlobal(this.Handle); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~Buffer() + { + ReleaseUnmanagedResources(); + } +} diff --git a/package/AgentWindowsManaged/Actions/CustomActions.cs b/package/AgentWindowsManaged/Actions/CustomActions.cs new file mode 100644 index 000000000..efc1aa472 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/CustomActions.cs @@ -0,0 +1,558 @@ +using DevolutionsAgent.Properties; +using DevolutionsAgent.Resources; +using Microsoft.Deployment.WindowsInstaller; +using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; +using System.ServiceProcess; +using System.Text; +using WixSharp; +using File = System.IO.File; + +namespace DevolutionsAgent.Actions +{ + public class CustomActions + { + private static readonly string[] ConfigFiles = new[] { + "agent.json", + }; + + private const int MAX_PATH = 260; // Defined in windows.h + + private static string ProgramDataDirectory => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Devolutions", "Agent"); + + [CustomAction] + public static ActionResult CheckInstalledNetFx45Version(Session session) + { + uint version = session.Get(AgentProperties.netFx45Version); + + if (version < 394802) //4.6.2 + { + session.Log($"netfx45 version: {version} is too old"); + return ActionResult.Failure; + } + + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult CleanAgentConfig(Session session) + { + if (!ConfigFiles.Any(x => File.Exists(Path.Combine(ProgramDataDirectory, x)))) + { + return ActionResult.Success; + } + + try + { + string zipFile = $"{Path.Combine(Path.GetTempPath(), session.Get(AgentProperties.installId).ToString())}.zip"; + using ZipArchive archive = ZipFile.Open(zipFile, ZipArchiveMode.Create); + + WinAPI.MoveFileEx(zipFile, IntPtr.Zero, WinAPI.MOVEFILE_DELAY_UNTIL_REBOOT); + + foreach (string configFile in ConfigFiles) + { + string configFilePath = Path.Combine(ProgramDataDirectory, configFile); + + if (File.Exists(configFilePath)) + { + archive.CreateEntryFromFile(configFilePath, configFile); + } + } + + foreach (string configFile in ConfigFiles) + { + try + { + File.Delete(Path.Combine(ProgramDataDirectory, configFile)); + } + catch + { + } + } + } + catch (Exception e) + { + session.Log($"failed to archive existing config: {e}"); + return ActionResult.Failure; + } + + + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult CleanAgentConfigRollback(Session session) + { + string zipFile = $"{Path.Combine(Path.GetTempPath(), session.Get(AgentProperties.installId).ToString())}.zip"; + + if (!File.Exists(zipFile)) + { + return ActionResult.Success; + } + + try + { + foreach (string configFile in ConfigFiles) + { + try + { + File.Delete(Path.Combine(ProgramDataDirectory, configFile)); + } + catch + { + } + } + + using ZipArchive archive = ZipFile.Open(zipFile, ZipArchiveMode.Read); + archive.ExtractToDirectory(ProgramDataDirectory); + + try + { + File.Delete(zipFile); + } + catch + { + } + } + catch (Exception e) + { + session.Log($"failed to restore existing config: {e}"); + return ActionResult.Failure; + } + + return ActionResult.Failure; + } + + [CustomAction] + public static ActionResult CreateProgramDataDirectory(Session session) + { + string path = ProgramDataDirectory; + + try + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + catch (Exception e) + { + session.Log($"failed to evaluate or create path {path}: {e}"); + return ActionResult.Failure; + } + + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult GetInstallDirFromRegistry(Session session) + { + try + { + using RegistryKey localKey = RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, RegistryView.Registry64); + using RegistryKey agentKey = localKey.OpenSubKey($@"Software\{Includes.VENDOR_NAME}\{Includes.SHORT_NAME}"); + string installDirValue = (string)agentKey.GetValue("InstallDir"); + + if (string.IsNullOrEmpty(installDirValue)) + { + throw new Exception("failed to read installdir path from registry: path is null or empty"); + } + + session.Log($"read installdir path from registry: {installDirValue}"); + session[AgentProperties.InstallDir] = installDirValue; + + return ActionResult.Success; + } + catch (Exception e) + { + session.Log($"failed to read installdir path from registry: {e}"); + } + + return ActionResult.Failure; + } + + [CustomAction] + public static ActionResult GetInstalledNetFx45Version(Session session) + { + if (!TryGetInstalledNetFx45Version(out uint version)) + { + return ActionResult.Failure; + } + + session.Log($"read netFxRelease path from registry: {version}"); + session.Set(AgentProperties.netFx45Version, version); + + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult RestartAgent(Session session) + { + try + { + using ServiceManager sm = new(WinAPI.SC_MANAGER_CONNECT, LogDelegate.WithSession(session)); + + if (!Service.TryOpen( + sm, Includes.SERVICE_NAME, + WinAPI.SERVICE_START | WinAPI.SERVICE_QUERY_STATUS | WinAPI.SERVICE_STOP, + out Service service, LogDelegate.WithSession(session))) + { + return ActionResult.Failure; + } + + using (service) + { + service.Restart(); + } + + return ActionResult.Success; + } + catch (Exception e) + { + session.Log($"failed to restart service: {e}"); + return ActionResult.Failure; + } + } + + [CustomAction] + public static ActionResult RollbackConfig(Session session) + { + string path = ProgramDataDirectory; + + foreach (string configFile in ConfigFiles.Select(x => Path.Combine(path, x))) + { + try + { + if (!File.Exists(configFile)) + { + continue; + } + + File.Delete(configFile); + } + catch (Exception e) + { + session.Log($"failed to rollback file {configFile}: {e}"); + } + } + + // Best effort, always return success + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult SetInstallId(Session session) + { + session.Set(AgentProperties.installId, Guid.NewGuid()); + return ActionResult.Success; + } + + [CustomAction] + public static ActionResult SetProgramDataDirectoryPermissions(Session session) + { + try + { + SetFileSecurity(session, ProgramDataDirectory, Includes.PROGRAM_DATA_SDDL); + return ActionResult.Success; + } + catch (Exception e) + { + session.Log($"failed to set permissions: {e}"); + return ActionResult.Failure; + } + } + + [CustomAction] + public static ActionResult StartAgentIfNeeded(Session session) + { + try + { + using ServiceManager sm = new(WinAPI.SC_MANAGER_CONNECT); + + if (!Service.TryOpen(sm, Includes.SERVICE_NAME, + WinAPI.SERVICE_START | WinAPI.SERVICE_QUERY_CONFIG, + out Service service, LogDelegate.WithSession(session))) + { + return ActionResult.Failure; + } + + using (service) + { + service.StartIfNeeded(); + } + + return ActionResult.Success; + } + catch (Exception e) + { + session.Log($"failed to start service: {e}"); + return ActionResult.Failure; + } + } + + private static SafeFileHandle CreateSharedTempFile(Session session) + { + string tempPath = Path.GetTempPath(); + StringBuilder sb = new(MAX_PATH); + + if (WinAPI.GetTempFileName(tempPath, "DGW", 0, sb) == 0) + { + session.Log($"GetTempFileName failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + string tempFilePath = sb.ToString(); + + WinAPI.SECURITY_ATTRIBUTES sa = new() + { + nLength = (uint)Marshal.SizeOf(), + lpSecurityDescriptor = IntPtr.Zero, + bInheritHandle = true + }; + + using Buffer pSa = new(Marshal.SizeOf()); + Marshal.StructureToPtr(sa, pSa, false); + + SafeFileHandle handle = WinAPI.CreateFile(tempFilePath, + WinAPI.GENERIC_WRITE, WinAPI.FILE_SHARE_READ | WinAPI.FILE_SHARE_WRITE, pSa, WinAPI.CREATE_ALWAYS, + WinAPI.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero); + + if (handle.IsInvalid) + { + int errno = Marshal.GetLastWin32Error(); + session.Log($"CreateFile failed (error: {errno})"); + + handle.Dispose(); + + if (!WinAPI.DeleteFile(tempFilePath)) + { + session.Log($"DeleteFile failed (error: {Marshal.GetLastWin32Error()})"); + } + + throw new Win32Exception(errno); + } + + if (!WinAPI.MoveFileEx(tempFilePath, IntPtr.Zero, WinAPI.MOVEFILE_DELAY_UNTIL_REBOOT)) + { + session.Log($"MoveFileEx failed (error: {Marshal.GetLastWin32Error()})"); + } + + return handle; + } + + private static uint ExecuteCommand(Session session, SafeFileHandle hTempFile, string command) + { + WinAPI.STARTUPINFO si = new() + { + cb = (uint)Marshal.SizeOf() + }; + + if (hTempFile.IsInvalid) + { + session.Log($"got an invalid file handle; command output will not be redirected"); + } + else + { + si.dwFlags = WinAPI.STARTF_USESTDHANDLES; + si.hStdInput = WinAPI.GetStdHandle(WinAPI.STD_INPUT_HANDLE); + si.hStdOutput = hTempFile.DangerousGetHandle(); + si.hStdError = hTempFile.DangerousGetHandle(); + } + + using Buffer pSi = new(Marshal.SizeOf()); + Marshal.StructureToPtr(si, pSi, false); + + WinAPI.PROCESS_INFORMATION pi = new(); + + using Buffer pPi = new(Marshal.SizeOf()); + Marshal.StructureToPtr(pi, pPi, false); + + if (session.Get(AgentProperties.debugPowerShell)) + { + session.Log($"Executing command: {command}"); + } + + if (!WinAPI.CreateProcess(null, command, IntPtr.Zero, IntPtr.Zero, + true, WinAPI.CREATE_NO_WINDOW, IntPtr.Zero, null, pSi, pPi)) + { + session.Log($"CreateProcess failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + uint exitCode = 1; + + try + { + pi = Marshal.PtrToStructure(pPi); + + // Give the process reasonable time to finish, don't hang the installer + if (WinAPI.WaitForSingleObject(pi.hProcess, (uint)TimeSpan.FromMinutes(1).TotalMilliseconds) != 0) // WAIT_OBJECT_0 + { + session.Log("timeout or error waiting for sub process"); + + if (!WinAPI.TerminateProcess(pi.hProcess, exitCode)) + { + session.Log($"TerminateProcess failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + if (!WinAPI.GetExitCodeProcess(pi.hProcess, out exitCode)) + { + session.Log($"GetExitCodeProcess failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (exitCode != 0) + { + session.Log($"sub process returned a non-zero exit code: {exitCode})"); + } + } + finally + { + WinAPI.CloseHandle(pi.hProcess); + WinAPI.CloseHandle(pi.hThread); + } + + return exitCode; + } + + private static ActionResult ExecuteCommand(Session session, string command) + { + using SafeFileHandle hTempFile = CreateSharedTempFile(session); + + uint exitCode; + + try + { + exitCode = ExecuteCommand(session, hTempFile, command); + } + catch (Exception e) + { + session.Log($"command execution failure: {e}"); + + using Record record = new(3) + { + FormatString = "Command execution failure: [1]", + }; + + record.SetString(1, e.ToString()); + session.Message(InstallMessage.Error | (uint)MessageButtons.OK, record); + return ActionResult.Failure; + } + + if (exitCode != 0) + { + StringBuilder tempFilePathBuilder = new(MAX_PATH); + uint pathLength = WinAPI.GetFinalPathNameByHandle(hTempFile.DangerousGetHandle(), tempFilePathBuilder, MAX_PATH, 0); + string result = "unknown"; + + try + { + if (pathLength is > 0 and < MAX_PATH) + { + string tempFilePath = tempFilePathBuilder.ToString().TrimStart('\\', '?'); + + using FileStream fileStream = new(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using StreamReader streamReader = new(fileStream); + result = streamReader.ReadToEnd(); + } + } + catch (Exception e) + { + session.Log($"error reading error from temp file: {e}"); + } + + using Record record = new(3) + { + FormatString = "Command execution failure: [1]", + }; + + hTempFile.Close(); + + record.SetString(1, result); + session.Message(InstallMessage.Error | (uint)MessageButtons.OK, record); + + return ActionResult.Failure; + } + + return ActionResult.Success; + } + + private static string FormatHttpUrl(string scheme, uint port) + { + string url = $"{scheme}://*"; + + if ((scheme.Equals(Constants.HttpProtocol) && port != 80) || (scheme.Equals(Constants.HttpsProtocol) && port != 443)) + { + url += $":{port}"; + } + + return url; + } + + public static void SetFileSecurity(Session session, string path, string sddl) + { + const uint sdRevision = 1; + IntPtr pSd = new IntPtr(); + UIntPtr pSzSd = new UIntPtr(); + + try + { + if (!WinAPI.ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl, sdRevision, out pSd, out pSzSd)) + { + session.Log($"ConvertStringSecurityDescriptorToSecurityDescriptorW failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!WinAPI.SetFileSecurityW(path, WinAPI.DACL_SECURITY_INFORMATION, pSd)) + { + session.Log($"SetFileSecurityW failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + finally + { + if (pSd != IntPtr.Zero) + { + WinAPI.LocalFree(pSd); + } + } + } + + public static bool TryGetInstalledNetFx45Version(out uint version) + { + version = 0; + + try + { + // https://learn.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed + using RegistryKey localKey = RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, RegistryView.Registry64); + using RegistryKey netFxKey = localKey.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"); + + if (netFxKey is null) + { + // If the Full subkey is missing, then .NET Framework 4.5 or above isn't installed + return false; + } + + version = Convert.ToUInt32(netFxKey.GetValue("Release")); + + return true; + } + catch + { + return false; + } + } + } +} diff --git a/package/AgentWindowsManaged/Actions/GatewayActions.cs b/package/AgentWindowsManaged/Actions/GatewayActions.cs new file mode 100644 index 000000000..2cc596651 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/GatewayActions.cs @@ -0,0 +1,234 @@ +using DevolutionsAgent.Properties; +using DevolutionsAgent.Resources; +using System; +using System.Collections.Generic; +using System.Linq; +using WixSharp; +using Action = WixSharp.Action; + +namespace DevolutionsAgent.Actions; + +internal static class GatewayActions +{ + // Immediate sequence + + // Set helper properties to determine what the installer is doing + private static readonly SetPropertyAction isFirstInstall = new( + new Id(nameof(isFirstInstall)), AgentProperties.firstInstall.Id, $"{true}", Return.check, When.After, Step.FindRelatedProducts, + new Condition("NOT Installed AND NOT WIX_UPGRADE_DETECTED AND NOT WIX_DOWNGRADE_DETECTED")); + + private static readonly SetPropertyAction isUpgrading = new( + new Id(nameof(isUpgrading)), AgentProperties.upgrading.Id, $"{true}", Return.check, When.After, new Step(isFirstInstall.Id), + new Condition("WIX_UPGRADE_DETECTED AND NOT(REMOVE= \"ALL\")")); + + private static readonly SetPropertyAction isRemovingForUpgrade = new( + new Id(nameof(isRemovingForUpgrade)), AgentProperties.removingForUpgrade.Id, Return.check, When.After, Step.RemoveExistingProducts, + new Condition("(REMOVE = \"ALL\") AND UPGRADINGPRODUCTCODE")); + + private static readonly SetPropertyAction isUninstalling = new( + new Id(nameof(isUninstalling)), AgentProperties.uninstalling.Id, $"{true}", Return.check, When.After, new Step(isUpgrading.Id), + new Condition("Installed AND REMOVE AND NOT(WIX_UPGRADE_DETECTED OR UPGRADINGPRODUCTCODE)")); + + private static readonly SetPropertyAction isMaintenance = new( + new Id(nameof(isMaintenance)), AgentProperties.maintenance.Id, $"{true}", Return.check, When.After, new Step(isUninstalling.Id), + new Condition($"Installed AND NOT {AgentProperties.upgrading.Id} AND NOT {AgentProperties.uninstalling.Id} AND NOT UPGRADINGPRODUCTCODE")); + + private static readonly ManagedAction getNetFxInstalledVersion = new( + new Id($"CA.{nameof(getNetFxInstalledVersion)}"), + CustomActions.GetInstalledNetFx45Version, + Return.check, When.After, Step.LaunchConditions, Condition.Always) + { + Execute = Execute.immediate, + }; + + private static readonly ManagedAction checkNetFxInstalledVersion = new( + CustomActions.CheckInstalledNetFx45Version, + Return.check, When.After, new Step(getNetFxInstalledVersion.Id), Condition.Always) + { + Execute = Execute.immediate, + }; + + private static readonly ManagedAction setInstallId = new( + CustomActions.SetInstallId, + Return.ignore, When.After, Step.InstallInitialize, Condition.Always) + { + Execute = Execute.immediate + }; + + /// + /// Set the ARP installation location to the chosen install directory + /// + private static readonly SetPropertyAction setArpInstallLocation = new("ARPINSTALLLOCATION", $"[{AgentProperties.InstallDir}]") + { + Execute = Execute.immediate, + Sequence = Sequence.InstallExecuteSequence, + When = When.After, + Step = Step.CostFinalize, + }; + + /// + /// Read the previous installation directory from the registry into the `INSTALLDIR` property + /// + private static readonly ManagedAction getInstallDirFromRegistry = new( + CustomActions.GetInstallDirFromRegistry, + Return.ignore, + When.Before, Step.LaunchConditions, + new Condition(AgentProperties.InstallDir, string.Empty), // If the property hasn't already been explicitly set + Sequence.InstallExecuteSequence); + + // Deferred sequence + + /// + /// Create the path %programdata%\Devolutions\Gateway if it does not exist + /// + /// + /// It's hard to tell the installer not to remove directories on uninstall. Since we want this folder to persist, + /// it's easy to create it with a custom action than workaround Windows Installer. + /// + private static readonly ElevatedManagedAction createProgramDataDirectory = new( + new Id($"CA.{nameof(createProgramDataDirectory)}"), + CustomActions.CreateProgramDataDirectory, + Return.check, + When.After, Step.CreateFolders, + Condition.Always, + Sequence.InstallExecuteSequence); + + /// + /// Set or reset the ACL on %programdata%\Devolutions\Gateway + /// + private static readonly ElevatedManagedAction setProgramDataDirectoryPermissions = new( + new Id($"CA.{nameof(setProgramDataDirectoryPermissions)}"), + CustomActions.SetProgramDataDirectoryPermissions, + Return.ignore, + When.After, new Step(createProgramDataDirectory.Id), + Condition.Always, + Sequence.InstallExecuteSequence) + { + Execute = Execute.deferred, + Impersonate = false, + }; + + private static readonly ElevatedManagedAction cleanAgentConfigIfNeeded = new( + new Id($"CA.{nameof(cleanAgentConfigIfNeeded)}"), + CustomActions.CleanAgentConfig, + Return.check, + When.Before, Step.StartServices, + AgentProperties.firstInstall.Equal(true) & AgentProperties.configureAgent.Equal(true), + Sequence.InstallExecuteSequence) + { + Execute = Execute.deferred, + Impersonate = false, + UsesProperties = UseProperties(new [] { AgentProperties.installId }) + }; + + private static readonly ElevatedManagedAction cleanAgentConfigIfNeededRollback = new( + new Id($"CA.{nameof(cleanAgentConfigIfNeededRollback)}"), + CustomActions.CleanAgentConfigRollback, + Return.ignore, + When.Before, new Step(cleanAgentConfigIfNeeded.Id), + AgentProperties.firstInstall.Equal(true) & AgentProperties.configureAgent.Equal(true), + Sequence.InstallExecuteSequence) + { + Execute = Execute.rollback, + Impersonate = false, + UsesProperties = UseProperties(new[] { AgentProperties.installId }) + }; + + /// + /// Execute the installed DevolutionsAgent with the --config-init-only argument + /// + /// + /// Ensures a default configuration file is created + /// + private static readonly WixQuietExecAction initAgentConfigIfNeeded = new( + new Id($"CA.{nameof(initAgentConfigIfNeeded)}"), + $"[{AgentProperties.InstallDir}]{Includes.EXECUTABLE_NAME}", + "config init", + Return.check, + When.After, new Step(cleanAgentConfigIfNeeded.Id), + AgentProperties.firstInstall.Equal(true) & AgentProperties.configureAgent.Equal(false), + Sequence.InstallExecuteSequence) + { + Execute = Execute.deferred, + Impersonate = false, + }; + + /// + /// Start the installed DevolutionsAgent service + /// + /// + /// The service will be started if it's StartMode is "Automatic". May be overridden with the + /// public property `NoStartService`. + /// + private static readonly ElevatedManagedAction startAgentIfNeeded = new( + CustomActions.StartAgentIfNeeded, + Return.ignore, + When.After, Step.StartServices, + Condition.NOT_BeingRemoved, + Sequence.InstallExecuteSequence); + + /// + /// Attempt to restart the DevolutionsAgent service (if it's running) on maintenance installs + /// + /// + /// This was necessary in the old Wayk installer to reread configurations that may have been updated + /// by the installer. It's usefulness os questionable with Devolutions Gateway. + /// + private static readonly ElevatedManagedAction restartAgent= new( + CustomActions.RestartAgent, + Return.ignore, + When.After, Step.StartServices, + AgentProperties.maintenance.Equal(true), + Sequence.InstallExecuteSequence); + + /// + /// Attempt to rollback any configuration files created + /// + private static readonly ElevatedManagedAction rollbackConfig = new( + CustomActions.RollbackConfig, + Return.ignore, + When.Before, new Step(cleanAgentConfigIfNeeded.Id), + AgentProperties.firstInstall.Equal(true), + Sequence.InstallExecuteSequence) + { + Execute = Execute.rollback, + }; + + private static string UseProperties(IEnumerable properties) + { + if (!properties?.Any() ?? false) + { + return null; + } + + if (properties.Any(p => p.Public && !p.Secure)) // Sanity check at project build time + { + throw new Exception($"property {properties.First(p => !p.Secure).Id} must be secure"); + } + + return string.Join(";", properties.Distinct().Select(x => $"{x.Id}=[{x.Id}]")); + } + + internal static readonly Action[] Actions = + { + isFirstInstall, + isUpgrading, + isRemovingForUpgrade, + isUninstalling, + isMaintenance, + setInstallId, + getNetFxInstalledVersion, + checkNetFxInstalledVersion, + getInstallDirFromRegistry, + setArpInstallLocation, + createProgramDataDirectory, + setProgramDataDirectoryPermissions, + + cleanAgentConfigIfNeeded, + cleanAgentConfigIfNeededRollback, + + startAgentIfNeeded, + restartAgent, + rollbackConfig, + }; +} diff --git a/package/AgentWindowsManaged/Actions/LogDelegate.cs b/package/AgentWindowsManaged/Actions/LogDelegate.cs new file mode 100644 index 000000000..9547e4725 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/LogDelegate.cs @@ -0,0 +1,43 @@ +using Microsoft.Deployment.WindowsInstaller; + +namespace DevolutionsAgent.Actions; + +internal interface ILogger +{ + void Log(string msg); + + void Log(string format, params object[] args); +} + +internal class NullLogger : ILogger +{ + public void Log(string msg) + { + } + + public void Log(string format, params object[] args) + { + } +} + +internal class LogDelegate : ILogger +{ + private readonly Session session; + + public LogDelegate(Session session) + { + this.session = session; + } + + public static LogDelegate WithSession(Session session) => new(session); + + public void Log(string msg) + { + this.session?.Log(msg); + } + + public void Log(string format, params object[] args) + { + this.session?.Log(format, args); + } +} diff --git a/package/AgentWindowsManaged/Actions/Service.cs b/package/AgentWindowsManaged/Actions/Service.cs new file mode 100644 index 000000000..604d1a4f6 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/Service.cs @@ -0,0 +1,251 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.ServiceProcess; +using System.Threading.Tasks; +using System.Threading; + +namespace DevolutionsAgent.Actions; + +internal class Service : IDisposable +{ + private IntPtr hService = IntPtr.Zero; + + private readonly ILogger logger; + + private readonly string name; + + internal IntPtr Handle => this.hService; + + public static bool TryOpen(ServiceManager serviceManager, string name, uint desiredAccess, out Service service, ILogger logger = null) + { + service = null; + + try + { + service = new Service(serviceManager, name, desiredAccess, logger); + return true; + } + catch + { + return false; + } + } + + public Service(ServiceManager serviceManager, string name, uint desiredAccess, ILogger logger = null) + { + this.logger = logger ??= new NullLogger(); + this.name = name; + + logger.Log($"opening service {name} with desired access {desiredAccess}"); + + this.hService = WinAPI.OpenService(serviceManager.Handle, name, desiredAccess); + + if (this.hService == IntPtr.Zero) + { + logger.Log($"failed to open service {name} with desired access {desiredAccess} (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + public void Dispose() + { + if (this.hService == IntPtr.Zero) + { + return; + } + + WinAPI.CloseServiceHandle(this.hService); + this.hService = IntPtr.Zero; + } + + public ServiceStartMode GetStartupType() + { + uint bytesNeeded = 0; + const int errorInsufficientBuffer = 122; + + if (!WinAPI.QueryServiceConfig(this.Handle, IntPtr.Zero, bytesNeeded, ref bytesNeeded)) + { + if (Marshal.GetLastWin32Error() != errorInsufficientBuffer) + { + logger.Log($"QueryServiceConfig failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + using Buffer buffer = new((int)bytesNeeded); + + if (!WinAPI.QueryServiceConfig(this.Handle, buffer, bytesNeeded, ref bytesNeeded)) + { + logger.Log($"QueryServiceConfig failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + WinAPI.QUERY_SERVICE_CONFIG serviceConfig = Marshal.PtrToStructure(buffer); + logger.Log($"service {name} has start type: {serviceConfig.dwStartType}"); + return (ServiceStartMode)serviceConfig.dwStartType; + } + + public void SetStartupType(ServiceStartMode startMode) + { + logger.Log($"setting service {name} start mode to {startMode}"); + + if (!WinAPI.ChangeServiceConfig(this.Handle, WinAPI.SERVICE_NO_CHANGE, + (uint)startMode, WinAPI.SERVICE_NO_CHANGE, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, + IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) + { + logger.Log($"ChangeServiceConfig failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + /// + /// Request the service to start if it's startup mode is "Automatic" + /// + public void StartIfNeeded() + { + ServiceStartMode currentStartMode = GetStartupType(); + + if (currentStartMode != ServiceStartMode.Automatic) + { + logger.Log($"service {name} not configured for automatic start, not starting"); + return; + } + + logger.Log($"starting service {name}"); + + if (WinAPI.StartService(this.Handle, 0, IntPtr.Zero)) + { + return; + } + + const int errorServiceAlreadyRunning = 1056; + + if (Marshal.GetLastWin32Error() != errorServiceAlreadyRunning) + { + logger.Log($"StartService failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + else + { + logger.Log($"service {name} is already running"); + } + } + + public void Restart() + { + if (!Stop(TimeSpan.FromSeconds(60))) + { + return; + } + + logger.Log($"starting service {name}"); + + if (!WinAPI.StartService(this.Handle, 0, IntPtr.Zero)) + { + logger.Log($"StartService failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + // Returns `false` if the service already stopped or stop pending, `true` if we stopped the service + private bool Stop(TimeSpan timeout) + { + logger.Log($"stopping service {name}"); + + using Buffer pSsp = new(Marshal.SizeOf()); + + if (!WinAPI.QueryServiceStatusEx(this.Handle, WinAPI.SC_STATUS_PROCESS_INFO, pSsp, (uint)Marshal.SizeOf(), out var dwBytesNeeded)) + { + logger.Log($"QueryServiceStatusEx failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + WinAPI.SERVICE_STATUS_PROCESS ssp = Marshal.PtrToStructure(pSsp); + + if (ssp.dwCurrentState == WinAPI.SERVICE_STOPPED) + { + logger.Log($"service {name} is already stopped"); + return false; + } + + using CancellationTokenSource cts = new(timeout); + + while (ssp.dwCurrentState == WinAPI.SERVICE_STOP_PENDING) + { + uint waitTime = ssp.dwWaitHint / 10; + + if (waitTime < TimeSpan.FromSeconds(1).TotalMilliseconds) + { + waitTime = (uint)TimeSpan.FromSeconds(1).TotalMilliseconds; + } + else if (waitTime > TimeSpan.FromSeconds(10).TotalMilliseconds) + { + waitTime = (uint)TimeSpan.FromSeconds(10).TotalMilliseconds; + } + + logger.Log($"service {name} is stop pending; waiting {waitTime}ms"); + + Task.Delay((int)waitTime, cts.Token); + + if (!WinAPI.QueryServiceStatusEx(this.Handle, WinAPI.SC_STATUS_PROCESS_INFO, pSsp, (uint)Marshal.SizeOf(), out dwBytesNeeded)) + { + logger.Log($"QueryServiceStatusEx failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + ssp = Marshal.PtrToStructure(pSsp); + + if (ssp.dwCurrentState == WinAPI.SERVICE_STOPPED) + { + logger.Log($"service {name} is stopped"); + return false; + } + + if (cts.IsCancellationRequested) + { + logger.Log($"timeout waiting for service {name} to stop"); + throw new OperationCanceledException(); + } + } + + // Currently Devolutions Agent has no dependent services; but if it did, we would need to stop them here + + using Buffer pSs = new(Marshal.SizeOf()); + + logger.Log($"requesting service {name} to stop"); + + if (!WinAPI.ControlService(this.Handle, WinAPI.SERVICE_CONTROL_STOP, pSs)) + { + logger.Log($"ControlService failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + while (ssp.dwCurrentState != WinAPI.SERVICE_STOPPED) + { + Task.Delay((int)ssp.dwWaitHint, cts.Token); + + if (!WinAPI.QueryServiceStatusEx(this.Handle, WinAPI.SC_STATUS_PROCESS_INFO, pSsp, (uint)Marshal.SizeOf(), out dwBytesNeeded)) + { + logger.Log($"QueryServiceStatusEx failed (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + ssp = Marshal.PtrToStructure(pSsp); + + if (ssp.dwCurrentState == WinAPI.SERVICE_STOPPED) + { + logger.Log($"service {name} is stopped"); + return true; + } + + if (cts.IsCancellationRequested) + { + logger.Log($"timeout waiting for service {name} to stop"); + throw new OperationCanceledException(); + } + } + + return true; + } +} diff --git a/package/AgentWindowsManaged/Actions/ServiceManager.cs b/package/AgentWindowsManaged/Actions/ServiceManager.cs new file mode 100644 index 000000000..69c94d03e --- /dev/null +++ b/package/AgentWindowsManaged/Actions/ServiceManager.cs @@ -0,0 +1,38 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace DevolutionsAgent.Actions; + +internal class ServiceManager : IDisposable +{ + private IntPtr hServiceManager = IntPtr.Zero; + + private readonly ILogger logger; + + internal IntPtr Handle => this.hServiceManager; + + public ServiceManager(uint desiredAccess, ILogger logger = null) + { + this.logger = logger ??= new NullLogger(); + + this.hServiceManager = WinAPI.OpenSCManager(IntPtr.Zero, IntPtr.Zero, desiredAccess); + + if (this.hServiceManager == IntPtr.Zero) + { + logger.Log($"failed to open service manager with desired access {desiredAccess} (error: {Marshal.GetLastWin32Error()})"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + public void Dispose() + { + if (this.hServiceManager == IntPtr.Zero) + { + return; + } + + WinAPI.CloseServiceHandle(this.hServiceManager); + this.hServiceManager = IntPtr.Zero; + } +} diff --git a/package/AgentWindowsManaged/Actions/WinAPI.cs b/package/AgentWindowsManaged/Actions/WinAPI.cs new file mode 100644 index 000000000..1b6b292b4 --- /dev/null +++ b/package/AgentWindowsManaged/Actions/WinAPI.cs @@ -0,0 +1,307 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace DevolutionsAgent.Actions; + +internal static class WinAPI +{ + internal static uint CREATE_ALWAYS = 2; + + internal static uint CREATE_NO_WINDOW = 0x08000000; + + internal const uint DACL_SECURITY_INFORMATION = 0x00000004; + + internal const int EM_SETCUEBANNER = 0x1501; + + internal static uint FILE_ATTRIBUTE_NORMAL = 0x00000080; + + internal static uint FILE_SHARE_READ = 0x00000001; + + internal static uint FILE_SHARE_WRITE = 0x00000002; + + internal static uint GENERIC_WRITE = 0x40000000; + + internal static uint MOVEFILE_DELAY_UNTIL_REBOOT = 0x04; + + internal const uint SC_MANAGER_ALL_ACCESS = 0xF003F; + + internal const uint SC_MANAGER_CONNECT = 0x0001; + + internal const uint SC_STATUS_PROCESS_INFO = 0; + + internal const uint SERVICE_CONTROL_STOP = 0x00000001; + + internal const uint SERVICE_NO_CHANGE = 0xFFFFFFFF; + + internal const uint SERVICE_CHANGE_CONFIG = 0x0002; + + internal const uint SERVICE_QUERY_CONFIG = 0x0001; + + internal const uint SERVICE_QUERY_STATUS = 0x0004; + + internal const uint SERVICE_START = 0x0010; + + internal const uint SERVICE_STOP = 0x0020; + + internal const uint SERVICE_STOP_PENDING = 0x00000003; + + internal const uint SERVICE_STOPPED = 0x00000001; + + internal static uint STARTF_USESTDHANDLES = 0x00000100; + + internal static int STD_INPUT_HANDLE = -10; + + internal static uint WAIT_TIMEOUT = 0x00000102; + + [StructLayout(LayoutKind.Sequential)] + internal struct QUERY_SERVICE_CONFIG + { + internal uint dwServiceType; + + internal uint dwStartType; + + internal uint dwErrorControl; + + internal IntPtr lpBinaryPathName; + + internal IntPtr lpLoadOrderGroup; + + internal uint dwTagId; + + internal IntPtr lpDependencies; + + internal IntPtr lpServiceStartName; + + internal IntPtr lpDisplayName; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_INFORMATION + { + internal IntPtr hProcess; + + internal IntPtr hThread; + + internal uint dwProcessId; + + internal uint dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SECURITY_ATTRIBUTES + { + internal uint nLength; + internal IntPtr lpSecurityDescriptor; + [MarshalAs(UnmanagedType.Bool)] internal bool bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_STATUS + { + public uint dwServiceType; + + public uint dwCurrentState; + + public uint dwControlsAccepted; + + public uint dwWin32ExitCode; + + public uint dwServiceSpecificExitCode; + + public uint dwCheckPoint; + + public uint dwWaitHint; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SERVICE_STATUS_PROCESS + { + internal uint dwServiceType; + + internal uint dwCurrentState; + + internal uint dwControlsAccepted; + + internal uint dwWin32ExitCode; + + internal uint dwServiceSpecificExitCode; + + internal uint dwCheckPoint; + + internal uint dwWaitHint; + + internal uint dwProcessId; + + internal uint dwServiceFlags; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct STARTUPINFO + { + internal uint cb; + + internal IntPtr lpReserved; + + internal IntPtr lpDesktop; + + internal IntPtr lpTitle; + + internal uint dwX; + + internal uint dwY; + + internal uint dwXSize; + + internal uint dwYSize; + + internal uint dwXCountChars; + + internal uint dwYCountChars; + + internal uint dwFillAttributes; + + internal uint dwFlags; + + internal short wShowWindow; + + internal short cbReserved; + + internal IntPtr lpReserved2; + + internal IntPtr hStdInput; + + internal IntPtr hStdOutput; + + internal IntPtr hStdError; + } + + [DllImport("advapi32", EntryPoint = "ChangeServiceConfigW", SetLastError = true)] + internal static extern bool ChangeServiceConfig( + IntPtr hService, + uint nServiceType, + uint nStartType, + uint nErrorControl, + IntPtr lpBinaryPathName, + IntPtr lpLoadOrderGroup, + IntPtr lpdwTagId, + IntPtr lpDependencies, + IntPtr lpServiceStartName, + IntPtr lpPassword, + IntPtr lpDisplayName); + + [DllImport("kernel32")] + internal static extern bool CloseHandle(IntPtr handle); + + [DllImport("advapi32", EntryPoint = "CloseServiceHandle")] + internal static extern int CloseServiceHandle(IntPtr hSCObject); + + [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW(string StringSecurityDescriptor, uint StringSDRevision, out IntPtr SecurityDescriptor, out UIntPtr SecurityDescriptorSize); + + + [DllImport("advapi32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ControlService(IntPtr hService, uint dwControl, IntPtr lpServiceStatus); + + [DllImport("kernel32", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern SafeFileHandle CreateFile( + [MarshalAs(UnmanagedType.LPWStr)] string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + + [DllImport("kernel32", EntryPoint = "CreateProcessW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool CreateProcess( + [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, + [MarshalAs(UnmanagedType.LPWStr)] string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + IntPtr lpStartupInfo, + IntPtr lpProcessInformation); + + [DllImport("kernel32", EntryPoint = "DeleteFileW", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DeleteFile( + [MarshalAs(UnmanagedType.LPWStr)] string lpFileName + ); + + [DllImport("kernel32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); + + [DllImport("Kernel32", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern uint GetFinalPathNameByHandle( + IntPtr hFile, + [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, + uint cchFilePath, + uint dwFlags); + + [DllImport("kernel32", SetLastError = true)] + internal static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32", EntryPoint = "GetTempFileNameW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern uint GetTempFileName( + [MarshalAs(UnmanagedType.LPWStr)] string lpPathName, + [MarshalAs(UnmanagedType.LPWStr)] string lpPrefixString, + uint uUnique, + [Out] StringBuilder lpTempFileName); + + [DllImport("kernel32", SetLastError = true)] + internal static extern IntPtr LocalFree(IntPtr hMem); + + [DllImport("kernel32", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool MoveFileEx( + [MarshalAs(UnmanagedType.LPWStr)] string lpExistingFileName, + IntPtr lpNewFileName, + uint dwFlags + ); + + [DllImport("advapi32", EntryPoint = "OpenSCManagerW", SetLastError = true)] + internal static extern IntPtr OpenSCManager(IntPtr machineName, IntPtr databaseName, uint dwAccess); + + [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr OpenService( + IntPtr hSCManager, + [MarshalAs(UnmanagedType.LPWStr)] string lpServiceName, + uint dwDesiredAccess); + + [DllImport("advapi32", EntryPoint = "QueryServiceConfigW", SetLastError = true)] + internal static extern bool QueryServiceConfig(IntPtr hService, IntPtr lpServiceConfig, uint cbBufSize, + ref uint pcbBytesNeeded); + + [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool QueryServiceStatusEx(IntPtr serviceHandle, uint infoLevel, IntPtr buffer, uint bufferSize, + out uint bytesNeeded); + + [DllImport("user32", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)] + internal static extern int SendMessage( + IntPtr hWnd, + int msg, + int wParam, + [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetFileSecurityW(string lpFileName, uint SecurityInformation, IntPtr pSecurityDescriptor); + + [DllImport("advapi32", EntryPoint = "StartServiceW", SetLastError = true)] + internal static extern bool StartService(IntPtr hService, uint dwNumServiceArgs, IntPtr lpServiceArgVectors); + + [DllImport("kernel32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32", SetLastError = true)] + internal static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); +} diff --git a/package/AgentWindowsManaged/App.config b/package/AgentWindowsManaged/App.config new file mode 100644 index 000000000..6c301d331 --- /dev/null +++ b/package/AgentWindowsManaged/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Controls/ElevatedButton.cs b/package/AgentWindowsManaged/Controls/ElevatedButton.cs new file mode 100644 index 000000000..e709e6b27 --- /dev/null +++ b/package/AgentWindowsManaged/Controls/ElevatedButton.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Windows.Forms; + +namespace DevolutionsAgent.Controls +{ + public class ElevatedButton : Button + { + /// + /// The constructor to create the button with a UAC shield if necessary. + /// + public ElevatedButton() + { + FlatStyle = FlatStyle.System; + + if (LicenseManager.UsageMode != LicenseUsageMode.Designtime) + { + if (!IsElevated()) ShowShield(); + } + } + + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + private uint BCM_SETSHIELD = 0x0000160C; + + private bool IsElevated() + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + private void ShowShield() + { + IntPtr wParam = new IntPtr(0); + IntPtr lParam = new IntPtr(1); + SendMessage(new HandleRef(this, Handle), BCM_SETSHIELD, wParam, lParam); + } + } +} diff --git a/package/AgentWindowsManaged/DevolutionsAgent.csproj b/package/AgentWindowsManaged/DevolutionsAgent.csproj new file mode 100644 index 000000000..3c48c0344 --- /dev/null +++ b/package/AgentWindowsManaged/DevolutionsAgent.csproj @@ -0,0 +1,83 @@ + + + + Exe + net452 + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1\System.ServiceProcess.dll + + + + + + + True + True + AgentProperties.g.tt + + + True + True + AgentProperties.g.tt + + + True + True + Strings.g.tt + + + True + True + AgentProperties.g.tt + + + True + True + Strings.g.tt + + + + + + + + + + TextTemplatingFileGenerator + AgentProperties.g.cs + + + TextTemplatingFileGenerator + Strings.g.cs + + + diff --git a/package/AgentWindowsManaged/DevolutionsAgent.csproj.user b/package/AgentWindowsManaged/DevolutionsAgent.csproj.user new file mode 100644 index 000000000..7e9b5231d --- /dev/null +++ b/package/AgentWindowsManaged/DevolutionsAgent.csproj.user @@ -0,0 +1,30 @@ + + + + + + Component + + + Form + + + Form + + + Form + + + Form + + + Form + + + Form + + + Form + + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/DevolutionsAgent.sln b/package/AgentWindowsManaged/DevolutionsAgent.sln new file mode 100644 index 000000000..e78e9a870 --- /dev/null +++ b/package/AgentWindowsManaged/DevolutionsAgent.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevolutionsAgent", "DevolutionsAgent.csproj", "{AEF5323D-2509-440D-B3F6-6F36B071A8AA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AEF5323D-2509-440D-B3F6-6F36B071A8AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEF5323D-2509-440D-B3F6-6F36B071A8AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEF5323D-2509-440D-B3F6-6F36B071A8AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEF5323D-2509-440D-B3F6-6F36B071A8AA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6B26DD11-2DE8-46E4-AC4B-6EF6BB032E0F} + EndGlobalSection +EndGlobal diff --git a/package/AgentWindowsManaged/Dialogs/AgentDialog.cs b/package/AgentWindowsManaged/Dialogs/AgentDialog.cs new file mode 100644 index 000000000..a6b53f0ef --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/AgentDialog.cs @@ -0,0 +1,83 @@ +using System; +using System.Windows.Forms; +using DevolutionsAgent.Resources; +using WixSharp; +using WixSharp.UI.Forms; + +namespace DevolutionsAgent.Dialogs; + +public class AgentDialog : ManagedForm +{ + public AgentDialog() + { + this.AutoScaleMode = AutoScaleMode.Font; + } + + public virtual void FromProperties() + { + } + + public virtual bool ToProperties() => true; + + public virtual bool DoValidate() => true; + + public virtual void OnLoad(object sender, EventArgs e) + { + this.Text = "[AgentDlg_Title]".LocalizeWith(this.MsiRuntime.Localize); + + this.FromProperties(); + } + + protected virtual void Back_Click(object sender, EventArgs e) + { + if (this.ToProperties()) + { + Shell.GoTo(Wizard.GetPrevious(this)); + } + } + + protected virtual void Next_Click(object sender, EventArgs e) + { + if (!this.DoValidate()) + { + return; + } + + if (this.ToProperties()) + { + Shell.GoTo(Wizard.GetNext(this)); + } + } + + protected virtual void Cancel_Click(object sender, EventArgs e) + { + if (MessageBox.Show( + this.Localize("[CancelDlgText]"), + this.Localize("[CancelDlg_Title]"), + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning) == DialogResult.Yes) + { + Shell.Cancel(); + } + } + + protected string I18n(string key) => MsiRuntime.I18n(key); + + protected void ShowValidationError(string message = null) + { + string errorMessage = string.IsNullOrEmpty(message) ? MsiRuntime.I18n(Strings.ThereIsAProblemWithTheEnteredData) : message; + + this.ShowValidationErrorString(errorMessage); + } + + protected void ShowValidationErrorString(string message) + { + MessageBox.Show( + message, + this.Localize("[AgentDlg_Title]"), + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + + protected string Localize(string message) => message.LocalizeWith(MsiRuntime.Localize); +} diff --git a/package/AgentWindowsManaged/Dialogs/ExitDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/ExitDialog.Designer.cs new file mode 100644 index 000000000..ae11228a7 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ExitDialog.Designer.cs @@ -0,0 +1,239 @@ +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class ExitDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.imgPanel = new System.Windows.Forms.Panel(); + this.textPanel = new System.Windows.Forms.Panel(); + this.title = new System.Windows.Forms.Label(); + this.description = new System.Windows.Forms.Label(); + this.image = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.viewLog = new System.Windows.Forms.LinkLabel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.border1 = new System.Windows.Forms.Panel(); + this.imgPanel.SuspendLayout(); + this.textPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.image)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // imgPanel + // + this.imgPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.imgPanel.Controls.Add(this.textPanel); + this.imgPanel.Controls.Add(this.image); + this.imgPanel.Location = new System.Drawing.Point(0, 0); + this.imgPanel.Name = "imgPanel"; + this.imgPanel.Size = new System.Drawing.Size(494, 312); + this.imgPanel.TabIndex = 8; + // + // textPanel + // + this.textPanel.Controls.Add(this.title); + this.textPanel.Controls.Add(this.description); + this.textPanel.Location = new System.Drawing.Point(162, 12); + this.textPanel.Name = "textPanel"; + this.textPanel.Size = new System.Drawing.Size(320, 289); + this.textPanel.TabIndex = 8; + // + // title + // + this.title.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.title.BackColor = System.Drawing.Color.Transparent; + this.title.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.title.Location = new System.Drawing.Point(3, 0); + this.title.Name = "title"; + this.title.Size = new System.Drawing.Size(314, 61); + this.title.TabIndex = 6; + this.title.Text = "[ExitDialogTitle]"; + // + // description + // + this.description.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.description.BackColor = System.Drawing.Color.Transparent; + this.description.Location = new System.Drawing.Point(4, 88); + this.description.Name = "description"; + this.description.Size = new System.Drawing.Size(313, 201); + this.description.TabIndex = 7; + this.description.Text = "[ExitDialogDescription]"; + // + // image + // + this.image.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.image.Location = new System.Drawing.Point(0, 0); + this.image.Name = "image"; + this.image.Size = new System.Drawing.Size(494, 312); + this.image.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.image.TabIndex = 4; + this.image.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.viewLog); + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.border1); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 5; + // + // viewLog + // + this.viewLog.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.viewLog.AutoSize = true; + this.viewLog.BackColor = System.Drawing.Color.Transparent; + this.viewLog.Location = new System.Drawing.Point(16, 17); + this.viewLog.Name = "viewLog"; + this.viewLog.Size = new System.Drawing.Size(85, 13); + this.viewLog.TabIndex = 1; + this.viewLog.TabStop = true; + this.viewLog.Text = "[ViewLogButton]"; + this.viewLog.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.viewLog_LinkClicked); + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(491, 43); + this.tableLayoutPanel1.TabIndex = 0; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Enabled = false; + this.back.Location = new System.Drawing.Point(218, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 1; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Location = new System.Drawing.Point(301, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(81, 23); + this.next.TabIndex = 0; + this.next.Text = "[WixUIFinish]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.finish_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Enabled = false; + this.cancel.Location = new System.Drawing.Point(402, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + // + // border1 + // + this.border1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.border1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.border1.Location = new System.Drawing.Point(0, 0); + this.border1.Name = "border1"; + this.border1.Size = new System.Drawing.Size(494, 1); + this.border1.TabIndex = 9; + // + // ExitDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.BackColor = System.Drawing.Color.White; + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.imgPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "ExitDialog"; + this.Text = "[ExitDialog_Title]"; + this.Load += new System.EventHandler(this.OnLoad); + this.imgPanel.ResumeLayout(false); + this.textPanel.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.image)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.bottomPanel.PerformLayout(); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label description; + private System.Windows.Forms.Label title; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.PictureBox image; + private System.Windows.Forms.LinkLabel viewLog; + private System.Windows.Forms.Panel imgPanel; + private System.Windows.Forms.Panel border1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Panel textPanel; + } +} \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/ExitDialog.cs b/package/AgentWindowsManaged/Dialogs/ExitDialog.cs new file mode 100644 index 000000000..6e660e475 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ExitDialog.cs @@ -0,0 +1,62 @@ +using System; +using DevolutionsAgent.Dialogs; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Windows.Forms; + +namespace WixSharpSetup.Dialogs; + +public partial class ExitDialog : AgentDialog +{ + public ExitDialog() + { + InitializeComponent(); + + this.textPanel.BackColor = Color.FromArgb(233, 233, 233); + } + + public override void OnLoad(object sender, System.EventArgs e) + { + image.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Dialog"); + + if (Shell.UserInterrupted || Shell.Log.Contains("User cancelled installation.")) + { + title.Text = "[UserExitTitle]"; + description.Text = "[UserExitDescription1]"; + this.Localize(); + } + else if (Shell.ErrorDetected) + { + title.Text = "[FatalErrorTitle]"; + description.Text = Shell.CustomErrorDescription ?? "[FatalErrorDescription1]"; + this.Localize(); + } + + base.OnLoad(sender, e); + } + + void finish_Click(object sender, System.EventArgs e) + { + Shell.Exit(); + } + + void viewLog_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + try + { + string wixSharpDir = Path.Combine(Path.GetTempPath(), @"WixSharp"); + if (!Directory.Exists(wixSharpDir)) + Directory.CreateDirectory(wixSharpDir); + + string logFile = Path.Combine(wixSharpDir, Runtime.ProductName + ".log"); + File.WriteAllText(logFile, Shell.Log); + Process.Start(logFile); + } + catch + { + //Catch all, we don't want the installer to crash in an + //attempt to view the log. + } + } +} diff --git a/package/AgentWindowsManaged/Dialogs/ExitDialog.resx b/package/AgentWindowsManaged/Dialogs/ExitDialog.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ExitDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/InstallDirDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.Designer.cs new file mode 100644 index 000000000..0f148c8fc --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.Designer.cs @@ -0,0 +1,315 @@ +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class InstallDirDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.copyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.middlePanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel(); + this.change = new System.Windows.Forms.Button(); + this.label3 = new System.Windows.Forms.Label(); + this.installDir = new System.Windows.Forms.TextBox(); + this.topBorder = new System.Windows.Forms.Panel(); + this.topPanel = new System.Windows.Forms.Panel(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.banner = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.border1 = new System.Windows.Forms.Panel(); + this.contextMenuStrip1.SuspendLayout(); + this.middlePanel.SuspendLayout(); + this.tableLayoutPanel2.SuspendLayout(); + this.topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // contextMenuStrip1 + // + this.contextMenuStrip1.ImageScalingSize = new System.Drawing.Size(32, 32); + this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.copyToolStripMenuItem}); + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(103, 26); + // + // copyToolStripMenuItem + // + this.copyToolStripMenuItem.Name = "copyToolStripMenuItem"; + this.copyToolStripMenuItem.Size = new System.Drawing.Size(102, 22); + this.copyToolStripMenuItem.Text = "Copy"; + // + // middlePanel + // + this.middlePanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.middlePanel.Controls.Add(this.tableLayoutPanel2); + this.middlePanel.Location = new System.Drawing.Point(22, 75); + this.middlePanel.Name = "middlePanel"; + this.middlePanel.Size = new System.Drawing.Size(449, 139); + this.middlePanel.TabIndex = 0; + // + // tableLayoutPanel2 + // + this.tableLayoutPanel2.ColumnCount = 1; + this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel2.Controls.Add(this.change, 0, 2); + this.tableLayoutPanel2.Controls.Add(this.label3, 0, 0); + this.tableLayoutPanel2.Controls.Add(this.installDir, 0, 1); + this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanel2.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanel2.Name = "tableLayoutPanel2"; + this.tableLayoutPanel2.RowCount = 3; + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.Size = new System.Drawing.Size(449, 139); + this.tableLayoutPanel2.TabIndex = 0; + // + // change + // + this.change.AutoSize = true; + this.change.Location = new System.Drawing.Point(3, 48); + this.change.Name = "change"; + this.change.Size = new System.Drawing.Size(116, 23); + this.change.TabIndex = 1; + this.change.Text = "[InstallDirDlgChange]"; + this.change.UseVisualStyleBackColor = true; + this.change.Click += new System.EventHandler(this.Change_Click); + // + // label3 + // + this.label3.AutoSize = true; + this.label3.BackColor = System.Drawing.Color.Transparent; + this.label3.Location = new System.Drawing.Point(3, 3); + this.label3.Margin = new System.Windows.Forms.Padding(3); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(124, 13); + this.label3.TabIndex = 11; + this.label3.Text = "[InstallDirDlgFolderLabel]"; + // + // installDir + // + this.installDir.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.installDir.Location = new System.Drawing.Point(3, 22); + this.installDir.Name = "installDir"; + this.installDir.Size = new System.Drawing.Size(443, 20); + this.installDir.TabIndex = 0; + // + // topBorder + // + this.topBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.topBorder.Location = new System.Drawing.Point(0, 58); + this.topBorder.Name = "topBorder"; + this.topBorder.Size = new System.Drawing.Size(494, 1); + this.topBorder.TabIndex = 15; + // + // topPanel + // + this.topPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topPanel.BackColor = System.Drawing.SystemColors.Control; + this.topPanel.Controls.Add(this.label2); + this.topPanel.Controls.Add(this.label1); + this.topPanel.Controls.Add(this.banner); + this.topPanel.Location = new System.Drawing.Point(0, 0); + this.topPanel.Name = "topPanel"; + this.topPanel.Size = new System.Drawing.Size(494, 58); + this.topPanel.TabIndex = 10; + // + // label2 + // + this.label2.AutoEllipsis = true; + this.label2.BackColor = System.Drawing.Color.Transparent; + this.label2.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label2.Location = new System.Drawing.Point(18, 31); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(409, 24); + this.label2.TabIndex = 1; + this.label2.Text = "[InstallDirDlgDescription]"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label1.Location = new System.Drawing.Point(11, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(109, 13); + this.label1.TabIndex = 1; + this.label1.Text = "[InstallDirDlgTitle]"; + // + // banner + // + this.banner.BackColor = System.Drawing.Color.White; + this.banner.Location = new System.Drawing.Point(0, 0); + this.banner.Name = "banner"; + this.banner.Size = new System.Drawing.Size(494, 58); + this.banner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.banner.TabIndex = 0; + this.banner.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.border1); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 9; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(493, 43); + this.tableLayoutPanel1.TabIndex = 8; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Location = new System.Drawing.Point(224, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 1; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + this.back.Click += new System.EventHandler(this.Back_Click); + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Location = new System.Drawing.Point(307, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(77, 23); + this.next.TabIndex = 0; + this.next.Text = "[WixUINext]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.Next_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Location = new System.Drawing.Point(404, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // border1 + // + this.border1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.border1.Dock = System.Windows.Forms.DockStyle.Top; + this.border1.Location = new System.Drawing.Point(0, 0); + this.border1.Name = "border1"; + this.border1.Size = new System.Drawing.Size(494, 1); + this.border1.TabIndex = 14; + // + // InstallDirDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.middlePanel); + this.Controls.Add(this.topBorder); + this.Controls.Add(this.topPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "InstallDirDialog"; + this.Load += new System.EventHandler(this.OnLoad); + this.contextMenuStrip1.ResumeLayout(false); + this.middlePanel.ResumeLayout(false); + this.tableLayoutPanel2.ResumeLayout(false); + this.tableLayoutPanel2.PerformLayout(); + this.topPanel.ResumeLayout(false); + this.topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox banner; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.ToolStripMenuItem copyToolStripMenuItem; + private System.Windows.Forms.Panel topPanel; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Button change; + private System.Windows.Forms.TextBox installDir; + private System.Windows.Forms.Panel border1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Panel topBorder; + private System.Windows.Forms.Panel middlePanel; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2; + } +} \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/InstallDirDialog.cs b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.cs new file mode 100644 index 000000000..4a61c17b5 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.cs @@ -0,0 +1,90 @@ +using DevolutionsAgent.Dialogs; +using DevolutionsAgent.Properties; +using DevolutionsAgent.Resources; + +using Microsoft.Win32; + +using System; +using System.Windows.Forms; + +using WixSharp; + +namespace WixSharpSetup.Dialogs; + +public partial class InstallDirDialog : AgentDialog +{ + public InstallDirDialog() + { + InitializeComponent(); + label1.MakeTransparentOn(banner); + label2.MakeTransparentOn(banner); + } + + public override bool ToProperties() + { + Runtime.Session[AgentProperties.InstallDir] = installDir.Text; + + return true; + } + + public override void OnLoad(object sender, EventArgs e) + { + banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner"); + + string installDirProperty = Runtime.Session.Property("WixSharp_UI_INSTALLDIR"); + string installDirValue = Runtime.Session.Property(installDirProperty); + + if (string.IsNullOrEmpty(installDirValue)) + { + try + { + RegistryKey localKey = RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, RegistryView.Registry64); + RegistryKey gatewayKey = localKey.OpenSubKey($@"Software\{Includes.VENDOR_NAME}\{Includes.SHORT_NAME}"); + installDirValue = (string)gatewayKey.GetValue("InstallDir"); + } + catch + { + } + + if (string.IsNullOrEmpty(installDirValue)) + { + //We are executed before any of the MSI actions are invoked so the INSTALLDIR (if set to absolute path) + //is not resolved yet. So we need to do it manually + this.installDir.Text = Runtime.Session.GetDirectoryPath(installDirProperty); + + if (this.installDir.Text == "ABSOLUTEPATH") + this.installDir.Text = Runtime.Session.Property("INSTALLDIR_ABSOLUTEPATH"); + } + else + { + this.installDir.Text = installDirValue; + } + } + else + { + //INSTALLDIR set either from the command line or by one of the early setup events (e.g. UILoaded) + this.installDir.Text = installDirValue; + } + + base.OnLoad(sender, e); + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Back_Click(object sender, EventArgs e) => base.Back_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Next_Click(object sender, EventArgs e) => base.Next_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Cancel_Click(object sender, EventArgs e) => base.Cancel_Click(sender, e); + + void Change_Click(object sender, EventArgs e) + { + using var dialog = new FolderBrowserDialog { SelectedPath = installDir.Text }; + + if (dialog.ShowDialog(this) == DialogResult.OK) + { + installDir.Text = dialog.SelectedPath; + } + } +} diff --git a/package/AgentWindowsManaged/Dialogs/InstallDirDialog.resx b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.resx new file mode 100644 index 000000000..ad537526f --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/InstallDirDialog.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.Designer.cs new file mode 100644 index 000000000..250b613b4 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.Designer.cs @@ -0,0 +1,388 @@ +using DevolutionsAgent.Controls; +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class MaintenanceTypeDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.topBorder = new System.Windows.Forms.Panel(); + this.label5 = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.remove = new DevolutionsAgent.Controls.ElevatedButton(); + this.repair = new DevolutionsAgent.Controls.ElevatedButton(); + this.change = new DevolutionsAgent.Controls.ElevatedButton(); + this.topPanel = new System.Windows.Forms.Panel(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.banner = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.border1 = new System.Windows.Forms.Panel(); + this.middlePanel = new System.Windows.Forms.TableLayoutPanel(); + this.panel3 = new System.Windows.Forms.Panel(); + this.panel4 = new System.Windows.Forms.Panel(); + this.panel5 = new System.Windows.Forms.Panel(); + this.topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.middlePanel.SuspendLayout(); + this.panel3.SuspendLayout(); + this.panel4.SuspendLayout(); + this.panel5.SuspendLayout(); + this.SuspendLayout(); + // + // topBorder + // + this.topBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.topBorder.Location = new System.Drawing.Point(0, 58); + this.topBorder.Name = "topBorder"; + this.topBorder.Size = new System.Drawing.Size(494, 1); + this.topBorder.TabIndex = 18; + // + // label5 + // + this.label5.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.label5.AutoEllipsis = true; + this.label5.BackColor = System.Drawing.Color.Transparent; + this.label5.Location = new System.Drawing.Point(28, 40); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(442, 38); + this.label5.TabIndex = 1; + this.label5.Text = "[MaintenanceTypeDlgRemoveText]"; + // + // label4 + // + this.label4.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.label4.AutoEllipsis = true; + this.label4.BackColor = System.Drawing.Color.Transparent; + this.label4.Location = new System.Drawing.Point(28, 40); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(440, 34); + this.label4.TabIndex = 1; + this.label4.Text = "[MaintenanceTypeDlgRepairText]"; + // + // label3 + // + this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.label3.AutoEllipsis = true; + this.label3.BackColor = System.Drawing.Color.Transparent; + this.label3.Location = new System.Drawing.Point(28, 40); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(440, 34); + this.label3.TabIndex = 1; + this.label3.Text = "[MaintenanceTypeDlgChangeDisabledText]"; + // + // remove + // + this.remove.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.remove.AutoSize = true; + this.remove.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.remove.Location = new System.Drawing.Point(0, 5); + this.remove.MaximumSize = new System.Drawing.Size(113, 0); + this.remove.MinimumSize = new System.Drawing.Size(113, 0); + this.remove.Name = "remove"; + this.remove.Size = new System.Drawing.Size(113, 22); + this.remove.TabIndex = 0; + this.remove.Text = "[MaintenanceTypeDlgRemoveButton]"; + this.remove.UseVisualStyleBackColor = true; + this.remove.Click += new System.EventHandler(this.remove_Click); + // + // repair + // + this.repair.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.repair.AutoSize = true; + this.repair.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.repair.Location = new System.Drawing.Point(0, 5); + this.repair.MaximumSize = new System.Drawing.Size(113, 0); + this.repair.MinimumSize = new System.Drawing.Size(113, 0); + this.repair.Name = "repair"; + this.repair.Size = new System.Drawing.Size(113, 22); + this.repair.TabIndex = 0; + this.repair.Text = "[MaintenanceTypeDlgRepairButton]"; + this.repair.UseVisualStyleBackColor = true; + this.repair.Click += new System.EventHandler(this.repair_Click); + // + // change + // + this.change.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.change.AutoSize = true; + this.change.Enabled = false; + this.change.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.change.Location = new System.Drawing.Point(0, 5); + this.change.MaximumSize = new System.Drawing.Size(113, 0); + this.change.MinimumSize = new System.Drawing.Size(113, 0); + this.change.Name = "change"; + this.change.Size = new System.Drawing.Size(113, 22); + this.change.TabIndex = 0; + this.change.Text = "[MaintenanceTypeDlgChangeButton]"; + this.change.UseVisualStyleBackColor = true; + this.change.Click += new System.EventHandler(this.change_Click); + // + // topPanel + // + this.topPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topPanel.BackColor = System.Drawing.SystemColors.Control; + this.topPanel.Controls.Add(this.label2); + this.topPanel.Controls.Add(this.label1); + this.topPanel.Controls.Add(this.banner); + this.topPanel.Location = new System.Drawing.Point(0, 0); + this.topPanel.Name = "topPanel"; + this.topPanel.Size = new System.Drawing.Size(494, 58); + this.topPanel.TabIndex = 13; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.BackColor = System.Drawing.Color.Transparent; + this.label2.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label2.Location = new System.Drawing.Point(19, 31); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(168, 13); + this.label2.TabIndex = 1; + this.label2.Text = "[MaintenanceTypeDlgDescription]"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label1.Location = new System.Drawing.Point(11, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(160, 13); + this.label1.TabIndex = 1; + this.label1.Text = "[MaintenanceTypeDlgTitle]"; + // + // banner + // + this.banner.BackColor = System.Drawing.Color.White; + this.banner.Location = new System.Drawing.Point(0, 0); + this.banner.Name = "banner"; + this.banner.Size = new System.Drawing.Size(494, 58); + this.banner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.banner.TabIndex = 0; + this.banner.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.border1); + this.bottomPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 12; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(491, 43); + this.tableLayoutPanel1.TabIndex = 7; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Enabled = false; + this.back.Location = new System.Drawing.Point(222, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 0; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + this.back.Click += new System.EventHandler(this.Back_Click); + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Enabled = false; + this.next.Location = new System.Drawing.Point(305, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(77, 23); + this.next.TabIndex = 1; + this.next.Text = "[WixUINext]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.Next_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Location = new System.Drawing.Point(402, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // border1 + // + this.border1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.border1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.border1.Location = new System.Drawing.Point(0, 0); + this.border1.Name = "border1"; + this.border1.Size = new System.Drawing.Size(494, 1); + this.border1.TabIndex = 17; + // + // middlePanel + // + this.middlePanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.middlePanel.ColumnCount = 1; + this.middlePanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.middlePanel.Controls.Add(this.panel3, 0, 0); + this.middlePanel.Controls.Add(this.panel4, 0, 1); + this.middlePanel.Controls.Add(this.panel5, 0, 2); + this.middlePanel.Location = new System.Drawing.Point(15, 61); + this.middlePanel.Name = "middlePanel"; + this.middlePanel.RowCount = 3; + this.middlePanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.middlePanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.middlePanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.middlePanel.Size = new System.Drawing.Size(479, 248); + this.middlePanel.TabIndex = 20; + // + // panel3 + // + this.panel3.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel3.Controls.Add(this.change); + this.panel3.Controls.Add(this.label3); + this.panel3.Location = new System.Drawing.Point(3, 3); + this.panel3.Name = "panel3"; + this.panel3.Size = new System.Drawing.Size(473, 76); + this.panel3.TabIndex = 0; + // + // panel4 + // + this.panel4.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel4.Controls.Add(this.repair); + this.panel4.Controls.Add(this.label4); + this.panel4.Location = new System.Drawing.Point(3, 85); + this.panel4.Name = "panel4"; + this.panel4.Size = new System.Drawing.Size(473, 76); + this.panel4.TabIndex = 1; + // + // panel5 + // + this.panel5.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel5.Controls.Add(this.remove); + this.panel5.Controls.Add(this.label5); + this.panel5.Location = new System.Drawing.Point(3, 167); + this.panel5.Name = "panel5"; + this.panel5.Size = new System.Drawing.Size(473, 78); + this.panel5.TabIndex = 2; + // + // MaintenanceTypeDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.middlePanel); + this.Controls.Add(this.topBorder); + this.Controls.Add(this.topPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "MaintenanceTypeDialog"; + this.Load += new System.EventHandler(this.OnLoad); + this.topPanel.ResumeLayout(false); + this.topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.middlePanel.ResumeLayout(false); + this.panel3.ResumeLayout(false); + this.panel3.PerformLayout(); + this.panel4.ResumeLayout(false); + this.panel4.PerformLayout(); + this.panel5.ResumeLayout(false); + this.panel5.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox banner; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Panel topPanel; + private System.Windows.Forms.Panel bottomPanel; + private ElevatedButton change; + private ElevatedButton repair; + private ElevatedButton remove; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.Panel border1; + private System.Windows.Forms.Panel topBorder; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.TableLayoutPanel middlePanel; + private System.Windows.Forms.Panel panel3; + private System.Windows.Forms.Panel panel4; + private System.Windows.Forms.Panel panel5; + } +} diff --git a/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.cs b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.cs new file mode 100644 index 000000000..c7debe517 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.cs @@ -0,0 +1,70 @@ +using DevolutionsAgent.Dialogs; +using System; +using System.Linq; +using WixSharp; + +namespace WixSharpSetup.Dialogs; + +public partial class MaintenanceTypeDialog : AgentDialog +{ + public MaintenanceTypeDialog() + { + InitializeComponent(); + + label1.MakeTransparentOn(banner); + label2.MakeTransparentOn(banner); + } + + Type ProgressDialog + { + get + { + return Shell.Dialogs + .FirstOrDefault(d => d.GetInterfaces().Contains(typeof(IProgressDialog))); + } + } + + void change_Click(object sender, System.EventArgs e) + { + Runtime.Session["MODIFY_ACTION"] = "Change"; + Shell.GoNext(); + } + + void repair_Click(object sender, System.EventArgs e) + { + Runtime.Session["MODIFY_ACTION"] = "Repair"; + int index = Shell.Dialogs.IndexOf(ProgressDialog); + if (index != -1) + Shell.GoTo(index); + else + Shell.GoNext(); + } + + void remove_Click(object sender, System.EventArgs e) + { + Runtime.Session["REMOVE"] = "ALL"; + Runtime.Session["MODIFY_ACTION"] = "Remove"; + + int index = Shell.Dialogs.IndexOf(ProgressDialog); + if (index != -1) + Shell.GoTo(index); + else + Shell.GoNext(); + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Back_Click(object sender, EventArgs e) => base.Back_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Next_Click(object sender, EventArgs e) => base.Next_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Cancel_Click(object sender, EventArgs e) => base.Cancel_Click(sender, e); + + public override void OnLoad(object sender, System.EventArgs e) + { + banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner"); + + base.OnLoad(sender, e); + } +} diff --git a/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.resx b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/MaintenanceTypeDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/ProgressDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/ProgressDialog.Designer.cs new file mode 100644 index 000000000..c49b91e16 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ProgressDialog.Designer.cs @@ -0,0 +1,292 @@ +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class ProgressDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.topBorder = new System.Windows.Forms.Panel(); + this.progress = new System.Windows.Forms.ProgressBar(); + this.currentAction = new System.Windows.Forms.Label(); + this.topPanel = new System.Windows.Forms.Panel(); + this.dialogText = new System.Windows.Forms.Label(); + this.banner = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.bottomBorder = new System.Windows.Forms.Panel(); + this.description = new System.Windows.Forms.Label(); + this.currentActionLabel = new System.Windows.Forms.Label(); + this.waitPrompt = new System.Windows.Forms.Label(); + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + this.SuspendLayout(); + // + // topBorder + // + this.topBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.topBorder.Location = new System.Drawing.Point(0, 58); + this.topBorder.Name = "topBorder"; + this.topBorder.Size = new System.Drawing.Size(494, 1); + this.topBorder.TabIndex = 22; + // + // progress + // + this.progress.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.progress.Location = new System.Drawing.Point(32, 165); + this.progress.Name = "progress"; + this.progress.Size = new System.Drawing.Size(434, 13); + this.progress.Step = 1; + this.progress.TabIndex = 20; + // + // currentAction + // + this.currentAction.AutoSize = true; + this.currentAction.Location = new System.Drawing.Point(34, 144); + this.currentAction.Name = "currentAction"; + this.currentAction.Size = new System.Drawing.Size(0, 13); + this.currentAction.TabIndex = 19; + // + // topPanel + // + this.topPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topPanel.BackColor = System.Drawing.SystemColors.Control; + this.topPanel.Controls.Add(this.dialogText); + this.topPanel.Controls.Add(this.banner); + this.topPanel.Location = new System.Drawing.Point(0, 0); + this.topPanel.Name = "topPanel"; + this.topPanel.Size = new System.Drawing.Size(494, 58); + this.topPanel.TabIndex = 15; + // + // dialogText + // + this.dialogText.AutoSize = true; + this.dialogText.BackColor = System.Drawing.Color.Transparent; + this.dialogText.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.dialogText.ForeColor = System.Drawing.SystemColors.HighlightText; + this.dialogText.Location = new System.Drawing.Point(11, 22); + this.dialogText.Name = "dialogText"; + this.dialogText.Size = new System.Drawing.Size(159, 13); + this.dialogText.TabIndex = 1; + this.dialogText.Text = "[ProgressDlgTitleInstalling]"; + // + // banner + // + this.banner.BackColor = System.Drawing.Color.White; + this.banner.Location = new System.Drawing.Point(0, 0); + this.banner.Name = "banner"; + this.banner.Size = new System.Drawing.Size(494, 58); + this.banner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.banner.TabIndex = 0; + this.banner.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.bottomBorder); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 14; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(491, 43); + this.tableLayoutPanel1.TabIndex = 7; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Enabled = false; + this.back.Location = new System.Drawing.Point(222, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 0; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Enabled = false; + this.next.Location = new System.Drawing.Point(305, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(77, 23); + this.next.TabIndex = 1; + this.next.Text = "[WixUINext]"; + this.next.UseVisualStyleBackColor = true; + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Location = new System.Drawing.Point(402, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // bottomBorder + // + this.bottomBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.bottomBorder.Location = new System.Drawing.Point(0, 0); + this.bottomBorder.Name = "bottomBorder"; + this.bottomBorder.Size = new System.Drawing.Size(494, 1); + this.bottomBorder.TabIndex = 21; + // + // description + // + this.description.AutoSize = true; + this.description.BackColor = System.Drawing.Color.Transparent; + this.description.Location = new System.Drawing.Point(29, 95); + this.description.Name = "description"; + this.description.Size = new System.Drawing.Size(132, 13); + this.description.TabIndex = 16; + this.description.Text = "[ProgressDlgTextInstalling]"; + // + // currentActionLabel + // + this.currentActionLabel.BackColor = System.Drawing.Color.Transparent; + this.currentActionLabel.Location = new System.Drawing.Point(29, 144); + this.currentActionLabel.Name = "currentActionLabel"; + this.currentActionLabel.Size = new System.Drawing.Size(132, 13); + this.currentActionLabel.TabIndex = 19; + this.currentActionLabel.Text = "[ProgressDlgStatusLabel]"; + this.currentActionLabel.Visible = false; + // + // waitPrompt + // + this.waitPrompt.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.waitPrompt.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.waitPrompt.ForeColor = System.Drawing.Color.Blue; + this.waitPrompt.Location = new System.Drawing.Point(29, 195); + this.waitPrompt.Name = "waitPrompt"; + this.waitPrompt.Size = new System.Drawing.Size(381, 114); + this.waitPrompt.TabIndex = 23; + this.waitPrompt.TabStop = true; + this.waitPrompt.Text = "[UACPromptLabel]"; + this.waitPrompt.Visible = false; + // + // pictureBox1 + // + this.pictureBox1.Location = new System.Drawing.Point(416, 195); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(50, 50); + this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage; + this.pictureBox1.TabIndex = 24; + this.pictureBox1.TabStop = false; + this.pictureBox1.Visible = false; + // + // ProgressDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.ClientSize = new System.Drawing.Size(494, 361); + this.ControlBox = false; + this.Controls.Add(this.pictureBox1); + this.Controls.Add(this.waitPrompt); + this.Controls.Add(this.topBorder); + this.Controls.Add(this.progress); + this.Controls.Add(this.currentAction); + this.Controls.Add(this.topPanel); + this.Controls.Add(this.bottomPanel); + this.Controls.Add(this.description); + this.Controls.Add(this.currentActionLabel); + this.Name = "ProgressDialog"; + this.Text = "[ProgressDlg_Title]"; + this.Load += new System.EventHandler(this.OnLoad); + this.topPanel.ResumeLayout(false); + this.topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.PictureBox banner; + private System.Windows.Forms.Panel topPanel; + private System.Windows.Forms.Label dialogText; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.Label description; + private System.Windows.Forms.ProgressBar progress; + private System.Windows.Forms.Label currentAction; + private System.Windows.Forms.Label currentActionLabel; + private System.Windows.Forms.Panel bottomBorder; + private System.Windows.Forms.Panel topBorder; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Label waitPrompt; + private System.Windows.Forms.PictureBox pictureBox1; + } +} \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/ProgressDialog.cs b/package/AgentWindowsManaged/Dialogs/ProgressDialog.cs new file mode 100644 index 000000000..f6089c578 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ProgressDialog.cs @@ -0,0 +1,200 @@ +using DevolutionsAgent.Dialogs; +using Microsoft.Deployment.WindowsInstaller; +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Security.Principal; +using DevolutionsAgent.Helpers; +using DevolutionsAgent.Properties; +using WixSharp; +using WixSharp.CommonTasks; + +namespace WixSharpSetup.Dialogs; + +public partial class ProgressDialog : AgentDialog, IProgressDialog +{ + private static Icon shieldIcon; + + public static Icon ShieldLarge => shieldIcon ??= StockIcon.GetStockIcon(StockIcon.SIID_SHIELD, StockIcon.SHGSI_LARGEICON); + + public ProgressDialog() + { + InitializeComponent(); + dialogText.MakeTransparentOn(banner); + + pictureBox1.Image = ShieldLarge.ToBitmap(); + + showWaitPromptTimer = new System.Windows.Forms.Timer() { Interval = 4000 }; + showWaitPromptTimer.Tick += (s, e) => + { + this.waitPrompt.Visible = true; + this.pictureBox1.Visible = true; + showWaitPromptTimer.Stop(); + }; + } + + private readonly System.Windows.Forms.Timer showWaitPromptTimer; + + public override void OnLoad(object sender, EventArgs e) + { + banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner"); + + if (!WindowsIdentity.GetCurrent().IsAdmin() && Uac.IsEnabled()) + { + showWaitPromptTimer.Start(); + } + + AgentProperties properties = new AgentProperties(this.Session()); + + base.OnLoad(sender, e); + + Shell.StartExecute(); + } + + /// + /// Called when Shell is changed. It is a good place to initialize the dialog to reflect the MSI session + /// (e.g. localize the view). + /// + protected override void OnShellChanged() + { + if (Runtime.Session.IsUninstalling()) + { + dialogText.Text = + Text = "[ProgressDlgTitleRemoving]"; + description.Text = "[ProgressDlgTextRemoving]"; + } + else if (Runtime.Session.IsRepairing()) + { + dialogText.Text = + Text = "[ProgressDlgTextRepairing]"; + description.Text = "[ProgressDlgTitleRepairing]"; + } + else if (Runtime.Session.IsInstalling()) + { + dialogText.Text = + Text = "[ProgressDlgTitleInstalling]"; + description.Text = "[ProgressDlgTextInstalling]"; + } + + this.Localize(); + } + + /// + /// Processes the message. + /// + /// Type of the message. + /// The message record. + /// The buttons. + /// The icon. + /// The default button. + /// + public override MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton) + { + switch (messageType) + { + case InstallMessage.InstallStart: + case InstallMessage.InstallEnd: + { + showWaitPromptTimer.Stop(); + waitPrompt.Visible = false; + pictureBox1.Visible = false; + } + break; + + case InstallMessage.ActionStart: + { + try + { + //messageRecord[0] - is reserved for FormatString value + + string message = null; + + bool simple = true; + if (simple) + { + /* + messageRecord[2] unconditionally contains the string to display + + Examples: + + messageRecord[0] "Action 23:14:50: [1]. [2]" + messageRecord[1] "InstallFiles" + messageRecord[2] "Copying new files" + messageRecord[3] "File: [1], Directory: [9], Size: [6]" + + messageRecord[0] "Action 23:15:21: [1]. [2]" + messageRecord[1] "RegisterUser" + messageRecord[2] "Registering user" + messageRecord[3] "[1]" + + */ + if (messageRecord.FieldCount >= 3) + { + message = messageRecord[2].ToString(); + } + } + else + { + message = messageRecord.FormatString; + if (message.IsNotEmpty()) + { + for (int i = 1; i < messageRecord.FieldCount; i++) + { + message = message.Replace("[" + i + "]", messageRecord[i].ToString()); + } + } + else + { + message = messageRecord[messageRecord.FieldCount - 1].ToString(); + } + } + + if (message.IsNotEmpty()) + currentAction.Text = "{0} {1}".FormatWith(currentActionLabel.Text, message); + } + catch + { + //Catch all, we don't want the installer to crash in an + //attempt to process message. + } + } + break; + } + return MessageResult.OK; + } + + /// + /// Called when MSI execution progress is changed. + /// + /// The progress percentage. + public override void OnProgress(int progressPercentage) + { + progress.Value = progressPercentage; + + if (progressPercentage > 0) + { + waitPrompt.Visible = false; + } + } + + /// + /// Called when MSI execution is complete. + /// + public override void OnExecuteComplete() + { + currentAction.Text = null; + Shell.GoNext(); + } + + protected override void Cancel_Click(object sender, EventArgs e) + { + if (Shell.IsDemoMode) + { + Shell.GoNext(); + } + else + { + Shell.Cancel(); + } + } +} diff --git a/package/AgentWindowsManaged/Dialogs/ProgressDialog.resx b/package/AgentWindowsManaged/Dialogs/ProgressDialog.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/ProgressDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.Designer.cs new file mode 100644 index 000000000..e7a848cd0 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.Designer.cs @@ -0,0 +1,274 @@ +using DevolutionsAgent.Controls; +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class VerifyReadyDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.copyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.middlePanel = new System.Windows.Forms.Panel(); + this.label3 = new System.Windows.Forms.Label(); + this.topBorder = new System.Windows.Forms.Panel(); + this.topPanel = new System.Windows.Forms.Panel(); + this.label1 = new System.Windows.Forms.Label(); + this.banner = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.generateCli = new System.Windows.Forms.LinkLabel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new DevolutionsAgent.Controls.ElevatedButton(); + this.cancel = new System.Windows.Forms.Button(); + this.border1 = new System.Windows.Forms.Panel(); + this.contextMenuStrip1.SuspendLayout(); + this.middlePanel.SuspendLayout(); + this.topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // contextMenuStrip1 + // + this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.copyToolStripMenuItem}); + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(103, 26); + // + // copyToolStripMenuItem + // + this.copyToolStripMenuItem.Name = "copyToolStripMenuItem"; + this.copyToolStripMenuItem.Size = new System.Drawing.Size(102, 22); + this.copyToolStripMenuItem.Text = "Copy"; + // + // middlePanel + // + this.middlePanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.middlePanel.Controls.Add(this.label3); + this.middlePanel.Location = new System.Drawing.Point(22, 75); + this.middlePanel.Name = "middlePanel"; + this.middlePanel.Size = new System.Drawing.Size(449, 139); + this.middlePanel.TabIndex = 16; + // + // label3 + // + this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label3.BackColor = System.Drawing.Color.Transparent; + this.label3.Location = new System.Drawing.Point(0, 3); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(446, 136); + this.label3.TabIndex = 11; + this.label3.Text = "[VerifyReadyDlgInstallText]"; + // + // topBorder + // + this.topBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.topBorder.Location = new System.Drawing.Point(0, 58); + this.topBorder.Name = "topBorder"; + this.topBorder.Size = new System.Drawing.Size(494, 1); + this.topBorder.TabIndex = 15; + // + // topPanel + // + this.topPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topPanel.BackColor = System.Drawing.SystemColors.Control; + this.topPanel.Controls.Add(this.label1); + this.topPanel.Controls.Add(this.banner); + this.topPanel.Location = new System.Drawing.Point(0, 0); + this.topPanel.Name = "topPanel"; + this.topPanel.Size = new System.Drawing.Size(494, 58); + this.topPanel.TabIndex = 10; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label1.Location = new System.Drawing.Point(11, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(161, 13); + this.label1.TabIndex = 1; + this.label1.Text = "[VerifyReadyDlgInstallTitle]"; + // + // banner + // + this.banner.BackColor = System.Drawing.Color.White; + this.banner.Location = new System.Drawing.Point(0, 0); + this.banner.Name = "banner"; + this.banner.Size = new System.Drawing.Size(494, 58); + this.banner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.banner.TabIndex = 0; + this.banner.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.generateCli); + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.border1); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 9; + // + // generateCli + // + this.generateCli.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.generateCli.AutoSize = true; + this.generateCli.BackColor = System.Drawing.Color.Transparent; + this.generateCli.Location = new System.Drawing.Point(16, 17); + this.generateCli.Name = "generateCli"; + this.generateCli.Size = new System.Drawing.Size(70, 13); + this.generateCli.TabIndex = 3; + this.generateCli.TabStop = true; + this.generateCli.Text = "Generate CLI"; + this.generateCli.Visible = false; + this.generateCli.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.generateCli_LinkClicked); + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(493, 43); + this.tableLayoutPanel1.TabIndex = 0; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Location = new System.Drawing.Point(174, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 1; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + this.back.Click += new System.EventHandler(this.Back_Click); + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.next.Location = new System.Drawing.Point(257, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(127, 23); + this.next.TabIndex = 0; + this.next.Text = "[VerifyReadyDlgInstall]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.Next_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Location = new System.Drawing.Point(404, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // border1 + // + this.border1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.border1.Dock = System.Windows.Forms.DockStyle.Top; + this.border1.Location = new System.Drawing.Point(0, 0); + this.border1.Name = "border1"; + this.border1.Size = new System.Drawing.Size(494, 1); + this.border1.TabIndex = 14; + // + // VerifyReadyDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.middlePanel); + this.Controls.Add(this.topBorder); + this.Controls.Add(this.topPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "VerifyReadyDialog"; + this.Load += new System.EventHandler(this.OnLoad); + this.contextMenuStrip1.ResumeLayout(false); + this.middlePanel.ResumeLayout(false); + this.topPanel.ResumeLayout(false); + this.topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.bottomPanel.PerformLayout(); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox banner; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.ToolStripMenuItem copyToolStripMenuItem; + private System.Windows.Forms.Panel topPanel; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Panel border1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private ElevatedButton next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Panel topBorder; + private System.Windows.Forms.Panel middlePanel; + private System.Windows.Forms.LinkLabel generateCli; + } +} diff --git a/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.cs b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.cs new file mode 100644 index 000000000..34ed0227a --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.cs @@ -0,0 +1,68 @@ +using DevolutionsAgent.Dialogs; +using System; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using DevolutionsAgent.Properties; +using WixSharp; + +namespace WixSharpSetup.Dialogs; + +public partial class VerifyReadyDialog : AgentDialog +{ + public VerifyReadyDialog() + { + InitializeComponent(); + label1.MakeTransparentOn(banner); + +#if DEBUG + this.generateCli.Visible = true; +#endif + } + + public override void OnLoad(object sender, EventArgs e) + { + banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner"); + + base.OnLoad(sender, e); + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Back_Click(object sender, EventArgs e) => base.Back_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Next_Click(object sender, EventArgs e) + { + Shell.GoNext(); + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Cancel_Click(object sender, EventArgs e) => base.Cancel_Click(sender, e); + + private void generateCli_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + StringBuilder builder = new(); + builder.Append("msiexec /i DevolutionsAgent.msi /qb /l*v install.log"); + + foreach (IWixProperty property in AgentProperties.Properties.Where(p => p.Public)) + { + string propertyValue = this.Session().Property(property.Id); + + if (propertyValue.Equals(property.DefaultValue)) + { + continue; + } + + builder.Append($" {property.Id}=\"{propertyValue}\""); + } + + builder.AppendLine(); + builder.AppendLine(); + builder.Append("Copy to clipboard?"); + + if (MessageBox.Show(builder.ToString(), "", MessageBoxButtons.YesNo) == DialogResult.Yes) + { + Clipboard.SetText(builder.ToString()); + } + } +} diff --git a/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.resx b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.resx new file mode 100644 index 000000000..ad537526f --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/VerifyReadyDialog.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/WelcomeDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.Designer.cs new file mode 100644 index 000000000..9d98e3bca --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.Designer.cs @@ -0,0 +1,230 @@ +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class WelcomeDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.imgPanel = new System.Windows.Forms.Panel(); + this.textPanel = new System.Windows.Forms.Panel(); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.image = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.bottomBorder = new System.Windows.Forms.Panel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.imgPanel.SuspendLayout(); + this.textPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.image)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // imgPanel + // + this.imgPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.imgPanel.Controls.Add(this.textPanel); + this.imgPanel.Controls.Add(this.image); + this.imgPanel.Location = new System.Drawing.Point(0, 0); + this.imgPanel.Name = "imgPanel"; + this.imgPanel.Size = new System.Drawing.Size(494, 312); + this.imgPanel.TabIndex = 4; + // + // textPanel + // + this.textPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textPanel.BackColor = System.Drawing.Color.White; + this.textPanel.Controls.Add(this.label1); + this.textPanel.Controls.Add(this.label2); + this.textPanel.Location = new System.Drawing.Point(162, 12); + this.textPanel.Name = "textPanel"; + this.textPanel.Size = new System.Drawing.Size(326, 294); + this.textPanel.TabIndex = 4; + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.Location = new System.Drawing.Point(3, 14); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(317, 61); + this.label1.TabIndex = 2; + this.label1.Text = "[WelcomeDlgTitle]"; + // + // label2 + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label2.BackColor = System.Drawing.Color.Transparent; + this.label2.Location = new System.Drawing.Point(4, 75); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(316, 209); + this.label2.TabIndex = 3; + this.label2.Text = "[WelcomeDlgDescription]"; + // + // image + // + this.image.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.image.BackColor = System.Drawing.Color.White; + this.image.Location = new System.Drawing.Point(0, 0); + this.image.Name = "image"; + this.image.Size = new System.Drawing.Size(494, 312); + this.image.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.image.TabIndex = 0; + this.image.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.bottomBorder); + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 1; + // + // bottomBorder + // + this.bottomBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.bottomBorder.Location = new System.Drawing.Point(0, 0); + this.bottomBorder.Name = "bottomBorder"; + this.bottomBorder.Size = new System.Drawing.Size(494, 1); + this.bottomBorder.TabIndex = 5; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(493, 43); + this.tableLayoutPanel1.TabIndex = 0; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Enabled = false; + this.back.Location = new System.Drawing.Point(224, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 1; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + this.back.Click += new System.EventHandler(this.Back_Click); + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Location = new System.Drawing.Point(307, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(77, 23); + this.next.TabIndex = 0; + this.next.Text = "[WixUINext]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.Next_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cancel.Location = new System.Drawing.Point(404, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // WelcomeDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.White; + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.imgPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "WelcomeDialog"; + this.Text = "[WelcomeDlg_Title]"; + this.Load += new System.EventHandler(this.OnLoad); + this.imgPanel.ResumeLayout(false); + this.textPanel.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.image)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox image; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Panel imgPanel; + private System.Windows.Forms.Panel bottomBorder; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Panel textPanel; + } +} \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/WelcomeDialog.cs b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.cs new file mode 100644 index 000000000..4bc160e7e --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.cs @@ -0,0 +1,32 @@ +using System; +using System.Drawing; + +using DevolutionsAgent.Dialogs; + +namespace WixSharpSetup.Dialogs; + +public partial class WelcomeDialog : AgentDialog +{ + public WelcomeDialog() + { + InitializeComponent(); + + this.textPanel.BackColor = Color.FromArgb(233, 233, 233); + } + + public override void OnLoad(object sender, EventArgs e) + { + image.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Dialog"); + + base.OnLoad(sender, e); + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Back_Click(object sender, EventArgs e) => base.Back_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Next_Click(object sender, EventArgs e) => base.Next_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Cancel_Click(object sender, EventArgs e) => base.Cancel_Click(sender, e); +} diff --git a/package/AgentWindowsManaged/Dialogs/WelcomeDialog.resx b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/WelcomeDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/package/AgentWindowsManaged/Dialogs/Wizard.cs b/package/AgentWindowsManaged/Dialogs/Wizard.cs new file mode 100644 index 000000000..519d1d07a --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/Wizard.cs @@ -0,0 +1,45 @@ +using DevolutionsAgent.Helpers; +using DevolutionsAgent.Properties; +using Microsoft.Deployment.WindowsInstaller; +using System; +using System.Collections.Generic; +using System.Linq; +using WixSharp; +using WixSharpSetup.Dialogs; + +namespace DevolutionsAgent.Dialogs; + +internal static class Wizard +{ + internal static Dictionary Globals = new Dictionary(); + + private static readonly Type[] Sequence; + + static Wizard() + { + List dialogs = new() + { + typeof(WelcomeDialog), + typeof(InstallDirDialog), + }; + + dialogs.Add(typeof(VerifyReadyDialog)); + + Sequence = dialogs.ToArray(); + } + + internal static IEnumerable Dialogs => Sequence; + + internal static int Move(IManagedDialog current, bool forward) + { + Type t = current.GetType(); + int index = Dialogs.FindIndex(t); + + index = forward ? index + 1 : index - 1; + return index; + } + + internal static int GetNext(IManagedDialog current) => Move(current, true); + + internal static int GetPrevious(IManagedDialog current) => Move(current, false); +} diff --git a/package/AgentWindowsManaged/Helpers/AppSearch.cs b/package/AgentWindowsManaged/Helpers/AppSearch.cs new file mode 100644 index 000000000..3229a12b8 --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/AppSearch.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using DevolutionsAgent.Resources; + +namespace DevolutionsAgent.Helpers +{ + internal static class AppSearch + { + internal static Version InstalledVersion => + WixSharp.CommonTasks.AppSearch.GetRelatedProducts("{" + Includes.UPGRADE_CODE + "}") + .Where(productCode => WixSharp.CommonTasks.AppSearch.GetProductName(productCode)?.Equals(Includes.PRODUCT_NAME) ?? false) + .Select(WixSharp.CommonTasks.AppSearch.GetProductVersion) + .FirstOrDefault(); + } +} diff --git a/package/AgentWindowsManaged/Helpers/Debouncer.cs b/package/AgentWindowsManaged/Helpers/Debouncer.cs new file mode 100644 index 000000000..c8b90d85c --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/Debouncer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace DevolutionsAgent.Helpers +{ + public class Debouncer : IDisposable + { + private readonly TimeSpan ts; + private readonly Action action; + private readonly object parameter; + private readonly HashSet resets = new(); + private readonly object mutex = new(); + + public Debouncer(TimeSpan timespan, Action action, object parameter) + { + this.ts = timespan; + this.action = action; + this.parameter = parameter; + } + + public void Invoke() + { + var thisReset = new ManualResetEvent(false); + + lock (mutex) + { + while (resets.Count > 0) + { + var otherReset = resets.First(); + resets.Remove(otherReset); + otherReset.Set(); + } + + resets.Add(thisReset); + } + + ThreadPool.QueueUserWorkItem(_ => + { + try + { + if (!thisReset.WaitOne(ts)) + { + this.action(this.parameter); + } + } + finally + { + lock (mutex) + { + using (thisReset) + { + resets.Remove(thisReset); + } + } + } + }); + } + + public void Dispose() + { + lock (mutex) + { + while (resets.Count > 0) + { + var reset = resets.First(); + resets.Remove(reset); + reset.Set(); + reset.Dispose(); + } + } + } + } +} diff --git a/package/AgentWindowsManaged/Helpers/DisplayEnum.cs b/package/AgentWindowsManaged/Helpers/DisplayEnum.cs new file mode 100644 index 000000000..1b43c3f8d --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/DisplayEnum.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using WixSharp; + +namespace DevolutionsAgent.Helpers +{ + internal class NamedItem + { + public NamedItem(string name, T value) + { + this.Name = name; + this.Value = value; + } + + public string Name { get; } + + public T Value { get; } + + public override string ToString() + { + return this.Name; + } + } + + internal class DisplayEnum where T : Enum + { + private readonly MsiRuntime runtime; + + private IEnumerable Values => (T[])Enum.GetValues(typeof(T)); + + internal IEnumerable> Items + { + get + { + Func fnLocalize = this.runtime.Localize; + string enumName = typeof(T).Name; + + foreach (T value in this.Values) + { + string key = $"{enumName}_{value}"; + string name = $"[{key}]".LocalizeWith(fnLocalize); + + if (name.Equals(key)) + { + name = Enum.GetName(typeof(T), value); + } + + yield return new NamedItem(name, value); + } + } + } + + public DisplayEnum(MsiRuntime runtime) + { + this.runtime = runtime; + } + } +} diff --git a/package/AgentWindowsManaged/Helpers/Localization.cs b/package/AgentWindowsManaged/Helpers/Localization.cs new file mode 100644 index 000000000..d4857f454 --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/Localization.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using WixSharp; +using static System.Windows.Forms.LinkLabel; + +namespace DevolutionsAgent.Helpers +{ + internal static class LocalizationExtensions + { + internal static void Source(this ComboBox comboBox, MsiRuntime runtime) where T : Enum + { + comboBox.DisplayMember = nameof(NamedItem.Name); + comboBox.ValueMember = nameof(NamedItem.Value); + comboBox.DataSource = new DisplayEnum(runtime).Items.ToArray(); + } + + internal static T Selected(this ComboBox comboBox) where T : Enum => ((NamedItem)comboBox.SelectedItem).Value; + + internal static void SetSelected(this ComboBox comboBox, T value) where T : Enum => comboBox.SelectedValue = value; + + internal static void SetLink(this LinkLabel label, MsiRuntime runtime, string labelFormat, params string[] linkText) + { + string linkFormat = $"[{labelFormat}]".LocalizeWith(runtime.Localize); + + object[] localizedLinkText = new object[linkText.Length]; + + for (int i = 0; i < linkText.Length; i++) + { + localizedLinkText[i] = $"[{linkText[i]}]".LocalizeWith(runtime.Localize); + } + + label.Text = string.Format(linkFormat, localizedLinkText); + + for (int i = 0; i < localizedLinkText.Length; i++) + { + string localizedLink = localizedLinkText[i].ToString(); + Link link = new Link(label.Text.IndexOf(localizedLink, StringComparison.CurrentCulture), + localizedLink.Length); + link.Tag = linkText[i]; + label.Links.Add(link); + } + } + } +} diff --git a/package/AgentWindowsManaged/Helpers/StockIcon.cs b/package/AgentWindowsManaged/Helpers/StockIcon.cs new file mode 100644 index 000000000..dd2707cd1 --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/StockIcon.cs @@ -0,0 +1,47 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace DevolutionsAgent.Helpers +{ + internal class StockIcon + { + internal static Icon GetStockIcon(uint type, uint size) + { + var info = new SHSTOCKICONINFO(); + info.cbSize = (uint)Marshal.SizeOf(info); + + SHGetStockIconInfo(type, SHGSI_ICON | size, ref info); + + var icon = (Icon)Icon.FromHandle(info.hIcon).Clone(); // Get a copy that doesn't use the original handle + DestroyIcon(info.hIcon); // Clean up native icon to prevent resource leak + + return icon; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct SHSTOCKICONINFO + { + public uint cbSize; + public IntPtr hIcon; + public int iSysIconIndex; + public int iIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string szPath; + } + + [DllImport("shell32.dll")] + private static extern int SHGetStockIconInfo(uint siid, uint uFlags, ref SHSTOCKICONINFO psii); + + [DllImport("user32.dll")] + private static extern bool DestroyIcon(IntPtr handle); + + internal const uint SIID_HELP = 23; + internal const uint SIID_SHIELD = 77; + internal const uint SIID_WARNING = 78; + internal const uint SIID_INFO = 79; + internal const uint SHGSI_ICON = 0x100; + internal const uint SHGSI_LARGEICON = 0x0; + internal const uint SHGSI_SMALLICON = 0x1; + } +} diff --git a/package/AgentWindowsManaged/Helpers/Validation.cs b/package/AgentWindowsManaged/Helpers/Validation.cs new file mode 100644 index 000000000..52d67e6e7 --- /dev/null +++ b/package/AgentWindowsManaged/Helpers/Validation.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DevolutionsAgent.Helpers +{ + internal class Validation + { + internal static bool IsValidPort(string port, out uint p) + { + p = 0; + + if (uint.TryParse(port, out p)) + { + return p is > 0 and <= 65535; + } + + return false; + } + } +} diff --git a/package/AgentWindowsManaged/Program.cs b/package/AgentWindowsManaged/Program.cs new file mode 100644 index 000000000..b0050aa93 --- /dev/null +++ b/package/AgentWindowsManaged/Program.cs @@ -0,0 +1,364 @@ +using DevolutionsAgent.Actions; +using DevolutionsAgent.Dialogs; +using DevolutionsAgent.Properties; +using DevolutionsAgent.Resources; +using Microsoft.Deployment.WindowsInstaller; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using WixSharp; +using WixSharp.CommonTasks; +using WixSharpSetup.Dialogs; +using Assembly = System.Reflection.Assembly; +using CompressionLevel = WixSharp.CompressionLevel; +using File = WixSharp.File; + +namespace DevolutionsAgent; + +internal class Program +{ + private const string PackageName = "DevolutionsAgent"; + + private static string DevolutionsAgentExePath + { + get + { + string path = Environment.GetEnvironmentVariable("DAGENT_EXECUTABLE"); + + if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) + { +#if DEBUG + path = "..\\..\\target\\x86_64-pc-windows-msvc\\release\\devolutionsagent.exe"; +#else + throw new Exception("The environment variable DAGENT_EXECUTABLE is not specified or the file does not exist"); +#endif + } + + if (!System.IO.File.Exists(path)) + { + throw new FileNotFoundException("The agent executable was not found", path); + } + + return path; + } + } + + private static Version DevolutionsAgentVersion + { + get + { + string versionString = Environment.GetEnvironmentVariable("DAGENT_VERSION"); + + if (string.IsNullOrEmpty(versionString) || !Version.TryParse(versionString, out Version version)) + { +#if DEBUG + versionString = FileVersionInfo.GetVersionInfo(DevolutionsAgentExePath).FileVersion; + + if (versionString.StartsWith("20")) + { + versionString = versionString.Substring(2); + } + + version = Version.Parse(versionString); +#else + throw new Exception("The environment variable DAGENT_VERSION is not specified or is invalid"); +#endif + } + + return version; + } + } + + private static bool SourceOnlyBuild => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DAGENT_MSI_SOURCE_ONLY_BUILD")); + + private static string ProjectLangId + { + get + { + string langId = Environment.GetEnvironmentVariable("DAGENT_MSI_LANG_ID"); + + if (string.IsNullOrWhiteSpace(langId)) + { + return "en-US"; + } + + // ReSharper disable once SimplifyLinqExpressionUseAll + if (!Languages.Any(x => x.Key == langId)) + { + throw new Exception($"unrecognized language id: {langId}"); + } + + return langId; + } + } + + private static readonly Dictionary Languages = new() + { + { "en-US", "DevolutionsAgent_en-us.wxl" }, + { "fr-FR", "DevolutionsAgent_fr-fr.wxl" }, + }; + + private static KeyValuePair enUS => Languages.First(x => x.Key == "en-US"); + + private static KeyValuePair frFR => Languages.First(x => x.Key == "fr-FR"); + + static void Main() + { + ManagedProject project = new(Includes.PRODUCT_NAME) + { + UpgradeCode = Includes.UPGRADE_CODE, + Version = DevolutionsAgentVersion, + Description = "!(loc.ProductDescription)", + InstallerVersion = 500, // Windows Installer 5.0; Server 2008 R2 / Windows 7 + InstallScope = InstallScope.perMachine, + InstallPrivileges = InstallPrivileges.elevated, + Platform = Platform.x64, +#if DEBUG + PreserveTempFiles = true, + OutDir = "Debug", +#else + OutDir = "Release", +#endif + BannerImage = "Resources/WixUIBanner.jpg", + BackgroundImage = "Resources/WixUIDialog.jpg", + ValidateBackgroundImage = false, + OutFileName = PackageName, + MajorUpgrade = new MajorUpgrade + { + AllowDowngrades = false, + AllowSameVersionUpgrades = true, + DowngradeErrorMessage = "!(loc.NewerInstalled)", + Schedule = UpgradeSchedule.afterInstallInitialize, + }, + Media = new List + { + new() + { + Cabinet = "dgateway.cab", + EmbedCab = true, + CompressionLevel = CompressionLevel.mszip, + } + }, + ControlPanelInfo = new ProductInfo + { + Manufacturer = Includes.VENDOR_NAME, + NoModify = true, + ProductIcon = "Resources/DevolutionsAgent.ico", + UrlInfoAbout = Includes.INFO_URL, + } + }; + + if (CryptoConfig.AllowOnlyFipsAlgorithms) + { + project.CandleOptions = "-fips"; + } + + project.Dirs = new Dir[] + { + new ("%ProgramFiles%", new Dir(Includes.VENDOR_NAME, new InstallDir(Includes.SHORT_NAME) + { + Files = new File[] + { + new (DevolutionsAgentExePath) + { + TargetFileName = Includes.EXECUTABLE_NAME, + FirewallExceptions = new FirewallException[] + { + new() + { + Name = Includes.SERVICE_DISPLAY_NAME, + Description = $"{Includes.SERVICE_DISPLAY_NAME} TCP", + Protocol = FirewallExceptionProtocol.tcp, + Profile = FirewallExceptionProfile.all, + Scope = FirewallExceptionScope.any, + IgnoreFailure = false + }, + new() + { + Name = Includes.SERVICE_DISPLAY_NAME, + Description = $"{Includes.SERVICE_DISPLAY_NAME} UDP", + Protocol = FirewallExceptionProtocol.udp, + Profile = FirewallExceptionProfile.all, + Scope = FirewallExceptionScope.any, + IgnoreFailure = false + }, + }, + ServiceInstaller = new ServiceInstaller() + { + Type = SvcType.ownProcess, + // In contrast to Devolutions Gateway, Devolutions Agent uses LocalSystem + // accout to be able to perform administrative operations + // such as MSI installation (Updating, restarting DevolutionsGateway). + Interactive = false, + Vital = true, + Name = Includes.SERVICE_NAME, + DisplayName = Includes.SERVICE_DISPLAY_NAME, + Description = Includes.SERVICE_DISPLAY_NAME, + FirstFailureActionType = FailureActionType.restart, + SecondFailureActionType = FailureActionType.restart, + ThirdFailureActionType = FailureActionType.restart, + RestartServiceDelayInSeconds = 900, + ResetPeriodInDays = 1, + RemoveOn = SvcEvent.Uninstall, + StopOn = SvcEvent.InstallUninstall, + }, + }, + } + })), + }; + project.ResolveWildCards(true); + + project.DefaultRefAssemblies.Add(typeof(ZipArchive).Assembly.Location); + project.Actions = GatewayActions.Actions; + project.RegValues = new RegValue[] + { + new (RegistryHive.LocalMachine, $"Software\\{Includes.VENDOR_NAME}\\{Includes.SHORT_NAME}", "InstallDir", $"[{AgentProperties.InstallDir}]") + { + AttributesDefinition = "Type=string; Component:Permanent=yes", + Win64 = project.Platform == Platform.x64, + RegistryKeyAction = RegistryKeyAction.create, + } + }; + project.Properties = AgentProperties.Properties.Select(x => x.ToWixSharpProperty()).ToArray(); + project.ManagedUI = new ManagedUI(); + project.ManagedUI.InstallDialogs.AddRange(Wizard.Dialogs); + project.ManagedUI.InstallDialogs + .Add() + .Add(); + project.ManagedUI.ModifyDialogs + .Add() + .Add() + .Add(); + + project.UnhandledException += Project_UnhandledException; + project.UIInitialized += Project_UIInitialized; + + if (SourceOnlyBuild) + { + project.Language = ProjectLangId; + project.LocalizationFile = $"Resources/{Languages.First(x => x.Key == ProjectLangId).Value}"; + + if (ProjectLangId != enUS.Key) + { + project.OutDir = Path.Combine(project.OutDir, ProjectLangId); + } + + project.BuildMsiCmd(); + } + else + { + // Build the multi-language MSI in the {Debug/Release} directory + + project.Language = enUS.Key; + project.LocalizationFile = $"Resources/{enUS.Value}"; + + string msi = project.BuildMsi(); + + foreach (KeyValuePair language in Languages.Where(x => x.Key != enUS.Key)) + { + project.Language = language.Key; + string mstFile = project.BuildLanguageTransform(msi, project.Language, $"Resources/{language.Value}"); + + msi.EmbedTransform(mstFile); + } + + msi.SetPackageLanguages(string.Join(",", Languages.Keys).ToLcidList()); + } + } + + private static void Project_UnhandledException(ExceptionEventArgs e) + { + string errorMessage = + $"An unhandled error has occurred. If this is recurring, please report the issue to {Includes.EMAIL_SUPPORT} or on {Includes.FORUM_SUPPORT}."; + errorMessage += Environment.NewLine; + errorMessage += Environment.NewLine; + errorMessage += "Error details:"; + errorMessage += Environment.NewLine; + errorMessage += e.Exception; + + MessageBox.Show(errorMessage, Includes.PRODUCT_NAME, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + private static void Project_UIInitialized(SetupEventArgs e) + { + string lcid = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "fr" ? frFR.Key : enUS.Key; + + using Stream stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream($"DevolutionsAgent.Resources.{Languages[lcid]}"); + + XmlDocument xml = new(); + xml.Load(stream); + + Dictionary strings = new(); + + foreach (XmlNode s in xml.GetElementsByTagName("String")) + { + strings.Add(s.Attributes["Id"].Value, s.InnerText); + } + + string I18n(string key) + { + if (!strings.TryGetValue(key, out string result)) + { + return key; + } + + return Regex.Replace(result, @"\[(.*?)]", (match) => + { + string property = match.Groups[1].Value; + string value = e.Session[property]; + + return string.IsNullOrEmpty(value) ? property : value; + }); + } + + if (!Environment.Is64BitOperatingSystem) + { + MessageBox.Show(I18n(Strings.x86VersionRequired), I18n(Strings.AgentDlg_Title)); + + e.ManagedUI.Shell.ErrorDetected = true; + e.Result = ActionResult.UserExit; + } + + Version thisVersion = e.Session.QueryProductVersion(); + Version installedVersion = Helpers.AppSearch.InstalledVersion; + + + if (thisVersion < installedVersion) + { + MessageBox.Show($"{I18n(Strings.NewerInstalled)} ({installedVersion})"); + + e.ManagedUI.Shell.ErrorDetected = true; + e.Result = ActionResult.UserExit; + } + + if (!CustomActions.TryGetInstalledNetFx45Version(out uint netfx45Version) || netfx45Version < 394802) + { + if (MessageBox.Show(I18n(Strings.Dotnet462IsRequired), I18n(Strings.AgentDlg_Title), + MessageBoxButtons.YesNo) == DialogResult.Yes) + { + Process.Start("https://go.microsoft.com/fwlink/?LinkId=2085155"); + } + + e.ManagedUI.Shell.ErrorDetected = true; + e.Result = ActionResult.UserExit; + } + + if (netfx45Version < 528040) + { + if (MessageBox.Show(I18n(Strings.DotNet48IsStrongRecommendedDownloadNow), I18n(Strings.AgentDlg_Title), + MessageBoxButtons.YesNo) == DialogResult.Yes) + { + Process.Start("https://go.microsoft.com/fwlink/?LinkId=2085155"); + } + } + } +} diff --git a/package/AgentWindowsManaged/Properties/AgentProperties.cs b/package/AgentWindowsManaged/Properties/AgentProperties.cs new file mode 100644 index 000000000..1010fb969 --- /dev/null +++ b/package/AgentWindowsManaged/Properties/AgentProperties.cs @@ -0,0 +1,41 @@ +using System; +using WixSharp; + +namespace DevolutionsAgent.Properties +{ + internal partial class AgentProperties + { + private readonly Microsoft.Deployment.WindowsInstaller.Session installerSession; + + private readonly ISession runtimeSession; + + private Func FnGetPropValue { get; } + + /// + /// The default WiX INSTALLDIR property name + /// + public static string InstallDir = "INSTALLDIR"; + + public AgentProperties(ISession runtimeSession) + { + this.runtimeSession = runtimeSession; + this.FnGetPropValue = GetPropertyValueRuntimeSession; + } + + public AgentProperties(Microsoft.Deployment.WindowsInstaller.Session installerSession) + { + this.installerSession = installerSession; + this.FnGetPropValue = GetPropertyValueInstallerSession; + } + + private string GetPropertyValueRuntimeSession(string name) + { + return this.runtimeSession.Property(name); + } + + private string GetPropertyValueInstallerSession(string name) + { + return this.installerSession.Property(name); + } + } +} diff --git a/package/AgentWindowsManaged/Properties/AgentProperties.g.cs b/package/AgentWindowsManaged/Properties/AgentProperties.g.cs new file mode 100644 index 000000000..6d844621f --- /dev/null +++ b/package/AgentWindowsManaged/Properties/AgentProperties.g.cs @@ -0,0 +1,314 @@ + +using StoreLocation = System.Security.Cryptography.X509Certificates.StoreLocation; +using StoreName = System.Security.Cryptography.X509Certificates.StoreName; +using ServiceStartMode = System.ServiceProcess.ServiceStartMode; +using System; +using static DevolutionsAgent.Properties.Constants; + +namespace DevolutionsAgent.Properties +{ + /// + /// do not modify the contents of this class with the code editor. + /// + public partial class Constants + { + + public const string HttpProtocol = "http"; + + public const string HttpsProtocol = "https"; + + public const string TcpProtocol = "tcp"; + + + public enum AuthenticationMode + { + None, + Custom, + } + + public enum CertificateMode + { + External, + System, + } + + public enum CertificateFindType + { + Thumbprint, + SubjectName, + } + + public enum CustomizeMode + { + Now, + Later, + } + } + + /// + /// do not modify the contents of this class with the code editor. + /// + internal partial class AgentProperties + { + + internal static readonly WixProperty configureAgent = new() + { + Id = "P.CONFIGUREAGENT", + Default = false, + Name = "ConfigureAgent", + Secure = false, + Hidden = false, + Public = true + }; + + /// `true` to configure the Gateway interactively + public Boolean ConfigureAgent + { + get + { + string stringValue = this.FnGetPropValue(configureAgent.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(configureAgent, value); + } + } + } + + internal static readonly WixProperty debugPowerShell = new() + { + Id = "P.DEBUGPOWERSHELL", + Default = false, + Name = "DebugPowerShell", + Secure = true, + Hidden = false, + Public = true + }; + + public Boolean DebugPowerShell + { + get + { + string stringValue = this.FnGetPropValue(debugPowerShell.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(debugPowerShell, value); + } + } + } + + internal static readonly WixProperty installId = new() + { + Id = "P.InstallId", + Default = new Guid("00000000-0000-0000-0000-000000000000"), + Name = "InstallId", + Secure = false, + Hidden = false, + Public = false + }; + + public Guid InstallId + { + get + { + string stringValue = this.FnGetPropValue(installId.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(installId, value); + } + } + } + + internal static readonly WixProperty netFx45Version = new() + { + Id = "P.NetFx45Version", + Default = 0, + Name = "NetFx45Version", + Secure = false, + Hidden = false, + Public = false + }; + + public UInt32 NetFx45Version + { + get + { + string stringValue = this.FnGetPropValue(netFx45Version.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(netFx45Version, value); + } + } + } + + internal static readonly WixProperty firstInstall = new() + { + Id = "P.FirstInstall", + Default = false, + Name = "FirstInstall", + Secure = false, + Hidden = false, + Public = false + }; + + public Boolean FirstInstall + { + get + { + string stringValue = this.FnGetPropValue(firstInstall.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(firstInstall, value); + } + } + } + + internal static readonly WixProperty upgrading = new() + { + Id = "P.Upgrading", + Default = false, + Name = "Upgrading", + Secure = false, + Hidden = false, + Public = false + }; + + public Boolean Upgrading + { + get + { + string stringValue = this.FnGetPropValue(upgrading.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(upgrading, value); + } + } + } + + internal static readonly WixProperty removingForUpgrade = new() + { + Id = "P.RemovingForUpgrade", + Default = false, + Name = "RemovingForUpgrade", + Secure = false, + Hidden = false, + Public = false + }; + + public Boolean RemovingForUpgrade + { + get + { + string stringValue = this.FnGetPropValue(removingForUpgrade.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(removingForUpgrade, value); + } + } + } + + internal static readonly WixProperty uninstalling = new() + { + Id = "P.Uninstalling", + Default = false, + Name = "Uninstalling", + Secure = false, + Hidden = false, + Public = false + }; + + public Boolean Uninstalling + { + get + { + string stringValue = this.FnGetPropValue(uninstalling.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(uninstalling, value); + } + } + } + + internal static readonly WixProperty maintenance = new() + { + Id = "P.Maintenance", + Default = false, + Name = "Maintenance", + Secure = false, + Hidden = false, + Public = false + }; + + public Boolean Maintenance + { + get + { + string stringValue = this.FnGetPropValue(maintenance.Id); + return WixProperties.GetPropertyValue(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(maintenance, value); + } + } + } + + + public static IWixProperty[] Properties = + { + + configureAgent, + + debugPowerShell, + + installId, + + netFx45Version, + + firstInstall, + + upgrading, + + removingForUpgrade, + + uninstalling, + + maintenance, + + }; + } +} + diff --git a/package/AgentWindowsManaged/Properties/AgentProperties.g.tt b/package/AgentWindowsManaged/Properties/AgentProperties.g.tt new file mode 100644 index 000000000..94ad1932f --- /dev/null +++ b/package/AgentWindowsManaged/Properties/AgentProperties.g.tt @@ -0,0 +1,217 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ assembly name="System.ServiceProcess" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.ServiceProcess" #> +<#@ import namespace="System.Security.Cryptography.X509Certificates" #> +<#@ output extension=".cs" #> + +using StoreLocation = System.Security.Cryptography.X509Certificates.StoreLocation; +using StoreName = System.Security.Cryptography.X509Certificates.StoreName; +using ServiceStartMode = System.ServiceProcess.ServiceStartMode; +using System; +using static DevolutionsAgent.Properties.Constants; + +namespace DevolutionsAgent.Properties +{ + /// + /// do not modify the contents of this class with the code editor. + /// + public partial class Constants + { +<# for (int idx = 0; idx < this.StaticStrings.GetLength(0); idx++) { #> + public const string <#=this.StaticStrings[idx, 0]#> = "<#=this.StaticStrings[idx, 1]#>"; +<# } #> + +<# for (int idx = 0; idx < this.Enums.GetLength(0); idx++) { #> + public enum <#=this.Enums[idx].Name #> + { + <# foreach (var value in Enum.GetValues(this.Enums[idx])) { #> + <#=value#>, + <# } #> + } +<# } #> + } + + /// + /// do not modify the contents of this class with the code editor. + /// + internal partial class AgentProperties + { +<# for (int idx = 0; idx < this.properties.GetLength(0); idx++) { #> + internal static readonly WixProperty<<#= this.properties[idx].TypeName #>> <#= this.properties[idx].PrivateName #> = new() + { +<# string id = string.IsNullOrEmpty(this.properties[idx].Id) ? this.properties[idx].Name : this.properties[idx].Id; #> +<# if (this.properties[idx].Public) { id = id.ToUpper(); } #> + Id = "P.<#=id#>", +<# if (this.properties[idx].PropertyType == typeof(string)) { #> + Default = "<#= this.properties[idx].Default #>", +<# } else if (this.properties[idx].PropertyType == typeof(Guid)) { #> + Default = new Guid("<#= this.properties[idx].Default.ToString() #>"), +<# } else { #> + Default = <#= this.properties[idx].Default #>, +<# } #> + Name = "<#= this.properties[idx].Name #>", + Secure = <#= this.properties[idx].Secure.ToString().ToLower() #>, + Hidden = <#= this.properties[idx].Hidden.ToString().ToLower() #>, + Public = <#= this.properties[idx].Public.ToString().ToLower() #> + }; + +<# if (!string.IsNullOrEmpty(this.properties[idx].Comment)) { #> + /// <#= this.properties[idx].Comment #> +<# } #> + public <#= this.properties[idx].TypeName #> <#= this.properties[idx].Name #> + { + get + { + string stringValue = this.FnGetPropValue(<#= this.properties[idx].PrivateName #>.Id); + return WixProperties.GetPropertyValue<<#= this.properties[idx].TypeName #>>(stringValue); + } + set + { + if (this.runtimeSession is not null) + { + this.runtimeSession.Set(<#= this.properties[idx].PrivateName #>, value); + } + } + } +<# } #> + + public static IWixProperty[] Properties = + { +<# for (int idx = 0; idx < this.properties.GetLength(0); idx++) { #> + <#= this.properties[idx].PrivateName #>, +<# } #> + }; + } +} + +<#+ + public class Statics + { + internal const string HttpProtocol = "http"; + + internal const string HttpsProtocol = "https"; + + internal const string TcpProtocol = "tcp"; + + public enum AuthenticationMode + { + None, + Custom, + } + + public enum CertificateMode + { + External, + System + } + + public enum CertificateFindType + { + Thumbprint, + SubjectName + } + + public enum CustomizeMode + { + Now, + Later, + } + } + + public string[,] StaticStrings = new[,] + { + {nameof(Statics.HttpProtocol), Statics.HttpProtocol}, + {nameof(Statics.HttpsProtocol), Statics.HttpsProtocol}, + {nameof(Statics.TcpProtocol), Statics.TcpProtocol}, + }; + + public Type[] Enums = new[] + { + typeof(Statics.AuthenticationMode), + typeof(Statics.CertificateMode), + typeof(Statics.CertificateFindType), + typeof(Statics.CustomizeMode), + }; + + public abstract class BasePropertyDefinition + { + public abstract string Comment { get; set; } + public abstract bool Hidden { get; set; } + public abstract string Id { get; set; } + public abstract string Name { get; set; } + public abstract string PrivateName { get; } + public abstract bool Public { get; set; } + public abstract bool Secure { get; set; } + public abstract string TypeName { get; } + + public abstract string Default { get; } + public abstract Type PropertyType { get; } + } + + public class PropertyDefinition : BasePropertyDefinition + { + public override string Name { get; set; } + public T DefaultValue { get; set; } + public override bool Public { get; set; } + public override bool Secure { get; set; } + public override bool Hidden { get; set; } + public override string Id { get; set; } + public override string Comment { get; set; } + public bool Private => !this.Public; + + public PropertyDefinition(string name, T defaultValue, bool isPublic = true, bool secure = true, bool hidden = false, string id = null, string comment = null) + { + this.Name = name; + this.DefaultValue = defaultValue; + this.Public = isPublic; + this.Secure = secure; + this.Hidden = hidden; + this.Id = id; + this.Comment = comment; + } + + public override string Default + { + get + { + if (PropertyType == typeof(bool)) + { + return this.DefaultValue.ToString().ToLower(); + } + + if (PropertyType.IsEnum) + { + return $"{TypeName}.{this.DefaultValue.ToString()}"; + } + + return this.DefaultValue.ToString(); + } + } + + public override string PrivateName => char.ToLower(this.Name[0]) + this.Name.Substring(1); + + public override Type PropertyType => typeof(T); + + public override string TypeName => typeof(T).Name; + } + + private static uint DefaultHttpPort = 7171; + + BasePropertyDefinition[] properties = { + new PropertyDefinition("ConfigureAgent", false, secure: false, comment: "`true` to configure the Gateway interactively"), + + new PropertyDefinition("DebugPowerShell", false), + + new PropertyDefinition("InstallId", Guid.Empty, isPublic: false, secure: false), + new PropertyDefinition("NetFx45Version", 0, isPublic: false, secure: false), + new PropertyDefinition("FirstInstall", false, isPublic: false, secure: false), + new PropertyDefinition("Upgrading", false, isPublic: false, secure: false), + new PropertyDefinition("RemovingForUpgrade", false, isPublic: false, secure: false), + new PropertyDefinition("Uninstalling", false, isPublic: false, secure: false), + new PropertyDefinition("Maintenance", false, isPublic: false, secure: false), + }; +#> diff --git a/package/AgentWindowsManaged/Properties/WixProperty.cs b/package/AgentWindowsManaged/Properties/WixProperty.cs new file mode 100644 index 000000000..1ef97e8af --- /dev/null +++ b/package/AgentWindowsManaged/Properties/WixProperty.cs @@ -0,0 +1,114 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using WixSharp; + +namespace DevolutionsAgent.Properties +{ + internal static class WixProperties + { + public static T Get(this Microsoft.Deployment.WindowsInstaller.Session session, WixProperty prop) + { + Debug.Assert(session is not null); + + string propertyValue = session.Property(prop.Id); + + return GetPropertyValue(propertyValue); + } + + public static T Get(this ISession session, WixProperty prop) + { + Debug.Assert(session is not null); + + string propertyValue = session.Property(prop.Id); + + return GetPropertyValue(propertyValue); + } + + public static void Set(this Microsoft.Deployment.WindowsInstaller.Session session, WixProperty prop, T value) + { + session[prop.Id] = value.ToString(); + } + + public static void Set(this ISession session, WixProperty prop, T value) + { + session[prop.Id] = value?.ToString(); + } + + public static Property ToWixSharpProperty(this IWixProperty property) + { + return new(property.Id) + { + Value = property.DefaultValue, + Hidden = property.Hidden, + Secure = property.Secure, + }; + } + + internal static T GetPropertyValue(string propertyValue) + { + if (string.IsNullOrWhiteSpace(propertyValue)) + { + return default; + } + + if (typeof(T).IsEnum) + { + return (T) Enum.Parse(typeof(T), propertyValue); + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(propertyValue); + } + } + + internal static class WixPropertyExtensions + { + // https://www.firegiant.com/docs/wix/v3/tutorial/com-expression-syntax-miscellanea/expression-syntax/ + + internal static Condition Equal(this WixProperty property, T value) + { + return new Condition($"{property.Id}=\"{value}\""); + } + + internal static Condition NotEqual(this WixProperty property, T value) + { + return new Condition($"{property.Id}<>\"{value}\""); + } + } + + internal interface IWixProperty + { + public string DefaultValue { get; } + + public bool Hidden { get; } + + public string Id { get; } + + public string Name { get; } + + public bool Secure { get; } + + public bool Public { get; } + + public Type PropertyType { get; } + } + + internal class WixProperty : IWixProperty + { + public T Default { get; set; } + + public string DefaultValue => Default.ToString(); + + public bool Hidden { get; set; } + + public string Id { get; set; } + + public string Name { get; set; } + + public bool Secure { get; set; } = false; + + public bool Public { get; set; } + + public Type PropertyType => typeof(T); + } +} diff --git a/package/AgentWindowsManaged/README.md b/package/AgentWindowsManaged/README.md new file mode 100644 index 000000000..49b79ff58 --- /dev/null +++ b/package/AgentWindowsManaged/README.md @@ -0,0 +1,23 @@ +# Devolutions Agent Windows Installer + +Windows Installer project for Devolutions Agent. + +## Overview + +Project structure is the same as Devolutions Gateway, see [README.md](../WindowsManaged/README.md) for more info. + +## Build + +### MSBuild + +`msbuild` must be in your PATH; it's easiest to use the Developer Command Prompt for VS 2022. + +The following environment variables should be defined: + +`DAGENT_EXECUTABLE` +The absolute path to the main executable (DevolutionsAgent.exe) to be packaged + +`DAGENT_VERSION` +The version to use for the installer. Note that in Windows Installer, the product version is restricted as follows: + +[0-255].[0-255].[0-65535] diff --git a/package/AgentWindowsManaged/Resources/DevolutionsAgent.ico b/package/AgentWindowsManaged/Resources/DevolutionsAgent.ico new file mode 100644 index 000000000..88cb15dd8 Binary files /dev/null and b/package/AgentWindowsManaged/Resources/DevolutionsAgent.ico differ diff --git a/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl b/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl new file mode 100644 index 000000000..9d84ea98d --- /dev/null +++ b/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl @@ -0,0 +1,52 @@ + + + + 1033 + System-wide service for extending Devolutions Gateway functionality. + Devolutions Inc. + Devolutions + + The product requires Microsoft .NET Framework 4.6.2. Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now? + Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now? + A newer version of this product is already installed. + This product requires at least Windows 8 / Windows Server 2012 R2 + There is a problem with the entered data. Please correct the issue and try again. + You need to install the 64-bit version of this product on 64-bit Windows. + You need to install the 32-bit version of this product on 32-bit Windows. + + Search + View + View Log + + Install Location + + Directory + + Please wait for UAC prompt to appear. + +If it appears minimized then active it from the taskbar. + + All Files + + [ProductName] Setup + + Browse to the destination folder + Change destination folder + + Click Next to install to the default folder or click Change to choose another. + Destination Folder + + Changing [ProductName] + Installing [ProductName] + Removing [ProductName] + Repairing [ProductName] + Updating [ProductName] + + Ready to change [ProductName] + Ready to install [ProductName] + Ready to remove [ProductName] + Ready to repair [ProductName] + Ready to update [ProductName] + + Welcome to the [ProductName] 20[ProductVersion] Setup Wizard + diff --git a/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl b/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl new file mode 100644 index 000000000..0082d6472 --- /dev/null +++ b/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl @@ -0,0 +1,60 @@ + + + + 1036 + Service à l’échelle du système pour étendre les fonctionnalités de Devolutions Gateway. + Devolutions Inc. + Devolutions + + Le produit nécessite Microsoft .NET Framework 4.6.2. Microsoft .NET Framework 4.8 est fortement conseillé. Souhaiteriez-vous le télécharger maintenant? + Microsoft .NET Framework 4.8 est fortement conseillé. Souhaiteriez-vous le télécharger maintenant? + Une version plus récente de ce produit est déjà installée. + Ce produit requiert Windows 8 ou Windows Server 2012 R2 au minimum. + Il y a un problème avec les informations fournies, veuillez les modifier et essayer de nouveau. + Vous devez installer la version 64 bits de ce produit avec l'environnement 64 bits de Windows. + Vous devez installer la version 32 bits de ce produit avec l'environnement 32 bits de Windows. + + Rechercher + Afficher + Afficher le journal + + Emplacement de l'installation + + Répertoire + + Veuillez attendre que l'invite UAC apparaisse. + +Si elle apparaît en mode réduit, alors vous devez l'activer à partir de la barre de tâches. + + Tous les fichiers + + Installation de [ProductName] + + Sélectionnez le dossier de destination + Modifier le dossier de destination + + Cliquez sur Suivant pour effectuer l'installation dans le dossier par défaut, ou cliquez sur Modifier pour choisir un autre dossier. + Dossier de destination + + Modification de [ProductName] + Installation de [ProductName] + Suppression de [ProductName] + Réparation de [ProductName] + Mise à jour de [ProductName] + + Prêt pour la modification de [ProductName] + Prêt pour l'installation de [ProductName] + Prêt pour la suppression de [ProductName] + Prêt pour la réparation de [ProductName] + Prêt pour la mise à jour de [ProductName] + + Bienvenue dans l'assistant d'installation de [ProductName] 20[ProductVersion] + + Impossible de se connecter au Pare-feu Windows. ([2] [3] [4] [5]) + Installation de la configuration du Pare-feu Windows + Désinstallation de la configuration du Pare-feu Windows + Restauration de la configuration du Pare-feu Windows + Restauration de la configuration du Pare-feu Windows + Configuration du Pare-feu Windows + Configuration du Pare-feu Windows + diff --git a/package/AgentWindowsManaged/Resources/Includes.cs b/package/AgentWindowsManaged/Resources/Includes.cs new file mode 100644 index 000000000..ae719023c --- /dev/null +++ b/package/AgentWindowsManaged/Resources/Includes.cs @@ -0,0 +1,55 @@ +using System; +namespace DevolutionsAgent.Resources +{ + internal static class Includes + { + internal static string VENDOR_NAME = "Devolutions"; + + internal static string PRODUCT_NAME = "Devolutions Agent"; + + internal static string SHORT_NAME = "Agent"; + + internal static string SERVICE_NAME = "DevolutionsAgent"; + + internal static string SERVICE_DISPLAY_NAME = "Devolutions Agent Service"; + + internal static string SERVICE_DESCRIPTION = "Devolutions Agent Service"; + + internal static string EXECUTABLE_NAME = "DevolutionsAgent.exe"; + + internal static string EMAIL_SUPPORT = "support@devolutions.net"; + + internal static string FORUM_SUPPORT = "forum.devolutions.net"; + + internal static Guid UPGRADE_CODE = new("82318D3C-811F-4D5D-9A82-B7C31B076755"); + + internal static string INFO_URL = "https://server.devolutions.net"; + + /// + /// SDDL string representing desired %programdata%\devolutions\agent ACL + /// Easiest way to generate an SDDL is to configure the required access, and then query the path with PowerShell: `Get-Acl | Format-List` + /// + /// + /// Owner : NT AUTHORITY\SYSTEM + /// Group : NT AUTHORITY\SYSTEM + /// Access : + /// NT AUTHORITY\SYSTEM Allow FullControl + /// NT AUTHORITY\LOCAL SERVICE Allow Write, ReadAndExecute, Synchronize + /// NT AUTHORITY\NETWORK SERVICE Allow Modify, Synchronize + /// BUILTIN\Administrators Allow FullControl + /// BUILTIN\Users Allow ReadAndExecute, Synchronize + /// + internal static string PROGRAM_DATA_SDDL = "O:SYG:SYD:PAI(A;OICI;FA;;;SY)(A;OICI;0x1201bf;;;LS)(A;OICI;0x1301bf;;;NS)(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)"; + + /// + /// Owner : NT AUTHORITY\SYSTEM + /// Group : NT AUTHORITY\SYSTEM + /// Access : + /// NT AUTHORITY\SYSTEM Allow FullControl + /// NT AUTHORITY\LOCAL SERVICE Allow Write, ReadAndExecute, Synchronize + /// NT AUTHORITY\NETWORK SERVICE Allow Write, ReadAndExecute, Synchronize + /// BUILTIN\Administrators Allow FullControl + /// + internal static string USERS_FILE_SDDL = "O:SYG:SYD:PAI(A;;FA;;;SY)(A;;0x1201bf;;;LS)(A;;0x1201bf;;;NS)(A;;FA;;;BA)"; + } +} diff --git a/package/AgentWindowsManaged/Resources/Newtonsoft.Json.dll b/package/AgentWindowsManaged/Resources/Newtonsoft.Json.dll new file mode 100644 index 000000000..341d08fc8 Binary files /dev/null and b/package/AgentWindowsManaged/Resources/Newtonsoft.Json.dll differ diff --git a/package/AgentWindowsManaged/Resources/Strings.g.cs b/package/AgentWindowsManaged/Resources/Strings.g.cs new file mode 100644 index 000000000..dfdc8588d --- /dev/null +++ b/package/AgentWindowsManaged/Resources/Strings.g.cs @@ -0,0 +1,148 @@ +using WixSharp; + +namespace DevolutionsAgent.Resources +{ + public static class Strings + { + public static string I18n(this MsiRuntime runtime, string res) + { + return $"[{res}]".LocalizeWith(runtime.Localize); + } + /// + /// 1033 + /// + public const string Language = "Language"; + /// + /// Devolutions + /// + public const string VendorName = "VendorName"; + /// + /// Devolutions Inc. + /// + public const string VendorFullName = "VendorFullName"; + /// + /// System-wide service for extending Devolutions Gateway functionality. + /// + public const string ProductDescription = "ProductDescription"; + /// + /// There is a problem with the entered data. Please correct the issue and try again. + /// + public const string ThereIsAProblemWithTheEnteredData = "ThereIsAProblemWithTheEnteredData"; + /// + /// This product requires at least Windows 8 / Windows Server 2012 R2 + /// + public const string OS2Old = "OS2Old"; + /// + /// A newer version of this product is already installed. + /// + public const string NewerInstalled = "NewerInstalled"; + /// + /// You need to install the 64-bit version of this product on 64-bit Windows. + /// + public const string x64VersionRequired = "x64VersionRequired"; + /// + /// You need to install the 32-bit version of this product on 32-bit Windows. + /// + public const string x86VersionRequired = "x86VersionRequired"; + /// + /// Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now? + /// + public const string DotNet48IsStrongRecommendedDownloadNow = "DotNet48IsStrongRecommendedDownloadNow"; + /// + /// The product requires Microsoft .NET Framework 4.6.2. Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now? + /// + public const string Dotnet462IsRequired = "Dotnet462IsRequired"; + /// + /// View + /// + public const string ViewButton = "ViewButton"; + /// + /// Search + /// + public const string SearchButton = "SearchButton"; + /// + /// View Log + /// + public const string ViewLogButton = "ViewLogButton"; + /// + /// Install Location + /// + public const string Group_InstallLocation = "Group_InstallLocation"; + /// + /// Directory + /// + public const string Property_Directory = "Property_Directory"; + /// + /// Please wait for UAC prompt to appear.If it appears minimized then active it from the taskbar. + /// + public const string UACPromptLabel = "UACPromptLabel"; + /// + /// All Files + /// + public const string Filter_AllFiles = "Filter_AllFiles"; + /// + /// [ProductName] Setup + /// + public const string AgentDlg_Title = "AgentDlg_Title"; + /// + /// Change destination folder + /// + public const string BrowseDlgTitle = "BrowseDlgTitle"; + /// + /// Browse to the destination folder + /// + public const string BrowseDlgDescription = "BrowseDlgDescription"; + /// + /// Destination Folder + /// + public const string InstallDirDlgTitle = "InstallDirDlgTitle"; + /// + /// Click Next to install to the default folder or click Change to choose another. + /// + public const string InstallDirDlgDescription = "InstallDirDlgDescription"; + /// + /// Installing [ProductName] + /// + public const string ProgressDlgTitleInstalling = "ProgressDlgTitleInstalling"; + /// + /// Changing [ProductName] + /// + public const string ProgressDlgTitleChanging = "ProgressDlgTitleChanging"; + /// + /// Repairing [ProductName] + /// + public const string ProgressDlgTitleRepairing = "ProgressDlgTitleRepairing"; + /// + /// Removing [ProductName] + /// + public const string ProgressDlgTitleRemoving = "ProgressDlgTitleRemoving"; + /// + /// Updating [ProductName] + /// + public const string ProgressDlgTitleUpdating = "ProgressDlgTitleUpdating"; + /// + /// Ready to install [ProductName] + /// + public const string VerifyReadyDlgInstallTitle = "VerifyReadyDlgInstallTitle"; + /// + /// Ready to change [ProductName] + /// + public const string VerifyReadyDlgChangeTitle = "VerifyReadyDlgChangeTitle"; + /// + /// Ready to repair [ProductName] + /// + public const string VerifyReadyDlgRepairTitle = "VerifyReadyDlgRepairTitle"; + /// + /// Ready to remove [ProductName] + /// + public const string VerifyReadyDlgRemoveTitle = "VerifyReadyDlgRemoveTitle"; + /// + /// Ready to update [ProductName] + /// + public const string VerifyReadyDlgUpdateTitle = "VerifyReadyDlgUpdateTitle"; + /// + /// Welcome to the [ProductName] 20[ProductVersion] Setup Wizard + /// + public const string WelcomeDlgTitle = "WelcomeDlgTitle"; + } +} diff --git a/package/AgentWindowsManaged/Resources/Strings.g.tt b/package/AgentWindowsManaged/Resources/Strings.g.tt new file mode 100644 index 000000000..0cd07f79b --- /dev/null +++ b/package/AgentWindowsManaged/Resources/Strings.g.tt @@ -0,0 +1,213 @@ +<#@ template debug="false" hostspecific="true" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ assembly name="System.Xml" #> +<#@ assembly name="System.Xml.Linq" #> +<#@ assembly name="$(SolutionDir)Resources\Newtonsoft.Json.dll" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Xml" #> +<#@ import namespace="System.Text.RegularExpressions" #> +<#@ import namespace="System.Xml.Linq" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="Newtonsoft.Json.Linq" #> +<#@ output extension=".cs" #> +<# + const string mainCulture = "en-US"; + string[] cultures = new string[] { mainCulture, "fr-FR" }; + + JToken mainCultureJson = JToken.Parse(File.ReadAllText(GetI18nFile(mainCulture, Host.TemplateFile))); + List mainCultureTokens = null; + + foreach (string culture in cultures) + { + string sourceFile = GetI18nFile(culture, Host.TemplateFile); + + if (!File.Exists(sourceFile)) + { + continue; + } + + JToken root = JToken.Parse(File.ReadAllText(sourceFile)); + JObject diff = null; + + if (!culture.Equals(mainCulture)) + { + diff = FindDiff(root, mainCultureJson); #> + <#= diff #> + <# SaveOutput($"DevolutionsAgent_{culture}_missing.json"); + } + + List idTextPairs = new List(); + FindIdTextPairs(root, idTextPairs); + + if (culture.Equals(mainCulture)) + { + mainCultureTokens = idTextPairs; + } + else + { + List missing = new List(); + FindIdTextPairs((JObject)diff, missing); + idTextPairs.AddRange(missing); + } + #> + +" xmlns="http://schemas.microsoft.com/wix/2006/localization"> + <# foreach (var tokenGroup in idTextPairs.GroupBy(x => x.path)) { #> + + <# foreach (var token in tokenGroup.OrderBy(x => x.id)) { #> + <#= token.text.Replace("\\n", " ") #> + <# } #> + <# } #> + +<# SaveOutput($"DevolutionsAgent_{culture.ToLower()}.wxl"); +} #> +using WixSharp; + +namespace DevolutionsAgent.Resources +{ + public static class Strings + { + public static string I18n(this MsiRuntime runtime, string res) + { + return $"[{res}]".LocalizeWith(runtime.Localize); + } +<# foreach (LocalizationToken token in mainCultureTokens) { #> + /// + /// <#= Regex.Replace(token.text.ToString(), @"\t|\n|\r", "") #> + /// + public const string <#= token.id #> = "<#= token.id #>"; +<# } #> + } +} +<#+ +class LocalizationToken +{ + public string id { get; set; } + public string text { get; set; } + public string path { get; set; } + public string fullPath { get; set; } +} + +static string GetI18nFile(string culture, string templateFile) +{ + string file = $"Strings_{culture}.json"; + string directory = System.IO.Path.GetDirectoryName(templateFile); + return Path.Combine(directory, file); +} + +static void FindIdTextPairs(JToken token, List pairs) +{ + string rootPath = "WixLocalization.strings."; + if (token.Type == JTokenType.Object) + { + var obj = (JObject)token; + var id = obj["id"]; + var text = obj["text"]; + + if (id != null && text != null) + { + pairs.Add(new LocalizationToken { + id = id.ToString(), + text = text.ToString(), + path = obj.Parent.Path.Replace(rootPath, ""), + fullPath = obj.Path + }); + } + + foreach (var property in obj.Properties()) + { + FindIdTextPairs(property.Value, pairs); + } + } + else if (token.Type == JTokenType.Array) + { + foreach (var item in token.Children()) + { + FindIdTextPairs(item, pairs); + } + } +} + +private void SaveOutput(string outputFileName) +{ + string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); + string outputFilePath = Path.Combine(templateDirectory, outputFileName); + File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); + this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length); +} + +private class LanguageTokenEqualityComparer : IEqualityComparer +{ + public bool Equals(JToken x, JToken y) + { + return ((JObject)x)["id"].Equals(((JObject)y)["id"]); + } + + public int GetHashCode(JToken obj) + { + if (obj == null) + { + return 0; + } + + return ((JObject)obj)["id"].GetHashCode(); + } +} + +private JObject FindDiff(JToken Current, JToken Model) +{ + var diff = new JObject(); + if (JToken.DeepEquals(Current, Model)) return diff; + + switch(Current.Type) + { + case JTokenType.Object: + { + var current = Current as JObject; + var model = Model as JObject; + var addedKeys = current.Properties().Select(c => c.Name).Except(model.Properties().Select(c => c.Name)); + var removedKeys = model.Properties().Select(c => c.Name).Except(current.Properties().Select(c => c.Name)); + var unchangedKeys = current.Properties().Where(c => JToken.DeepEquals(c.Value, Model[c.Name])).Select(c => c.Name); + foreach (var k in addedKeys) + { + diff[k] = new JObject + { + //["+"] = Current[k] + }; + } + foreach (var k in removedKeys) + { + diff[k] = new JObject + { + ["-"] = Model[k] + }; + } + var potentiallyModifiedKeys = current.Properties().Select(c => c.Name).Except(addedKeys).Except(unchangedKeys); + foreach (var k in potentiallyModifiedKeys) + { + var foundDiff = FindDiff(current[k], model[k]); + if(foundDiff.HasValues) diff[k] = foundDiff; + } + } + break; + case JTokenType.Array: + { + var current = Current as JArray; + var model = Model as JArray; + var plus = new JArray(current.Except(model, new LanguageTokenEqualityComparer())); + var minus = new JArray(model.Except(current, new LanguageTokenEqualityComparer())); + if (plus.HasValues) diff["+"] = plus; + if (minus.HasValues) diff["-"] = minus; + } + break; + default: + diff["+"] = Current; + diff["-"] = Model; + break; + } + + return diff; +} +#> diff --git a/package/AgentWindowsManaged/Resources/Strings_en-US.json b/package/AgentWindowsManaged/Resources/Strings_en-US.json new file mode 100644 index 000000000..24bdda455 --- /dev/null +++ b/package/AgentWindowsManaged/Resources/Strings_en-US.json @@ -0,0 +1,171 @@ +{ + "WixLocalization": { + "strings": { + "metadata": [ + { + "id": "Language", + "text": "1033" + }, + { + "id": "VendorName", + "text": "Devolutions" + }, + { + "id": "VendorFullName", + "text": "Devolutions Inc." + }, + { + "id": "ProductDescription", + "text": "System-wide service for extending Devolutions Gateway functionality." + } + ], + "messages": [ + { + "id": "ThereIsAProblemWithTheEnteredData", + "text": "There is a problem with the entered data. Please correct the issue and try again." + }, + { + "id": "OS2Old", + "text": "This product requires at least Windows 8 / Windows Server 2012 R2" + }, + { + "id": "NewerInstalled", + "text": "A newer version of this product is already installed." + }, + { + "id": "x64VersionRequired", + "text": "You need to install the 64-bit version of this product on 64-bit Windows." + }, + { + "id": "x86VersionRequired", + "text": "You need to install the 32-bit version of this product on 32-bit Windows." + }, + { + "id": "DotNet48IsStrongRecommendedDownloadNow", + "text": "Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now?" + }, + { + "id": "Dotnet462IsRequired", + "text": "The product requires Microsoft .NET Framework 4.6.2. Microsoft .NET Framework 4.8 is strongly recommended. Would you like to download it now?" + } + ], + "buttons": [ + { + "id": "ViewButton", + "text": "View" + }, + { + "id": "SearchButton", + "text": "Search" + }, + { + "id": "ViewLogButton", + "text": "View Log" + } + ], + "propertyGroups": [ + { + "id": "Group_InstallLocation", + "text": "Install Location" + } + ], + "properties": [ + { + "id": "Property_Directory", + "text": "Directory" + } + ], + "static": [ + { + "id": "UACPromptLabel", + "text": "Please wait for UAC prompt to appear.\n\nIf it appears minimized then active it from the taskbar." + } + ], + "filters": [ + { + "id": "Filter_AllFiles", + "text": "All Files" + } + ], + "dialogs": { + "static": [ + { + "id": "AgentDlg_Title", + "text": "[ProductName] Setup" + } + ], + "browse": [ + { + "id": "BrowseDlgTitle", + "text": "Change destination folder" + }, + { + "id": "BrowseDlgDescription", + "text": "Browse to the destination folder" + } + ], + "installDir": [ + { + "id": "InstallDirDlgTitle", + "text": "Destination Folder" + }, + { + "id": "InstallDirDlgDescription", + "text": "Click Next to install to the default folder or click Change to choose another." + } + ], + "progress": [ + { + "id": "ProgressDlgTitleInstalling", + "text": "Installing [ProductName]" + }, + { + "id": "ProgressDlgTitleChanging", + "text": "Changing [ProductName]" + }, + { + "id": "ProgressDlgTitleRepairing", + "text": "Repairing [ProductName]" + }, + { + "id": "ProgressDlgTitleRemoving", + "text": "Removing [ProductName]" + }, + { + "id": "ProgressDlgTitleUpdating", + "text": "Updating [ProductName]" + } + ], + "verifyReady": [ + { + "id": "VerifyReadyDlgInstallTitle", + "text": "Ready to install [ProductName]" + }, + { + "id": "VerifyReadyDlgChangeTitle", + "text": "Ready to change [ProductName]" + }, + { + "id": "VerifyReadyDlgRepairTitle", + "text": "Ready to repair [ProductName]" + }, + { + "id": "VerifyReadyDlgRemoveTitle", + "text": "Ready to remove [ProductName]" + }, + { + "id": "VerifyReadyDlgUpdateTitle", + "text": "Ready to update [ProductName]" + } + ], + "welcome": [ + { + "id": "WelcomeDlgTitle", + "overridable": "yes", + "text": "Welcome to the [ProductName] 20[ProductVersion] Setup Wizard" + } + ] + } + } + } +} diff --git a/package/AgentWindowsManaged/Resources/Strings_fr-FR.json b/package/AgentWindowsManaged/Resources/Strings_fr-FR.json new file mode 100644 index 000000000..dc40c7ab8 --- /dev/null +++ b/package/AgentWindowsManaged/Resources/Strings_fr-FR.json @@ -0,0 +1,201 @@ +{ + "WixLocalization": { + "strings": { + "metadata": [ + { + "id": "Language", + "text": "1036" + }, + { + "id": "VendorName", + "text": "Devolutions" + }, + { + "id": "VendorFullName", + "text": "Devolutions Inc." + }, + { + "id": "ProductDescription", + "text": "Service à l’échelle du système pour étendre les fonctionnalités de Devolutions Gateway." + } + ], + "messages": [ + { + "id": "ThereIsAProblemWithTheEnteredData", + "text": "Il y a un problème avec les informations fournies, veuillez les modifier et essayer de nouveau." + }, + { + "id": "OS2Old", + "text": "Ce produit requiert Windows 8 ou Windows Server 2012 R2 au minimum." + }, + { + "id": "NewerInstalled", + "text": "Une version plus récente de ce produit est déjà installée." + }, + { + "id": "x64VersionRequired", + "text": "Vous devez installer la version 64 bits de ce produit avec l'environnement 64 bits de Windows." + }, + { + "id": "x86VersionRequired", + "text": "Vous devez installer la version 32 bits de ce produit avec l'environnement 32 bits de Windows." + }, + { + "id": "DotNet48IsStrongRecommendedDownloadNow", + "text": "Microsoft .NET Framework 4.8 est fortement conseillé. Souhaiteriez-vous le télécharger maintenant?" + }, + { + "id": "Dotnet462IsRequired", + "text": "Le produit nécessite Microsoft .NET Framework 4.6.2. Microsoft .NET Framework 4.8 est fortement conseillé. Souhaiteriez-vous le télécharger maintenant?" + } + ], + "buttons": [ + { + "id": "ViewButton", + "text": "Afficher" + }, + { + "id": "SearchButton", + "text": "Rechercher" + }, + { + "id": "ViewLogButton", + "text": "Afficher le journal" + } + ], + "propertyGroups": [ + { + "id": "Group_InstallLocation", + "text": "Emplacement de l'installation" + } + ], + "properties": [ + { + "id": "Property_Directory", + "text": "Répertoire" + } + ], + "static": [ + { + "id": "UACPromptLabel", + "text": "Veuillez attendre que l'invite UAC apparaisse.\n\nSi elle apparaît en mode réduit, alors vous devez l'activer à partir de la barre de tâches." + } + ], + "filters": [ + { + "id": "Filter_AllFiles", + "text": "Tous les fichiers" + } + ], + "dialogs": { + "static": [ + { + "id": "AgentDlg_Title", + "text": "Installation de [ProductName]" + } + ], + "browse": [ + { + "id": "BrowseDlgTitle", + "text": "Modifier le dossier de destination" + }, + { + "id": "BrowseDlgDescription", + "text": "Sélectionnez le dossier de destination" + } + ], + "installDir": [ + { + "id": "InstallDirDlgTitle", + "text": "Dossier de destination" + }, + { + "id": "InstallDirDlgDescription", + "text": "Cliquez sur Suivant pour effectuer l'installation dans le dossier par défaut, ou cliquez sur Modifier pour choisir un autre dossier." + } + ], + "progress": [ + { + "id": "ProgressDlgTitleInstalling", + "text": "Installation de [ProductName]" + }, + { + "id": "ProgressDlgTitleChanging", + "text": "Modification de [ProductName]" + }, + { + "id": "ProgressDlgTitleRepairing", + "text": "Réparation de [ProductName]" + }, + { + "id": "ProgressDlgTitleRemoving", + "text": "Suppression de [ProductName]" + }, + { + "id": "ProgressDlgTitleUpdating", + "text": "Mise à jour de [ProductName]" + } + ], + "verifyReady": [ + { + "id": "VerifyReadyDlgInstallTitle", + "text": "Prêt pour l'installation de [ProductName]" + }, + { + "id": "VerifyReadyDlgChangeTitle", + "text": "Prêt pour la modification de [ProductName]" + }, + { + "id": "VerifyReadyDlgRepairTitle", + "text": "Prêt pour la réparation de [ProductName]" + }, + { + "id": "VerifyReadyDlgRemoveTitle", + "text": "Prêt pour la suppression de [ProductName]" + }, + { + "id": "VerifyReadyDlgUpdateTitle", + "text": "Prêt pour la mise à jour de [ProductName]" + } + ], + "welcome": [ + { + "id": "WelcomeDlgTitle", + "overridable": "yes", + "text": "Bienvenue dans l'assistant d'installation de [ProductName] 20[ProductVersion]" + } + ], + "firewall": [ + { + "id": "msierrFirewallCannotConnect", + "text": "Impossible de se connecter au Pare-feu Windows. ([2] [3] [4] [5])" + }, + { + "id": "WixSchedFirewallExceptionsInstall", + "text": "Configuration du Pare-feu Windows" + }, + { + "id": "WixSchedFirewallExceptionsUninstall", + "text": "Configuration du Pare-feu Windows" + }, + { + "id": "WixRollbackFirewallExceptionsInstall", + "text": "Restauration de la configuration du Pare-feu Windows" + }, + { + "id": "WixExecFirewallExceptionsInstall", + "text": "Installation de la configuration du Pare-feu Windows" + }, + { + "id": "WixRollbackFirewallExceptionsUninstall", + "text": "Restauration de la configuration du Pare-feu Windows" + }, + { + "id": "WixExecFirewallExceptionsUninstall", + "text": "Désinstallation de la configuration du Pare-feu Windows" + } + ] + } + } + } +} diff --git a/package/AgentWindowsManaged/Resources/WixUIBanner.jpg b/package/AgentWindowsManaged/Resources/WixUIBanner.jpg new file mode 100644 index 000000000..732caf6e6 Binary files /dev/null and b/package/AgentWindowsManaged/Resources/WixUIBanner.jpg differ diff --git a/package/AgentWindowsManaged/Resources/WixUIDialog.jpg b/package/AgentWindowsManaged/Resources/WixUIDialog.jpg new file mode 100644 index 000000000..2cc9af97c Binary files /dev/null and b/package/AgentWindowsManaged/Resources/WixUIDialog.jpg differ