Skip to content

[Code Quality] Servy ServiceCommands.InstallService rebuilds Config→DTO mapping (drifts from MainViewModel.ModelToServiceDto, different sentinel values) #1021

@Christophe-Rogiers

Description

@Christophe-Rogiers

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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions