Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,9 @@ jobs:

$DAgentSessionExecutable = Join-Path $TargetOutputPath "DevolutionsSession.exe"
echo "dagent-session-executable=$DAgentSessionExecutable" >> $Env:GITHUB_OUTPUT

$DAgentUpdaterExecutable = Join-Path $TargetOutputPath "devolutions-agent-updater.exe"
echo "dagent-updater-executable=$DAgentUpdaterExecutable" >> $Env:GITHUB_OUTPUT
}

$DAgentExecutable = Join-Path $TargetOutputPath $ExecutableFileName
Expand Down Expand Up @@ -919,6 +922,7 @@ jobs:
- name: Build
run: |
if ($Env:RUNNER_OS -eq "Windows") {
$Env:DAGENT_UPDATER_EXECUTABLE = "${{ steps.load-variables.outputs.dagent-updater-executable }}"
$Env:DAGENT_PEDM_SHELL_EXT_DLL = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-dll }}"
$Env:DAGENT_PEDM_SHELL_EXT_MSIX = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-msix }}"
$Env:DAGENT_SESSION_EXECUTABLE = "${{ steps.load-variables.outputs.dagent-session-executable }}"
Expand Down Expand Up @@ -951,6 +955,7 @@ jobs:

$Env:DAGENT_PACKAGE = "${{ steps.load-variables.outputs.dagent-package }}"
$Env:DAGENT_DESKTOP_AGENT_PATH = $DesktopStagingPath
$Env:DAGENT_UPDATER_EXECUTABLE = "${{ steps.load-variables.outputs.dagent-updater-executable }}"
$Env:DAGENT_PEDM_SHELL_EXT_DLL = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-dll }}"
$Env:DAGENT_PEDM_SHELL_EXT_MSIX = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-msix }}"
$Env:DAGENT_SESSION_EXECUTABLE = "${{ steps.load-variables.outputs.dagent-session-executable }}"
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ jobs:
run: |
$IncludePattern = @(switch ('${{ matrix.project }}') {
'devolutions-gateway' { @('DevolutionsGateway_*.exe') }
'devolutions-agent' { @('DevolutionsAgent_*.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe') }
'devolutions-agent' { @('DevolutionsAgent_*.exe', 'devolutions-agent-updater.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe') }
'jetsocat' { @('jetsocat_*') }
})
$ExcludePattern = "*.pdb"
Expand Down Expand Up @@ -473,7 +473,8 @@ jobs:
run: |
$PackageRoot = Join-Path ${{ runner.temp }} ${{ matrix.project}}

$Env:DAGENT_EXECUTABLE = Get-ChildItem -Path $PackageRoot -Recurse -Include '*DevolutionsAgent*.exe' | Select -First 1
$Env:DAGENT_EXECUTABLE = Get-ChildItem -Path $PackageRoot -Recurse -Include '*DevolutionsAgent_*.exe' | Select -First 1
$Env:DAGENT_UPDATER_EXECUTABLE = Get-ChildItem -Path $PackageRoot -Recurse -Include 'devolutions-agent-updater.exe' | Select -First 1
$Env:DAGENT_DESKTOP_AGENT_PATH = Resolve-Path -Path "devolutions-pedm-desktop"
$Env:DAGENT_PEDM_SHELL_EXT_DLL = Get-ChildItem -Path $PackageRoot -Recurse -Include 'DevolutionsPedmShellExt.dll' | Select -First 1
$Env:DAGENT_PEDM_SHELL_EXT_MSIX = Get-ChildItem -Path $PackageRoot -Recurse -Include 'DevolutionsPedmShellExt.msix' | Select -First 1
Expand All @@ -482,6 +483,7 @@ jobs:
$Env:DAGENT_WINTUN_DLL = Get-ChildItem -Path $PackageRoot -Recurse -Include 'wintun.dll' | Select -First 1

