Class & Method Reference

The .NET 10 version consists of 14 source files split across interfaces, implementations, CLI, and a central BackgroundService.


ServiceSettings

File: ServiceSettings.cs

Simple POCO bound from appsettings.json via IOptions<ServiceSettings>. Properties:

Property Type Default Description
JenkinsURL string "" Jenkins URL with explicit port
AgentName string "" Agent name (defaults to hostname at runtime)
AgentSecret string "" JNLP secret (or reference/ciphertext depending on SecretMode)
SecretMode SecretMode Unprotected How AgentSecret is stored/resolved
JavaPath string "" Path to JDK bin folder
CustomArguments string "" Extra java.exe arguments (supports quoted values)
DebugMode bool false Verbose logging
CompactLog bool false CLEF JSON log output (agent.clef instead of agent.log)
RetainedLogs int 3 Number of rolled log files to keep (oldest permanently deleted)
MaxRetries int 0 Max recovery attempts (0 = infinite)

TelemetrySettings

File: TelemetrySettings.cs

POCO bound from appsettings.json section Telemetry. All optional — service functions normally with defaults.

Property Type Default Description
Enabled bool false Enable OpenTelemetry export
OtlpEndpoint string "" OTLP gRPC exporter endpoint (empty = export disabled even when Enabled: true)
ServiceName string "JenkinsAsService" service.name resource attribute

SecretWriter / SecretResolver / UpdateSecretCommand

Files: SecretWriter.cs, SecretResolver.cs, UpdateSecretCommand.cs, SecretMode.cs

SecretWriter.WriteConfig(basePath, secret, mode, url, agentName, javaPath)

Processes secret for the given mode (DPAPI encrypt / store env var / store CredMgr entry / leave plaintext), then writes appsettings.json. Preserves all existing top-level JSON sections (e.g. Telemetry) and existing Jenkins fields not explicitly provided (CustomArguments, DebugMode, etc.).

SecretResolver.Resolve(ServiceSettings)

Resolves the raw secret at service startup:

UpdateSecretCommand.Run(args, basePath?)

CLI subcommand (JenkinsAsService.exe update-secret). Parses args manually (no System.CommandLine dependency). Returns 0 on success, 1 on error. Supports interactive and --silent modes; --impersonate wraps the write in WindowsIdentity.RunImpersonated via P/Invoke LogonUser.

Argument parsing internals:


IJarDownloader / HttpJarDownloader

Files: IJarDownloader.cs, HttpJarDownloader.cs

IJarDownloader (Interface)

Task DownloadAsync(string jenkinsUrl, string destinationPath, CancellationToken ct);

HttpJarDownloader (Implementation)

Downloads agent.jar from $jenkinsUrl/jnlpJars/agent.jar using a named HttpClient ("JarDownloader") with Polly resilience (automatic retry, circuit breaker, timeout).

ETag caching: Stores the server's ETag response header in agent.jar.etag. On subsequent calls, sends If-None-Match — a 304 Not Modified skips the download entirely. If the jar file is missing, the etag file is ignored (preventing a 304 that would leave the caller without a jar). On 200 with no ETag header, the etag file is deleted so the next call is unconditional.


IConnectivityChecker / TcpConnectivityChecker

Files: IConnectivityChecker.cs, TcpConnectivityChecker.cs

IConnectivityChecker (Interface)

Task TestAsync(string host, int port, int timeoutMs, CancellationToken ct);

TcpConnectivityChecker (Implementation)

Creates TcpClient, connects async with configurable timeout via CancellationTokenSource.CreateLinkedTokenSource. Distinguishes timeout (OperationCanceledException) from connection refused (SocketException).


Program.cs

