diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 291eae5..52dae35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,21 +2,34 @@ name: OpenVPNServ2 on: push: - branches: - - master pull_request: - branches: - - master jobs: ubuntu: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install mono-devel - - name: Build - run: ./build.sh + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Latest Mono & MSBuild + run: | + sudo apt update + sudo apt install -y gnupg ca-certificates + + sudo gpg --homedir /tmp --no-default-keyring \ + --keyring /usr/share/keyrings/mono-official-archive-keyring.gpg \ + --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF + + echo \ + "deb [signed-by=/usr/share/keyrings/mono-official-archive-keyring.gpg] https://download.mono-project.com/repo/ubuntu stable-focal main" | \ + sudo tee /etc/apt/sources.list.d/mono-official-stable.list + + # Update and install Mono & MSBuild + sudo apt update + sudo apt install -y mono-devel msbuild + + - name: Build + run: ./build.sh win: runs-on: windows-latest diff --git a/.gitignore b/.gitignore index df11d43..357eb4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ obj /bin +.vs +.vscode +AssemblyInfo.cs +git_hash.txt diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs deleted file mode 100644 index 6fdbde7..0000000 --- a/AssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -[assembly: AssemblyTitle("Openvpnserv2")] -[assembly: AssemblyDescription("Windows service for running OpenVPN connections in the background")] -[assembly: AssemblyProduct("Openvpnserv2")] -[assembly: AssemblyVersion("1.4.0.1")] -[assembly: AssemblyFileVersion("1.4.0.1")] -[assembly: AssemblyInformationalVersion("1.4.0.1")] -[assembly: AssemblyCompany("The OpenVPN project")] -[assembly: AssemblyCopyright("Copyright © OpenVPN project 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] \ No newline at end of file diff --git a/OpenVPNChild.cs b/OpenVPNChild.cs new file mode 100644 index 0000000..485efe4 --- /dev/null +++ b/OpenVPNChild.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; + +namespace OpenVpn +{ + /// + /// Represents single OpenVPN connection + /// + class OpenVpnChild + { + string logFile; + Process process; + System.Timers.Timer restartTimer; + OpenVpnServiceConfiguration config; + string configFile; + string exitEvent; + private CancellationTokenSource exitPollingToken = new CancellationTokenSource(); + + /// + /// Constructs OpenVpnChild object + /// + /// + /// path to ovpn profile + public OpenVpnChild(OpenVpnServiceConfiguration config, string configFile) + { + this.config = config; + this.configFile = configFile; + this.exitEvent = Path.GetFileName(configFile) + "_" + Process.GetCurrentProcess().Id.ToString(); + var justFilename = Path.GetFileName(configFile); + logFile = Path.Combine(config.logDir, justFilename.Substring(0, justFilename.Length - config.configExt.Length) + ".log"); + } + + /// + /// Signal OpenVPN process exit event and cancel polling task. + /// + public void SignalProcess() + { + if (restartTimer != null) + { + restartTimer.Stop(); + } + try + { + if (process != null && !process.HasExited) + { + try + { + config.LogMessage($"Signalling PID {process.Id} for config {configFile} to exit"); + + using (var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, exitEvent)) + { + waitHandle.Set(); // Signal OpenVPN to exit gracefully + } + + exitPollingToken.Cancel(); // Stop monitoring + } + catch (IOException e) + { + config.LogMessage("IOException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); + } + catch (UnauthorizedAccessException e) + { + config.LogMessage("UnauthorizedAccessException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); + } + catch (WaitHandleCannotBeOpenedException e) + { + config.LogMessage("WaitHandleCannotBeOpenedException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); + } + catch (ArgumentException e) + { + config.LogMessage("ArgumentException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); + } + } + } + catch (InvalidOperationException) { } + } + + /// + /// Polling task to detect process exit and restart it. + /// + private async void MonitorProcessExit() + { + if (process == null) return; + + config.LogMessage($"Started polling for OpenVPN process, PID {process.Id}"); + + try + { + while (!process.HasExited) + { + await Task.Delay(1000, exitPollingToken.Token); + } + + config.LogMessage($"Process {process.Id} has exited.", EventLogEntryType.Warning); + RestartAfterDelay(10000); + } + catch (TaskCanceledException) + { + config.LogMessage("Process monitoring cancelled."); + } + catch (Exception ex) + { + config.LogMessage($"Error in MonitorProcessExit: {ex.Message}", EventLogEntryType.Error); + } + } + + /// + /// Restart OpenVPN process after delay + /// + /// + private void RestartAfterDelay(int delayMs) + { + config.LogMessage($"Restarting process for {configFile} in {delayMs / 1000} sec."); + + restartTimer = new System.Timers.Timer(delayMs); + restartTimer.AutoReset = false; + restartTimer.Elapsed += (object source, ElapsedEventArgs ev) => + { + Start(); + }; + restartTimer.Start(); + } + + /// + /// Name of the OpenVPN interactive service pipe + /// + private const string PipeName = @"openvpn\service"; + + /// + /// Start OpenVPN child process. + /// Connect to interactive service via named pipe and pass a startup info. + /// Read OpenVPN process PID from the pipe and set up polling task + /// to detect process exit. + /// + public void Start() + { + using (var pipeClient = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous)) + { + config.LogMessage("Connecting to iservice pipe..."); + pipeClient.Connect(5000); + + using (var writer = new BinaryWriter(pipeClient, Encoding.Unicode)) + using (var reader = new StreamReader(pipeClient, Encoding.Unicode)) + { + // send startup info + var logOption = config.logAppend ? "--log-append " : "--log"; + var cmdLine = $"{logOption} \"{logFile}\" --config \"{configFile}\" --service \"{exitEvent}\" 0 --pull-filter ignore route-method"; + + // config_dir + \0 + options + \0 + password + \0 + var startupInfo = $"{config.configDir}\0{cmdLine}\0\0"; + + byte[] messageBytes = Encoding.Unicode.GetBytes(startupInfo); + writer.Write(messageBytes); + writer.Flush(); + + config.LogMessage("Sent startupInfo to iservice"); + + // read openvpn process pid from the pipe + string[] lines = { reader.ReadLine(), reader.ReadLine() }; + + config.LogMessage($"Read from iservice: {string.Join(" ", lines)}"); + var errorCode = Convert.ToInt32(lines[0], 16); + + if (errorCode == 0) + { + var pid = Convert.ToInt32(lines[1], 16); + process = Process.GetProcessById(pid); + + exitPollingToken = new CancellationTokenSource(); + Task.Run(() => MonitorProcessExit(), exitPollingToken.Token); + + config.LogMessage($"Started monitoring OpenVPN process, PID {pid}"); + } else + { + config.LogMessage("Error getting openvpn process PID", EventLogEntryType.Error); + } + } + } + } + } +} diff --git a/OpenVPNServiceConfiguration.cs b/OpenVPNServiceConfiguration.cs new file mode 100644 index 0000000..9b7fdea --- /dev/null +++ b/OpenVPNServiceConfiguration.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; + +namespace OpenVpn +{ + class OpenVpnServiceConfiguration + { + public string exePath { get; set; } + public string configExt { get; set; } + public string configDir { get; set; } + public string logDir { get; set; } + public bool logAppend { get; set; } + + /// + /// Delegate used to log messages with a specified severity level. + /// + public Action Log; + + /// + /// Constructs OpenVpnServiceConfiguration object + /// + /// Log callback + public OpenVpnServiceConfiguration(Action logAction) + { + Log = logAction; + } + + /// + /// Writes log message via log callback + /// + /// + /// + public void LogMessage(string message, EventLogEntryType type = EventLogEntryType.Information) + { + Log(message, type); + } + } +} diff --git a/OpenVPNServiceRunner.cs b/OpenVPNServiceRunner.cs new file mode 100644 index 0000000..a872680 --- /dev/null +++ b/OpenVPNServiceRunner.cs @@ -0,0 +1,170 @@ +using Microsoft.Win32; +using OpenVpn; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace OpenVpn +{ + /// + /// Main class implementing OpenVPN service functionality, + /// without dependencies to Windows service infrastructure + /// + class OpenVPNServiceRunner + { + private List Subprocesses; + private EventLog _eventLog; + + /// + /// Creates OpenVPNServiceRunner object + /// + /// EventLog or null + public OpenVPNServiceRunner(EventLog eventLog) + { + this.Subprocesses = new List(); + _eventLog = eventLog; + } + + /// + /// Stops all OpenVPN child processes + /// + public void Stop() + { + foreach (var child in Subprocesses) + { + child.SignalProcess(); + } + } + + /// + /// Gets registry subkey + /// + /// + /// Registry key, null if not found + private RegistryKey GetRegistrySubkey(RegistryView rView) + { + try + { + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, rView) + .OpenSubKey("Software\\OpenVPN"); + } + catch (ArgumentException) + { + return null; + } + catch (NullReferenceException) + { + return null; + } + } + + /// + /// Reads configuration from the registry and starts OpenVPN process for every ovpn config found. + /// + /// + public void Start(string[] args) + { + try + { + List rkOvpns = new List(); + + // Search 64-bit registry, then 32-bit registry for OpenVpn + var key = GetRegistrySubkey(RegistryView.Registry64); + if (key != null) rkOvpns.Add(key); + key = GetRegistrySubkey(RegistryView.Registry32); + if (key != null) rkOvpns.Add(key); + + if (rkOvpns.Count() == 0) + throw new Exception("Registry key missing"); + + var configDirsConsidered = new HashSet(); + + foreach (var rkOvpn in rkOvpns) + { + try + { + bool append = false; + { + var logAppend = (string)rkOvpn.GetValue("log_append"); + if (logAppend[0] == '0' || logAppend[0] == '1') + append = logAppend[0] == '1'; + else + throw new Exception("Log file append flag must be 1 or 0"); + } + + var config = new OpenVpnServiceConfiguration(Log) + { + exePath = (string)rkOvpn.GetValue("exe_path"), + configDir = (string)rkOvpn.GetValue("autostart_config_dir"), + configExt = "." + (string)rkOvpn.GetValue("config_ext"), + logDir = (string)rkOvpn.GetValue("log_dir"), + logAppend = append + }; + + if (String.IsNullOrEmpty(config.configDir) || configDirsConsidered.Contains(config.configDir)) + { + continue; + } + configDirsConsidered.Add(config.configDir); + + /// Only attempt to start the service + /// if openvpn.exe is present. This should help if there are old files + /// and registry settings left behind from a previous OpenVPN 32-bit installation + /// on a 64-bit system. + if (!File.Exists(config.exePath)) + { + Log("OpenVPN binary does not exist at " + config.exePath); + continue; + } + + foreach (var configFilename in Directory.EnumerateFiles(config.configDir, + "*" + config.configExt, + System.IO.SearchOption.AllDirectories)) + { + try + { + var child = new OpenVpnChild(config, configFilename); + Subprocesses.Add(child); + child.Start(); + } + catch (Exception e) + { + Log("Caught exception " + e.Message + " when starting openvpn for " + + configFilename, EventLogEntryType.Error); + } + } + } + catch (NullReferenceException e) /* e.g. missing registry values */ + { + Log("Registry values are incomplete for " + rkOvpn.View.ToString() + e.StackTrace, EventLogEntryType.Error); + } + } + + } + catch (Exception e) + { + Log("Exception occured during OpenVPN service start: " + e.Message + e.StackTrace, EventLogEntryType.Error); + throw e; + } + } + + /// + /// Writes log message either to event log or console + /// + /// + /// + private void Log(string message, EventLogEntryType type = EventLogEntryType.Information) + { + if (_eventLog != null) + { + _eventLog.WriteEntry(message, type); + } + else + { + Console.WriteLine($"[{type}] {message}"); + } + } + } +} diff --git a/OpenVpnService.csproj b/OpenVpnService.csproj index 87b02b1..1576dee 100644 --- a/OpenVpnService.csproj +++ b/OpenVpnService.csproj @@ -15,7 +15,7 @@ 512 false - 1.4.0.1 + 2.0.0.0 publish\ true Disk @@ -32,7 +32,7 @@ true - OpenVpn.OpenVpnService + OpenVpn.Program x86 @@ -93,16 +93,14 @@ 4 - - Component - - - ProjectInstaller.cs - + + + Component + @@ -111,9 +109,6 @@ - - ProjectInstaller.cs - Service.cs @@ -148,4 +143,33 @@ --> - \ No newline at end of file + + + + + + + + + + + + unknown + + + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..6fce30a --- /dev/null +++ b/Program.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceProcess; +using System.Text; +using System.Threading.Tasks; + +namespace OpenVpn +{ + internal class Program + { + public static int Main(string[] args) + { + if (!Environment.UserInteractive) + { + // Running as a Windows Service + ServiceBase.Run(new OpenVpnService()); + } + else + { + // Running as a console application + Console.WriteLine("Running in console mode..."); + var runner = new OpenVPNServiceRunner(null); + runner.Start(args); + + Console.WriteLine("Press Enter to stop..."); + Console.ReadLine(); + + runner.Stop(); + } + return 0; + } + } +} diff --git a/ProjectInstaller.Designer.cs b/ProjectInstaller.Designer.cs deleted file mode 100644 index cd7a928..0000000 --- a/ProjectInstaller.Designer.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace OpenVpn -{ - partial class ProjectInstaller - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.serviceProcessInstaller = new System.ServiceProcess.ServiceProcessInstaller(); - this.serviceInstaller = new System.ServiceProcess.ServiceInstaller(); - // - // serviceProcessInstaller - // - this.serviceProcessInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem; - this.serviceProcessInstaller.Password = null; - this.serviceProcessInstaller.Username = null; - // - // serviceInstaller - // - this.serviceInstaller.ServiceName = "OpenVpnService"; - this.serviceInstaller.ServicesDependedOn = new string[] { - "Dhcp", - "tap0901"}; - this.serviceInstaller.StartType = System.ServiceProcess.ServiceStartMode.Automatic; - // - // ProjectInstaller - // - this.Installers.AddRange(new System.Configuration.Install.Installer[] { - this.serviceProcessInstaller, - this.serviceInstaller}); - - } - - #endregion - - private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller; - private System.ServiceProcess.ServiceInstaller serviceInstaller; - } -} diff --git a/ProjectInstaller.cs b/ProjectInstaller.cs deleted file mode 100644 index b91065f..0000000 --- a/ProjectInstaller.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Configuration.Install; -using System.Linq; -using System.ServiceProcess; - -namespace OpenVpn -{ - [RunInstaller(true)] - public partial class ProjectInstaller : System.Configuration.Install.Installer - { - public ProjectInstaller() - { - InitializeComponent(); - } - - - // References http://stackoverflow.com/questions/1195478/how-to-make-a-net-windows-service-start-right-after-the-installation/1195621#1195621 - public static void Install() - { - using (AssemblyInstaller installer = - new AssemblyInstaller(typeof(OpenVpnService).Assembly, null)) - { - installer.UseNewContext = true; - var state = new System.Collections.Hashtable(); - try - { - installer.Install(state); - installer.Commit(state); - } catch - { - installer.Rollback(state); - throw; - } - } - } - - public static void Uninstall() - { - using (AssemblyInstaller installer = - new AssemblyInstaller(typeof(OpenVpnService).Assembly, null)) - { - installer.UseNewContext = true; - var state = new System.Collections.Hashtable(); - try - { - installer.Uninstall(state); - } - catch - { - throw; - } - } - } - - public static void Stop() - { - using (ServiceController controller = - new ServiceController(OpenVpnService.DefaultServiceName)) - { - try - { - if (controller.Status != ServiceControllerStatus.Stopped) - { - controller.Stop(); - controller.WaitForStatus(ServiceControllerStatus.Stopped, - TimeSpan.FromSeconds(10)); - } - } - catch - { - throw; - } - } - } - - public static void Start() - { - using (ServiceController controller = - new ServiceController(OpenVpnService.DefaultServiceName)) - { - try - { - if (controller.Status != ServiceControllerStatus.Running) - { - controller.Start(); - controller.WaitForStatus(ServiceControllerStatus.Running, - TimeSpan.FromSeconds(10)); - } - } - catch - { - throw; - } - } - } - } -} diff --git a/ProjectInstaller.resx b/ProjectInstaller.resx deleted file mode 100755 index 27dace8..0000000 --- a/ProjectInstaller.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 56 - - - 17, 17 - - - False - - \ No newline at end of file diff --git a/Service.cs b/Service.cs index 19766eb..82567dc 100644 --- a/Service.cs +++ b/Service.cs @@ -10,13 +10,11 @@ namespace OpenVpn { - - class OpenVpnService : System.ServiceProcess.ServiceBase + public class OpenVpnService : ServiceBase { public static string DefaultServiceName = "OpenVpnService"; - - public const string Package = "openvpn"; - private List Subprocesses; + private OpenVPNServiceRunner _serviceRunner; + private EventLog _eventLog; public OpenVpnService() { @@ -29,388 +27,26 @@ public OpenVpnService() this.CanHandlePowerEvent = false; this.AutoLog = true; - this.Subprocesses = new List(); - } - - protected override void OnStop() - { - RequestAdditionalTime(3000); - foreach (var child in Subprocesses) + _eventLog = new EventLog(); + if (!EventLog.SourceExists(this.ServiceName)) { - child.SignalProcess(); + EventLog.CreateEventSource(this.ServiceName, "Application"); } - // Kill all processes -- wait for 2500 msec at most - DateTime tEnd = DateTime.Now.AddMilliseconds(2500.0); - foreach (var child in Subprocesses) - { - int timeout = (int) (tEnd - DateTime.Now).TotalMilliseconds; - child.StopProcess(timeout > 0 ? timeout : 0); - } - } + _eventLog.Source = this.ServiceName; + _eventLog.Log = "Application"; - private RegistryKey GetRegistrySubkey(RegistryView rView) - { - try - { - return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, rView) - .OpenSubKey("Software\\OpenVPN"); - } - catch (ArgumentException) - { - return null; - } - catch (NullReferenceException) - { - return null; - } + _serviceRunner = new OpenVPNServiceRunner(_eventLog); // Decoupled service logic } protected override void OnStart(string[] args) { - try - { - List rkOvpns = new List(); - - // Search 64-bit registry, then 32-bit registry for OpenVpn - var key = GetRegistrySubkey(RegistryView.Registry64); - if (key != null) rkOvpns.Add(key); - key = GetRegistrySubkey(RegistryView.Registry32); - if (key != null) rkOvpns.Add(key); - - if (rkOvpns.Count() == 0) - throw new Exception("Registry key missing"); - - var configDirsConsidered = new HashSet(); - - foreach (var rkOvpn in rkOvpns) - { - try { - bool append = false; - { - var logAppend = (string)rkOvpn.GetValue("log_append"); - if (logAppend[0] == '0' || logAppend[0] == '1') - append = logAppend[0] == '1'; - else - throw new Exception("Log file append flag must be 1 or 0"); - } - - var config = new OpenVpnServiceConfiguration() - { - exePath = (string)rkOvpn.GetValue("exe_path"), - configDir = (string)rkOvpn.GetValue("autostart_config_dir"), - configExt = "." + (string)rkOvpn.GetValue("config_ext"), - logDir = (string)rkOvpn.GetValue("log_dir"), - logAppend = append, - priorityClass = GetPriorityClass((string)rkOvpn.GetValue("priority")), - - eventLog = EventLog, - }; - - if (String.IsNullOrEmpty(config.configDir) || configDirsConsidered.Contains(config.configDir)) { - continue; - } - configDirsConsidered.Add(config.configDir); - - /// Only attempt to start the service - /// if openvpn.exe is present. This should help if there are old files - /// and registry settings left behind from a previous OpenVPN 32-bit installation - /// on a 64-bit system. - if (!File.Exists(config.exePath)) - { - EventLog.WriteEntry("OpenVPN binary does not exist at " + config.exePath); - continue; - } - - foreach (var configFilename in Directory.EnumerateFiles(config.configDir, - "*" + config.configExt, - System.IO.SearchOption.AllDirectories)) - { - try { - var child = new OpenVpnChild(config, configFilename); - Subprocesses.Add(child); - child.Start(); - } - catch (Exception e) - { - EventLog.WriteEntry("Caught exception " + e.Message + " when starting openvpn for " - + configFilename); - } - } - } - catch (NullReferenceException e) /* e.g. missing registry values */ - { - EventLog.WriteEntry("Registry values are incomplete for " + rkOvpn.View.ToString() + e.StackTrace); - } - } - - } - catch (Exception e) - { - EventLog.WriteEntry("Exception occured during OpenVPN service start: " + e.Message + e.StackTrace); - throw e; - } - } - - private System.Diagnostics.ProcessPriorityClass GetPriorityClass(string priorityString) - { - if (String.Equals(priorityString, "IDLE_PRIORITY_CLASS", StringComparison.InvariantCultureIgnoreCase)) { - return System.Diagnostics.ProcessPriorityClass.Idle; - } - else if (String.Equals(priorityString, "BELOW_NORMAL_PRIORITY_CLASS", StringComparison.InvariantCultureIgnoreCase)) - { - return System.Diagnostics.ProcessPriorityClass.BelowNormal; - } - else if (String.Equals(priorityString, "NORMAL_PRIORITY_CLASS", StringComparison.InvariantCultureIgnoreCase)) - { - return System.Diagnostics.ProcessPriorityClass.Normal; - } - else if (String.Equals(priorityString, "ABOVE_NORMAL_PRIORITY_CLASS", StringComparison.InvariantCultureIgnoreCase)) - { - return System.Diagnostics.ProcessPriorityClass.AboveNormal; - } - else if (String.Equals(priorityString, "HIGH_PRIORITY_CLASS", StringComparison.InvariantCultureIgnoreCase)) - { - return System.Diagnostics.ProcessPriorityClass.High; - } - else { - throw new Exception("Unknown priority name: " + priorityString); - } + _serviceRunner.Start(args); } - public static int Main(string[] args) - { - if (args.Length == 0) - { - Run(new OpenVpnService()); - } - else if (args[0] == "-install") - { - try - { - ProjectInstaller.Install(); - } - catch (Exception e) - { - Console.Error.WriteLine(e.Message); - Console.Error.WriteLine(e.StackTrace); - return 1; - } - } - else if (args[0] == "-remove") - { - try - { - ProjectInstaller.Stop(); - ProjectInstaller.Uninstall(); - } - catch (Exception e) - { - Console.Error.WriteLine(e.Message); - Console.Error.WriteLine(e.StackTrace); - return 1; - } - } - else - { - Console.Error.WriteLine("Unknown command: " + args[0]); - return 1; - } - return 0; - } - - } - - class OpenVpnServiceConfiguration { - public string exePath {get;set;} - public string configExt {get;set;} - public string configDir {get;set;} - public string logDir {get;set;} - public bool logAppend {get;set;} - public System.Diagnostics.ProcessPriorityClass priorityClass {get;set;} - - public EventLog eventLog {get;set;} - } - - class OpenVpnChild { - StreamWriter logFile; - Process process; - ProcessStartInfo startInfo; - System.Timers.Timer restartTimer; - OpenVpnServiceConfiguration config; - string configFile; - string exitEvent; - - public OpenVpnChild(OpenVpnServiceConfiguration config, string configFile) { - this.config = config; - /// SET UP LOG FILES - /* Because we will be using the filenames in our closures, - * so make sure we are working on a copy */ - this.configFile = String.Copy(configFile); - this.exitEvent = Path.GetFileName(configFile) + "_" + Process.GetCurrentProcess().Id.ToString(); - var justFilename = System.IO.Path.GetFileName(configFile); - var logFilename = config.logDir + "\\" + - justFilename.Substring(0, justFilename.Length - config.configExt.Length) + ".log"; - - // FIXME: if (!init_security_attributes_allow_all (&sa)) - //{ - // MSG (M_SYSERR, "InitializeSecurityDescriptor start_" PACKAGE " failed"); - // goto finish; - //} - - logFile = new StreamWriter(File.Open(logFilename, - config.logAppend ? FileMode.Append : FileMode.Create, - FileAccess.Write, - FileShare.Read), new UTF8Encoding(false)); - logFile.AutoFlush = true; - - /// SET UP PROCESS START INFO - string[] procArgs = { - "--config", - "\"" + configFile + "\"", - "--service ", - "\"" + exitEvent + "\"" + " 0" - }; - this.startInfo = new System.Diagnostics.ProcessStartInfo() - { - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden, - - FileName = config.exePath, - Arguments = String.Join(" ", procArgs), - WorkingDirectory = config.configDir, - - UseShellExecute = false, - /* create_new_console is not exposed -- but we probably don't need it?*/ - }; - } - - // set exit event so that openvpn will terminate - public void SignalProcess() { - if (restartTimer != null) { - restartTimer.Stop(); - } - try - { - if (!process.HasExited) - { - - try { - var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, exitEvent); - - process.Exited -= Watchdog; // Don't restart the process after exit - - waitHandle.Set(); - waitHandle.Close(); - } catch (IOException e) { - config.eventLog.WriteEntry("IOException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); - } catch (UnauthorizedAccessException e) { - config.eventLog.WriteEntry("UnauthorizedAccessException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); - } catch (WaitHandleCannotBeOpenedException e) { - config.eventLog.WriteEntry("WaitHandleCannotBeOpenedException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); - } catch (ArgumentException e) { - config.eventLog.WriteEntry("ArgumentException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); - } - } - } - catch (InvalidOperationException) { } - } - - // terminate process after a timeout - public void StopProcess(int timeout) { - if (restartTimer != null) { - restartTimer.Stop(); - } - try - { - if (!process.WaitForExit(timeout)) - { - process.Exited -= Watchdog; // Don't restart the process after kill - process.Kill(); - } - } - catch (InvalidOperationException) { } - } - - public void Wait() { - process.WaitForExit(); - logFile.Close(); - } - - public void Restart() { - if (restartTimer != null) { - restartTimer.Stop(); - } - /* try-catch... because there could be a concurrency issue (write-after-read) here? */ - if (!process.HasExited) - { - process.Exited -= Watchdog; - process.Exited += FastRestart; // Restart the process after kill - try - { - process.Kill(); - } - catch (InvalidOperationException) - { - Start(); - } - } - else - { - Start(); - } - } - - private void WriteToLog(object sendingProcess, DataReceivedEventArgs e) { - if (e != null) - logFile.WriteLine(e.Data); - } - - /// Restart after 10 seconds - /// For use with unexpected terminations - private void Watchdog(object sender, EventArgs e) - { - config.eventLog.WriteEntry("Process for " + configFile + " exited. Restarting in 10 sec."); - - restartTimer = new System.Timers.Timer(10000); - restartTimer.AutoReset = false; - restartTimer.Elapsed += (object source, System.Timers.ElapsedEventArgs ev) => - { - Start(); - }; - restartTimer.Start(); - } - - /// Restart after 3 seconds - /// For use with Restart() (e.g. after a resume) - private void FastRestart(object sender, EventArgs e) + protected override void OnStop() { - config.eventLog.WriteEntry("Process for " + configFile + " restarting in 3 sec"); - restartTimer = new System.Timers.Timer(3000); - restartTimer.AutoReset = false; - restartTimer.Elapsed += (object source, System.Timers.ElapsedEventArgs ev) => - { - Start(); - }; - restartTimer.Start(); - } - - public void Start() { - process = new System.Diagnostics.Process(); - - process.StartInfo = startInfo; - process.EnableRaisingEvents = true; - - process.OutputDataReceived += WriteToLog; - process.ErrorDataReceived += WriteToLog; - process.Exited += Watchdog; - - process.Start(); - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - process.PriorityClass = config.priorityClass; + RequestAdditionalTime(3000); + _serviceRunner.Stop(); } - } } diff --git a/build.sh b/build.sh index 8d4b483..90b3742 100755 --- a/build.sh +++ b/build.sh @@ -2,10 +2,8 @@ set -eux -xbuild /p:Configuration=Release "/p:Platform=Any CPU" OpenVpnService.sln -xbuild /p:Configuration=Release "/p:Platform=x86" OpenVpnService.sln -xbuild /p:Configuration=Release "/p:Platform=x64" OpenVpnService.sln - -xbuild /p:Configuration=Debug "/p:Platform=Any CPU" OpenVpnService.sln -xbuild /p:Configuration=Debug "/p:Platform=x86" OpenVpnService.sln -xbuild /p:Configuration=Debug "/p:Platform=x64" OpenVpnService.sln +for config in Release Debug; do + for platform in "Any CPU" x86 x64; do + msbuild /p:Configuration=$config /p:Platform="$platform" OpenVpnService.sln + done +done