Skip to content
Merged
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ Most AV kit ships set to DHCP, and on a bench or a fresh site there's usually no
2. Double-click it. Windows asks for administrator rights, because changing adapter settings and serving DHCP require them.
3. The exe isn't code-signed yet, so the first time Windows SmartScreen says "Windows protected your PC". Click **More info**, then **Run anyway**.

### Updates

When the laptop has internet, ProDHCPServer checks GitHub for a newer release a few seconds after it starts. If there is one, a green banner offers **Install and restart**: it downloads the new exe, checks it against the SHA-256 published with the release, swaps it in place of the old one and restarts. The server has to be stopped first. Nothing is changed if any step fails, and the previous exe is deleted the next time the app starts.

On an isolated AV network the check simply doesn't happen. Turn the start-up check off, or check by hand, under Settings > Updates. On a work laptop that only runs approved programs, Windows may block the new version from starting; the app then puts the old one back and says so.

## Using it

1. Connect the device to your laptop's Ethernet port (directly or through a small switch) and power it on.
Expand Down Expand Up @@ -104,9 +110,10 @@ Each phase ends with a hardware test pass on real AV equipment before the next o

| What | Where |
|---|---|
| Your settings (last adapter, profile in use, look and theme) | `%LOCALAPPDATA%\ProDHCPServer\settings.json` |
| Your settings (last adapter, profile in use, look and theme, update check) | `%LOCALAPPDATA%\ProDHCPServer\settings.json` |
| Your profiles | `%LOCALAPPDATA%\ProDHCPServer\profiles.json` |
| Adapter backup (only while a session runs, or after a crash) | `%ProgramData%\ProDHCPServer\adapter-backup.json` |
| The previous version, for a moment after an update | `ProDHCPServer.exe.old` next to the exe, deleted on the next start |

While serving, the app also adds two Windows Firewall rules for this exe only: one so it can receive DHCP traffic (UDP 67 and 68), and one on wired adapters so it can hear devices announcing themselves (mDNS, UPnP, vendor discovery). Both are removed on Stop.

Expand Down
47 changes: 47 additions & 0 deletions docs/hardware-test-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Update checker test

About 15 minutes on the unmanaged laptop, with internet. No AV devices needed.

The updater can only update to a release that's already published on GitHub, so this test updates a test build to the newest published release (v1.1.0). v1.1.0 doesn't have the updater itself, so after the update it leaves `ProDHCPServer.exe.old` behind; delete it by hand at the end.

Download `ProDHCPServer.exe` from this PR's latest **Build and test** run (Actions tab → run → Artifacts → `ProDHCPServer-win-x64`) and put it in a folder of its own, for example `C:\Tools\ProDHCPServer\`. Click **More info → Run anyway** on the SmartScreen warning.

Tick each box. If something fails, use **Export log…** and send me the file.

## 1. Test builds don't nag

- [ ] Start the app with internet. After 10 seconds there is **no** green update banner (test builds only check when asked).
- [ ] Open Settings (gear, or Ctrl+,) and scroll to **Updates**. It says "This is test build 0.1.… It doesn't check for updates by itself; use Check now."

## 2. Offline

- [ ] Unplug or turn off the internet connection (Wi-Fi off, no network cable to the internet).
- [ ] Press **Check now**. Within about 10 seconds it says "Couldn't reach GitHub to check for updates. That's normal on an isolated AV network…". No error banner, no popup.
- [ ] The app otherwise works as normal: **Start** and **Stop** a session on a device or an empty port.

## 3. Check and skip

- [ ] Reconnect the internet. Press **Check now**. It says "Version 1.1.0 is available. You have test build 0.1.…", and the green banner appears at the top of the main window with **Install and restart**, **What's new**, **Not now** and **Skip this version**.
- [ ] **What's new** opens the release page in your normal browser.
- [ ] **Not now** hides the banner.

## 4. Can't install while serving

- [ ] Press **Check now** again so the banner is back, then **Start** the server.
- [ ] **Install and restart** is greyed out and the banner says "Stop the server first: installing restarts ProDHCPServer."
- [ ] Press **Stop**. **Install and restart** becomes clickable.

## 5. Install

- [ ] Press **Install and restart**. The banner shows "Downloading 1.1.0… nn%" with a progress bar, then the window closes and ProDHCPServer opens again by itself within a few seconds, with **no** UAC prompt.
- [ ] The log's first line says "ProDHCPServer 1.1.0 started (administrator)."
- [ ] In the folder, `ProDHCPServer.exe` is the new version (right-click → Properties → Details → Product version 1.1.0) and `ProDHCPServer.exe.old` is the test build. Close the app and delete the `.old` file.

## Later, once a release with the updater is out

When the release after this one is published, run the earlier release with the updater and check:

- [ ] A few seconds after start, the green banner appears without pressing anything.
- [ ] **Skip this version**, then restart the app: no banner. **Check now** in Settings still offers it.
- [ ] Untick **Check for a new version when ProDHCPServer starts**, restart: no check in the log.
- [ ] After **Install and restart**, the log says "started (administrator) after updating." and `ProDHCPServer.exe.old` is gone within a few seconds.
30 changes: 28 additions & 2 deletions src/ProDHCPServer.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ protected override void OnStartup(StartupEventArgs e)
return;
}

_singleInstance = new Mutex(true, @"Global\ProDHCPServer.SingleInstance", out bool first);
_singleInstance = new Mutex(true, SingleInstanceName, out bool first);
// Started by an update: the previous version is still closing, so give it time to let go.
bool updated = e.Args.Contains(UpdateViewModel.UpdatedFlag);
if (!first && updated) first = WaitForPreviousVersion(_singleInstance);
if (!first)
{
MessageBox.Show("ProDHCPServer is already running. Two copies would fight over the adapter, so this one will close.",
Expand All @@ -52,7 +55,9 @@ protected override void OnStartup(StartupEventArgs e)
}

_log = new SessionLog();
_log.Add(LogSeverity.Info, LogCategory.App, $"ProDHCPServer {Version} started (administrator).");
_log.Add(LogSeverity.Info, LogCategory.App, updated
? $"ProDHCPServer {Version} started (administrator) after updating."
: $"ProDHCPServer {Version} started (administrator).");
var adapters = new WindowsAdapterControl();
var sessions = new AdapterSessionManager(adapters, new FileSessionStore(Path.Combine(DataFolder, "adapter-backup.json")));
_controller = new ServerController(adapters, sessions, _log, Version);
Expand All @@ -78,6 +83,27 @@ protected override void OnStartup(StartupEventArgs e)
window.Show();
}

private const string SingleInstanceName = @"Global\ProDHCPServer.SingleInstance";

/// <summary>
/// Lets go of the one-copy lock just before an update starts the new version, so it can start even if
/// it's an older build without <see cref="WaitForPreviousVersion"/>. <see cref="RetakeSingleInstance"/> undoes it.
/// </summary>
public void ReleaseSingleInstance()
{
_singleInstance?.ReleaseMutex();
_singleInstance?.Dispose();
_singleInstance = null;
}

public void RetakeSingleInstance() => _singleInstance ??= new Mutex(true, SingleInstanceName, out _);

private static bool WaitForPreviousVersion(Mutex mutex)
{
try { return mutex.WaitOne(TimeSpan.FromSeconds(30)); }
catch (AbandonedMutexException) { return true; } // it exited without releasing: the mutex is ours now
}

private void OnUiException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
// Keep running: show the error rather than vanish with the adapter half-configured.
Expand Down
51 changes: 51 additions & 0 deletions src/ProDHCPServer.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,35 @@

<!-- ===== Banners ===== -->
<StackPanel Grid.Row="2">
<!-- A newer release is on GitHub -->
<Border Style="{DynamicResource Banner}" Background="{DynamicResource GoodSoft}" DataContext="{Binding Update}"
Visibility="{Binding BannerText, Converter={StaticResource VisibleWhen}}">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<Ellipse Width="16" Height="16" Fill="{DynamicResource GoodBrush}" VerticalAlignment="Center" Margin="0,0,16,0" />
<TextBlock Grid.Column="1" Text="{Binding BannerText}" FontSize="{StaticResource TextBody}" Style="{StaticResource Primary}" VerticalAlignment="Center"
TextWrapping="Wrap" AutomationProperties.LiveSetting="Polite" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="24,0,0,0" VerticalAlignment="Center">
<Button Content="Install and restart" Style="{DynamicResource AccentButton}" Command="{Binding InstallCommand}"
ToolTip="Downloads the new version from GitHub, checks it, and restarts ProDHCPServer" />
<Button Content="What's new" Style="{DynamicResource SecondaryButton}" Margin="8,0,0,0" Command="{Binding OpenReleaseCommand}"
ToolTip="Opens the release page on GitHub" />
<Button Content="Not now" Style="{DynamicResource SecondaryButton}" Margin="8,0,0,0" Command="{Binding NotNowCommand}" />
<Button Content="Skip this version" Style="{DynamicResource SecondaryButton}" Margin="8,0,0,0" Command="{Binding SkipCommand}" />
</StackPanel>
</Grid>
<TextBlock Text="{Binding InstallHint}" Margin="32,8,0,0" FontSize="{StaticResource TextSmall}" Foreground="{DynamicResource TextSecondary}" TextWrapping="Wrap"
Visibility="{Binding InstallHint, Converter={StaticResource VisibleWhen}}" />
<StackPanel Margin="32,8,0,0" Visibility="{Binding IsInstalling, Converter={StaticResource VisibleWhen}}">
<TextBlock Text="{Binding Status}" FontSize="{StaticResource TextSmall}" Foreground="{DynamicResource TextSecondary}" AutomationProperties.LiveSetting="Polite" />
<ProgressBar Value="{Binding Progress}" Maximum="100" Height="4" Margin="0,4,0,0" />
</StackPanel>
<TextBlock Text="{Binding InstallError}" Margin="32,8,0,0" FontSize="{StaticResource TextBody}" Style="{StaticResource Primary}" TextWrapping="Wrap"
Visibility="{Binding InstallError, Converter={StaticResource VisibleWhen}}" AutomationProperties.LiveSetting="Polite" />
</StackPanel>
</Border>

