Devolution Gateway Update

Implemented

Devolution Gateway Update

0 vote

avatar

It would be good if there was a way to update many gateways from one console. Currently you have to update every system on which a gateway is installed and update it manually if the version of the Devolution Server changes. With many gateways, this is of course a considerable investment of time and makes it impractical.
Perhaps it would be possible for you to implement this option on the Devolutions server for the individual gateways?

All Comments (6)

avatar

Hi

Indeed, with the popularity of our Gateway we'll put this at the top of list. Normally such a change would be in the next planned release (October), but we've agreed to add it ASAP. It may not be in our first beta, but it should appear in this 2023.2 cycle.

Best Regards,

Maurice

avatar

Hi,

This is indeed a recurring feature request. I am unsure about the best approach at this point - since installing the Devolutions Gateway requires more rights than to simply run it (the service account itself has fewer rights, but the MSI installer requires elevated rights) it would be difficult to make a simple self-update mechanism.

This means we should probably look into a way to either install it with fewer rights (like a user-local installation, like those programs that don't require elevated rights to update) or we'd need to allow remote execution through the Devolutions Gateway with supplied credentials to run a PowerShell command to download the newer MSI, install it, then re-launch the service.

The PowerShell remoting approach would be the most flexible - is that something that could work for you? Maybe this could be built into RDM instead of the console, such that you could leverage your existing connection entries, etc.

I'm throwing a few ideas out there, it still isn't clear in my mind which approach would be the simplest to build quickly

Best regards,

Marc-André Moreau

avatar

Hi,

I have written a simple Devolutions Gateway Updater tool as a PowerShell script to be registered as a scheduled task that runs once a day to check for updates + download and install them automatically.

I am including a copy of the instructions and script here, such that we can remember the starting point. Please take a look at it, and tell me if this fits your needs, and what you'd like done differently. I don't expect it to be final, this is just a first (working) version to collect feedback and better align the development of a proper way to update a large number of Devolutions Gateway servers:

Installing Updater

Open an elevated PowerShell terminal, move to the directory containing the GatewayUpdater.ps1 script, and then run it with the 'install' parameter:

PS > .\GatewayUpdater.ps1 install

TaskPath                                       TaskName                          State
--------                                       --------                          -----
\                                              Devolutions Gateway Updater       Ready
Updater script installed to 'C:\Program Files\Devolutions\Gateway Updater\GatewayUpdater.ps1' and registered as 'Devolutions Gateway Updater' scheduled task


The GatewayUpdater.ps1 script will be copied to "$Env:ProgramFiles\Devolutions\Gateway Updater" and registered as a scheduled task named 'Devolutions Gateway Updater" that runs once per day at 3AM.

Running Updater

You can wait for the scheduled task to run automatically at 3AM, or manually trigger it to see if it works:

& schtasks.exe /Run /TN "Devolutions Gateway Updater"


You can then query the status of the 'Devolutions Gateway Updater' scheduled task:

PS > schtasks.exe /Query /TN "Devolutions Gateway Updater"

Folder: \
TaskName                                 Next Run Time          Status
======================================== ====================== ===============
Devolutions Gateway Updater              2023-06-17 3:00:00 AM  Ready


The updater checks if a new version of Devolutions Gateway has been published, and then proceeds to automatically download the installer, check its file hash before running it silently.

Uninstalling Updater

To uninstall the Devolutions Gateway Updater, run the GatewayUpdater.ps1 script with the 'uninstall' parameter:

PS > .\GatewayUpdater.ps1 uninstall

Folder: \
TaskName                                 Next Run Time          Status
======================================== ====================== ===============
Devolutions Gateway Updater              2023-06-17 3:00:00 AM  Ready
SUCCESS: The scheduled task "Devolutions Gateway Updater" was successfully deleted.


This will unregister the scheduled task, and delete the GatewayUpdater.ps1 script from 'C:\Program Files\Devolutions\Gateway Updater'

GatewayUpdater.ps1 script:

function Install-DGatewayUpdater
{
    [CmdletBinding()]
    param (
    )

    $InstallPath = "$Env:ProgramFiles\Devolutions\Gateway Updater"
    $ScriptPath = Join-Path $InstallPath "GatewayUpdater.ps1"
    New-Item -Path $InstallPath -ItemType Directory -Force | Out-Null
    Copy-Item -Path $PSCommandPath -Destination $ScriptPath -Force
    Register-DGatewayUpdater -ScriptPath $ScriptPath

    $TaskName = "Devolutions Gateway Updater"
    Write-Host "Updater script installed to '$ScriptPath' and registered as '$TaskName' scheduled task"
}

function Uninstall-DGatewayUpdater
{
    [CmdletBinding()]
    param (
    )

    Unregister-DGatewayUpdater

    $InstallPath = "$Env:ProgramFiles\Devolutions\Gateway Updater"
    $ScriptPath = Join-Path $InstallPath "GatewayUpdater.ps1"
    Remove-Item $ScriptPath -ErrorAction SilentlyContinue -Force | Out-Null
}

function Register-DGatewayUpdater
{
    [CmdletBinding()]
    param (
        [string] $ScriptPath
    )

    if ([string]::IsNullOrEmpty($ScriptPath)) {
        $ScriptPath = $PSCommandPath
    }

    Unregister-DGatewayUpdater

    $TaskName = "Devolutions Gateway Updater"
    $TaskUser = "NT AUTHORITY\SYSTEM"
    $TaskPrincipal = New-ScheduledTaskPrincipal -UserID $TaskUser -LogonType ServiceAccount -RunLevel Highest
    $TaskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
    $TaskTrigger = New-ScheduledTaskTrigger -Daily -At 3AM
    Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $TaskTrigger -Principal $TaskPrincipal
}

function Unregister-DGatewayUpdater
{
    [CmdletBinding()]
    param (
    )

    $TaskName = "Devolutions Gateway Updater"
    & schtasks.exe /Query /TN $TaskName 2>$null
    $TaskExists = [bool] ($LASTEXITCODE -eq 0)

    if ($TaskExists) {
        & schtasks.exe /Delete /TN $TaskName /F
    }
}

function Get-DGatewayInstalledVersion
{
    [CmdletBinding()]
    param(
    )

    $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
    | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' }
    if ($UninstallReg) {
        $DGatewayVersion = '20' + $UninstallReg.DisplayVersion
    }
    $DGatewayVersion
}

function Get-DGatewayPackageLocation
{
    [CmdletBinding()]
    param(
        [string] $RequiredVersion
    )

    $VersionQuad = '';
    $ProductsUrl = "https://devolutions.net/productinfo.htm"

    $ProductsHtm = Invoke-RestMethod -Uri $ProductsUrl -Method 'GET' -ContentType 'text/plain'
    $VersionMatches = $($ProductsHtm | Select-String -AllMatches -Pattern "Gatewaybin.Version=(\S+)").Matches
    $LatestVersion = $VersionMatches.Groups[1].Value

    if ($RequiredVersion) {
        if ($RequiredVersion -Match "^\d+`.\d+`.\d+$") {
            $RequiredVersion = $RequiredVersion + ".0"
        }
        $VersionQuad = $RequiredVersion
    } else {
        $VersionQuad = $LatestVersion
    }

    $VersionMatches = $($VersionQuad | Select-String -AllMatches -Pattern "(\d+)`.(\d+)`.(\d+)`.(\d+)").Matches
    $VersionMajor = $VersionMatches.Groups[1].Value
    $VersionMinor = $VersionMatches.Groups[2].Value
    $VersionPatch = $VersionMatches.Groups[3].Value
    $VersionTriple = "${VersionMajor}.${VersionMinor}.${VersionPatch}"

    $GatewayUrlMatches = $($ProductsHtm | Select-String -AllMatches -Pattern "(Gateway\S+).Url=(\S+)").Matches
    $GatewayHashMatches = $($ProductsHtm | Select-String -AllMatches -Pattern "(Gateway\S+).hash=(\S+)").Matches
    $GatewayMsiUrl = $GatewayUrlMatches | Where-Object { $_.Groups[1].Value -eq 'Gatewaybin' }
    $GatewayMsiHash = $GatewayHashMatches | Where-Object { $_.Groups[1].Value -eq 'Gatewaybin' }
    
    if ($GatewayMsiUrl) {
        $DownloadUrl = $GatewayMsiUrl.Groups[2].Value
        $DownloadHash = $GatewayMsiHash.Groups[2].Value
    }

    if ($RequiredVersion) {
        $DownloadUrl = $DownloadUrl -Replace $LatestVersion, $RequiredVersion
    }
 
    [PSCustomObject]@{
        Url = $DownloadUrl
        Hash = $DownloadHash
        Version = $VersionTriple
    }
}

function Get-DGatewayPackageFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DownloadUrl,
        [Parameter(Mandatory = $true)]
        [string] $DownloadHash,
        [string] $DownloadPath
    )

    $FileName = [System.IO.Path]::GetFileName($DownloadUrl)

    if ([string]::IsNullOrEmpty($DownloadPath)) {
        $TempPath = [System.IO.Path]::GetTempPath()
        $DownloadPath = Join-Path -Path $TempPath -ChildPath $FileName
    }

    $webClient = New-Object System.Net.WebClient
    $webClient.DownloadFile($DownloadUrl, $DownloadPath)
    $FileHash = (Get-FileHash -Path $DownloadPath -Algorithm SHA256).Hash

    if ($FileHash -ne $DownloadHash) {
        throw "$FileName hash mismatch: Actual: $FileHash, Expected: $DownloadHash"
    }

    $DownloadPath
}

