Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8689b1f
gracefully handle the configuration file error
soujay Aug 1, 2026
d9baf1e
refresh existing instances
soujay Aug 1, 2026
4687b13
remove temp logging code
soujay Aug 2, 2026
184978e
remove logging of xml config error
soujay Aug 2, 2026
757df2d
remove unsed log code
soujay Aug 2, 2026
53ce8b4
Add spec and executable spec tests for corrupt instance configuration…
Aug 4, 2026
860b773
Rule 1: never silently skip an instance that fails to load
Aug 5, 2026
d3ef0d6
Rule 1: protect monitoring instances from corrupt configs like the ot…
Aug 5, 2026
f536887
Rule 2: error banners name the config file that needs fixing
Aug 5, 2026
f027f58
Rule 4: refresh rejects an update from an instance of a different type
Aug 5, 2026
34457fb
Rule 5: make the list-level banner logic testable via an instances-so…
Aug 5, 2026
d0a078f
Rule 5: show the configuration error summary banner above the instanc…
Aug 5, 2026
4a009f4
Rule 4: cover both refresh directions through the real UI flows
Aug 5, 2026
ee3bc3c
Add manual test plan covering what the executable spec cannot reach
Aug 5, 2026
27acee8
Add vocabulary section to the spec
Aug 5, 2026
0033fa1
notifyproperty change to HasConfigurationErrors and configurationerro…
soujay Aug 5, 2026
d68ba08
fix test build error
soujay Aug 5, 2026
3c9effc
refresh after adding and removing instances when there are other corr…
soujay Aug 6, 2026
1ce97b6
Text wrap teh configuration error message
soujay Aug 6, 2026
9d1b839
add copy error message functionality
soujay Aug 6, 2026
5e913ce
Change to copy icon and position to top right
soujay Aug 6, 2026
9ea0228
auto refresh while adding and removing instances.
soujay Aug 6, 2026
bb9786b
remove licnese check while adding monitoring instance
soujay Aug 6, 2026
e5eaef1
Revert "remove licnese check while adding monitoring instance"
soujay Aug 6, 2026
aed34df
Update src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsVi…
soujay Aug 7, 2026
2da682d
Update src/ServiceControl.Config/UI/InstanceDetails/InstanceDetailsVi…
soujay Aug 7, 2026
92337ea
Clean up spec files
npink42 Aug 7, 2026
5a8f7b6
Merge remote-tracking branch 'origin/scmu-corrupt-xml-config' into sc…
npink42 Aug 7, 2026
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace ServiceControl.Config.Commands
{
using System;
using System.Threading.Tasks;
using Caliburn.Micro;
using Events;
using Framework;
using Framework.Commands;
using ServiceControlInstaller.Engine.Instances;
Expand All @@ -10,20 +12,22 @@ namespace ServiceControl.Config.Commands

class AdvancedMonitoringOptionsCommand : AwaitableAbstractCommand<InstanceDetailsViewModel>
{
public AdvancedMonitoringOptionsCommand(IServiceControlWindowManager windowManager, Func<BaseService, MonitoringAdvancedViewModel> advancedOptionsModel)
public AdvancedMonitoringOptionsCommand(IServiceControlWindowManager windowManager, IEventAggregator eventAggregator, Func<BaseService, MonitoringAdvancedViewModel> advancedOptionsModel)
{
this.windowManager = windowManager;
this.eventAggregator = eventAggregator;
this.advancedOptionsModel = advancedOptionsModel;
}

public override Task ExecuteAsync(InstanceDetailsViewModel viewModel)
public override async Task ExecuteAsync(InstanceDetailsViewModel viewModel)
{
var screen = advancedOptionsModel(viewModel.MonitoringInstance);

return windowManager.ShowInnerDialog(screen);
await windowManager.ShowInnerDialog(screen);
await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances());
}

readonly Func<BaseService, MonitoringAdvancedViewModel> advancedOptionsModel;
readonly IEventAggregator eventAggregator;
readonly IServiceControlWindowManager windowManager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace ServiceControl.Config.Commands
{
using System;
using System.Threading.Tasks;
using Caliburn.Micro;
using Events;
using Framework;
using Framework.Commands;
using ServiceControlInstaller.Engine.Instances;
Expand All @@ -10,16 +12,18 @@ namespace ServiceControl.Config.Commands

class AdvancedServiceControlOptionsCommand : AwaitableAbstractCommand<InstanceDetailsViewModel>
{
public AdvancedServiceControlOptionsCommand(IServiceControlWindowManager windowManager, Func<BaseService, ServiceControlAdvancedViewModel> advancedOptionsModel)
public AdvancedServiceControlOptionsCommand(IServiceControlWindowManager windowManager, IEventAggregator eventAggregator, Func<BaseService, ServiceControlAdvancedViewModel> advancedOptionsModel)
{
this.windowManager = windowManager;
this.eventAggregator = eventAggregator;
this.advancedOptionsModel = advancedOptionsModel;
}

public override Task ExecuteAsync(InstanceDetailsViewModel viewModel)
public override async Task ExecuteAsync(InstanceDetailsViewModel viewModel)
{
var screen = CreateAdvancedScreen(viewModel);
return windowManager.ShowInnerDialog(screen);
await windowManager.ShowInnerDialog(screen);
await eventAggregator.PublishOnUIThreadAsync(new RefreshInstances());
}

ServiceControlAdvancedViewModel CreateAdvancedScreen(InstanceDetailsViewModel viewModel)
Expand All @@ -34,6 +38,7 @@ ServiceControlAdvancedViewModel CreateAdvancedScreen(InstanceDetailsViewModel vi
}

readonly Func<BaseService, ServiceControlAdvancedViewModel> advancedOptionsModel;
readonly IEventAggregator eventAggregator;
readonly IServiceControlWindowManager windowManager;
}
}
24 changes: 24 additions & 0 deletions src/ServiceControl.Config/Styles/Icons.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@
</Viewbox>
</ControlTemplate>

<ControlTemplate x:Key="CopyIcon">
<Viewbox Stretch="Uniform">
<Canvas Width="16" Height="16">
<!-- back page -->
<Rectangle Canvas.Left="4" Canvas.Top="2" Width="9" Height="11"
Fill="{TemplateBinding Foreground}" />
<Rectangle Canvas.Left="5" Canvas.Top="3" Width="7" Height="9"
Fill="White" />
<!-- front page -->
<Rectangle Canvas.Left="2" Canvas.Top="4" Width="9" Height="11"
Fill="{TemplateBinding Foreground}" />
<Rectangle Canvas.Left="3" Canvas.Top="5" Width="7" Height="9"
Fill="White" />
<!-- lines on front page -->
<Rectangle Canvas.Left="4" Canvas.Top="7" Width="5" Height="1"
Fill="{TemplateBinding Foreground}" />
<Rectangle Canvas.Left="4" Canvas.Top="9" Width="5" Height="1"
Fill="{TemplateBinding Foreground}" />
<Rectangle Canvas.Left="4" Canvas.Top="11" Width="3" Height="1"
Fill="{TemplateBinding Foreground}" />
</Canvas>
</Viewbox>
</ControlTemplate>

<ControlTemplate x:Key="RemoveIcon">
<Viewbox Stretch="Uniform">
<Canvas Width="24" Height="24">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,53 @@
Text="{Binding InstanceTypeDisplayName}" />

</StackPanel>

<!-- Configuration Error Display -->
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Margin="0 0 0 10"
Visibility="{Binding HasConfigurationError, Converter={StaticResource boolToVis}}">
<Border Background="#FFF4E5" BorderBrush="#FFA500" BorderThickness="1" Padding="10" CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="⚠" FontSize="16" Foreground="#FFA500" VerticalAlignment="Top" Margin="0,0,8,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="CONFIGURATION ERROR"
FontWeight="Bold"
Foreground="#D84315"
FontSize="13"
Margin="0,0,0,5"/>
<TextBlock Text="{Binding ConfigurationErrorMessage}"
TextWrapping="Wrap"
Foreground="#5D4037"
FontSize="12"/>
</StackPanel>
<Button Grid.Column="2" Padding="0" Background="Transparent" BorderThickness="0"
VerticalAlignment="Top" Margin="8,0,0,0"
Command="{Binding CopyToClipboard}" CommandParameter="{Binding ConfigurationErrorMessage}"
ToolTip="Copy error message" Cursor="Hand">
<ContentControl Template="{StaticResource CopyIcon}" Foreground="#5D4037" Width="14" Height="14" />
</Button>
</Grid>
</Border>
</StackPanel>

<StackPanel Grid.Row="2" Grid.Column="1" Margin="0 0 0 10">
<StackPanel Orientation="Horizontal">
<!-- Show warning icon for configuration errors -->
<TextBlock Margin="0,0,3,0"
VerticalAlignment="Center"
FontSize="16px"
Foreground="#FFA500"
Text="⚠"
Visibility="{Binding HasConfigurationError, Converter={StaticResource boolToVis}}" />

Comment thread
soujay marked this conversation as resolved.
<ContentControl Margin="0,0,0,0"
VerticalAlignment="Center"
Template="{StaticResource RunningIcon}"
Visibility="{Binding IsRunning,
Converter={StaticResource boolToVis}}" />
VerticalAlignment="Center"
Template="{StaticResource RunningIcon}"
Visibility="{Binding IsRunning, Converter={StaticResource boolToVis}}" />