<!-- Crash recovery -->
<Border Style="{DynamicResource Banner}" Background="{DynamicResource WarnSoft}"
Visibility="{Binding PendingRecovery, Converter={StaticResource VisibleWhen}}">
Expand Down Expand Up @@ -754,6 +783,28 @@
<Label Content="Dark or l_ight" Target="{Binding ElementName=ThemeBox}" Style="{StaticResource FieldLabel}" />
<ComboBox x:Name="ThemeBox" ItemsSource="{x:Static vm:SettingsViewModel.Themes}" SelectedItem="{Binding Theme}" FontSize="{StaticResource TextBody}" HorizontalAlignment="Stretch" />
<TextBlock Text="System follows Windows. Both apply straight away, for every profile." Style="{StaticResource FieldHelp}" />

<!-- Updates: checked against the latest GitHub release -->
<StackPanel DataContext="{Binding DataContext.Update, RelativeSource={RelativeSource AncestorType=Window}}">
<TextBlock Text="Updates" FontFamily="{DynamicResource DisplayFont}" FontSize="{StaticResource TextBody}" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" Margin="0,32,0,8" />
<CheckBox IsChecked="{Binding CheckOnStart}">
<TextBlock Text="Check for a new version when ProDHCPServer starts" FontSize="{StaticResource TextBody}" Style="{StaticResource Primary}" TextWrapping="Wrap" />
</CheckBox>
<TextBlock Text="Needs internet. On an isolated AV network the check just doesn't happen; nothing else is affected." Style="{StaticResource FieldHelp}" />
<TextBlock Text="{Binding Status}" FontSize="{StaticResource TextBody}" Style="{StaticResource Primary}" TextWrapping="Wrap" Margin="0,16,0,0"
AutomationProperties.LiveSetting="Polite" />
<ProgressBar Value="{Binding Progress}" Maximum="100" Height="4" Margin="0,8,0,0"
Visibility="{Binding IsInstalling, Converter={StaticResource VisibleWhen}}" />
<TextBlock Text="{Binding InstallHint}" Style="{StaticResource FieldHelp}" Visibility="{Binding InstallHint, Converter={StaticResource VisibleWhen}}" />
<WrapPanel Margin="0,12,0,0">
<Button Content="Check now" Style="{DynamicResource SecondaryButton}" Margin="0,0,8,8" Command="{Binding CheckNowCommand}" />
<Button Content="Install and restart" Style="{DynamicResource AccentButton}" Margin="0,0,8,8" Command="{Binding InstallCommand}"
Visibility="{Binding Available, Converter={StaticResource VisibleWhen}}" />
<Button Content="Release page" Style="{DynamicResource SecondaryButton}" Margin="0,0,8,8" Command="{Binding OpenReleaseCommand}"
Visibility="{Binding Available, Converter={StaticResource VisibleWhen}}" />
</WrapPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</DockPanel>
Expand Down
8 changes: 8 additions & 0 deletions src/ProDHCPServer.App/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public MainViewModel(IAdapterControl adapters, AdapterSessionManager sessions, S
SaveSettings();
ApplyTheme();
};
Update = new UpdateViewModel(App.Version, log, () => _settings, updated =>
{
_settings = updated;
SaveSettings();
}, OpenUrl);
Steps = new ObservableCollection<StepViewModel>(Enum.GetValues<StepId>().Select(id => new StepViewModel(id)));

RebuildLog();
Expand All @@ -89,10 +94,12 @@ public MainViewModel(IAdapterControl adapters, AdapterSessionManager sessions, S
_timer.Tick += async (_, _) => await RefreshAsync();
_timer.Start();
_ = RefreshAsync();
_ = Update.CheckOnStartupAsync();
}

public SessionLog Log { get; }
public SettingsViewModel Settings { get; }
public UpdateViewModel Update { get; }
public ObservableCollection<AdapterItemViewModel> Adapters { get; } = [];
public ObservableCollection<StepViewModel> Steps { get; }
public ObservableCollection<DeviceViewModel> Devices { get; } = [];
Expand Down Expand Up @@ -124,6 +131,7 @@ public MainViewModel(IAdapterControl adapters, AdapterSessionManager sessions, S
partial void OnIsRunningChanged(bool value)
{
Settings.IsLocked = value;
Update.ServerRunning = value;
SweepCommand.NotifyCanExecuteChanged();
}

Expand Down
Loading
Loading