function Install-DGatewayPackage
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $InstallerPath,
        [switch] $Quiet,
        [switch] $Force
    )

    $Display = '/passive'
    if ($Quiet) {
        $Display = '/quiet'
    }

    $TempPath = Join-Path $([System.IO.Path]::GetTempPath()) "dgateway-${Version}"
    New-Item -ItemType Directory -Path $TempPath -ErrorAction SilentlyContinue | Out-Null
    $InstallLogFile = Join-Path $TempPath 'DGateway_Install.log'
    
    $MsiArgs = @(
        '/i', "`"$InstallerPath`"",
        $Display,
        '/norestart',
        '/log', "`"$InstallLogFile`""
    )

    Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait -NoNewWindow
    Remove-Item -Path $InstallLogFile -Force -ErrorAction SilentlyContinue
    Remove-Item -Path $TempPath -Force -Recurse
}

function Uninstall-DGatewayPackage
{
    [CmdletBinding()]
    param(
        [switch] $Quiet
    )

    $UninstallReg = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
    | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_ -Match 'Devolutions Gateway' }
    if ($UninstallReg) {
        $UninstallString = $($UninstallReg.UninstallString `
                -Replace 'msiexec.exe', '' -Replace '/I', '' -Replace '/X', '').Trim()
        $Display = '/passive'
        if ($Quiet) {
            $Display = '/quiet'
        }
        $MsiArgs = @(
            '/X', $UninstallString, $Display
        )
        Start-Process 'msiexec.exe' -ArgumentList $MsiArgs -Wait
    }
}

function Invoke-DGatewayUpdater
{
    [CmdletBinding()]
    param(
    )

    $CurrentVersion = Get-DGatewayInstalledVersion
    $Package = Get-DGatewayPackageLocation

    if ($CurrentVersion -ne $Package.Version) {
        $DownloadPath = Get-DGatewayPackageFile -DownloadUrl $Package.Url -DownloadHash $Package.Hash

        if ($DownloadPath) {
            if ([Version] $Package.Version -lt [Version] $CurrentVersion) {
                Uninstall-DGatewayPackage -Quiet
            }
            Install-DGatewayPackage -InstallerPath $DownloadPath -Quiet
        }
    }
}

$CmdVerbs = @('run', 'install', 'uninstall', 'register', 'unregister')

if ($args.Count -lt 1) {
    $CmdVerb = "run"
    $CmdArgs = @()
} else {
    $CmdVerb = $args[0]
    $CmdArgs = $args[1..$args.Count]
}

if ($CmdVerbs -NotContains $CmdVerb) {
    throw "invalid verb $CmdVerb, use one of: [$($CmdVerbs -Join ',')]"
}

switch ($CmdVerb) {
    "run" { Invoke-DGatewayUpdater @CmdArgs }
    "install" { Install-DGatewayUpdater @CmdArgs }
    "uninstall" { Uninstall-DGatewayUpdater @CmdArgs }
    "register" { Register-DGatewayUpdater @CmdArgs }
    "unregister" { Unregister-DGatewayUpdater @CmdArgs }
}


I hope this helps,

Best regards,

Marc-André Moreau

avatar

Thank you Marc-André for producing this code, it is appreciated.

It looks like this would be distributed and run at each gateway host. Am I correct? Or are there plans to run this centrally, which could be a timesaver.

Looking at the current releases out there, we are at 2023.3.16 DSRVR. Do I understand that if we run this updated it would pick up the 2024 code base? Something like 2024.1.3. Would this be a conflict with the 2023.3.16 schema? We have to watch our RDM upgrades so as to not have schema issues, especially between the 2023 and 2024 releases. If so, could we restrict the Devolutions Gateway Updater to certain releases?

With best regards and thank you again for this project.
Phil

avatar

Hi,

A cleaned up copy of the same updater script is on the Devolutions Gateway GitHub repository and it is also referred to in our documentation.

Yes, it would need to be installed on each Devolutions Gateway host. I wrote it in a day, as we've had a couple of requests from customers, but I wanted to provide a quick V1 for feedback. I suspected that the true request would be for centralized update management, because it is not a trivial thing to implement properly.

Devolutions Gateway backward and forward compatibility is very good, but you need to use the latest version for the latest features to work, obviously. Devolutions Gateway 2024.1 will definitely work with RDM and DVLS 2023.3. If you intend to just keep it to the very latest version, maybe the current script is good enough.

Implementing a self-updating mechanism is tricky for multiple reasons: Devolutions Gateway is the service that would receive the "update yourself" call, but we can't assume it runs with sufficient rights to launch an installer elevated. I'd have to check if it would be possible to trigger the scheduled task running elevated to do the update process though, and write the desired version to a JSON file to be picked up.

We'd obviously need to do the proper checks to ensure it only downloads Devolutions Gateway installers from our CDN and signed by us, but beyond that, there's the issue of obtaining the installer file. In your case, can we assume Devolutions Gateway has Internet access to the Devolutions CDN? Some customers run it without Internet access, and I'd have to come up with a method for pushing the the installer files if that's something you need.

Anyway, I'm just throwing a couple of ideas out there, maybe this could be a good hackathon project. You're not the first to ask about centralized updates, it just isn't very straightforward to implement properly and especially in a secure manner.

Best regards,

Marc-André Moreau

avatar

This would be a great feature.

I think the easiest way would be:

  • In DVLS/Administration/Gateway add a section where you can specify time/date to check for updates
  • Should also be able to set desired version - you would have "Latest" as an option, but maybe have previous version numbers so that admins can choose when to update to the latest version themselves. For example they might set it at 2024.1.5 now and then in a few months come back and set it at 2024.1.6
  • In the same area add a field that displays the version number of each gateway next to active sessions
  • Add a feature in to the DVLS.Console to install a scheduler/service that runs as a service on the Devolutions Gateway machine
  • The scheduler should check in with the DVLS at the scheduled time and if it needs an update, runs it


Similar to how windows updates work