<ContentControl Margin="0,0,0,0"
VerticalAlignment="Center"
Expand All @@ -75,8 +115,19 @@
<TextBlock Margin="3,0,0,0"
VerticalAlignment="Center"
FontSize="13px"
Foreground="{StaticResource Gray40Brush}"
Text="{Binding Status}" />
Text="{Binding Status}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource Gray40Brush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasConfigurationError}" Value="True">
<Setter Property="Foreground" Value="#D84315" />
<Setter Property="FontWeight" Value="Bold" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>

<TextBlock Margin="3,0,0,0"
VerticalAlignment="Center"
Expand Down Expand Up @@ -107,12 +158,14 @@
Command="{Binding EditCommand}"
CommandParameter="{Binding DataContext,
ElementName=root}"
Style="{StaticResource ConfigurationButton}" />
Style="{StaticResource ConfigurationButton}"
Visibility="{Binding AllowEdit, Converter={StaticResource boolToVis}}" />
<Button Margin="5,0"
Command="{Binding AdvancedOptionsCommand}"
CommandParameter="{Binding DataContext,
ElementName=root}"
Style="{StaticResource AdvancedOptionsButton}" />
Style="{StaticResource AdvancedOptionsButton}"
Visibility="{Binding AllowEdit, Converter={StaticResource boolToVis}}" />
</StackPanel>

