using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Management; using System.Runtime.InteropServices; using System.ServiceProcess; using System.Text; using System.Threading; using Microsoft.Win32; using WMI; using ServiceType = WMI.ServiceType; namespace winsw { public class WrapperService : ServiceBase, EventLogger { private SERVICE_STATUS _wrapperServiceStatus; private readonly Process _process = new Process(); private readonly ServiceDescriptor _descriptor; private Dictionary _envs; /// /// Indicates to the watch dog thread that we are going to terminate the process, /// so don't try to kill us when the child exits. /// private bool _orderlyShutdown; private bool _systemShuttingdown; public WrapperService() { _descriptor = new ServiceDescriptor(); ServiceName = _descriptor.Id; CanShutdown = true; CanStop = true; CanPauseAndContinue = false; AutoLog = true; _systemShuttingdown = false; } /// /// Process the file copy instructions, so that we can replace files that are always in use while /// the service runs. /// private void HandleFileCopies() { var file = _descriptor.BasePath + ".copies"; if (!File.Exists(file)) return; // nothing to handle try { using (var tr = new StreamReader(file,Encoding.UTF8)) { string line; while ((line = tr.ReadLine()) != null) { LogEvent("Handling copy: " + line); string[] tokens = line.Split('>'); if (tokens.Length > 2) { LogEvent("Too many delimiters in " + line); continue; } CopyFile(tokens[0], tokens[1]); } } } finally { File.Delete(file); } } /// /// File replacement. /// private void CopyFile(string sourceFileName, string destFileName) { try { File.Delete(destFileName); File.Move(sourceFileName, destFileName); } catch (IOException e) { LogEvent("Failed to copy :" + sourceFileName + " to " + destFileName + " because " + e.Message); } } /// /// Starts a thread that protects the execution with a try/catch block. /// It appears that in .NET, unhandled exception in any thread causes the app to terminate /// http://msdn.microsoft.com/en-us/library/ms228965.aspx /// private void StartThread(ThreadStart main) { new Thread(delegate() { try { main(); } catch (Exception e) { WriteEvent("Thread failed unexpectedly",e); } }).Start(); } /// /// Handle the creation of the logfiles based on the optional logmode setting. /// private void HandleLogfiles() { string logDirectory = _descriptor.LogDirectory; if (!Directory.Exists(logDirectory)) { Directory.CreateDirectory(logDirectory); } LogHandler logAppender = _descriptor.LogHandler; logAppender.EventLogger = this; logAppender.log(_process.StandardOutput.BaseStream, _process.StandardError.BaseStream); } public void LogEvent(String message) { if (_systemShuttingdown) { /* NOP - cannot call EventLog because of shutdown. */ } else { try { EventLog.WriteEntry(message); } catch (Exception e) { WriteEvent("Failed to log event in Windows Event Log: " + message + "; Reason: ", e); } } } public void LogEvent(String message, EventLogEntryType type) { if (_systemShuttingdown) { /* NOP - cannot call EventLog because of shutdown. */ } else { try { EventLog.WriteEntry(message, type); } catch (Exception e) { WriteEvent("Failed to log event in Windows Event Log. Reason: ", e); } } } private void WriteEvent(Exception exception) { WriteEvent(exception.Message + "\nStacktrace:" + exception.StackTrace); } private void WriteEvent(String message, Exception exception) { WriteEvent(message + "\nMessage:" + exception.Message + "\nStacktrace:" + exception.StackTrace); } private void WriteEvent(String message) { string logfilename = Path.Combine(_descriptor.LogDirectory, _descriptor.BaseName + ".wrapper.log"); StreamWriter log = new StreamWriter(logfilename, true); log.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " - " + message); log.Flush(); log.Close(); } protected override void OnStart(string[] _) { _envs = _descriptor.EnvironmentVariables; foreach (string key in _envs.Keys) { LogEvent("envar " + key + '=' + _envs[key]); } HandleFileCopies(); // handle downloads foreach (Download d in _descriptor.Downloads) { LogEvent("Downloading: " + d.From+ " to "+d.To); try { d.Perform(); } catch (Exception e) { LogEvent("Failed to download " + d.From + " to " + d.To + "\n" + e.Message); WriteEvent("Failed to download " + d.From +" to "+d.To, e); // but just keep going } } string startarguments = _descriptor.Startarguments; if (startarguments == null) { startarguments = _descriptor.Arguments; } else { startarguments += " " + _descriptor.Arguments; } LogEvent("Starting " + _descriptor.Executable + ' ' + startarguments); WriteEvent("Starting " + _descriptor.Executable + ' ' + startarguments); StartProcess(_process, startarguments, _descriptor.Executable); // send stdout and stderr to its respective output file. HandleLogfiles(); _process.StandardInput.Close(); // nothing for you to read! } protected override void OnShutdown() { // WriteEvent("OnShutdown"); try { _systemShuttingdown = true; StopIt(); } catch (Exception ex) { WriteEvent("Shutdown exception", ex); } } protected override void OnStop() { // WriteEvent("OnStop"); try { StopIt(); } catch (Exception ex) { WriteEvent("Stop exception", ex); } } /// /// Called when we are told by Windows SCM to exit. /// private void StopIt() { string stoparguments = _descriptor.Stoparguments; LogEvent("Stopping " + _descriptor.Id); WriteEvent("Stopping " + _descriptor.Id); _orderlyShutdown = true; if (stoparguments == null) { try { WriteEvent("ProcessKill " + _process.Id); StopProcessAndChildren(_process.Id); } catch (InvalidOperationException) { // already terminated } } else { SignalShutdownPending(); stoparguments += " " + _descriptor.Arguments; Process stopProcess = new Process(); String executable = _descriptor.StopExecutable; if (executable == null) { executable = _descriptor.Executable; } StartProcess(stopProcess, stoparguments, executable); WriteEvent("WaitForProcessToExit "+_process.Id+"+"+stopProcess.Id); WaitForProcessToExit(_process); WaitForProcessToExit(stopProcess); SignalShutdownComplete(); } if (_systemShuttingdown && _descriptor.BeepOnShutdown) { Console.Beep(); } WriteEvent("Finished " + _descriptor.Id); } private void StopProcessAndChildren(int pid) { var childPids = GetChildPids(pid); if (_descriptor.StopParentProcessFirst) { StopProcess(pid); foreach (var childPid in childPids) { StopProcessAndChildren(childPid); } } else { foreach (var childPid in childPids) { StopProcessAndChildren(childPid); } StopProcess(pid); } } private List GetChildPids(int pid) { var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); var childPids = new List(); foreach (var mo in searcher.Get()) { var childProcessId = mo["ProcessID"]; WriteEvent("Found child process: " + childProcessId + " Name: " + mo["Name"]); childPids.Add(Convert.ToInt32(childProcessId)); } return childPids; } private void StopProcess(int pid) { WriteEvent("Stopping process " + pid); Process proc; try { proc = Process.GetProcessById(pid); } catch (ArgumentException) { WriteEvent("Process " + pid + " is already stopped"); return; } WriteEvent("Send SIGINT " + pid); bool successful = SigIntHelper.SendSIGINTToProcess(proc, _descriptor.StopTimeout); if (successful) { WriteEvent("SIGINT to" + pid + " successful"); } else { try { WriteEvent("SIGINT to " + pid + " failed - Killing as fallback"); proc.Kill(); } catch (ArgumentException) { // Process already exited. } } } private void WaitForProcessToExit(Process processoWait) { SignalShutdownPending(); try { // WriteEvent("WaitForProcessToExit [start]"); while (!processoWait.WaitForExit(_descriptor.SleepTime.Milliseconds)) { SignalShutdownPending(); // WriteEvent("WaitForProcessToExit [repeat]"); } } catch (InvalidOperationException) { // already terminated } // WriteEvent("WaitForProcessToExit [finished]"); } private void SignalShutdownPending() { IntPtr handle = ServiceHandle; _wrapperServiceStatus.checkPoint++; _wrapperServiceStatus.waitHint = _descriptor.WaitHint.Milliseconds; // WriteEvent("SignalShutdownPending " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint); _wrapperServiceStatus.currentState = (int)State.SERVICE_STOP_PENDING; Advapi32.SetServiceStatus(handle, ref _wrapperServiceStatus); } private void SignalShutdownComplete() { IntPtr handle = ServiceHandle; _wrapperServiceStatus.checkPoint++; // WriteEvent("SignalShutdownComplete " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint); _wrapperServiceStatus.currentState = (int)State.SERVICE_STOPPED; Advapi32.SetServiceStatus(handle, ref _wrapperServiceStatus); } private void StartProcess(Process processToStart, string arguments, String executable) { var ps = processToStart.StartInfo; ps.FileName = executable; ps.Arguments = arguments; ps.WorkingDirectory = _descriptor.WorkingDirectory; ps.CreateNoWindow = false; ps.UseShellExecute = false; ps.RedirectStandardInput = true; // this creates a pipe for stdin to the new process, instead of having it inherit our stdin. ps.RedirectStandardOutput = true; ps.RedirectStandardError = true; foreach (string key in _envs.Keys) Environment.SetEnvironmentVariable(key, _envs[key]); // ps.EnvironmentVariables[key] = envs[key]; // bugged (lower cases all variable names due to StringDictionary being used, see http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=326163) processToStart.Start(); WriteEvent("Started " + processToStart.Id); var priority = _descriptor.Priority; if (priority != ProcessPriorityClass.Normal) processToStart.PriorityClass = priority; // monitor the completion of the process StartThread(delegate { string msg = processToStart.Id + " - " + processToStart.StartInfo.FileName + " " + processToStart.StartInfo.Arguments; processToStart.WaitForExit(); try { if (_orderlyShutdown) { LogEvent("Child process [" + msg + "] terminated with " + processToStart.ExitCode, EventLogEntryType.Information); } else { LogEvent("Child process [" + msg + "] finished with " + processToStart.ExitCode, EventLogEntryType.Warning); // if we finished orderly, report that to SCM. // by not reporting unclean shutdown, we let Windows SCM to decide if it wants to // restart the service automatically if (processToStart.ExitCode == 0) SignalShutdownComplete(); Environment.Exit(processToStart.ExitCode); } } catch (InvalidOperationException ioe) { LogEvent("WaitForExit " + ioe.Message); } try { processToStart.Dispose(); } catch (InvalidOperationException ioe) { LogEvent("Dispose " + ioe.Message); } }); } public static int Main(string[] args) { try { Run(args); return 0; } catch (WmiException e) { Console.Error.WriteLine(e); return (int)e.ErrorCode; } catch (Exception e) { Console.Error.WriteLine(e); return -1; } } private static void ThrowNoSuchService() { throw new WmiException(ReturnValue.NoSuchService); } // ReSharper disable once InconsistentNaming public static void Run(string[] _args) { if (_args.Length > 0) { var d = new ServiceDescriptor(); Win32Services svc = new WmiRoot().GetCollection(); Win32Service s = svc.Select(d.Id); var args = new List(Array.AsReadOnly(_args)); if (args[0] == "/redirect") { // Redirect output // One might ask why we support this when the caller // can redirect the output easily. The answer is for supporting UAC. // On UAC-enabled Windows such as Vista, SCM operation requires // elevated privileges, thus winsw.exe needs to be launched // accordingly. This in turn limits what the caller can do, // and among other things it makes it difficult for the caller // to read stdout/stderr. Thus redirection becomes handy. var f = new FileStream(args[1], FileMode.Create); var w = new StreamWriter(f) {AutoFlush = true}; Console.SetOut(w); Console.SetError(w); var handle = f.Handle; Kernel32.SetStdHandle(-11, handle); // set stdout Kernel32.SetStdHandle(-12, handle); // set stder args = args.GetRange(2, args.Count - 2); } args[0] = args[0].ToLower(); if (args[0] == "install") { string username=null, password=null; bool setallowlogonasaserviceright = false; if (args.Count > 1 && args[1] == "/p") { // we expected username/password on stdin Console.Write("Username: "); username = Console.ReadLine(); Console.Write("Password: "); password = ReadPassword(); Console.WriteLine(); Console.Write("Set Account rights to allow log on as a service (y/n)?: "); var keypressed = Console.ReadKey(); Console.WriteLine(); if (keypressed.Key == ConsoleKey.Y) { setallowlogonasaserviceright = true; } } else { if (d.HasServiceAccount()) { username = d.ServiceAccountUser; password = d.ServiceAccountPassword; setallowlogonasaserviceright = d.AllowServiceAcountLogonRight; } } if (setallowlogonasaserviceright) { LogonAsAService.AddLogonAsAServiceRight(username); } svc.Create ( d.Id, d.Caption, "\"" + d.ExecutablePath + "\"", ServiceType.OwnProcess, ErrorControl.UserNotified, d.StartMode, d.Interactive, username, password, d.ServiceDependencies); // update the description /* Somehow this doesn't work, even though it doesn't report an error Win32Service s = svc.Select(d.Id); s.Description = d.Description; s.Commit(); */ // so using a classic method to set the description. Ugly. Registry.LocalMachine.OpenSubKey("System").OpenSubKey("CurrentControlSet").OpenSubKey("Services") .OpenSubKey(d.Id, true).SetValue("Description", d.Description); var actions = d.FailureActions; if (actions.Count > 0) {// set the failure actions using (ServiceManager scm = new ServiceManager()) { using (Service sc = scm.Open(d.Id)) { sc.ChangeConfig(d.ResetFailureAfter, actions); } } } } if (args[0] == "uninstall") { if (s == null) return; // there's no such service, so consider it already uninstalled try { s.Delete(); } catch (WmiException e) { if (e.ErrorCode == ReturnValue.ServiceMarkedForDeletion) return; // it's already uninstalled, so consider it a success throw e; } } if (args[0] == "start") { if (s == null) ThrowNoSuchService(); s.StartService(); } if (args[0] == "stop") { if (s == null) ThrowNoSuchService(); s.StopService(); } if (args[0] == "restart") { if (s == null) ThrowNoSuchService(); if(s.Started) s.StopService(); while (s.Started) { Thread.Sleep(1000); s = svc.Select(d.Id); } s.StartService(); } if (args[0] == "restart!") { // run restart from another process group. see README.md for why this is useful. STARTUPINFO si = new STARTUPINFO(); PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); bool result = Kernel32.CreateProcess(null, d.ExecutablePath+" restart", IntPtr.Zero, IntPtr.Zero, false, 0x200/*CREATE_NEW_PROCESS_GROUP*/, IntPtr.Zero, null, ref si, out pi); if (!result) { throw new Exception("Failed to invoke restart: "+Marshal.GetLastWin32Error()); } } if (args[0] == "status") { if (s == null) Console.WriteLine("NonExistent"); else if (s.Started) Console.WriteLine("Started"); else Console.WriteLine("Stopped"); } if (args[0] == "test") { WrapperService wsvc = new WrapperService(); wsvc.OnStart(args.ToArray()); Thread.Sleep(1000); wsvc.OnStop(); } return; } Run(new WrapperService()); } private static string ReadPassword() { StringBuilder buf = new StringBuilder(); ConsoleKeyInfo key; while (true) { key = Console.ReadKey(true); if (key.Key == ConsoleKey.Enter) { return buf.ToString(); } else if (key.Key == ConsoleKey.Backspace) { buf.Remove(buf.Length - 1, 1); Console.Write("\b \b"); } else { Console.Write('*'); buf.Append(key.KeyChar); } } } } }