Top-level statements. Responsibilities:

  1. Reads config early to determine DebugMode, CompactLog, and RetainedLogs for Serilog configuration
  2. Configures Serilog:
    • Rolling file sink (10MB, configurable retention via RetainedLogs, default 3) — human-readable (agent.log) or CLEF JSON (agent.clef) based on CompactLog
    • EventLog sink (Warning+)
    • Enrichers: Pid, MachineName, EnvironmentName
  3. Builds the host with:
    • AddWindowsService
    • IOptions<ServiceSettings>
    • Named HttpClient ("JarDownloader") with AddStandardResilienceHandler()
    • IJarDownloaderHttpJarDownloader
    • IConnectivityCheckerTcpConnectivityChecker
    • JenkinsAgentWorker (hosted service)
  4. Validates mandatory fields before host.RunAsync()
  5. Global try/catch logs fatal errors via Serilog

JenkinsAgentWorker : BackgroundService

File: JenkinsAgentWorker.cs — The main service class.

Constructor

Injected dependencies: ILogger<JenkinsAgentWorker>, IOptions<ServiceSettings>, IJarDownloader, IConnectivityChecker, ISecretResolver.

Sets _basePath to AppContext.BaseDirectory. Creates Meter("JenkinsAsService", "1.0.0") and two counters (jenkins_agent_restarts_total, jenkins_agent_severe_events_total) — zero-overhead when no OTel listener is registered.

ExecuteAsync(CancellationToken)

Service entry point. Calls in sequence:

  1. ValidateSettings()ResolveJavaPath()
  2. TestConnectivityAsync()IJarDownloader.DownloadAsync()
  3. StartAgentProcess()RunWatchdogAsync()

Catches OperationCanceledException for clean shutdown. Any other exception is logged and re-thrown (stops the host).

StopAsync(CancellationToken)

Logs stop request, calls KillAgent(), logs confirmation, calls base.StopAsync().


Validation Methods

ValidateSettings(ServiceSettings) (internal static)

Testable static method. Checks JenkinsURL and AgentSecret are non-empty. Parses URL via System.Uri and rejects Uri.IsDefaultPort (requires explicit port).

ValidateSettings() (private instance)

Calls the static overload, then resolves AgentName to Environment.MachineName if empty — stored in a private _agentName field without mutating the ServiceSettings object.

ResolveJavaPath(string? configuredPath, string? javaHome) (internal static)

Testable static method. Builds candidate list: configuredPathjavaHomejavaHome\bin. Returns the first path containing java.exe. Throws if none found.


Agent Lifecycle Methods

StartAgentProcess()Process

Creates ProcessStartInfo with ArgumentList (no shell interpretation):

Hooks OutputDataReceived and ErrorDataReceived to ParseAgentOutput(). Returns the started Process.

ParseArguments(string?) (internal static)

Parses a string of arguments respecting double-quoted values:

ParseAgentOutput(string line) (internal)

Routes Java output by prefix. Redacts _resolvedSecret from the line before logging (protects against Jenkins echoing the secret on auth failure). All log calls use the shared OutputMessageTemplate constant ("{Output}") to avoid repeating the structured-logging key.


Watchdog Methods

RunWatchdogAsync(CancellationToken)

Event-driven loop using Task.WhenAny(exitTask, Task.Delay(StabilityMs)). exitTask is a TaskCompletionSource.Task completed by process.Exited. No polling.

On exit detected:

  1. Logs SEVERE count (via Interlocked.Exchange(ref _severeCount, 0))
  2. Captures exit code, calls KillAgent() to dispose
  3. Increments retry counter, checks MaxRetries
  4. Calls RecoverAgentAsync(retryCount, ct)

On stability window elapsed (agent still running):

RecoverAgentAsync(int retryCount, CancellationToken)

  1. Waits exponential backoff (10s base, 300s cap)
  2. Tests TCP connectivity — if unreachable, logs warning, resets _agentExitTcs, returns false
  3. Downloads agent.jar via IJarDownloader (ETag cached)
  4. Starts new process via StartAgentProcess()
  5. Returns true on success; caller increments _restartCounter only on true

On failure: a fresh uncompleted TaskCompletionSource is assigned so the watchdog loop waits properly instead of immediately re-entering recovery.

KillAgent()

Kills the Java process tree (Kill(entireProcessTree: true)), disposes the Process object, sets reference to null. Catches InvalidOperationException (process already exited).