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:
Unprotected— returnsAgentSecretas-isDpapi—ProtectedData.Unprotect(machine-scoped, same entropy as writer)EnvironmentVariable— reads machine-level env var named byAgentSecretCredentialManager— reads Credential Manager entry named byAgentSecret
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:
ParseState— private mutable accumulator class; avoids a 10-parameter method signatureTryApplyValueArg(args, ref i, state)— switch over value-args (--secret,--url,--mode, ...); CC ≤ 9TryApplyFlagArg(arg, state)— switch over boolean flags (--silent,--impersonate); CC = 3ParseArgsitself has CC = 4 (loop + two-condition if)
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:
- Reads config early to determine
DebugMode,CompactLog, andRetainedLogsfor Serilog configuration - Configures Serilog:
- Rolling file sink (10MB, configurable retention via
RetainedLogs, default 3) — human-readable (agent.log) or CLEF JSON (agent.clef) based onCompactLog - EventLog sink (Warning+)
- Enrichers:
Pid,MachineName,EnvironmentName
- Rolling file sink (10MB, configurable retention via
- Builds the host with:
AddWindowsServiceIOptions<ServiceSettings>- Named
HttpClient("JarDownloader") withAddStandardResilienceHandler() IJarDownloader→HttpJarDownloaderIConnectivityChecker→TcpConnectivityCheckerJenkinsAgentWorker(hosted service)
- Validates mandatory fields before
host.RunAsync() - 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:
ValidateSettings()→ResolveJavaPath()TestConnectivityAsync()→IJarDownloader.DownloadAsync()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: configuredPath → javaHome → javaHome\bin. Returns the first path containing java.exe. Throws if none found.
Agent Lifecycle Methods
StartAgentProcess() → Process
Creates ProcessStartInfo with ArgumentList (no shell interpretation):
-jar agent.jar -url ... -secret ... -name ... -workDir ...- Parses
CustomArgumentsviaParseArguments()(supports quoted values with spaces)
Hooks OutputDataReceived and ErrorDataReceived to ParseAgentOutput(). Returns the started Process.
ParseArguments(string?) (internal static)
Parses a string of arguments respecting double-quoted values:
"-Dpath=C:\Program Files\Java"→ single argument with spaces preserved- Escaped quotes inside quoted arguments:
\"→ literal" - Unquoted tokens split on whitespace
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.
INFO:→LogInformationWARNING:→LogWarningSEVERE:→LogError+Interlocked.Increment(ref _severeCount)+_severeEventCounter.Add(1)- Other →
LogInformation(orLogDebugwhenDebugModeis on)
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:
- Logs SEVERE count (via
Interlocked.Exchange(ref _severeCount, 0)) - Captures exit code, calls
KillAgent()to dispose - Increments retry counter, checks
MaxRetries - Calls
RecoverAgentAsync(retryCount, ct)
On stability window elapsed (agent still running):
- If
retryCount > 0: resets to 0 and logs stability message
RecoverAgentAsync(int retryCount, CancellationToken)
- Waits exponential backoff (10s base, 300s cap)
- Tests TCP connectivity — if unreachable, logs warning, resets
_agentExitTcs, returnsfalse - Downloads
agent.jarviaIJarDownloader(ETag cached) - Starts new process via
StartAgentProcess() - Returns
trueon success; caller increments_restartCounteronly ontrue
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).