Write-Host "DAGENT_EXECUTABLE = ${Env:DAGENT_EXECUTABLE}"
Write-Host "DAGENT_UPDATER_EXECUTABLE = ${Env:DAGENT_UPDATER_EXECUTABLE}"
Write-Host "DAGENT_DESKTOP_AGENT_PATH = ${Env:DAGENT_DESKTOP_AGENT_PATH}"
Write-Host "DAGENT_PEDM_SHELL_EXT_DLL = ${Env:DAGENT_PEDM_SHELL_EXT_DLL}"
Write-Host "DAGENT_PEDM_SHELL_EXT_MSIX = ${Env:DAGENT_PEDM_SHELL_EXT_MSIX}"
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"crates/*",
"devolutions-agent",
"devolutions-agent-updater",
"devolutions-gateway",
"devolutions-session",
"jetsocat",
Expand All @@ -11,6 +12,7 @@ members = [
]
default-members = [
"devolutions-agent",
"devolutions-agent-updater",
"devolutions-gateway",
"devolutions-session",
"jetsocat",
Expand Down
19 changes: 14 additions & 5 deletions ci/package-agent-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ param(
[parameter(Mandatory = $true)]
[string] $Exe,
[parameter(Mandatory = $true)]
[string] $UpdaterExe,
[parameter(Mandatory = $true)]
[string] $PedmDll,
[parameter(Mandatory = $true)]
[string] $PedmMsix,
Expand Down Expand Up @@ -31,7 +33,7 @@ function Set-FileNameAndCopy {
[string]$Path,
[string]$NewName
)

if (-Not (Test-Path $Path)) {
throw "File not found: $Path"
}
Expand Down Expand Up @@ -84,6 +86,9 @@ function New-AgentMsi() {
# The path to the devolutions-agent.exe file.
[string] $Exe,
[parameter(Mandatory = $true)]
# The path to the devolutions-agent-updater.exe file.
[string] $UpdaterExe,
[parameter(Mandatory = $true)]
# The path to the devolutions_pedm_shell_ext.dll file.
[string] $PedmDll,
[parameter(Mandatory = $true)]
Expand Down Expand Up @@ -111,6 +116,7 @@ function New-AgentMsi() {

# Convert slashes. This does not affect function. It's just for display.
$Exe = Convert-Path -Path $Exe
$UpdaterExe = Convert-Path -Path $UpdaterExe
$PedmDll = Convert-Path -Path $PedmDll
$PedmMsix = Convert-Path -Path $PedmMsix
$SessionExe = Convert-Path -Path $SessionExe
Expand All @@ -127,20 +133,23 @@ function New-AgentMsi() {
# These file names don't matter for building, but we will clean them up anyways for consistency. The names can be seen if inspecting the MSI.
# The Agent exe will get copied to `C:\Program Files\Devolutions\Agent\DevolutionsAgent.exe` after install.
$myExe = Set-FileNameAndCopy -Path $Exe -NewName 'DevolutionsAgent.exe'
# The updater shim is a detached helper for installing MSI updates.
$myUpdaterExe = Set-FileNameAndCopy -Path $UpdaterExe -NewName 'devolutions-agent-updater.exe'
# The session is a service that gets launched on demand.
$mySessionExe = Set-FileNameAndCopy -Path $SessionExe -NewName 'DevolutionsSession.exe'

Write-Output "$repoDir\dotnet\DesktopAgent\bin\Release\net48\DevolutionsDesktopAgent.exe"

Set-EnvVarPath 'DAGENT_EXECUTABLE' $myExe
Set-EnvVarPath 'DAGENT_UPDATER_EXECUTABLE' $myUpdaterExe
Set-EnvVarPath 'DAGENT_PEDM_SHELL_EXT_DLL' $myPedmDll
Set-EnvVarPath 'DAGENT_PEDM_SHELL_EXT_MSIX' $myPedmMsix
Set-EnvVarPath 'DAGENT_SESSION_EXECUTABLE' $mySessionExe

# The actual DevolutionsDesktopAgent.exe will be `\dotnet\DesktopAgent\bin\Release\net48\DevolutionsDesktopAgent.exe`.
# After install, the contsnts of `net48` will be copied to `C:\Program Files\Devolutions\Agent\desktop\`.
# After install, the contsnts of `net48` will be copied to `C:\Program Files\Devolutions\Agent\desktop\`.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: contsntscontents.

Suggested change
# After install, the contsnts of `net48` will be copied to `C:\Program Files\Devolutions\Agent\desktop\`.
# After install, the contents of `net48` will be copied to `C:\Program Files\Devolutions\Agent\desktop\`.

Copilot uses AI. Check for mistakes.
Set-EnvVarPath 'DAGENT_DESKTOP_AGENT_PATH' "$repoDir\dotnet\DesktopAgent\bin\Release\net48"

$version = Get-Version

Push-Location
Expand All @@ -152,7 +161,7 @@ function New-AgentMsi() {
if ($Generate) {
# This is used by `package/WindowsManaged/Program.cs`.
$Env:DAGENT_MSI_SOURCE_ONLY_BUILD = '1'

foreach ($lang in Get-PackageLanguages) {
$Env:DAGENT_MSI_LANG_ID = $lang.Name
& 'MSBuild.exe' 'DevolutionsAgent.sln' '/t:restore,build' '/p:Configuration=Release' | Out-Host
Expand All @@ -175,4 +184,4 @@ function New-AgentMsi() {
Pop-Location
}

New-AgentMsi -Generate:($Generate.IsPresent) -Exe $Exe -PedmDll $PedmDll -PedmMsix $PedmMsix -SessionExe $SessionExe -Architecture $Architecture -Outfile $Outfile
New-AgentMsi -Generate:($Generate.IsPresent) -Exe $Exe -UpdaterExe $UpdaterExe -PedmDll $PedmDll -PedmMsix $PedmMsix -SessionExe $SessionExe -Architecture $Architecture -Outfile $Outfile
7 changes: 5 additions & 2 deletions ci/tlk.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class TlkRecipe
$agentPackages = @([TlkPackage]::new("devolutions-agent", "devolutions-agent", $false))

if ($this.Target.IsWindows()) {
$agentPackages += [TlkPackage]::new("devolutions-agent-updater", "devolutions-agent", $false)
$agentPackages += [TlkPackage]::new("devolutions-pedm-shell-ext", "crates/devolutions-pedm-shell-ext", $true)
$agentPackages += [TlkPackage]::new("devolutions-session", "devolutions-session", $false)
}
Expand Down Expand Up @@ -387,6 +388,8 @@ class TlkRecipe
"agent" {
if ($CargoPackage.Name -Eq "devolutions-agent" -And (Test-Path Env:DAGENT_EXECUTABLE)) {
$Env:DAGENT_EXECUTABLE
} elseif ($CargoPackage.Name -Eq "devolutions-agent-updater" -And (Test-Path Env:DAGENT_UPDATER_EXECUTABLE)) {
$Env:DAGENT_UPDATER_EXECUTABLE
} elseif ($CargoPackage.Name -Eq "devolutions-pedm-shell-ext" -And (Test-Path Env:DAGENT_PEDM_SHELL_EXT_DLL)) {
$Env:DAGENT_PEDM_SHELL_EXT_DLL
} elseif ($CargoPackage.Name -Eq "devolutions-session" -And (Test-Path Env:DAGENT_SESSION_EXECUTABLE)) {
Expand Down Expand Up @@ -760,7 +763,7 @@ class TlkRecipe
}

$DebUpstreamChangelogFile = Join-Path $OutputPath "changelog_deb_upstream"

Merge-Tokens -TemplateFile $RulesTemplate -Tokens @{
dh_shlibdeps = $DhShLibDepsOverride
upstream_changelog = $DebUpstreamChangelogFile
Expand Down Expand Up @@ -799,7 +802,7 @@ class TlkRecipe

# input for debian/changelog is the package-specific CHANGELOG.md
$PackagingChangelogFile = Join-Path $InputPackagePath "CHANGELOG.md"

$s = New-Changelog `
-Format 'Deb' `
-InputFile $UpstreamChangelogFile `
Expand Down
1 change: 1 addition & 0 deletions crates/devolutions-agent-shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ workspace = true
camino = "1.1"
cfg-if = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"

[target.'cfg(windows)'.dependencies]
Expand Down
148 changes: 148 additions & 0 deletions crates/devolutions-agent-shared/src/agent_auto_update.rs
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: And now in this module we use serde_json directly, and perform I/O with the filesystem. Just thought it was worth pointing out the difference, although I see the nature of the operations is materially different.

Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Agent auto-update configuration and helpers.
//!
//! This module defines the `AgentAutoUpdateConf` structure and provides helpers to read
//! and write the auto-update section of `agent.json` without touching the rest of the file.

use camino::Utf8PathBuf;

use crate::get_data_dir;

/// Default check interval expressed as a humantime string (`"1d"` = 24 hours).
pub const DEFAULT_INTERVAL: &str = "1d";
pub const DEFAULT_WINDOW_START: &str = "02:00";
/// Default maintenance window end time (exclusive, local time `HH:MM`).
pub const DEFAULT_WINDOW_END: &str = "04:00";

fn default_interval() -> String {
DEFAULT_INTERVAL.to_owned()
}

fn default_window_start() -> String {
DEFAULT_WINDOW_START.to_owned()
}

fn default_window_end() -> Option<String> {
Some(DEFAULT_WINDOW_END.to_owned())
}

/// Auto-update schedule configuration for Devolutions Agent.
///
/// When enabled, the agent periodically checks whether a new version of itself is
/// available in the Devolutions productinfo database. If a newer version is found
/// and the current local time falls inside the configured maintenance window, the
/// agent writes the new target version to `update.json`, which is then picked up by
/// the updater task to perform the silent MSI installation.
///
/// Settings changes take effect when the agent service (re)starts.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct AgentAutoUpdateConf {
/// Enable periodic Devolutions Agent self-update checks.
pub enabled: bool,

/// Minimum interval between auto-update checks.
///
/// Accepts humantime duration strings such as `"1d"`, `"12h"`, `"30m 20s"`, or a
/// bare integer treated as seconds (e.g. `"3600"`). Defaults to `"1d"`.
#[serde(default = "default_interval")]
pub interval: String,

/// Start of the maintenance window (local time, `HH:MM` format).
///
/// Defaults to `"02:00"`.
#[serde(default = "default_window_start")]
pub update_window_start: String,

/// End of the maintenance window (local time, `HH:MM` format, exclusive).
///
/// Defaults to `"04:00"`. When `null`, the window has no upper bound.
/// If the end time is earlier than or equal to the start time the window is
/// assumed to cross midnight (e.g. `"22:00"`–`"03:00"` is a 5-hour window).
#[serde(default = "default_window_end")]
pub update_window_end: Option<String>,
}

impl Default for AgentAutoUpdateConf {
fn default() -> Self {
Self {
enabled: false,
interval: DEFAULT_INTERVAL.to_owned(),
update_window_start: DEFAULT_WINDOW_START.to_owned(),
update_window_end: Some(DEFAULT_WINDOW_END.to_owned()),
}
}
}

/// Returns the path to `agent.json`.
pub fn get_agent_config_path() -> Utf8PathBuf {
get_data_dir().join("agent.json")
}

/// Read the current auto-update configuration from `agent.json`.
///
/// Returns defaults when the file is absent or the `AgentAutoUpdate` section is missing.
pub fn read_agent_auto_update_conf() -> std::io::Result<AgentAutoUpdateConf> {
let path = get_agent_config_path();

let content = match std::fs::read(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(AgentAutoUpdateConf::default()),
Err(e) => return Err(e),
};

// Strip UTF-8 BOM if present.
let content = if content.starts_with(&[0xEF, 0xBB, 0xBF]) {
&content[3..]
} else {
&content
};

let value: serde_json::Value =
serde_json::from_slice(content).map_err(|e| std::io::Error::other(format!("invalid agent.json: {e}")))?;

let section = value
.get("Updater")
.and_then(|u| u.get("AgentAutoUpdate"))
.cloned()
.unwrap_or(serde_json::Value::Null);

if section.is_null() {
return Ok(AgentAutoUpdateConf::default());
}

serde_json::from_value(section).map_err(|e| std::io::Error::other(format!("invalid AgentAutoUpdate section: {e}")))
}

/// Write `conf` into the `Updater.AgentAutoUpdate` section of `agent.json`.
///
/// All other keys in the file are preserved.
pub fn write_agent_auto_update_conf(conf: &AgentAutoUpdateConf) -> std::io::Result<()> {
let path = get_agent_config_path();

// Read or create the root JSON value.
let mut root: serde_json::Value = if path.exists() {
let content = std::fs::read(&path)?;
let content = if content.starts_with(&[0xEF, 0xBB, 0xBF]) {
content[3..].to_vec()
} else {
content
};
serde_json::from_slice(&content)
.map_err(|e| std::io::Error::other(format!("invalid agent.json: {e}")))?
} else {
serde_json::json!({})
};

// Ensure the `Updater` object exists (with `Enabled: true` as default).
if root.get("Updater").is_none() {
root["Updater"] = serde_json::json!({ "Enabled": true });
}

root["Updater"]["AgentAutoUpdate"] =
serde_json::to_value(conf).map_err(|e| std::io::Error::other(format!("serialization error: {e}")))?;

let json = serde_json::to_string_pretty(&root)
.map_err(|e| std::io::Error::other(format!("serialization error: {e}")))?;

std::fs::write(&path, json)
}
5 changes: 4 additions & 1 deletion crates/devolutions-agent-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extern crate serde;
#[cfg(windows)]
pub mod windows;

pub mod agent_auto_update;
mod date_version;
mod update_json;

Expand All @@ -12,10 +13,12 @@ use std::env;
use camino::Utf8PathBuf;
use cfg_if::cfg_if;

#[rustfmt::skip]
pub use agent_auto_update::AgentAutoUpdateConf;
#[rustfmt::skip]
pub use date_version::{DateVersion, DateVersionError};
#[rustfmt::skip]
pub use update_json::{ProductUpdateInfo, UpdateJson, VersionSpecification};
pub use update_json::{ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, UpdateJson, UpdateManifest, UpdateManifestV2, UpdateProductKey, VersionedManifest, VersionSpecification};

cfg_if! {
if #[cfg(target_os = "windows")] {
Expand Down
Loading
Loading