| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935 |
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Diagnostics;
- using System.Runtime.InteropServices;
- using System.ServiceProcess;
- using System.Text;
- using System.IO;
- using System.Net;
- using WMI;
- using System.Xml;
- using System.Threading;
- using Microsoft.Win32;
- namespace winsw
- {
- public struct SERVICE_STATUS
- {
- public int serviceType;
- public int currentState;
- public int controlsAccepted;
- public int win32ExitCode;
- public int serviceSpecificExitCode;
- public int checkPoint;
- public int waitHint;
- }
- public enum State
- {
- SERVICE_STOPPED = 0x00000001,
- SERVICE_START_PENDING = 0x00000002,
- SERVICE_STOP_PENDING = 0x00000003,
- SERVICE_RUNNING = 0x00000004,
- SERVICE_CONTINUE_PENDING = 0x00000005,
- SERVICE_PAUSE_PENDING = 0x00000006,
- SERVICE_PAUSED = 0x00000007,
- }
-
- /// <summary>
- /// In-memory representation of the configuration file.
- /// </summary>
- public class ServiceDescriptor
- {
- private readonly XmlDocument dom = new XmlDocument();
- /// <summary>
- /// Where did we find the configuration file?
- ///
- /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml"
- /// </summary>
- public readonly string BasePath;
- /// <summary>
- /// The file name portion of the configuration file.
- ///
- /// In the above example, this would be "ghi".
- /// </summary>
- public readonly string BaseName;
- public static string ExecutablePath
- {
- get
- {
- // this returns the executable name as given by the calling process, so
- // it needs to be absolutized.
- string p = Environment.GetCommandLineArgs()[0];
- return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, p);
- }
- }
- public ServiceDescriptor()
- {
- // find co-located configuration xml. We search up to the ancestor directories to simplify debugging,
- // as well as trimming off ".vshost" suffix (which is used during debugging)
- string p = ExecutablePath;
- string baseName = Path.GetFileNameWithoutExtension(p);
- if (baseName.EndsWith(".vshost")) baseName = baseName.Substring(0, baseName.Length - 7);
- while (true)
- {
- p = Path.GetDirectoryName(p);
- if (File.Exists(Path.Combine(p, baseName + ".xml")))
- break;
- }
- // register the base directory as environment variable so that future expansions can refer to this.
- Environment.SetEnvironmentVariable("BASE", p);
- BaseName = baseName;
- BasePath = Path.Combine(p, BaseName);
- dom.Load(BasePath+".xml");
- }
- private string SingleElement(string tagName)
- {
- var n = dom.SelectSingleNode("//" + tagName);
- if (n == null) throw new InvalidDataException("<" + tagName + "> is missing in configuration XML");
- return Environment.ExpandEnvironmentVariables(n.InnerText);
- }
- /// <summary>
- /// Path to the executable.
- /// </summary>
- public string Executable
- {
- get
- {
- return SingleElement("executable");
- }
- }
- /// <summary>
- /// Optionally specify a different Path to an executable to shutdown the service.
- /// </summary>
- public string StopExecutable
- {
- get
- {
- return AppendTags("stopexecutable");
- }
- }
- /// <summary>
- /// Arguments or multiple optional argument elements which overrule the arguments element.
- /// </summary>
- public string Arguments
- {
- get
- {
- string arguments = AppendTags("argument");
- if (arguments == null)
- {
- var tagName = "arguments";
- var argumentsNode = dom.SelectSingleNode("//" + tagName);
- if (argumentsNode == null)
- {
- if (AppendTags("startargument") == null)
- {
- throw new InvalidDataException("<" + tagName + "> is missing in configuration XML");
- }
- else
- {
- return "";
- }
- }
- return Environment.ExpandEnvironmentVariables(argumentsNode.InnerText);
- }
- else
- {
- return arguments;
- }
- }
- }
- /// <summary>
- /// Multiple optional startargument elements.
- /// </summary>
- public string Startarguments
- {
- get
- {
- return AppendTags("startargument");
- }
- }
- /// <summary>
- /// Multiple optional stopargument elements.
- /// </summary>
- public string Stoparguments
- {
- get
- {
- return AppendTags("stopargument");
- }
- }
- /// <summary>
- /// Combines the contents of all the elements of the given name,
- /// or return null if no element exists.
- /// </summary>
- private string AppendTags(string tagName)
- {
- XmlNode argumentNode = dom.SelectSingleNode("//" + tagName);
- if (argumentNode == null)
- {
- return null;
- }
- else
- {
- string arguments = "";
- foreach (XmlNode argument in dom.SelectNodes("//" + tagName))
- {
- arguments += " " + argument.InnerText;
- }
- return Environment.ExpandEnvironmentVariables(arguments);
- }
- }
- /// <summary>
- /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
- /// </summary>
- public string LogDirectory
- {
- get
- {
- XmlNode loggingNode = dom.SelectSingleNode("//logpath");
- if (loggingNode != null)
- {
- return loggingNode.InnerText;
- }
- else
- {
- return Path.GetDirectoryName(ExecutablePath);
- }
- }
- }
- /// <summary>
- /// Logmode to 'reset', 'roll' once or 'append' [default] the out.log and err.log files.
- /// </summary>
- public string Logmode
- {
- get
- {
- XmlNode logmodeNode = dom.SelectSingleNode("//logmode");
- if (logmodeNode == null)
- {
- return "append";
- }
- else
- {
- return logmodeNode.InnerText;
- }
- }
- }
- /// <summary>
- /// Optionally specified depend services that must start before this service starts.
- /// </summary>
- public string[] ServiceDependencies
- {
- get
- {
- System.Collections.ArrayList serviceDependencies = new System.Collections.ArrayList();
- foreach (XmlNode depend in dom.SelectNodes("//depend"))
- {
- serviceDependencies.Add(depend.InnerText);
- }
- return (string[])serviceDependencies.ToArray(typeof(string));
- }
- }
- public string Id
- {
- get
- {
- return SingleElement("id");
- }
- }
- public string Caption
- {
- get
- {
- return SingleElement("name");
- }
- }
- public string Description
- {
- get
- {
- return SingleElement("description");
- }
- }
- /// <summary>
- /// True if the service should when finished on shutdown.
- /// </summary>
- public bool BeepOnShutdown
- {
- get
- {
- return dom.SelectSingleNode("//beeponshutdown") != null;
- }
- }
- /// <summary>
- /// The estimated time required for a pending stop operation, in milliseconds (default 15 secs).
- /// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function
- /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx)
- /// </summary>
- public int WaitHint
- {
- get
- {
- XmlNode waithintNode = dom.SelectSingleNode("//waithint");
- if (waithintNode == null)
- {
- return 15000;
- }
- else
- {
- return int.Parse(waithintNode.InnerText);
- }
- }
- }
- /// <summary>
- /// The time, in milliseconds (default 1 sec), before the service should make its next call to the SetServiceStatus function
- /// with an incremented checkPoint value.
- /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds.
- /// </summary>
- public int SleepTime
- {
- get
- {
- XmlNode sleeptimeNode = dom.SelectSingleNode("//sleeptime");
- if (sleeptimeNode == null)
- {
- return 1000;
- }
- else
- {
- return int.Parse(sleeptimeNode.InnerText);
- }
- }
- }
- /// <summary>
- /// True if the service can interact with the desktop.
- /// </summary>
- public bool Interactive
- {
- get
- {
- return dom.SelectSingleNode("//interactive") != null;
- }
- }
- /// <summary>
- /// Environment variable overrides
- /// </summary>
- public Dictionary<string, string> EnvironmentVariables
- {
- get
- {
- Dictionary<string, string> map = new Dictionary<string, string>();
- foreach (XmlNode n in dom.SelectNodes("//env"))
- {
- string key = n.Attributes["name"].Value;
- string value = Environment.ExpandEnvironmentVariables(n.Attributes["value"].Value);
- map[key] = value;
- Environment.SetEnvironmentVariable(key, value);
- }
- return map;
- }
- }
- /// <summary>
- /// List of downloads to be performed by the wrapper before starting
- /// a service.
- /// </summary>
- public List<Download> Downloads
- {
- get
- {
- List<Download> r = new List<Download>();
- foreach (XmlNode n in dom.SelectNodes("//download"))
- {
- r.Add(new Download(n));
- }
- return r;
- }
- }
- }
- /// <summary>
- /// Specify the download activities prior to the launch.
- /// This enables self-updating services.
- /// </summary>
- public class Download
- {
- public readonly string From;
- public readonly string To;
- internal Download(XmlNode n)
- {
- From = n.Attributes["from"].Value;
- To = n.Attributes["to"].Value;
- }
- public void Perform()
- {
- WebRequest req = WebRequest.Create(From);
- WebResponse rsp = req.GetResponse();
- FileStream tmpstream = new FileStream(To+".tmp", FileMode.Create);
- CopyStream(rsp.GetResponseStream(), tmpstream);
- // only after we successfully downloaded a file, overwrite the existing one
- File.Delete(To);
- File.Move(To + ".tmp", To);
- }
- private static void CopyStream(Stream i, Stream o)
- {
- byte[] buf = new byte[8192];
- while (true)
- {
- int len = i.Read(buf, 0, buf.Length);
- if (len <= 0) return;
- o.Write(buf, 0, len);
- }
- i.Close();
- o.Close();
- }
- }
- public class WrapperService : ServiceBase
- {
- [DllImport("ADVAPI32.DLL", EntryPoint = "SetServiceStatus")]
- private static extern bool SetServiceStatus(IntPtr hServiceStatus, ref SERVICE_STATUS lpServiceStatus);
- private SERVICE_STATUS wrapperServiceStatus;
- private Process process = new Process();
- private ServiceDescriptor descriptor;
- private Dictionary<string, string> envs;
- /// <summary>
- /// 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.
- /// </summary>
- private bool orderlyShutdown;
- private bool systemShuttingdown;
- public WrapperService()
- {
- this.descriptor = new ServiceDescriptor();
- this.ServiceName = descriptor.Id;
- this.CanShutdown = true;
- this.CanStop = true;
- this.CanPauseAndContinue = false;
- this.AutoLog = true;
- this.systemShuttingdown = false;
- }
- /// <summary>
- /// Copy stuff from StreamReader to StreamWriter
- /// </summary>
- private void CopyStream(StreamReader i, StreamWriter o)
- {
- char[] buf = new char[1024];
- while (true)
- {
- int sz = i.Read(buf, 0, buf.Length);
- if (sz == 0) break;
- o.Write(buf, 0, sz);
- o.Flush();
- }
- i.Close();
- o.Close();
- }
- /// <summary>
- /// Process the file copy instructions, so that we can replace files that are always in use while
- /// the service runs.
- /// </summary>
- 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);
- }
- }
- 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);
- }
- }
- /// <summary>
- /// Handle the creation of the logfiles based on the optional logmode setting.
- /// </summary>
- private void HandleLogfiles()
- {
- string logDirectory = descriptor.LogDirectory;
- if (!Directory.Exists(logDirectory))
- {
- Directory.CreateDirectory(logDirectory);
- }
- string baseName = descriptor.BaseName;
- string errorLogfilename = Path.Combine(logDirectory, baseName + ".err.log");
- string outputLogfilename = Path.Combine(logDirectory, baseName + ".out.log");
- System.IO.FileMode fileMode = FileMode.Append;
- if (descriptor.Logmode == "reset")
- {
- fileMode = FileMode.Create;
- }
- else if (descriptor.Logmode == "roll")
- {
- CopyFile(outputLogfilename, outputLogfilename + ".old");
- CopyFile(errorLogfilename, errorLogfilename + ".old");
- }
- new Thread(delegate() { CopyStream(process.StandardOutput, new StreamWriter(new FileStream(outputLogfilename, fileMode))); }).Start();
- new Thread(delegate() { CopyStream(process.StandardError, new StreamWriter(new FileStream(errorLogfilename, fileMode))); }).Start();
- }
- private void LogEvent(String message)
- {
- if (systemShuttingdown)
- {
- /* NOP - cannot call EventLog because of shutdown. */
- }
- else
- {
- EventLog.WriteEntry(message);
- }
- }
- private void LogEvent(String message, EventLogEntryType type)
- {
- if (systemShuttingdown)
- {
- /* NOP - cannot call EventLog because of shutdown. */
- }
- else
- {
- EventLog.WriteEntry(message, type);
- }
- }
- 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[] args)
- {
- 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, EventLogEntryType.Warning);
- // 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
- {
- this.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);
- }
- }
- 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);
- process.Kill();
- }
- 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 WaitForProcessToExit(Process process)
- {
- SignalShutdownPending();
- try
- {
- // WriteEvent("WaitForProcessToExit [start]");
- while (!process.WaitForExit(descriptor.SleepTime))
- {
- SignalShutdownPending();
- // WriteEvent("WaitForProcessToExit [repeat]");
- }
- }
- catch (InvalidOperationException)
- {
- // already terminated
- }
- // WriteEvent("WaitForProcessToExit [finished]");
- }
- private void SignalShutdownPending()
- {
- IntPtr handle = this.ServiceHandle;
- wrapperServiceStatus.checkPoint++;
- wrapperServiceStatus.waitHint = descriptor.WaitHint;
- // WriteEvent("SignalShutdownPending " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint);
- wrapperServiceStatus.currentState = (int)State.SERVICE_STOP_PENDING;
- SetServiceStatus(handle, ref wrapperServiceStatus);
- }
- private void SignalShutdownComplete()
- {
- IntPtr handle = this.ServiceHandle;
- wrapperServiceStatus.checkPoint++;
- // WriteEvent("SignalShutdownComplete " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint);
- wrapperServiceStatus.currentState = (int)State.SERVICE_STOPPED;
- SetServiceStatus(handle, ref wrapperServiceStatus);
- }
- private void StartProcess(Process process, string arguments, String executable)
- {
- var ps = process.StartInfo;
- ps.FileName = executable;
- ps.Arguments = arguments;
- 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)
- System.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)
- process.Start();
- WriteEvent("Started " + process.Id);
- // monitor the completion of the process
- new Thread(delegate()
- {
- string msg = process.Id + " - " + process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- process.WaitForExit();
- try
- {
- if (orderlyShutdown)
- {
- LogEvent("Child process [" + msg + "] terminated with " + process.ExitCode, EventLogEntryType.Information);
- }
- else
- {
- LogEvent("Child process [" + msg + "] terminated with " + process.ExitCode, EventLogEntryType.Warning);
- Environment.Exit(process.ExitCode);
- }
- }
- catch (InvalidOperationException ioe)
- {
- LogEvent("WaitForExit " + ioe.Message);
- }
- try
- {
- process.Dispose();
- }
- catch (InvalidOperationException ioe)
- {
- LogEvent("Dispose " + ioe.Message);
- }
- }).Start();
- }
- 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);
- }
- public static void Run(string[] args)
- {
- if (args.Length > 0)
- {
- var d = new ServiceDescriptor();
- Win32Services svc = new WmiRoot().GetCollection<Win32Services>();
- Win32Service s = svc.Select(d.Id);
- args[0] = args[0].ToLower();
- if (args[0] == "install")
- {
- svc.Create(
- d.Id,
- d.Caption,
- ServiceDescriptor.ExecutablePath,
- WMI.ServiceType.OwnProcess,
- ErrorControl.UserNotified,
- StartMode.Automatic,
- d.Interactive,
- 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);
- }
- 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] == "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);
- Thread.Sleep(1000);
- wsvc.OnStop();
- }
- return;
- }
- ServiceBase.Run(new WrapperService());
- }
- }
- }
|