From dfd5e61d841b747b876b6be93ea622ca38e753b6 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Mon, 10 Mar 2025 11:58:01 +0200 Subject: [PATCH 1/9] Split into multiple files No functional changes. GitHub: #19 Signed-off-by: Lev Stipakov --- OpenVPNChild.cs | 205 ++++++++++++++++++++++++++++ OpenVPNServiceConfiguration.cs | 16 +++ OpenVpnService.csproj | 5 +- Program.cs | 53 ++++++++ Service.cs | 236 --------------------------------- 5 files changed, 278 insertions(+), 237 deletions(-) create mode 100644 OpenVPNChild.cs create mode 100644 OpenVPNServiceConfiguration.cs create mode 100644 Program.cs diff --git a/OpenVPNChild.cs b/OpenVPNChild.cs new file mode 100644 index 0000000..6b3afdb --- /dev/null +++ b/OpenVPNChild.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenVpn +{ + 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; + this.configFile = 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"; + + 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) + { + 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; + } + } +} diff --git a/OpenVPNServiceConfiguration.cs b/OpenVPNServiceConfiguration.cs new file mode 100644 index 0000000..1c07488 --- /dev/null +++ b/OpenVPNServiceConfiguration.cs @@ -0,0 +1,16 @@ +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; } + public System.Diagnostics.ProcessPriorityClass priorityClass { get; set; } + + public EventLog eventLog { get; set; } + } +} diff --git a/OpenVpnService.csproj b/OpenVpnService.csproj index 87b02b1..e99b449 100644 --- a/OpenVpnService.csproj +++ b/OpenVpnService.csproj @@ -32,7 +32,7 @@ true - OpenVpn.OpenVpnService + OpenVpn.Program x86 @@ -93,6 +93,9 @@ 4 + + + Component diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..735016a --- /dev/null +++ b/Program.cs @@ -0,0 +1,53 @@ +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 (args.Length == 0) + { + ServiceBase.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; + } + } +} diff --git a/Service.cs b/Service.cs index 19766eb..f3a3774 100644 --- a/Service.cs +++ b/Service.cs @@ -176,241 +176,5 @@ private System.Diagnostics.ProcessPriorityClass GetPriorityClass(string priority throw new Exception("Unknown priority name: " + priorityString); } } - - 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) - { - 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; - } - } } From 66b2bc6dfaec734041d52cea9d57f7c9d9546151 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Mon, 10 Mar 2025 12:04:45 +0200 Subject: [PATCH 2/9] GHA: enable for all branches Signed-off-by: Lev Stipakov --- .github/workflows/build.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 291eae5..b4c9e05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,11 +2,7 @@ name: OpenVPNServ2 on: push: - branches: - - master pull_request: - branches: - - master jobs: ubuntu: From 71703ce97fb0999479bbdd27e33b5c67c5a5c510 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Mon, 10 Mar 2025 15:34:04 +0200 Subject: [PATCH 3/9] Decouple OpenVPN service logic from Windows service infrastructure Move OpenVPN service logic into a separate class which doesn't have any dependencies on Windows service infrastructure. Add a log delegate which is used to write logs either to console or event log. Add ability to run service logic as a standalone app. GitHub: #21 Signed-off-by: Lev Stipakov --- OpenVPNChild.cs | 12 +- OpenVPNServiceConfiguration.cs | 27 ++++- OpenVPNServiceRunner.cs | 206 +++++++++++++++++++++++++++++++++ OpenVpnService.csproj | 1 + Program.cs | 18 ++- Service.cs | 154 +++--------------------- 6 files changed, 268 insertions(+), 150 deletions(-) create mode 100644 OpenVPNServiceRunner.cs diff --git a/OpenVPNChild.cs b/OpenVPNChild.cs index 6b3afdb..516df28 100644 --- a/OpenVPNChild.cs +++ b/OpenVPNChild.cs @@ -81,19 +81,19 @@ public void SignalProcess() } catch (IOException e) { - config.eventLog.WriteEntry("IOException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); + config.LogMessage("IOException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); } catch (UnauthorizedAccessException e) { - config.eventLog.WriteEntry("UnauthorizedAccessException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); + config.LogMessage("UnauthorizedAccessException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); } catch (WaitHandleCannotBeOpenedException e) { - config.eventLog.WriteEntry("WaitHandleCannotBeOpenedException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); + config.LogMessage("WaitHandleCannotBeOpenedException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); } catch (ArgumentException e) { - config.eventLog.WriteEntry("ArgumentException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace); + config.LogMessage("ArgumentException creating exit event named '" + exitEvent + "' " + e.Message + e.StackTrace, EventLogEntryType.Error); } } } @@ -160,7 +160,7 @@ private void WriteToLog(object sendingProcess, DataReceivedEventArgs e) /// For use with unexpected terminations private void Watchdog(object sender, EventArgs e) { - config.eventLog.WriteEntry("Process for " + configFile + " exited. Restarting in 10 sec."); + config.LogMessage("Process for " + configFile + " exited. Restarting in 10 sec."); restartTimer = new System.Timers.Timer(10000); restartTimer.AutoReset = false; @@ -175,7 +175,7 @@ private void Watchdog(object sender, EventArgs e) /// For use with Restart() (e.g. after a resume) private void FastRestart(object sender, EventArgs e) { - config.eventLog.WriteEntry("Process for " + configFile + " restarting in 3 sec"); + config.LogMessage("Process for " + configFile + " restarting in 3 sec"); restartTimer = new System.Timers.Timer(3000); restartTimer.AutoReset = false; restartTimer.Elapsed += (object source, System.Timers.ElapsedEventArgs ev) => diff --git a/OpenVPNServiceConfiguration.cs b/OpenVPNServiceConfiguration.cs index 1c07488..79b3071 100644 --- a/OpenVPNServiceConfiguration.cs +++ b/OpenVPNServiceConfiguration.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace OpenVpn { @@ -11,6 +12,28 @@ class OpenVpnServiceConfiguration public bool logAppend { get; set; } public System.Diagnostics.ProcessPriorityClass priorityClass { get; set; } - public EventLog eventLog { 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..eb3e00c --- /dev/null +++ b/OpenVPNServiceRunner.cs @@ -0,0 +1,206 @@ +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(); + } + // 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); + } + } + + /// + /// 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, + priorityClass = GetPriorityClass((string)rkOvpn.GetValue("priority")) + }; + + 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; + } + } + + 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); + } + } + + /// + /// 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 e99b449..45396f8 100644 --- a/OpenVpnService.csproj +++ b/OpenVpnService.csproj @@ -106,6 +106,7 @@ Component + diff --git a/Program.cs b/Program.cs index 735016a..b277c17 100644 --- a/Program.cs +++ b/Program.cs @@ -13,7 +13,23 @@ public static int Main(string[] args) { if (args.Length == 0) { - ServiceBase.Run(new OpenVpnService()); + 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(); + } } else if (args[0] == "-install") { diff --git a/Service.cs b/Service.cs index f3a3774..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,152 +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) - { - child.SignalProcess(); - } - // Kill all processes -- wait for 2500 msec at most - DateTime tEnd = DateTime.Now.AddMilliseconds(2500.0); - foreach (var child in Subprocesses) + _eventLog = new EventLog(); + if (!EventLog.SourceExists(this.ServiceName)) { - int timeout = (int) (tEnd - DateTime.Now).TotalMilliseconds; - child.StopProcess(timeout > 0 ? timeout : 0); + EventLog.CreateEventSource(this.ServiceName, "Application"); } - } + _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; - } + _serviceRunner.Start(args); } - private System.Diagnostics.ProcessPriorityClass GetPriorityClass(string priorityString) + protected override void OnStop() { - 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); - } + RequestAdditionalTime(3000); + _serviceRunner.Stop(); } } } From e21b7ed3e06fb3533467c418fdccf11f34704ece Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Wed, 12 Mar 2025 11:54:26 +0200 Subject: [PATCH 4/9] Remove unused functions Signed-off-by: Lev Stipakov --- OpenVPNChild.cs | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/OpenVPNChild.cs b/OpenVPNChild.cs index 516df28..7270197 100644 --- a/OpenVPNChild.cs +++ b/OpenVPNChild.cs @@ -124,32 +124,6 @@ public void Wait() 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) @@ -171,20 +145,6 @@ private void Watchdog(object sender, EventArgs e) restartTimer.Start(); } - /// Restart after 3 seconds - /// For use with Restart() (e.g. after a resume) - private void FastRestart(object sender, EventArgs e) - { - config.LogMessage("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(); From 5f916688e320f8e7105aea449cb138360bec63d0 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Tue, 11 Mar 2025 12:40:38 +0200 Subject: [PATCH 5/9] Use interactice service to start openvpn process Instead of starting openvpn process directly: - connect to interactive service pipe - send startup info - read openvpn process pid (for polling) Since we use virtual service account, we don't have privileges to subscribe for process events, so instead we use async polling to check if process is alive and restart it of it is not. Removed Stop() since due to lack of privileges the only way now we can stop the process is to signal the event. GitHub: #23 Signed-off-by: Lev Stipakov --- OpenVPNChild.cs | 182 +++++++++++++++++++++++----------------- OpenVPNServiceRunner.cs | 7 -- 2 files changed, 103 insertions(+), 86 deletions(-) diff --git a/OpenVPNChild.cs b/OpenVPNChild.cs index 7270197..485efe4 100644 --- a/OpenVPNChild.cs +++ b/OpenVPNChild.cs @@ -2,63 +2,45 @@ 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 { - StreamWriter logFile; + string logFile; Process process; - ProcessStartInfo startInfo; 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 = System.IO.Path.GetFileName(configFile); - var logFilename = config.logDir + "\\" + - justFilename.Substring(0, justFilename.Length - config.configExt.Length) + ".log"; - - 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?*/ - }; + var justFilename = Path.GetFileName(configFile); + logFile = Path.Combine(config.logDir, justFilename.Substring(0, justFilename.Length - config.configExt.Length) + ".log"); } - // set exit event so that openvpn will terminate + /// + /// Signal OpenVPN process exit event and cancel polling task. + /// public void SignalProcess() { if (restartTimer != null) @@ -67,17 +49,18 @@ public void SignalProcess() } try { - if (!process.HasExited) + if (process != null && !process.HasExited) { - try { - var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, exitEvent); + config.LogMessage($"Signalling PID {process.Id} for config {configFile} to exit"); - process.Exited -= Watchdog; // Don't restart the process after exit + using (var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, exitEvent)) + { + waitHandle.Set(); // Signal OpenVPN to exit gracefully + } - waitHandle.Set(); - waitHandle.Close(); + exitPollingToken.Cancel(); // Stop monitoring } catch (IOException e) { @@ -100,66 +83,107 @@ public void SignalProcess() catch (InvalidOperationException) { } } - // terminate process after a timeout - public void StopProcess(int timeout) + /// + /// Polling task to detect process exit and restart it. + /// + private async void MonitorProcessExit() { - if (restartTimer != null) - { - restartTimer.Stop(); - } + if (process == null) return; + + config.LogMessage($"Started polling for OpenVPN process, PID {process.Id}"); + try { - if (!process.WaitForExit(timeout)) + while (!process.HasExited) { - process.Exited -= Watchdog; // Don't restart the process after kill - process.Kill(); + await Task.Delay(1000, exitPollingToken.Token); } - } - catch (InvalidOperationException) { } - } - - public void Wait() - { - process.WaitForExit(); - logFile.Close(); - } - private void WriteToLog(object sendingProcess, DataReceivedEventArgs e) - { - if (e != null) - logFile.WriteLine(e.Data); + 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 after 10 seconds - /// For use with unexpected terminations - private void Watchdog(object sender, EventArgs e) + /// + /// Restart OpenVPN process after delay + /// + /// + private void RestartAfterDelay(int delayMs) { - config.LogMessage("Process for " + configFile + " exited. Restarting in 10 sec."); + config.LogMessage($"Restarting process for {configFile} in {delayMs / 1000} sec."); - restartTimer = new System.Timers.Timer(10000); + restartTimer = new System.Timers.Timer(delayMs); restartTimer.AutoReset = false; - restartTimer.Elapsed += (object source, System.Timers.ElapsedEventArgs ev) => + 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() { - process = new System.Diagnostics.Process(); + 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"; - process.StartInfo = startInfo; - process.EnableRaisingEvents = true; + byte[] messageBytes = Encoding.Unicode.GetBytes(startupInfo); + writer.Write(messageBytes); + writer.Flush(); - process.OutputDataReceived += WriteToLog; - process.ErrorDataReceived += WriteToLog; - process.Exited += Watchdog; + config.LogMessage("Sent startupInfo to iservice"); - process.Start(); - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - process.PriorityClass = config.priorityClass; + // 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/OpenVPNServiceRunner.cs b/OpenVPNServiceRunner.cs index eb3e00c..256dc17 100644 --- a/OpenVPNServiceRunner.cs +++ b/OpenVPNServiceRunner.cs @@ -36,13 +36,6 @@ public void Stop() { child.SignalProcess(); } - // 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); - } } /// From 8f7be16ab4fa9df16407f00b4febf831da50257e Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Wed, 12 Mar 2025 12:35:15 +0200 Subject: [PATCH 6/9] Remove priority class code Priority class cannot be set by the virtual service account so remove the code. Signed-off-by: Lev Stipakov --- OpenVPNServiceConfiguration.cs | 1 - OpenVPNServiceRunner.cs | 31 +------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/OpenVPNServiceConfiguration.cs b/OpenVPNServiceConfiguration.cs index 79b3071..9b7fdea 100644 --- a/OpenVPNServiceConfiguration.cs +++ b/OpenVPNServiceConfiguration.cs @@ -10,7 +10,6 @@ class OpenVpnServiceConfiguration public string configDir { get; set; } public string logDir { get; set; } public bool logAppend { get; set; } - public System.Diagnostics.ProcessPriorityClass priorityClass { get; set; } /// /// Delegate used to log messages with a specified severity level. diff --git a/OpenVPNServiceRunner.cs b/OpenVPNServiceRunner.cs index 256dc17..a872680 100644 --- a/OpenVPNServiceRunner.cs +++ b/OpenVPNServiceRunner.cs @@ -100,8 +100,7 @@ public void Start(string[] args) 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")) + logAppend = append }; if (String.IsNullOrEmpty(config.configDir) || configDirsConsidered.Contains(config.configDir)) @@ -151,34 +150,6 @@ public void Start(string[] args) } } - 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); - } - } - /// /// Writes log message either to event log or console /// From eedeab1a197e84929eb61475f86c1e52baeb2516 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Thu, 13 Mar 2025 10:45:58 +0200 Subject: [PATCH 7/9] Remove installer code The service is installed via MSI Wix script. GitHub: #24 Signed-off-by: Lev Stipakov --- OpenVpnService.csproj | 9 --- Program.cs | 59 ++++------------ ProjectInstaller.Designer.cs | 61 ----------------- ProjectInstaller.cs | 100 --------------------------- ProjectInstaller.resx | 129 ----------------------------------- 5 files changed, 12 insertions(+), 346 deletions(-) delete mode 100644 ProjectInstaller.Designer.cs delete mode 100644 ProjectInstaller.cs delete mode 100755 ProjectInstaller.resx diff --git a/OpenVpnService.csproj b/OpenVpnService.csproj index 45396f8..d56c7b3 100644 --- a/OpenVpnService.csproj +++ b/OpenVpnService.csproj @@ -96,12 +96,6 @@ - - Component - - - ProjectInstaller.cs - Component @@ -115,9 +109,6 @@ - - ProjectInstaller.cs - Service.cs diff --git a/Program.cs b/Program.cs index b277c17..6fce30a 100644 --- a/Program.cs +++ b/Program.cs @@ -11,57 +11,22 @@ internal class Program { public static int Main(string[] args) { - if (args.Length == 0) + if (!Environment.UserInteractive) { - 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(); - } - } - 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; - } + // Running as a Windows Service + ServiceBase.Run(new OpenVpnService()); } else { - Console.Error.WriteLine("Unknown command: " + args[0]); - return 1; + // 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 From 83c6d32d58343145de73b4c37af802d3c8154aa1 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Thu, 13 Mar 2025 11:28:06 +0200 Subject: [PATCH 8/9] Generate assembly metadata during build Instead of having version number in multiple places, keep it in one place and generate AssemblyInfo.cs on the fly. While on it, add git commit hash to product version and bump version to 2.0.0.0. GitHub: #25 Signed-off-by: Lev Stipakov --- .gitignore | 4 ++++ AssemblyInfo.cs | 13 ------------- OpenVpnService.csproj | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 15 deletions(-) delete mode 100644 AssemblyInfo.cs 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/OpenVpnService.csproj b/OpenVpnService.csproj index d56c7b3..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 @@ -143,4 +143,33 @@ --> - \ No newline at end of file + + + + + + + + + + + + unknown + + + + + + + + From db8cb6e83a895916d4ee2f9b28cabf6e5209624e Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Thu, 13 Mar 2025 12:05:53 +0200 Subject: [PATCH 9/9] mono: switch to MSBuild Mono version coming with Ubuntu 22.04 is an outdated and have bugs like inability to handle escaped semicolons (https://github.com/mono/mono/pull/1580/files). Bump to Mono 6.12 and switch to MSBuild (since xbuild is outdated). Signed-off-by: Lev Stipakov --- .github/workflows/build.yml | 29 +++++++++++++++++++++++------ build.sh | 12 +++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4c9e05..52dae35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,13 +6,30 @@ on: 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/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