Severity: Warning
File: src/Servy/Services/ServiceCommands.cs lines 109–173 vs src/Servy/ViewModels/MainViewModel.cs lines 1463–1528
Finding
MainViewModel.ModelToServiceDto() is registered as _modelToServiceDto in the ServiceCommands constructor (line 81 of ServiceCommands.cs, supplied via the parameterless ctor wiring at MainViewModel.cs line 953). ExportConfigAsync correctly uses it (line 549). But InstallService (line 98) does not — it rebuilds the same ServiceConfiguration → ServiceDto mapping inline, ~65 lines.
The two mappings have drifted:
| Field |
MainViewModel.ModelToServiceDto (line 1479+) |
ServiceCommands.InstallService (line 123+) |
RotationSize |
int.TryParse(...) ? rs : DefaultRotationSize |
int.TryParse(...) ? rs : -1 |
MaxRotations |
int.TryParse(...) ? mrs : DefaultMaxRotations |
int.TryParse(...) ? mrn : -1 |
HeartbeatInterval |
int.TryParse(...) ? hi : DefaultHeartbeatInterval |
int.TryParse(...) ? hi : -1 |
MaxFailedChecks |
... DefaultMaxFailedChecks |
... -1 |
MaxRestartAttempts |
... DefaultMaxRestartAttempts |
... -1 |
PreLaunchTimeoutSeconds |
... DefaultPreLaunchTimeoutSeconds |
... -1 |
PreLaunchRetryAttempts |
... DefaultPreLaunchRetryAttempts |
... -1 |
StartTimeout |
... DefaultStartTimeout |
... -1 |
StopTimeout |
... DefaultStopTimeout |
... -1 |
PreStopTimeoutSeconds |
... DefaultPreStopTimeoutSeconds |
... -1 |
UserAccount / Password |
direct copy |
RunAsLocalSystem ? null : config.UserAccount (extra logic) |
So a user who clears the RotationSize text field will get:
0 MB rotation in DTO if exporting (export → ModelToServiceDto → DefaultRotationSize=10)
-1 MB (sentinel) rotation in DTO if installing (install → inline mapper → -1)
The -1 sentinel then takes a different code path inside InstallServiceAsync, producing inconsistent behavior between Export-then-Install and direct Install. The duplication is also a maintenance hazard: any new field added to ServiceConfiguration requires touching both blocks (and the recently-added EnableConsoleUI, EnableDebugLogs, EnableHealthMonitoring already have at least one inconsistency between the two).
Suggested fix
ServiceCommands.InstallService should call _modelToServiceDto() to obtain the canonical DTO, then attach only the install-specific extras (e.g. Description ?? string.Empty, RunAsLocalSystem ? null : UserAccount masking):
public async Task<bool> InstallService(ServiceConfiguration config, CancellationToken cancellationToken = default)
{
var wrapperExePath = AppConfig.GetServyUIServicePath();
if (!File.Exists(wrapperExePath))
{
await _messageBoxService.ShowErrorAsync(Strings.Msg_InvalidWrapperExePath, Caption);
return false;
}
var dto = _modelToServiceDto();
if (dto == null) return false;
// Install-specific masking: never persist password if running as LocalSystem
if (config.RunAsLocalSystem)
{
dto.UserAccount = null;
dto.Password = null;
}
// ... rest of method
}
This collapses the two mappings into one and removes the sentinel-vs-default divergence.
Severity: Warning
File:
src/Servy/Services/ServiceCommands.cslines 109–173 vssrc/Servy/ViewModels/MainViewModel.cslines 1463–1528Finding
MainViewModel.ModelToServiceDto()is registered as_modelToServiceDtoin theServiceCommandsconstructor (line 81 of ServiceCommands.cs, supplied via the parameterless ctor wiring at MainViewModel.cs line 953).ExportConfigAsynccorrectly uses it (line 549). ButInstallService(line 98) does not — it rebuilds the sameServiceConfiguration → ServiceDtomapping inline, ~65 lines.The two mappings have drifted:
MainViewModel.ModelToServiceDto(line 1479+)ServiceCommands.InstallService(line 123+)RotationSizeint.TryParse(...) ? rs : DefaultRotationSizeint.TryParse(...) ? rs : -1MaxRotationsint.TryParse(...) ? mrs : DefaultMaxRotationsint.TryParse(...) ? mrn : -1HeartbeatIntervalint.TryParse(...) ? hi : DefaultHeartbeatIntervalint.TryParse(...) ? hi : -1MaxFailedChecks... DefaultMaxFailedChecks... -1MaxRestartAttempts... DefaultMaxRestartAttempts... -1PreLaunchTimeoutSeconds... DefaultPreLaunchTimeoutSeconds... -1PreLaunchRetryAttempts... DefaultPreLaunchRetryAttempts... -1StartTimeout... DefaultStartTimeout... -1StopTimeout... DefaultStopTimeout... -1PreStopTimeoutSeconds... DefaultPreStopTimeoutSeconds... -1UserAccount/PasswordRunAsLocalSystem ? null : config.UserAccount(extra logic)So a user who clears the
RotationSizetext field will get:0 MBrotation in DTO if exporting (export → ModelToServiceDto → DefaultRotationSize=10)-1 MB(sentinel) rotation in DTO if installing (install → inline mapper → -1)The
-1sentinel then takes a different code path insideInstallServiceAsync, producing inconsistent behavior between Export-then-Install and direct Install. The duplication is also a maintenance hazard: any new field added toServiceConfigurationrequires touching both blocks (and the recently-addedEnableConsoleUI,EnableDebugLogs,EnableHealthMonitoringalready have at least one inconsistency between the two).Suggested fix
ServiceCommands.InstallServiceshould call_modelToServiceDto()to obtain the canonical DTO, then attach only the install-specific extras (e.g.Description ?? string.Empty,RunAsLocalSystem ? null : UserAccountmasking):This collapses the two mappings into one and removes the sentinel-vs-default divergence.