/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using Polly; using Polly.Retry; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.OperatingSystemLogs; using ProtonVPN.OperatingSystems.Processes.Contracts; using ProtonVPN.OperatingSystems.Services.Contracts; using static Vanara.PInvoke.AdvApi32; namespace ProtonVPN.OperatingSystems.Services; public class Service : IService { private const int RETRY_COUNT = 2; private const int RETRY_DELAY_IN_SECONDS = 1; private const int ERROR_SERVICE_ALREADY_RUNNING = 1056; private const int ERROR_SERVICE_NOT_ACTIVE = 1062; private const int ERROR_SERVICE_DOES_NOT_EXIST = 1060; private const int ERROR_INSUFFICIENT_BUFFER = 122; private const uint SERVICE_SID_TYPE_UNRESTRICTED = 0x00000001; private readonly TimeSpan _timeoutInterval = TimeSpan.FromSeconds(10); private readonly ILogger _logger; private readonly ICommandLineCaller _commandLineCaller; public string Name { get; } public Service(string name, ILogger logger, ICommandLineCaller commandLineCaller) { Name = name; _logger = logger; _commandLineCaller = commandLineCaller; } public bool IsCreated() { try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_QUERY_STATUS); return true; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { return false; } catch (Exception ex) { _logger.Error($"Unable to determine whether Windows service '{Name}' exists.", ex); return false; } } public bool IsEnabled() { try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_QUERY_CONFIG); QUERY_SERVICE_CONFIG config = GetServiceConfig(handle); return config.dwStartType != ServiceStartType.SERVICE_DISABLED; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { _logger.Error($"The service '{Name}' does not exist.", ex); return false; } catch (Exception ex) { _logger.Error($"Unable to determine whether Windows service '{Name}' is enabled.", ex); return false; } } private SafeSC_HANDLE GetServiceHandle(ServiceAccessTypes desiredAccess) { SafeSC_HANDLE scManagerHandle = OpenSCManager(null, null, ScManagerAccessTypes.SC_MANAGER_CONNECT); if (scManagerHandle.IsInvalid) { scManagerHandle.Dispose(); throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to open the Service Control Manager to query '{Name}'."); } try { SafeSC_HANDLE serviceHandle = OpenService(scManagerHandle, Name, desiredAccess); if (serviceHandle.IsInvalid) { int errorCode = Marshal.GetLastWin32Error(); serviceHandle.Dispose(); throw new Win32Exception(errorCode, $"Failed to open Windows service '{Name}'."); } return serviceHandle; } finally { scManagerHandle.Dispose(); } } private QUERY_SERVICE_CONFIG GetServiceConfig(SafeSC_HANDLE serviceHandle) { if (!QueryServiceConfig(serviceHandle, IntPtr.Zero, 0, out uint bytesNeeded)) { int queryError = Marshal.GetLastWin32Error(); if (queryError != ERROR_INSUFFICIENT_BUFFER) { throw new Win32Exception(queryError, $"Failed to query configuration size for service '{Name}'."); } } if (bytesNeeded == 0) { throw new Win32Exception($"Service '{Name}' returned no configuration data."); } IntPtr buffer = Marshal.AllocHGlobal((int)bytesNeeded); try { if (!QueryServiceConfig(serviceHandle, buffer, bytesNeeded, out _)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to query configuration for service '{Name}'."); } return Marshal.PtrToStructure(buffer); } finally { Marshal.FreeHGlobal(buffer); } } public void Enable() { _logger.Info($"Enabling the Windows service '{Name}'."); _commandLineCaller.ExecuteElevated($"/c sc config \"{Name}\" start= demand"); } public void UpdatePathAndArgs(string pathAndArgs) { try { using SafeSC_HANDLE scManagerHandle = OpenSCManager(null, null, ScManagerAccessTypes.SC_MANAGER_CONNECT); if (scManagerHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error()); } using SafeSC_HANDLE serviceHandle = OpenService(scManagerHandle, Name, ServiceAccessTypes.SERVICE_CHANGE_CONFIG); if (serviceHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error()); } if (!ChangeServiceConfig(serviceHandle, ServiceTypes.SERVICE_NO_CHANGE, ServiceStartType.SERVICE_NO_CHANGE, ServiceErrorControlType.SERVICE_NO_CHANGE, pathAndArgs, null, IntPtr.Zero, null, null, null, null)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to update Windows service '{Name}' path."); } } catch (Exception ex) { _logger.Error($"Failed to update Windows service '{Name}' path.", ex); } } public string? GetBinaryPath() { try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_QUERY_CONFIG); QUERY_SERVICE_CONFIG config = GetServiceConfig(handle); return config.lpBinaryPathName; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { _logger.Error($"The service '{Name}' does not exist.", ex); return null; } catch (Exception ex) { _logger.Error($"Failed to retrieve Windows service '{Name}' binary path.", ex); return null; } } public bool IsRunning() { return GetServiceState() == ServiceState.SERVICE_RUNNING; } public bool IsStopped() { ServiceState? state = GetServiceState(); return !state.HasValue || state.Value == ServiceState.SERVICE_STOPPED; } public bool Start() { return StartInternalAsync(CancellationToken.None).GetAwaiter().GetResult(); } public bool StartWithRetry() { return GetRetryPolicy().Execute(() => StartInternalAsync(CancellationToken.None).GetAwaiter().GetResult()); } private static RetryPolicy GetRetryPolicy() { return Policy .HandleResult(result => false) .WaitAndRetry(RETRY_COUNT, attempt => TimeSpan.FromSeconds(RETRY_DELAY_IN_SECONDS)); } public Task StartAsync(CancellationToken cancellationToken) { return StartInternalAsync(cancellationToken); } private async Task StartInternalAsync(CancellationToken cancellationToken) { _logger.Info($"Starting the Windows service '{Name}'."); try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_START | ServiceAccessTypes.SERVICE_QUERY_STATUS); if (!StartService(handle)) { int errorCode = Marshal.GetLastWin32Error(); if (errorCode != ERROR_SERVICE_ALREADY_RUNNING) { throw new Win32Exception(errorCode, $"Failed to start Windows service '{Name}'."); } } if (!await WaitForServiceStateAsync(handle, ServiceState.SERVICE_RUNNING, cancellationToken).ConfigureAwait(false)) { if (!cancellationToken.IsCancellationRequested) { _logger.Warn($"The service '{Name}' did not reach the running state within {_timeoutInterval.TotalSeconds} seconds."); } return false; } return true; } catch (OperationCanceledException) { return false; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { _logger.Error($"The service '{Name}' does not exist.", ex); return false; } catch (Exception ex) { _logger.Error($"Failed to start Windows service '{Name}'.", ex); return false; } } public bool Stop() { return StopInternalAsync(CancellationToken.None).GetAwaiter().GetResult(); } public bool StopWithRetry() { return GetRetryPolicy().Execute(() => StopInternalAsync(CancellationToken.None).GetAwaiter().GetResult()); } public Task StopAsync(CancellationToken cancellationToken) { return StopInternalAsync(cancellationToken); } private async Task StopInternalAsync(CancellationToken cancellationToken) { _logger.Info($"Stopping the Windows service '{Name}'."); try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_STOP | ServiceAccessTypes.SERVICE_QUERY_STATUS); if (!ControlService(handle, ServiceControl.SERVICE_CONTROL_STOP, out SERVICE_STATUS status)) { int errorCode = Marshal.GetLastWin32Error(); if (errorCode == ERROR_SERVICE_NOT_ACTIVE) { return true; } throw new Win32Exception(errorCode, $"Failed to send stop command to Windows service '{Name}'."); } if (!await WaitForServiceStateAsync(handle, ServiceState.SERVICE_STOPPED, cancellationToken).ConfigureAwait(false)) { _logger.Warn($"The service '{Name}' did not reach the stopped state within {_timeoutInterval.TotalSeconds} seconds."); return false; } return true; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { _logger.Error($"The service '{Name}' does not exist.", ex); return false; } catch (Exception ex) { _logger.Error($"Failed to stop Windows service '{Name}'.", ex); return false; } } private async Task WaitForServiceStateAsync(SafeSC_HANDLE serviceHandle, ServiceState desiredState, CancellationToken cancellationToken) { DateTime start = DateTime.UtcNow; while (true) { cancellationToken.ThrowIfCancellationRequested(); if (!QueryServiceStatus(serviceHandle, out SERVICE_STATUS status)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to query status for service '{Name}'."); } if (status.dwCurrentState == desiredState) { return true; } if (DateTime.UtcNow - start >= _timeoutInterval) { return false; } await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken).ConfigureAwait(false); } } public ServiceStatus? GetStatus() { ServiceState? state = GetServiceState(); return state.HasValue ? MapServiceState(state.Value) : null; } public void Create(ServiceCreationOptions options) { try { using SafeSC_HANDLE scManagerHandle = OpenSCManager(dwDesiredAccess: ScManagerAccessTypes.SC_MANAGER_CREATE_SERVICE); if (scManagerHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error()); } string[]? dependencies = options.Dependencies.Count > 0 ? [.. options.Dependencies] : null; using SafeSC_HANDLE serviceHandle = CreateService( scManagerHandle, Name, Name, (uint)ServiceAccessRights.SERVICE_ALL_ACCESS, ServiceTypes.SERVICE_WIN32_OWN_PROCESS, ServiceStartType.SERVICE_DEMAND_START, ServiceErrorControlType.SERVICE_ERROR_NORMAL, options.PathAndArguments, null, IntPtr.Zero, dependencies); if (serviceHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to create Windows service '{Name}'."); } if (options.IsUnrestricted) { SERVICE_SID_INFO sidInfo = new() { dwServiceSidType = SERVICE_SID_TYPE_UNRESTRICTED, }; if (!ChangeServiceConfig2(serviceHandle, ServiceConfigOption.SERVICE_CONFIG_SERVICE_SID_INFO, sidInfo)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to set SID type for service '{Name}'."); } } } catch (Exception ex) { _logger.Error($"Failed to create Windows service '{Name}'.", ex); } } public bool Kill() { _logger.Info($"Killing the Windows service '{Name}'."); try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_QUERY_STATUS); SERVICE_STATUS_PROCESS statusProcess = GetServiceStatusProcess(handle); if (statusProcess.dwCurrentState == ServiceState.SERVICE_STOPPED) { _logger.Info($"Service '{Name}' is stopped, no need to kill."); return true; } if (statusProcess.dwProcessId == 0) { _logger.Warn($"Cannot kill Windows service '{Name}' because the process ID is not available."); return false; } using Process process = Process.GetProcessById((int)statusProcess.dwProcessId); process.Kill(); _logger.Info($"Successfully killed Windows service '{Name}' process (PID: {statusProcess.dwProcessId})."); return true; } catch (Exception ex) { _logger.Error($"Failed to kill Windows service '{Name}'.", ex); return false; } } private SERVICE_STATUS_PROCESS GetServiceStatusProcess(SafeSC_HANDLE serviceHandle) { int size = Marshal.SizeOf(); IntPtr buffer = Marshal.AllocHGlobal(size); try { if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_TYPE.SC_STATUS_PROCESS_INFO, buffer, (uint)size, out _)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to query service status process for '{Name}'."); } return Marshal.PtrToStructure(buffer); } finally { Marshal.FreeHGlobal(buffer); } } private ServiceState? GetServiceState() { try { using SafeSC_HANDLE handle = GetServiceHandle(ServiceAccessTypes.SERVICE_QUERY_STATUS); if (!QueryServiceStatus(handle, out SERVICE_STATUS status)) { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to query status for service '{Name}'."); } return status.dwCurrentState; } catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { _logger.Error($"The service '{Name}' does not exist.", ex); return null; } catch (Exception ex) { _logger.Error($"Unable to query Windows service '{Name}' status.", ex); return null; } } private static ServiceStatus MapServiceState(ServiceState state) { return state switch { ServiceState.SERVICE_STOPPED => ServiceStatus.Stopped, ServiceState.SERVICE_START_PENDING => ServiceStatus.StartPending, ServiceState.SERVICE_STOP_PENDING => ServiceStatus.StopPending, ServiceState.SERVICE_RUNNING => ServiceStatus.Running, ServiceState.SERVICE_CONTINUE_PENDING => ServiceStatus.ContinuePending, ServiceState.SERVICE_PAUSE_PENDING => ServiceStatus.PausePending, ServiceState.SERVICE_PAUSED => ServiceStatus.Paused, _ => throw new ArgumentOutOfRangeException(nameof(state), state, "Unknown service state.") }; } }