diff --git a/Installer/Files/config.ini b/Installer/Files/config.ini
index e66b0a9..05280d2 100644
--- a/Installer/Files/config.ini
+++ b/Installer/Files/config.ini
@@ -1,3 +1,7 @@
[plc]
port = COM1
+
+[update]
+url = https://elwig.at/files/pamhagen-sysctrl/?format=json
+auto = true
diff --git a/PamhagenSysCtrl/App.xaml.cs b/PamhagenSysCtrl/App.xaml.cs
index 48041b3..5e64ed2 100644
--- a/PamhagenSysCtrl/App.xaml.cs
+++ b/PamhagenSysCtrl/App.xaml.cs
@@ -1,3 +1,4 @@
+using PamhagenSysCtrl.Dialogs;
using PamhagenSysCtrl.Helpers;
using System.IO;
using System.Reflection;
@@ -10,6 +11,9 @@ namespace PamhagenSysCtrl {
public static PamhagenPlant? Plant;
public static Dispatcher? MainDispatcher;
+ private readonly DispatcherTimer AutoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
+ private bool IsCheckingForUpdates;
+
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Anlagensteuerung Pamhagen");
public static readonly string ConfigPath = Path.Combine(DataPath, "config.ini");
public static readonly string InstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Anlagensteuerung Pamhagen");
@@ -45,6 +49,71 @@ namespace PamhagenSysCtrl {
}
base.OnStartup(evt);
+
+ if (Config.UpdateAuto && Config.UpdateUrl != null) {
+ AutoUpdateTimer.Tick += async (_, _) => await CheckForUpdates();
+ AutoUpdateTimer.Start();
+ _ = Dispatcher.BeginInvoke(async () => {
+ await Task.Delay(1500);
+ await CheckForUpdates();
+ });
+ }
+ }
+
+ public async Task CheckForUpdates(bool showResult = false) {
+ if (IsCheckingForUpdates) {
+ if (showResult) {
+ MessageBox.Show("Es wird bereits nach Updates gesucht.", "Nach Updates suchen", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ return;
+ }
+ if (Config.UpdateUrl == null) {
+ if (showResult) {
+ MessageBox.Show("Die automatische Update-Suche ist deaktiviert.", "Nach Updates suchen", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ return;
+ }
+
+ IsCheckingForUpdates = true;
+ try {
+ using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
+ var latest = await UpdateService.GetLatestInstallerAsync(Config.UpdateUrl, timeout.Token);
+ if (latest.Version > Version) {
+ var dialog = new UpdateDialog(latest) {
+ Owner = MainWindow,
+ };
+ if (dialog.ShowDialog() == true) {
+ Shutdown();
+ }
+ } else if (showResult) {
+ MessageBox.Show(
+ $"Die Anlagensteuerung ist auf dem aktuellen Stand. (Version {Version})",
+ "Nach Updates suchen",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ }
+ } catch (OperationCanceledException) {
+ if (showResult) {
+ MessageBox.Show(
+ "Zeitüberschreitung beim Abrufen der Update-Informationen.",
+ "Nach Updates suchen",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ } catch (Exception exc) {
+ if (showResult) {
+ MessageBox.Show(
+ $"Die Update-Informationen konnten nicht abgerufen werden:\n\n{exc.Message}",
+ "Nach Updates suchen",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ } finally {
+ IsCheckingForUpdates = false;
+ }
}
protected async void Application_Exit(object sender, ExitEventArgs evt) {
diff --git a/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml b/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml
new file mode 100644
index 0000000..da0ab88
--- /dev/null
+++ b/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+ Version 0.0.0 ist verfügbar.
+ Soll das Update heruntergeladen und installiert werden?
+ (ca. 0 MB)
+ Hinweis: Die Anlagensteuerung wird zur Installation geschlossen.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml.cs b/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml.cs
new file mode 100644
index 0000000..93e8c71
--- /dev/null
+++ b/PamhagenSysCtrl/Dialogs/UpdateDialog.xaml.cs
@@ -0,0 +1,49 @@
+using PamhagenSysCtrl.Helpers;
+using System.Windows;
+
+namespace PamhagenSysCtrl.Dialogs {
+ public partial class UpdateDialog : Window {
+
+ private readonly UpdateInstaller Installer;
+ private readonly CancellationTokenSource Cancellation = new();
+
+ public UpdateDialog(UpdateInstaller installer) {
+ Installer = installer;
+ InitializeComponent();
+ VersionText.Text = installer.Version.ToString();
+ SizeText.Text = Math.Ceiling(installer.Size / 1024d / 1024d).ToString("N0");
+ }
+
+ private void OnClosed(object? sender, EventArgs evt) {
+ Cancellation.Cancel();
+ Cancellation.Dispose();
+ }
+
+ private async void InstallButton_Click(object sender, RoutedEventArgs evt) {
+ Description.Visibility = Visibility.Collapsed;
+ DownloadPanel.Visibility = Visibility.Visible;
+ InstallButton.IsEnabled = false;
+
+ try {
+ var progress = new Progress(value => DownloadProgress.Value = value * 100);
+ var fileName = await UpdateService.DownloadInstallerAsync(Installer, App.TempPath, progress, Cancellation.Token);
+ StatusText.Text = "Installer wird gestartet...";
+ UpdateService.StartInstaller(fileName);
+ DialogResult = true;
+ } catch (OperationCanceledException) {
+ // Closing the dialog cancels an active download.
+ } catch (Exception exc) {
+ MessageBox.Show(
+ this,
+ $"Das Update konnte nicht installiert werden:\n\n{exc.Message}",
+ "Update installieren",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ Description.Visibility = Visibility.Visible;
+ DownloadPanel.Visibility = Visibility.Collapsed;
+ InstallButton.IsEnabled = true;
+ }
+ }
+ }
+}
diff --git a/PamhagenSysCtrl/Helpers/Config.cs b/PamhagenSysCtrl/Helpers/Config.cs
index d55caf5..f90de4e 100644
--- a/PamhagenSysCtrl/Helpers/Config.cs
+++ b/PamhagenSysCtrl/Helpers/Config.cs
@@ -9,6 +9,8 @@ namespace PamhagenSysCtrl.Helpers {
private readonly string FileName;
public string PlcPort = "COM1";
+ public string? UpdateUrl = "https://elwig.at/files/pamhagen-sysctrl/?format=json";
+ public bool UpdateAuto = true;
public Config(string filename) {
FileName = filename;
@@ -19,6 +21,9 @@ namespace PamhagenSysCtrl.Helpers {
try {
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
PlcPort = config["plc:port"] ?? "COM1";
+ UpdateUrl = config["update:url"] ?? UpdateUrl;
+ var updateAuto = config["update:auto"];
+ UpdateAuto = updateAuto == null || TrueValues.Contains(updateAuto.ToLower());
} catch (Exception exc) {
MessageBox.Show($"Die Konfigurationsdatei konnte nicht gelesen werden:\n\n{exc.Message}", "Konfigurationsdatei lesen", MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
diff --git a/PamhagenSysCtrl/Helpers/UpdateService.cs b/PamhagenSysCtrl/Helpers/UpdateService.cs
new file mode 100644
index 0000000..fc8a9d7
--- /dev/null
+++ b/PamhagenSysCtrl/Helpers/UpdateService.cs
@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Text.Json.Nodes;
+
+namespace PamhagenSysCtrl.Helpers {
+ public sealed record UpdateInstaller(Version Version, Uri Url, long Size);
+
+ public static class UpdateService {
+
+ private static readonly HttpClient HttpClient = new() {
+ Timeout = Timeout.InfiniteTimeSpan,
+ };
+
+ public static async Task GetLatestInstallerAsync(string feedUrl, CancellationToken cancellationToken = default) {
+ using var response = await HttpClient.GetAsync(feedUrl, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ var json = JsonNode.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
+ var latest = json!["data"]!.AsArray()[^1]!;
+ return new(
+ new Version((string)latest["version"]!),
+ new Uri((string)latest["url"]!),
+ (long)latest["size"]!
+ );
+ }
+
+ public static async Task DownloadInstallerAsync(UpdateInstaller installer, string targetDirectory, IProgress? progress = null, CancellationToken cancellationToken = default) {
+ Directory.CreateDirectory(targetDirectory);
+ var fileName = Path.Combine(targetDirectory, $"PamhagenSysCtrl-{installer.Version}.msi");
+
+ try {
+ using var response = await HttpClient.GetAsync(installer.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ response.EnsureSuccessStatusCode();
+ var contentLength = response.Content.Headers.ContentLength;
+
+ await using (var destination = new FileStream(fileName, FileMode.Create)) {
+ await using var source = await response.Content.ReadAsStreamAsync(cancellationToken);
+ var buffer = new byte[81920];
+ long downloaded = 0;
+ int read;
+ while ((read = await source.ReadAsync(buffer, cancellationToken)) != 0) {
+ await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
+ downloaded += read;
+ if (contentLength.HasValue) {
+ progress?.Report((double)downloaded / contentLength.Value);
+ }
+ }
+ }
+
+ progress?.Report(1);
+ return fileName;
+ } catch {
+ File.Delete(fileName);
+ throw;
+ }
+ }
+
+ public static void StartInstaller(string fileName) {
+ var startInfo = new ProcessStartInfo {
+ FileName = "msiexec.exe",
+ UseShellExecute = true,
+ Verb = "runas",
+ };
+ startInfo.ArgumentList.Add("/i");
+ startInfo.ArgumentList.Add(Path.GetFullPath(fileName));
+
+ if (Process.Start(startInfo) == null) {
+ throw new InvalidOperationException("Der Installer konnte nicht gestartet werden.");
+ }
+ }
+
+ }
+}
diff --git a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml
index 5a6f623..0a0e86f 100644
--- a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml
+++ b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml
@@ -50,6 +50,11 @@
+
diff --git a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs
index f3200c9..0279e7c 100644
--- a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs
+++ b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs
@@ -75,6 +75,12 @@ namespace PamhagenSysCtrl.Windows {
} catch { }
}
+ private async void Menu_Help_CheckForUpdates_Click(object sender, RoutedEventArgs evt) {
+ if (Application.Current is App app) {
+ await app.CheckForUpdates(true);
+ }
+ }
+
private void SchemeCanvas_MouseMove(object sender, MouseEventArgs evt) {
var p = evt.GetPosition(SchemeCanvas);
UpdateScheme(evt.GetPosition(SchemeCanvas), evt.LeftButton == MouseButtonState.Pressed);