<GroupBox Grid.Row="4" Header="VERSION" Margin="0 5 0 0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public InstanceDetailsViewModel(
StartCommand = Command.Create(() => StartService());
StopCommand = Command.Create(() => StopService());

ServiceInstance = instance;

if (instance.GetType() == typeof(ServiceControlInstance))
{
ServiceControlInstance = (ServiceControlInstance)instance;
Expand Down Expand Up @@ -72,7 +70,37 @@ public InstanceDetailsViewModel(
throw new Exception("Unknown instance type");
}

public BaseService ServiceInstance { get; }
public void UpdateServiceInstance(BaseService updatedInstance)
{
if (updatedInstance.Name != ServiceInstance.Name)
{
throw new ArgumentException("Cannot update with an instance of a different name");
}

if (updatedInstance.GetType() != ServiceInstance.GetType())
{
throw new ArgumentException("Cannot update with an instance of a different type");
}

// Update the internal reference based on type
if (updatedInstance.GetType() == typeof(ServiceControlInstance))
{
ServiceControlInstance = (ServiceControlInstance)updatedInstance;
}
else if (updatedInstance.GetType() == typeof(MonitoringInstance))
{
MonitoringInstance = (MonitoringInstance)updatedInstance;
}
else if (updatedInstance.GetType() == typeof(ServiceControlAuditInstance))
{
ServiceControlAuditInstance = (ServiceControlAuditInstance)updatedInstance;
}
}

public BaseService ServiceInstance =>
(BaseService)ServiceControlInstance ??
(BaseService)MonitoringInstance ??
ServiceControlAuditInstance;

public bool InMaintenanceMode =>
ServiceControlInstance?.InMaintenanceMode == true ||
Expand Down Expand Up @@ -165,20 +193,25 @@ string GetDBPathIfAvailable()

public bool HasNewVersion => Version < NewVersion;

public TransportInfo Transport => ((ITransportConfig)ServiceInstance).TransportPackage;
public TransportInfo Transport => HasConfigurationError ? null : ((ITransportConfig)ServiceInstance).TransportPackage;

public string Persister
{
get
{
if (HasConfigurationError)
{
return string.Empty; // Leave blank for corrupt instances
}

if (ServiceInstance is IServiceControlInstance primaryInstance)
{
return primaryInstance.PersistenceManifest.DisplayName;
return primaryInstance.PersistenceManifest?.DisplayName ?? "Unknown";
}

if (ServiceInstance is IServiceControlAuditInstance auditInstance)
{
return auditInstance.PersistenceManifest.DisplayName;
return auditInstance.PersistenceManifest?.DisplayName ?? "Unknown";
}

if (ServiceInstance is IMonitoringInstance)
Expand All @@ -194,6 +227,12 @@ public string Status
{
get
{
// If there's a configuration error, show that instead of service status
if (HasConfigurationError)
{
return "ERROR";
}

try
{
return ServiceInstance.Service.Status.ToString().ToUpperInvariant();
Expand All @@ -209,6 +248,12 @@ public bool IsRunning
{
get
{
// If there's a configuration error, don't show running icon
if (HasConfigurationError)
{
return false;
}

try
{
return ServiceInstance.Service.Status != ServiceControllerStatus.Stopped;
Expand All @@ -224,6 +269,12 @@ public bool IsStopped
{
get
{
// If there's a configuration error, don't show stopped icon either
if (HasConfigurationError)
{
return false;
}

try
{
return ServiceInstance.Service.Status == ServiceControllerStatus.Stopped;
Expand All @@ -239,6 +290,12 @@ public bool AllowStart
{
get
{
// Don't allow start for instances with configuration errors
if (HasConfigurationError)
{
return false;
}

try
{
var dontAllowStartOn = new[]
Expand All @@ -260,6 +317,12 @@ public bool AllowStop
{
get
{
// Don't allow stop for instances with configuration errors
if (HasConfigurationError)
{
return false;
}

try
{
var dontAllowStopOn = new[]
Expand All @@ -277,6 +340,12 @@ public bool AllowStop
}
}

public bool HasConfigurationError => !string.IsNullOrEmpty(ServiceInstance?.ConfigurationLoadError);

public bool AllowEdit => !HasConfigurationError; // Disable edit for corrupt instances

public string ConfigurationErrorMessage => ServiceInstance?.ConfigurationLoadError;

public ICommand OpenUrl { get; private set; }

public ICommand CopyToClipboard { get; private set; }
Expand Down Expand Up @@ -308,6 +377,9 @@ public Task HandleAsync(PostRefreshInstances message, CancellationToken cancella
NotifyOfPropertyChange("Transport");
NotifyOfPropertyChange("BrowsableUrl");
NotifyOfPropertyChange("UrlHeading");
NotifyOfPropertyChange(nameof(HasConfigurationError));
NotifyOfPropertyChange(nameof(AllowEdit));
NotifyOfPropertyChange(nameof(ConfigurationErrorMessage));
return Task.CompletedTask;
}

Expand Down Expand Up @@ -376,7 +448,20 @@ await Task.Run(() =>

void UpdateServiceProperties()
{
ServiceInstance.Reload();
try
{
ServiceInstance.Reload();
}
catch (Exception ex)
{
// Handle reload failure gracefully - configuration error will be shown in UI
ServiceInstance.ConfigurationLoadError = $"Failed to load configuration file '{ServiceInstance.ConfigurationFilePath}': {ex.Message}";
// Ensure basic properties are set so UI can still display the instance
if (string.IsNullOrEmpty(ServiceInstance.InstanceName))
{
ServiceInstance.InstanceName = ServiceInstance.Name;
}
}

NotifyOfPropertyChange("Status");
NotifyOfPropertyChange("AllowStop");
Expand Down
Loading