Fix XML queries

pull/724/head
NextTurn 2020-09-04 00:00:00 +08:00 committed by Next Turn
parent 822397e263
commit 04f6c30d7c
2 changed files with 61 additions and 158 deletions

View File

@ -20,6 +20,7 @@ namespace WinSW
{ {
protected readonly XmlDocument dom = new XmlDocument(); protected readonly XmlDocument dom = new XmlDocument();
private readonly XmlNode root;
private readonly Dictionary<string, string> environmentVariables; private readonly Dictionary<string, string> environmentVariables;
internal static XmlServiceConfig? TestConfig; internal static XmlServiceConfig? TestConfig;
@ -63,6 +64,8 @@ namespace WinSW
throw new InvalidDataException(e.Message, e); throw new InvalidDataException(e.Message, e);
} }
this.root = this.dom.SelectSingleNode(Names.Service) ?? throw new InvalidDataException("<" + Names.Service + "> is missing in configuration XML");
// register the base directory as environment variable so that future expansions can refer to this. // register the base directory as environment variable so that future expansions can refer to this.
Environment.SetEnvironmentVariable("BASE", baseDir); Environment.SetEnvironmentVariable("BASE", baseDir);
@ -86,7 +89,7 @@ namespace WinSW
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
{ {
this.dom = dom; this.dom = dom;
this.root = this.dom.SelectSingleNode(Names.Service) ?? throw new InvalidDataException("<" + Names.Service + "> is missing in configuration XML");
this.environmentVariables = this.LoadEnvironmentVariables(); this.environmentVariables = this.LoadEnvironmentVariables();
} }
@ -120,30 +123,25 @@ namespace WinSW
private string SingleElement(string tagName) private string SingleElement(string tagName)
{ {
return this.SingleElement(tagName, false)!; return this.SingleElementOrNull(tagName) ?? throw new InvalidDataException("<" + tagName + "> is missing in configuration XML");
} }
private string? SingleElement(string tagName, bool optional) private string? SingleElementOrNull(string tagName)
{ {
XmlNode? n = this.dom.SelectSingleNode("//" + tagName); XmlNode? n = this.root.SelectSingleNode(tagName);
if (n is null && !optional)
{
throw new InvalidDataException("<" + tagName + "> is missing in configuration XML");
}
return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText);
} }
private bool SingleBoolElement(string tagName, bool defaultValue) private bool SingleBoolElementOrDefault(string tagName, bool defaultValue)
{ {
XmlNode? e = this.dom.SelectSingleNode("//" + tagName); XmlNode? e = this.root.SelectSingleNode(tagName);
return e is null ? defaultValue : bool.Parse(e.InnerText); return e is null ? defaultValue : bool.Parse(e.InnerText);
} }
private TimeSpan SingleTimeSpanElement(XmlNode parent, string tagName, TimeSpan defaultValue) private TimeSpan SingleTimeSpanElement(XmlNode parent, string tagName, TimeSpan defaultValue)
{ {
string? value = this.SingleElement(tagName, true); string? value = this.SingleElementOrNull(tagName);
return value is null ? defaultValue : ParseTimeSpan(value); return value is null ? defaultValue : ParseTimeSpan(value);
} }
@ -167,48 +165,27 @@ namespace WinSW
/// </summary> /// </summary>
public override string Executable => this.SingleElement("executable"); public override string Executable => this.SingleElement("executable");
public override bool HideWindow => this.SingleBoolElement("hidewindow", base.HideWindow); public override bool HideWindow => this.SingleBoolElementOrDefault("hidewindow", base.HideWindow);
/// <summary> /// <summary>
/// Optionally specify a different Path to an executable to shutdown the service. /// Optionally specify a different Path to an executable to shutdown the service.
/// </summary> /// </summary>
public override string? StopExecutable => this.SingleElement("stopexecutable", true); public override string? StopExecutable => this.SingleElementOrNull("stopexecutable");
/// <summary> /// <summary>
/// The <c>arguments</c> element. /// The <c>arguments</c> element.
/// </summary> /// </summary>
public override string Arguments public override string Arguments => this.SingleElementOrNull("arguments") ?? base.Arguments;
{
get
{
XmlNode? argumentsNode = this.dom.SelectSingleNode("//arguments");
return argumentsNode is null ? base.Arguments : Environment.ExpandEnvironmentVariables(argumentsNode.InnerText);
}
}
/// <summary> /// <summary>
/// The <c>startarguments</c> element. /// The <c>startarguments</c> element.
/// </summary> /// </summary>
public override string? StartArguments public override string? StartArguments => this.SingleElementOrNull("startarguments");
{
get
{
XmlNode? startArgumentsNode = this.dom.SelectSingleNode("//startarguments");
return startArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(startArgumentsNode.InnerText);
}
}
/// <summary> /// <summary>
/// The <c>stoparguments</c> element. /// The <c>stoparguments</c> element.
/// </summary> /// </summary>
public override string? StopArguments public override string? StopArguments => this.SingleElementOrNull("stoparguments");
{
get
{
XmlNode? stopArgumentsNode = this.dom.SelectSingleNode("//stoparguments");
return stopArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(stopArgumentsNode.InnerText);
}
}
public ProcessCommand Prestart => this.GetProcessCommand(Names.Prestart); public ProcessCommand Prestart => this.GetProcessCommand(Names.Prestart);
@ -222,7 +199,7 @@ namespace WinSW
{ {
get get
{ {
var wd = this.SingleElement("workingdirectory", true); string? wd = this.SingleElementOrNull("workingdirectory");
return string.IsNullOrEmpty(wd) ? base.WorkingDirectory : wd!; return string.IsNullOrEmpty(wd) ? base.WorkingDirectory : wd!;
} }
} }
@ -248,64 +225,12 @@ namespace WinSW
} }
} }
public override XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions"); public override XmlNode? ExtensionsConfiguration => this.root.SelectSingleNode("extensions");
/// <summary>
/// Combines the contents of all the elements of the given name,
/// or return null if no element exists. Handles whitespace quotation.
/// </summary>
private string? AppendTags(string tagName, string? defaultValue = null)
{
XmlNode? argumentNode = this.dom.SelectSingleNode("//" + tagName);
if (argumentNode is null)
{
return defaultValue;
}
StringBuilder arguments = new StringBuilder();
XmlNodeList argumentNodeList = this.dom.SelectNodes("//" + tagName)!;
for (int i = 0; i < argumentNodeList.Count; i++)
{
arguments.Append(' ');
string token = Environment.ExpandEnvironmentVariables(argumentNodeList[i]!.InnerText);
if (token.StartsWith("\"") && token.EndsWith("\""))
{
// for backward compatibility, if the argument is already quoted, leave it as is.
// in earlier versions we didn't handle quotation, so the user might have worked
// around it by themselves
}
else
{
if (token.Contains(" "))
{
arguments.Append('"').Append(token).Append('"');
continue;
}
}
arguments.Append(token);
}
return arguments.ToString();
}
/// <summary> /// <summary>
/// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
/// </summary> /// </summary>
public override string LogDirectory public override string LogDirectory => this.SingleElementOrNull("logpath") ?? base.LogDirectory;
{
get
{
XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath");
return loggingNode is null
? base.LogDirectory
: Environment.ExpandEnvironmentVariables(loggingNode.InnerText);
}
}
public override string LogMode public override string LogMode
{ {
@ -314,7 +239,7 @@ namespace WinSW
string? mode = null; string? mode = null;
// first, backward compatibility with older configuration // first, backward compatibility with older configuration
XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); XmlElement? e = (XmlElement?)this.root.SelectSingleNode("logmode");
if (e != null) if (e != null)
{ {
mode = e.InnerText; mode = e.InnerText;
@ -322,7 +247,7 @@ namespace WinSW
else else
{ {
// this is more modern way, to support nested elements as configuration // this is more modern way, to support nested elements as configuration
e = (XmlElement?)this.dom.SelectSingleNode("//log"); e = (XmlElement?)this.root.SelectSingleNode("log");
if (e != null) if (e != null)
{ {
mode = e.GetAttribute("mode"); mode = e.GetAttribute("mode");
@ -333,48 +258,24 @@ namespace WinSW
} }
} }
public string LogName public string LogName => this.SingleElementOrNull("logname") ?? this.BaseName;
{
get
{
XmlNode? loggingName = this.dom.SelectSingleNode("//logname");
return loggingName is null ? this.BaseName : Environment.ExpandEnvironmentVariables(loggingName.InnerText); public override bool OutFileDisabled => this.SingleBoolElementOrDefault("outfiledisabled", base.OutFileDisabled);
}
}
public override bool OutFileDisabled => this.SingleBoolElement("outfiledisabled", base.OutFileDisabled); public override bool ErrFileDisabled => this.SingleBoolElementOrDefault("errfiledisabled", base.ErrFileDisabled);
public override bool ErrFileDisabled => this.SingleBoolElement("errfiledisabled", base.ErrFileDisabled); public override string OutFilePattern => this.SingleElementOrNull("outfilepattern") ?? base.OutFilePattern;
public override string OutFilePattern public override string ErrFilePattern => this.SingleElementOrNull("errfilepattern") ?? base.ErrFilePattern;
{
get
{
XmlNode? loggingName = this.dom.SelectSingleNode("//outfilepattern");
return loggingName is null ? base.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText);
}
}
public override string ErrFilePattern
{
get
{
XmlNode? loggingName = this.dom.SelectSingleNode("//errfilepattern");
return loggingName is null ? base.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText);
}
}
public LogHandler LogHandler public LogHandler LogHandler
{ {
get get
{ {
XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); XmlElement? e = (XmlElement?)this.root.SelectSingleNode("logmode");
// this is more modern way, to support nested elements as configuration // this is more modern way, to support nested elements as configuration
e ??= (XmlElement?)this.dom.SelectSingleNode("//log")!; // WARNING: NRE e ??= (XmlElement?)this.root.SelectSingleNode("log")!; // WARNING: NRE
int sizeThreshold; int sizeThreshold;
switch (this.LogMode) switch (this.LogMode)
@ -462,7 +363,7 @@ namespace WinSW
{ {
get get
{ {
XmlNodeList? nodeList = this.dom.SelectNodes("//depend"); XmlNodeList? nodeList = this.root.SelectNodes("depend");
if (nodeList is null) if (nodeList is null)
{ {
return base.ServiceDependencies; return base.ServiceDependencies;
@ -480,9 +381,9 @@ namespace WinSW
public override string Name => this.SingleElement("id"); public override string Name => this.SingleElement("id");
public override string DisplayName => this.SingleElement("name", true) ?? base.DisplayName; public override string DisplayName => this.SingleElementOrNull("name") ?? base.DisplayName;
public override string Description => this.SingleElement("description", true) ?? base.Description; public override string Description => this.SingleElementOrNull("description") ?? base.Description;
/// <summary> /// <summary>
/// Start mode of the Service /// Start mode of the Service
@ -491,7 +392,7 @@ namespace WinSW
{ {
get get
{ {
string? p = this.SingleElement("startmode", true); string? p = this.SingleElementOrNull("startmode");
if (p is null) if (p is null)
{ {
return base.StartMode; return base.StartMode;
@ -519,15 +420,15 @@ namespace WinSW
/// True if the service should be installed with the DelayedAutoStart flag. /// True if the service should be installed with the DelayedAutoStart flag.
/// This setting will be applyed only during the install command and only when the Automatic start mode is configured. /// This setting will be applyed only during the install command and only when the Automatic start mode is configured.
/// </summary> /// </summary>
public override bool DelayedAutoStart => this.SingleBoolElement("delayedAutoStart", base.DelayedAutoStart); public override bool DelayedAutoStart => this.SingleBoolElementOrDefault("delayedAutoStart", base.DelayedAutoStart);
public override bool Preshutdown => this.SingleBoolElement("preshutdown", base.Preshutdown); public override bool Preshutdown => this.SingleBoolElementOrDefault("preshutdown", base.Preshutdown);
public TimeSpan? PreshutdownTimeout public TimeSpan? PreshutdownTimeout
{ {
get get
{ {
string? value = this.SingleElement("preshutdownTimeout", true); string? value = this.SingleElementOrNull("preshutdownTimeout");
return value is null ? default : ParseTimeSpan(value); return value is null ? default : ParseTimeSpan(value);
} }
} }
@ -536,12 +437,12 @@ namespace WinSW
/// True if the service should beep when finished on shutdown. /// True if the service should beep when finished on shutdown.
/// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx
/// </summary> /// </summary>
public override bool BeepOnShutdown => this.SingleBoolElement("beeponshutdown", base.DelayedAutoStart); public override bool BeepOnShutdown => this.SingleBoolElementOrDefault("beeponshutdown", base.DelayedAutoStart);
/// <summary> /// <summary>
/// True if the service can interact with the desktop. /// True if the service can interact with the desktop.
/// </summary> /// </summary>
public override bool Interactive => this.SingleBoolElement("interactive", base.DelayedAutoStart); public override bool Interactive => this.SingleBoolElementOrDefault("interactive", base.DelayedAutoStart);
/// <summary> /// <summary>
/// Environment variable overrides /// Environment variable overrides
@ -556,7 +457,7 @@ namespace WinSW
{ {
get get
{ {
XmlNodeList? nodeList = this.dom.SelectNodes("//download"); XmlNodeList? nodeList = this.root.SelectNodes("download");
if (nodeList is null) if (nodeList is null)
{ {
return base.Downloads; return base.Downloads;
@ -579,7 +480,7 @@ namespace WinSW
{ {
get get
{ {
XmlNodeList? childNodes = this.dom.SelectNodes("//onfailure"); XmlNodeList? childNodes = this.root.SelectNodes("onfailure");
if (childNodes is null) if (childNodes is null)
{ {
return Array.Empty<SC_ACTION>(); return Array.Empty<SC_ACTION>();
@ -605,11 +506,11 @@ namespace WinSW
} }
} }
public override TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.dom, "resetfailure", base.ResetFailureAfter); public override TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.root, "resetfailure", base.ResetFailureAfter);
protected string? GetServiceAccountPart(string subNodeName) protected string? GetServiceAccountPart(string subNodeName)
{ {
XmlNode? node = this.dom.SelectSingleNode("//serviceaccount"); XmlNode? node = this.root.SelectSingleNode("serviceaccount");
if (node != null) if (node != null)
{ {
@ -633,7 +534,7 @@ namespace WinSW
public bool HasServiceAccount() public bool HasServiceAccount()
{ {
return this.dom.SelectSingleNode("//serviceaccount") != null; return this.root.SelectSingleNode("serviceaccount") != null;
} }
public override bool AllowServiceAcountLogonRight public override bool AllowServiceAcountLogonRight
@ -655,7 +556,7 @@ namespace WinSW
/// <summary> /// <summary>
/// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it
/// </summary> /// </summary>
public override TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", base.StopTimeout); public override TimeSpan StopTimeout => this.SingleTimeSpanElement(this.root, "stoptimeout", base.StopTimeout);
public int StopTimeoutInMs => (int)this.StopTimeout.TotalMilliseconds; public int StopTimeoutInMs => (int)this.StopTimeout.TotalMilliseconds;
@ -666,7 +567,7 @@ namespace WinSW
{ {
get get
{ {
string? p = this.SingleElement("priority", true); string? p = this.SingleElementOrNull("priority");
if (p is null) if (p is null)
{ {
return base.Priority; return base.Priority;
@ -676,13 +577,13 @@ namespace WinSW
} }
} }
public string? SecurityDescriptor => this.SingleElement("securityDescriptor", true); public string? SecurityDescriptor => this.SingleElementOrNull("securityDescriptor");
public bool AutoRefresh => this.SingleBoolElement("autoRefresh", true); public bool AutoRefresh => this.SingleBoolElementOrDefault("autoRefresh", true);
private Dictionary<string, string> LoadEnvironmentVariables() private Dictionary<string, string> LoadEnvironmentVariables()
{ {
XmlNodeList nodeList = this.dom.SelectNodes("//env")!; XmlNodeList nodeList = this.root.SelectNodes("env")!;
Dictionary<string, string> environment = new Dictionary<string, string>(nodeList.Count); Dictionary<string, string> environment = new Dictionary<string, string>(nodeList.Count);
for (int i = 0; i < nodeList.Count; i++) for (int i = 0; i < nodeList.Count; i++)
{ {
@ -699,7 +600,7 @@ namespace WinSW
private ProcessCommand GetProcessCommand(string name) private ProcessCommand GetProcessCommand(string name)
{ {
XmlNode? node = this.dom.SelectSingleNode(Names.Service)?.SelectSingleNode(name); XmlNode? node = this.root.SelectSingleNode(name);
return node is null ? default : new ProcessCommand return node is null ? default : new ProcessCommand
{ {
Executable = GetInnerText(Names.Executable), Executable = GetInnerText(Names.Executable),

View File

@ -355,24 +355,24 @@ $@"<service>
}; };
var poststart = new ProcessCommand var poststart = new ProcessCommand
{ {
Executable = "a1", Executable = "b1",
Arguments = "a2", Arguments = "b2",
StdoutPath = "a3", StdoutPath = "b3",
StderrPath = "a4", StderrPath = "b4",
}; };
var prestop = new ProcessCommand var prestop = new ProcessCommand
{ {
Executable = "a1", Executable = "c1",
Arguments = "a2", Arguments = "c2",
StdoutPath = "a3", StdoutPath = "c3",
StderrPath = "a4", StderrPath = "c4",
}; };
var poststop = new ProcessCommand var poststop = new ProcessCommand
{ {
Executable = "a1", Executable = "d1",
Arguments = "a2", Arguments = "d2",
StdoutPath = "a3", StdoutPath = "d3",
StderrPath = "a4", StderrPath = "d4",
}; };
string seedXml = string seedXml =
@ -401,6 +401,8 @@ $@"<service>
<stdoutPath>{poststop.StdoutPath}</stdoutPath> <stdoutPath>{poststop.StdoutPath}</stdoutPath>
<stderrPath>{poststop.StderrPath}</stderrPath> <stderrPath>{poststop.StderrPath}</stderrPath>
</poststop> </poststop>
<executable>executable</executable>
<arguments>arguments</arguments>
</service>"; </service>";
XmlServiceConfig config = XmlServiceConfig.FromXml(seedXml); XmlServiceConfig config = XmlServiceConfig.FromXml(seedXml);