diff --git a/ATxCommon/Serializables/ServiceConfig.cs b/ATxCommon/Serializables/ServiceConfig.cs index acc1bc8a88b3355f256b95146b8ec9037ba06538..c7e622a57aba46aba2317eb4a3db11947444b6e1 100644 --- a/ATxCommon/Serializables/ServiceConfig.cs +++ b/ATxCommon/Serializables/ServiceConfig.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Configuration; using System.IO; using System.Xml; +using System.Xml.Linq; using System.Xml.Serialization; using NLog; @@ -16,35 +17,6 @@ namespace ATxCommon.Serializables { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public ServiceConfig() { - Log.Trace("ServiceConfig() constructor, setting defaults."); - // set values for the optional XML elements: - SmtpPort = 25; - GraceNotificationDelta = 720; - - EnforceInheritedACLs = true; - } - - /// <summary> - /// Dummy method raising an exception (this class must not be serialized). - /// </summary> - public static void Serialize(string file, ServiceConfig c) { - // the config is never meant to be written by us, therefore: - throw new SettingsPropertyIsReadOnlyException("The config file must not be written by the service!"); - } - - public static ServiceConfig Deserialize(string file) { - Log.Debug("Trying to read service configuration XML file: [{0}]", file); - var serializer = new XmlSerializer(typeof(ServiceConfig)); - ServiceConfig config; - using (var reader = XmlReader.Create(file)) { - config = (ServiceConfig) serializer.Deserialize(reader); - } - ValidateConfiguration(config); - Log.Debug("Finished deserializing service configuration XML file."); - return config; - } - #region required configuration parameters @@ -53,11 +25,6 @@ namespace ATxCommon.Serializables /// </summary> public string HostAlias { get; set; } - /// <summary> - /// A human friendly name for the target, to be used in emails etc. - /// </summary> - public string DestinationAlias { get; set; } - /// <summary> /// The base drive for the spooling directories (incoming and managed). /// </summary> @@ -68,18 +35,17 @@ namespace ATxCommon.Serializables /// </summary> public string IncomingDirectory { get; set; } - /// <summary> - /// The name of a marker file to be placed in all **sub**directories - /// inside the IncomingDirectory. - /// </summary> - public string MarkerFile { get; set; } - /// <summary> /// A directory on SourceDrive to hold the three subdirectories "DONE", /// "PROCESSING" and "UNMATCHED" used during and after transfers. /// </summary> public string ManagedDirectory { get; set; } + /// <summary> + /// A human friendly name for the target, to be used in emails etc. + /// </summary> + public string DestinationAlias { get; set; } + /// <summary> /// Target path to transfer files to. Usually a UNC location. /// </summary> @@ -91,78 +57,85 @@ namespace ATxCommon.Serializables /// </summary> public string TmpTransferDir { get; set; } - /// <summary> - /// The email address to be used as "From:" when sending mail notifications. - /// </summary> - public string EmailFrom { get; set; } - - /// <summary> - /// The interval (in ms) for checking for new files and system parameters. - /// </summary> - public int ServiceTimer { get; set; } - /// <summary> /// Maximum allowed CPU usage across all cores in percent. Running transfers will be paused /// if this limit is exceeded. /// </summary> public int MaxCpuUsage { get; set; } - + /// <summary> /// Minimum amount of free RAM (in MB) required for the service to operate. /// </summary> public int MinAvailableMemory { get; set; } + #endregion + + + #region optional configuration parameters + /// <summary> - /// Minimum amount of time in minutes between two mail notifications to the admin address. + /// Switch on debug log messages. Default: false. /// </summary> - public int AdminNotificationDelta { get; set; } + public bool Debug { get; set; } = false; /// <summary> - /// Minimum amount of time in minutes between two low-storage-space notifications. + /// The interval (in ms) for checking for new files and system parameters. Default: 1000. /// </summary> - public int StorageNotificationDelta { get; set; } + public int ServiceTimer { get; set; } = 1000; /// <summary> - /// GracePeriod: number of days after data in the "DONE" location expires, - /// which will trigger a summary email to the admin address. + /// The name of a marker file to be placed in all **sub**directories + /// inside the IncomingDirectory. /// </summary> - public int GracePeriod { get; set; } + public string MarkerFile { get; set; } /// <summary> - /// Flag whether to send explicit mail notifications to the admin on selected events. + /// Number of days after data in the "DONE" location expires. Default: 30. /// </summary> - public bool SendAdminNotification { get; set; } - + public int GracePeriod { get; set; } = 30; + /// <summary> - /// Flag whether to send a mail notification to the user upon completed transfers. + /// Whether to enforce ACL inheritance when moving files and directories, see + /// https://support.microsoft.com/en-us/help/320246 for more details. Default: false. /// </summary> - public bool SendTransferNotification { get; set; } + // ReSharper disable once InconsistentNaming + public bool EnforceInheritedACLs { get; set; } = false; /// <summary> - /// Switch on debug log messages. + /// Limit RoboCopy transfer bandwidth (mostly for testing purposes). Default: 0. /// </summary> - public bool Debug { get; set; } - - [XmlArray] - [XmlArrayItem(ElementName = "DriveToCheck")] - public List<DriveToCheck> SpaceMonitoring { get; set; } + /// See the RoboCopy documentation for more details. + public int InterPacketGap { get; set; } = 0; + /// <summary> + /// A list of process names causing transfers to be suspended if running. + /// </summary> [XmlArray] [XmlArrayItem(ElementName = "ProcessName")] public List<string> BlacklistedProcesses { get; set; } + /// <summary> + /// A list of drives and thresholds to monitor free space. + /// </summary> + [XmlArray] + [XmlArrayItem(ElementName = "DriveToCheck")] + public List<DriveToCheck> SpaceMonitoring { get; set; } + #endregion - #region optional configuration parameters + #region optional configuration parameters - notification settings /// <summary> - /// SMTP server used to send mails (if configured) and Fatal/Error log messages. - /// - /// No mails will be sent if this is omitted. + /// SMTP server to send mails and Fatal/Error log messages. No mails if omitted. /// </summary> public string SmtpHost { get; set; } + /// <summary> + /// SMTP port for sending emails. Default: 25. + /// </summary> + public int SmtpPort { get; set; } = 25; + /// <summary> /// SMTP username to authenticate when sending emails (if required). /// </summary> @@ -174,45 +147,53 @@ namespace ATxCommon.Serializables public string SmtpPasswortCredential { get; set; } /// <summary> - /// SMTP port for sending emails (25 will be used if this entry is omitted). + /// The email address to be used as "From:" when sending mail notifications. /// </summary> - public int SmtpPort { get; set; } + public string EmailFrom { get; set; } /// <summary> - /// A string to be added as a prefix to the subject when sending emails. + /// A prefix to be added to any email subject. Default: "[AutoTx Service] ". /// </summary> - public string EmailPrefix { get; set; } + public string EmailPrefix { get; set; } = "[AutoTx Service] "; /// <summary> /// The mail recipient address for admin notifications (including "Fatal" log messages). /// </summary> public string AdminEmailAdress { get; set; } - + /// <summary> /// The mail recipient address for debug notifications (including "Error" log messages). /// </summary> public string AdminDebugEmailAdress { get; set; } /// <summary> - /// Minimum time in minutes between two mails about expired folders in the grace location. + /// Send an email to the user upon completed transfers. Default: true. /// </summary> - public int GraceNotificationDelta { get; set; } + public bool SendTransferNotification { get; set; } = true; /// <summary> - /// RoboCopy parameter for limiting the bandwidth (mostly for testing purposes). + /// Send email notifications to the admin on selected events. Default: true. /// </summary> - /// See the RoboCopy documentation for more details. - public int InterPacketGap { get; set; } + public bool SendAdminNotification { get; set; } = true; + + /// <summary> + /// Minimum time in minutes between two notifications to the admin. Default: 60. + /// </summary> + public int AdminNotificationDelta { get; set; } = 60; + + /// <summary> + /// Minimum time in minutes between two mails about expired folders. Default: 720 (12h). + /// </summary> + public int GraceNotificationDelta { get; set; } = 720; /// <summary> - /// EnforceInheritedACLs: whether to enforce ACL inheritance when moving files and - /// directories, see https://support.microsoft.com/en-us/help/320246 for more details. + /// Minimum time in minutes between two low-space notifications. Default: 720 (12h). /// </summary> - public bool EnforceInheritedACLs { get; set; } + public int StorageNotificationDelta { get; set; } = 720; #endregion - + #region wrappers for derived parameters /// <summary> @@ -248,55 +229,189 @@ namespace ATxCommon.Serializables #endregion + /// <summary> + /// ServiceConfig constructor, currently empty. + /// </summary> + private ServiceConfig() { + Log.Trace("ServiceConfig() constructor."); + } + + /// <summary> + /// Dummy method raising an exception (this class must not be serialized). + /// </summary> + public static void Serialize(string file, ServiceConfig c) { + // the config is never meant to be written by us, therefore: + throw new SettingsPropertyIsReadOnlyException("The config file must not be written by the service!"); + } + + /// <summary> + /// Load the host specific and the common XML configuration files, combine them and + /// deserialize them into a ServiceConfig object. The host specific configuration file's + /// name is defined as the hostname with an ".xml" suffix. + /// </summary> + /// <param name="path">The path to the configuration files.</param> + /// <returns>A ServiceConfig object with validated settings.</returns> + public static ServiceConfig Deserialize(string path) { + Log.Trace("ServiceConfig.Deserialize({0})", path); + ServiceConfig config; + + var commonFile = Path.Combine(path, "config.common.xml"); + var specificFile = Path.Combine(path, Environment.MachineName + ".xml"); + + // for parsing the configuration from two separate files we are using the default + // behaviour of the .NET XmlSerializer on duplicates: only the first occurrence is + // used, all other ones are silentley being discarded - this way we simply append the + // contents of the common config file to the host-specific and deserialize then: + Log.Debug("Loading host specific configuration XML file: [{0}]", specificFile); + var combined = XElement.Load(specificFile); + // the common configuration file is optional, so check if it exists at all: + if (File.Exists(commonFile)) { + Log.Debug("Loading common configuration XML file: [{0}]", commonFile); + var common = XElement.Load(commonFile); + combined.Add(common.Nodes()); + Log.Trace("Combined XML structure:\n\n{0}\n\n", combined); + } + + using (var reader = XmlReader.Create(new StringReader(combined.ToString()))) { + Log.Debug("Trying to parse combined XML..."); + var serializer = new XmlSerializer(typeof(ServiceConfig)); + config = (ServiceConfig) serializer.Deserialize(reader); + } + + ValidateConfiguration(config); + + Log.Debug("Successfully parsed a valid configuration from [{0}].", path); + return config; + } + /// <summary> /// Validate the configuration, throwing exceptions on invalid parameters. /// </summary> private static void ValidateConfiguration(ServiceConfig c) { - if (string.IsNullOrEmpty(c.SourceDrive) || - string.IsNullOrEmpty(c.IncomingDirectory) || - string.IsNullOrEmpty(c.ManagedDirectory)) - throw new ConfigurationErrorsException("mandatory parameter missing!"); + Log.Debug("Validating configuration..."); + var errmsg = ""; + + string CheckEmpty(string value, string name) { + // if the string is null terminate the validation immediately since this means the + // file doesn't contain a required parameter at all: + if (value == null) { + var msg = $"mandatory parameter missing: <{name}>"; + Log.Error(msg); + throw new ConfigurationErrorsException(msg); + } + + if (string.IsNullOrWhiteSpace(value)) + return $"mandatory parameter unset: <{name}>\n"; + + return string.Empty; + } - if (c.SourceDrive.Substring(1) != @":\") - throw new ConfigurationErrorsException("SourceDrive must be a drive " + - @"letter followed by a colon and a backslash, e.g. 'D:\'!"); + string CheckMinValue(int value, string name, int min) { + if (value == 0) + return $"<{name}> is unset (or set to 0), minimal accepted value is {min}\n"; + + if (value < min) + return $"<{name}> must not be smaller than {min}\n"; + + return string.Empty; + } + + string CheckLocalDrive(string value, string name) { + var driveType = new DriveInfo(value).DriveType; + if (driveType != DriveType.Fixed) + return $"<{name}> ({value}) must be a local fixed drive, not '{driveType}'!\n"; + return string.Empty; + } + + void WarnOnHighValue(int value, string name, int thresh) { + if (value > thresh) + SubOptimal(value.ToString(), name, "value is set very high, please check!"); + } + + void SubOptimal(string value, string name, string msg) { + Log.Warn(">>> Sub-optimal setting detected: <{0}> [{1}] {2}", name, value, msg); + } + + void LogAndThrow(string msg) { + msg = $"Configuration issues detected:\n{msg}"; + Log.Error(msg); + throw new ConfigurationErrorsException(msg); + } + + // check if all required parameters are there and non-empty / non-zero: + errmsg += CheckEmpty(c.HostAlias, nameof(c.HostAlias)); + errmsg += CheckEmpty(c.SourceDrive, nameof(c.SourceDrive)); + errmsg += CheckEmpty(c.IncomingDirectory, nameof(c.IncomingDirectory)); + errmsg += CheckEmpty(c.ManagedDirectory, nameof(c.ManagedDirectory)); + errmsg += CheckEmpty(c.DestinationAlias, nameof(c.DestinationAlias)); + errmsg += CheckEmpty(c.DestinationDirectory, nameof(c.DestinationDirectory)); + errmsg += CheckEmpty(c.TmpTransferDir, nameof(c.TmpTransferDir)); + + errmsg += CheckMinValue(c.ServiceTimer, nameof(c.ServiceTimer), 1000); + errmsg += CheckMinValue(c.MaxCpuUsage, nameof(c.MaxCpuUsage), 5); + errmsg += CheckMinValue(c.MinAvailableMemory, nameof(c.MinAvailableMemory), 256); + + // if any of the required parameter checks failed we terminate now as many of the + // string checks below would fail on empty strings: + if (!string.IsNullOrWhiteSpace(errmsg)) + LogAndThrow(errmsg); - // make sure SourceDrive is a local (fixed) disk: - var driveInfo = new DriveInfo(c.SourceDrive); - if (driveInfo.DriveType != DriveType.Fixed) - throw new ConfigurationErrorsException("SourceDrive (" + c.SourceDrive + - ") must be a local (fixed) drive, OS reports '" + - driveInfo.DriveType + "')!"); + ////////// REQUIRED PARAMETERS SETTINGS VALIDATION ////////// + + // SourceDrive + if (c.SourceDrive.Substring(1) != @":\") + errmsg += "<SourceDrive> must be of form [X:\\]\n!"; + errmsg += CheckLocalDrive(c.SourceDrive, nameof(c.SourceDrive)); // spooling directories: IncomingDirectory + ManagedDirectory if (c.IncomingDirectory.StartsWith(@"\")) - throw new ConfigurationErrorsException("IncomingDirectory must not start with a backslash!"); + errmsg += "<IncomingDirectory> must not start with a backslash!\n"; if (c.ManagedDirectory.StartsWith(@"\")) - throw new ConfigurationErrorsException("ManagedDirectory must not start with a backslash!"); + errmsg += "<ManagedDirectory> must not start with a backslash!\n"; + // DestinationDirectory if (!Directory.Exists(c.DestinationDirectory)) - throw new ConfigurationErrorsException("can't find destination: " + c.DestinationDirectory); + errmsg += $"can't find (or reach) destination: {c.DestinationDirectory}\n"; + // TmpTransferDir var tmpTransferPath = Path.Combine(c.DestinationDirectory, c.TmpTransferDir); if (!Directory.Exists(tmpTransferPath)) - throw new ConfigurationErrorsException("temporary transfer dir doesn't exist: " + tmpTransferPath); + errmsg += $"can't find (or reach) temporary transfer dir: {tmpTransferPath}\n"; - if (c.ServiceTimer < 1000) - throw new ConfigurationErrorsException("ServiceTimer must not be smaller than 1000 ms!"); + ////////// OPTIONAL PARAMETERS SETTINGS VALIDATION ////////// - // NON-CRITICAL stuff is simply reported to the logs: - if (!c.DestinationDirectory.StartsWith(@"\\")) { - ReportNonOptimal("DestinationDirectory", c.DestinationDirectory, "is not a UNC path!"); + // EmailFrom + if (!string.IsNullOrWhiteSpace(c.SmtpHost) && + string.IsNullOrWhiteSpace(c.EmailFrom)) + errmsg += "<EmailFrom> must not be empty if <SmtpHost> is configured!\n"; + + // DriveName + foreach (var driveToCheck in c.SpaceMonitoring) { + errmsg += CheckLocalDrive(driveToCheck.DriveName, nameof(driveToCheck.DriveName)); } - } - /// <summary> - /// Print a standardized msg about a non-optimal configuration setting to the log. - /// </summary> - private static void ReportNonOptimal(string attribute, string value, string msg) { - Log.Warn(">>> Non-optimal setting detected: <{0}> [{1}] {2}", attribute, value, msg); + + ////////// WEAK CHECKS ON PARAMETERS SETTINGS ////////// + // those checks are non-critical and are simply reported to the logs + + WarnOnHighValue(c.ServiceTimer, nameof(c.ServiceTimer), 10000); + WarnOnHighValue(c.MaxCpuUsage, nameof(c.MaxCpuUsage), 75); + WarnOnHighValue(c.MinAvailableMemory, nameof(c.MinAvailableMemory), 8192); + WarnOnHighValue(c.AdminNotificationDelta, nameof(c.AdminNotificationDelta), 1440); + WarnOnHighValue(c.GraceNotificationDelta, nameof(c.GraceNotificationDelta), 10080); + WarnOnHighValue(c.StorageNotificationDelta, nameof(c.StorageNotificationDelta), 10080); + WarnOnHighValue(c.GracePeriod, nameof(c.GracePeriod), 100); + + if (!c.DestinationDirectory.StartsWith(@"\\")) + SubOptimal(c.DestinationDirectory, "DestinationDirectory", "is not a UNC path!"); + + + if (string.IsNullOrWhiteSpace(errmsg)) + return; + + LogAndThrow(errmsg); } /// <summary> @@ -305,43 +420,64 @@ namespace ATxCommon.Serializables /// <returns>A string with details on the configuration.</returns> public string Summary() { var msg = + "############### REQUIRED PARAMETERS ###############\n" + $"HostAlias: {HostAlias}\n" + $"SourceDrive: {SourceDrive}\n" + $"IncomingDirectory: {IncomingDirectory}\n" + - $"MarkerFile: {MarkerFile}\n" + $"ManagedDirectory: {ManagedDirectory}\n" + - $"GracePeriod: {GracePeriod} (" + - TimeUtils.DaysToHuman(GracePeriod, false) + ")\n" + + $"DestinationAlias: {DestinationAlias}\n" + $"DestinationDirectory: {DestinationDirectory}\n" + $"TmpTransferDir: {TmpTransferDir}\n" + - $"EnforceInheritedACLs: {EnforceInheritedACLs}\n" + + $"MaxCpuUsage: {MaxCpuUsage}%\n" + + $"MinAvailableMemory: {MinAvailableMemory} MB\n" + + "\n" + + "############### OPTIONAL PARAMETERS ###############\n" + + $"Debug: {Debug}\n" + $"ServiceTimer: {ServiceTimer} ms\n" + + $"MarkerFile: {MarkerFile}\n" + + $"GracePeriod: {GracePeriod} days (" + + TimeUtils.DaysToHuman(GracePeriod, false) + ")\n" + + $"EnforceInheritedACLs: {EnforceInheritedACLs}\n" + $"InterPacketGap: {InterPacketGap}\n" + - $"MaxCpuUsage: {MaxCpuUsage}%\n" + - $"MinAvailableMemory: {MinAvailableMemory}\n"; + ""; + + var blacklist = ""; foreach (var processName in BlacklistedProcesses) { - msg += $"BlacklistedProcess: {processName}\n"; + blacklist += $" ProcessName: {processName}\n"; } + if (!string.IsNullOrWhiteSpace(blacklist)) + msg += $"BlacklistedProcesses:\n{blacklist}"; + + + var space = ""; foreach (var drive in SpaceMonitoring) { - msg += $"Drive to check free space: {drive.DriveName} " + + space += $" DriveName: {drive.DriveName} " + $"(threshold: {Conv.MegabytesToString(drive.SpaceThreshold)})\n"; } - if (string.IsNullOrEmpty(SmtpHost)) { + if (!string.IsNullOrWhiteSpace(space)) + msg += $"SpaceMonitoring:\n{space}"; + + if (string.IsNullOrWhiteSpace(SmtpHost)) { msg += "SmtpHost: ====== Not configured, disabling email! ======" + "\n"; } else { msg += $"SmtpHost: {SmtpHost}\n" + + $"SmtpPort: {SmtpPort}\n" + $"SmtpUserCredential: {SmtpUserCredential}\n" + - $"EmailPrefix: {EmailPrefix}\n" + + $"SmtpPasswortCredential: --- not showing ---\n" + $"EmailFrom: {EmailFrom}\n" + + $"EmailPrefix: {EmailPrefix}\n" + $"AdminEmailAdress: {AdminEmailAdress}\n" + $"AdminDebugEmailAdress: {AdminDebugEmailAdress}\n" + - $"StorageNotificationDelta: {StorageNotificationDelta} (" + - TimeUtils.MinutesToHuman(StorageNotificationDelta, false) + ")\n" + - $"AdminNotificationDelta: {AdminNotificationDelta} (" + + $"SendTransferNotification: {SendTransferNotification}\n" + + $"SendAdminNotification: {SendAdminNotification}\n" + + $"AdminNotificationDelta: {AdminNotificationDelta} min (" + TimeUtils.MinutesToHuman(AdminNotificationDelta, false) + ")\n" + - $"GraceNotificationDelta: {GraceNotificationDelta} (" + - TimeUtils.MinutesToHuman(GraceNotificationDelta, false) + ")\n"; + $"GraceNotificationDelta: {GraceNotificationDelta} min (" + + TimeUtils.MinutesToHuman(GraceNotificationDelta, false) + ")\n" + + $"StorageNotificationDelta: {StorageNotificationDelta} min (" + + TimeUtils.MinutesToHuman(StorageNotificationDelta, false) + ")\n" + + ""; } return msg; } diff --git a/ATxCommon/Serializables/ServiceStatus.cs b/ATxCommon/Serializables/ServiceStatus.cs index e7321a7fbd6c0564276d6b9b27c619b936cc186a..a7082c0aa9345ac4adc92c254b162478b43767c2 100644 --- a/ATxCommon/Serializables/ServiceStatus.cs +++ b/ATxCommon/Serializables/ServiceStatus.cs @@ -40,7 +40,7 @@ namespace ATxCommon.Serializables /// <summary> /// The constructor, setting default values. /// </summary> - public ServiceStatus() { + private ServiceStatus() { _currentTransferSrc = ""; _currentTargetTmp = ""; _transferInProgress = false; diff --git a/ATxConfigTest/AutoTxConfigTest.cs b/ATxConfigTest/AutoTxConfigTest.cs index 679659e9ad476f889ca3d26b60c647a3dabf35e8..b4cbd9c2502c56961f749cb1c4f20c1da68a7dea 100644 --- a/ATxConfigTest/AutoTxConfigTest.cs +++ b/ATxConfigTest/AutoTxConfigTest.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using ATxCommon.Serializables; using NLog; using NLog.Config; @@ -9,40 +8,45 @@ namespace ATxConfigTest { internal class AutoTxConfigTest { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private static ServiceConfig _config; - private static ServiceStatus _status; private static void Main(string[] args) { + var logLevel = LogLevel.Info; + var logPrefix = ""; + + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + if (args.Length > 0) + baseDir = args[0]; + + if (args.Length > 1) { + if (args[1] == "debug") { + logLevel = LogLevel.Debug; + logPrefix = @"${date:format=yyyy-MM-dd HH\:mm\:ss} "; + } + if (args[1] == "trace") { + logLevel = LogLevel.Trace; + logPrefix = @"${date:format=yyyy-MM-dd HH\:mm\:ss} (${logger}) "; + } + } + var logConfig = new LoggingConfiguration(); var consoleTarget = new ConsoleTarget { Name = "console", - Layout = @"${date:format=yyyy-MM-dd HH\:mm\:ss} [${level}] (${logger}) ${message}", + Layout = logPrefix + @"[${level}] ${message}", }; logConfig.AddTarget("console", consoleTarget); - var logRuleConsole = new LoggingRule("*", LogLevel.Debug, consoleTarget); + var logRuleConsole = new LoggingRule("*", logLevel, consoleTarget); logConfig.LoggingRules.Add(logRuleConsole); LogManager.Configuration = logConfig; - var baseDir = AppDomain.CurrentDomain.BaseDirectory; - if (args.Length > 0) - baseDir = args[0]; - - var configPath = Path.Combine(baseDir, "configuration.xml"); - var statusPath = Path.Combine(baseDir, "status.xml"); + const string mark = "----------------------------"; try { - string msg; - Console.WriteLine($"\nTrying to parse configuration file [{configPath}]...\n"); - _config = ServiceConfig.Deserialize(configPath); - msg = "------------------ configuration settings ------------------"; - Console.WriteLine($"{msg}\n{_config.Summary()}{msg}\n"); - - Console.WriteLine($"\nTrying to parse status file [{statusPath}]...\n"); - _status = ServiceStatus.Deserialize(statusPath, _config); - msg = "------------------ status parameters ------------------"; - Console.WriteLine($"{msg}\n{_status.Summary()}{msg}\n"); + Console.WriteLine($"\nTrying to parse configuration files from [{baseDir}]...\n"); + _config = ServiceConfig.Deserialize(baseDir); + Console.WriteLine($"\n{mark} configuration settings {mark}"); + Console.Write(_config.Summary()); + Console.WriteLine($"{mark} configuration settings {mark}\n"); } catch (Exception ex) { Console.WriteLine(ex); diff --git a/ATxService/AutoTx.cs b/ATxService/AutoTx.cs index 5aebbb0749921f44ca93a129a749a51ef9547390..2df178a3f40c0e628334d54a362f8f945a19e872 100644 --- a/ATxService/AutoTx.cs +++ b/ATxService/AutoTx.cs @@ -187,8 +187,8 @@ namespace ATxService /// </summary> private void LoadSettings() { try { - LoadConfigXml(); - LoadStatusXml(); + LoadConfig(); + LoadStatus(); } catch (Exception ex) { Log.Error("LoadSettings() failed: {0}\n{1}", ex.Message, ex.StackTrace); @@ -200,31 +200,28 @@ namespace ATxService } /// <summary> - /// Load the configuration xml file. + /// Load the configuration. /// </summary> - private void LoadConfigXml() { - var confPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, - "configuration.xml"); + private void LoadConfig() { try { - _config = ServiceConfig.Deserialize(confPath); - Log.Debug("Loaded config from [{0}]", confPath); + _config = ServiceConfig.Deserialize( + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "conf")); } catch (ConfigurationErrorsException ex) { - Log.Error("ERROR validating configuration file [{0}]: {1}", - confPath, ex.Message); + Log.Error("Validating configuration failed: {0}", ex.Message); throw new Exception("Error validating configuration."); } catch (Exception ex) { - Log.Error("loading configuration XML failed: {0}", ex.Message); + Log.Error("Loading configuration failed: {0}", ex.Message); // this should terminate the service process: - throw new Exception("Error loading config."); + throw new Exception("Error loading configuration."); } } /// <summary> - /// Load the status xml file. + /// Load the status. /// </summary> - private void LoadStatusXml() { + private void LoadStatus() { var statusPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "status.xml"); try { diff --git a/ATxTray/AutoTxTray.cs b/ATxTray/AutoTxTray.cs index 6fbc684f18cdfe64f783a9a3cfe5ce8c4749d2a0..fe0b01fa528d7fc9172711f1c9440ef51cfe1e82 100644 --- a/ATxTray/AutoTxTray.cs +++ b/ATxTray/AutoTxTray.cs @@ -91,7 +91,7 @@ namespace ATxTray Log.Trace("Trying to read service config and status files..."); try { - _config = ServiceConfig.Deserialize(Path.Combine(baseDir, "configuration.xml")); + _config = ServiceConfig.Deserialize(Path.Combine(baseDir, "conf")); UpdateStatusInformation(); SetupContextMenu(); } diff --git a/Resources/conf-minimal/config.common.xml b/Resources/conf-minimal/config.common.xml new file mode 100644 index 0000000000000000000000000000000000000000..6f2bc1b1b0cc329ab360a9d88b6a818b78fdf74a --- /dev/null +++ b/Resources/conf-minimal/config.common.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<ServiceConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + + <!-- IncomingDirectory: directory on SourceDrive to watch for new files --> + <IncomingDirectory>ProgramData\AUTOTRANSFER\INCOMING</IncomingDirectory> + + <!-- ManagedDirectory: directory on SourceDrive where files and folders are + moved while queueing for their transfer (sub-directory "PROCESSING") + and to store them for deferred delection after a grace period after + the transfer (sub-directory "DONE"). --> + <ManagedDirectory>ProgramData\AUTOTRANSFER</ManagedDirectory> + + <!-- DestinationAlias: friendly name for the target to be used in mails --> + <DestinationAlias>Core Facility Storage</DestinationAlias> + + <!-- DestinationDirectory: where files should be transferred to --> + <DestinationDirectory>\\fileserver.mydomain.xy\share\</DestinationDirectory> + + <!-- TmpTransferDir: temporary directory relative to DestinationDirectory + to be used for running transfers --> + <TmpTransferDir>AUTOTRANSFER-TMP</TmpTransferDir> + + <!-- MaxCpuUsage: pause transfer if CPU usage is above this value (in %)--> + <MaxCpuUsage>25</MaxCpuUsage> + + <!-- MinAvailableMemory: pause transfer if free RAM is below (in MB) --> + <MinAvailableMemory>512</MinAvailableMemory> + + + <!-- OPTIONAL CONFIGURATION SETTINGS --> + + <!-- OPTIONAL CONFIGURATION SETTINGS --> + +</ServiceConfig> \ No newline at end of file diff --git a/Resources/conf-minimal/host-specific.template.xml b/Resources/conf-minimal/host-specific.template.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf9f260d5cc4deaac1bc0401b4d0d699fd6e5360 --- /dev/null +++ b/Resources/conf-minimal/host-specific.template.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<ServiceConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + + <!-- HostAlias: friendly name to be used for this machine in mails --> + <HostAlias>Confocal Microscope (Room 123)</HostAlias> + + <!-- SourceDrive: local drive to operate on (include the backslash!) --> + <SourceDrive>D:\</SourceDrive> + + + <!-- OPTIONAL CONFIGURATION SETTINGS --> + + <!-- OPTIONAL CONFIGURATION SETTINGS --> + + </ServiceConfig> \ No newline at end of file diff --git a/Resources/conf/config.common.xml b/Resources/conf/config.common.xml index fe28063b5f015800a0cd4cf944611e8b31c1befc..81094939de77e47ea5cb7588a55ff6ba0f5638df 100644 --- a/Resources/conf/config.common.xml +++ b/Resources/conf/config.common.xml @@ -3,30 +3,23 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <!-- IncomingDirectory: directory on SourceDrive to watch for new files --> - <IncomingDirectory>ATX\INCOMING</IncomingDirectory> + <IncomingDirectory>ProgramData\AUTOTRANSFER\INCOMING</IncomingDirectory> <!-- ManagedDirectory: directory on SourceDrive where files and folders are moved while queueing for their transfer (sub-directory "PROCESSING") and to store them for deferred delection after a grace period after the transfer (sub-directory "DONE"). --> - <ManagedDirectory>ATX\MANAGED</ManagedDirectory> - - <!-- GracePeriod: number of days after data in the "DONE" location expires, - which will trigger a summary email to the admin address. --> - <GracePeriod>30</GracePeriod> + <ManagedDirectory>ProgramData\AUTOTRANSFER</ManagedDirectory> <!-- DestinationAlias: friendly name for the target to be used in mails --> <DestinationAlias>Core Facility Storage</DestinationAlias> <!-- DestinationDirectory: where files should be transferred to --> - <DestinationDirectory>C:\ATX\TARGET</DestinationDirectory> + <DestinationDirectory>\\fileserver.mydomain.xy\share\</DestinationDirectory> <!-- TmpTransferDir: temporary directory relative to DestinationDirectory to be used for running transfers --> - <TmpTransferDir>d-vamp-dw</TmpTransferDir> - - <!-- ServiceTimer: interval (in ms) for checking files and parameters --> - <ServiceTimer>1000</ServiceTimer> + <TmpTransferDir>AUTOTRANSFER-TMP</TmpTransferDir> <!-- MaxCpuUsage: pause transfer if CPU usage is above this value (in %)--> <MaxCpuUsage>25</MaxCpuUsage> @@ -40,10 +33,36 @@ <!-- Debug: enable or disable debug log messages --> <Debug>true</Debug> + <!-- ServiceTimer: interval (in ms) for checking files and parameters --> + <ServiceTimer>1000</ServiceTimer> + <!-- MarkerFile: a file to place in each user's incoming directory, the file itself will be ignored for the transfers --> <MarkerFile>_DO_NOT_ACQUIRE_HERE_.txt</MarkerFile> + <!-- GracePeriod: number of days after data in the "DONE" location expires, + which will trigger a summary email to the admin address. --> + <GracePeriod>30</GracePeriod> + + <!-- EnforceInheritedACLs: whether to enforce ACL inheritance when moving + files and directories, see this page for details (DEFAULT: true) + https://support.microsoft.com/en-us/help/320246 --> + <EnforceInheritedACLs>false</EnforceInheritedACLs> + + <!-- BlacklistedProcesses: a list of "ProcessName" entries denoting + programs that will cause a transfer to be suspended immediately if the + name is found in the list of running processes --> + <BlacklistedProcesses> + <ProcessName>calc</ProcessName> + <ProcessName>notepad</ProcessName> + <ProcessName>wordpad</ProcessName> + </BlacklistedProcesses> + + <!-- OPTIONAL CONFIGURATION SETTINGS --> + + + <!-- OPTIONAL NOTIFICATION / EMAIL SETTINGS --> + <!-- SmtpHost: SMTP server hostname --> <SmtpHost /> <!-- SmtpPort: SMTP server port, defaults to 25 if omitted --> @@ -85,20 +104,6 @@ in case one of the drives is below the threshold (in minutes) --> <StorageNotificationDelta>720</StorageNotificationDelta> - <!-- BlacklistedProcesses: a list of "ProcessName" entries denoting - programs that will cause a transfer to be suspended immediately if the - name is found in the list of running processes --> - <BlacklistedProcesses> - <ProcessName>calc</ProcessName> - <ProcessName>notepad</ProcessName> - <ProcessName>wordpad</ProcessName> - </BlacklistedProcesses> - - <!-- EnforceInheritedACLs: whether to enforce ACL inheritance when moving - files and directories, see this page for details (DEFAULT: true) - https://support.microsoft.com/en-us/help/320246 --> - <EnforceInheritedACLs>false</EnforceInheritedACLs> - - <!-- OPTIONAL CONFIGURATION SETTINGS --> + <!-- OPTIONAL NOTIFICATION / EMAIL SETTINGS --> </ServiceConfig> \ No newline at end of file diff --git a/Resources/conf/host-specific.template.xml b/Resources/conf/host-specific.template.xml index 10613b46c055cb94bd4f771e1c66c47c63c8d4b8..0feb144ea34072fa6b91ef471f62cd04204b59f9 100644 --- a/Resources/conf/host-specific.template.xml +++ b/Resources/conf/host-specific.template.xml @@ -6,11 +6,14 @@ <HostAlias>Confocal Microscope (Room 123)</HostAlias> <!-- SourceDrive: local drive to operate on (include the backslash!) --> - <SourceDrive>C:\</SourceDrive> + <SourceDrive>D:\</SourceDrive> <!-- OPTIONAL CONFIGURATION SETTINGS --> + <!-- InterPacketGap: RoboCopy parameter to limit the bandwidth --> + <InterPacketGap>0</InterPacketGap> + <!-- A list of drive names and space thresholds to be used for monitoring the free space and send notifications if below. --> <SpaceMonitoring> @@ -26,9 +29,6 @@ </DriveToCheck><--> </SpaceMonitoring> - <!-- InterPacketGap: RoboCopy parameter to limit the bandwidth --> - <InterPacketGap /> - <!-- OPTIONAL CONFIGURATION SETTINGS --> </ServiceConfig> \ No newline at end of file diff --git a/Resources/configuration-example.xml b/Resources/configuration-example.xml deleted file mode 100644 index ec6bc1400363489de81452289fb7ef0cfee55619..0000000000000000000000000000000000000000 --- a/Resources/configuration-example.xml +++ /dev/null @@ -1,103 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<ServiceConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <!-- HostAlias: friendly name to be used for this machine in mails --> - <HostAlias>Confocal Microscope 2</HostAlias> - <!-- DestinationAlias: friendly name for the target to be used in mails --> - <DestinationAlias>Core Facility Storage</DestinationAlias> - - <!-- enable or disable debug log messages --> - <Debug>true</Debug> - - <!-- SourceDrive: base drive including backslash, e.g. "D:\" --> - <SourceDrive>XXX</SourceDrive> - <!-- IncomingDirectory: directory on SourceDrive to watch for new files --> - <IncomingDirectory>AUTOTRANSFER</IncomingDirectory> - <!-- [OPTIONAL] MarkerFile: a file to place in each user's incoming - directory, the file itself will be ignored for the transfers --> - <MarkerFile>_DO_NOT_ACQUIRE_HERE_.txt</MarkerFile> - <!-- ManagedDirectory: directory on SourceDrive where files and folders are - moved while queueing for their transfer (sub-directory "PROCESSING") - and to store them for deferred delection after a grace period after - the transfer (sub-directory "DONE"). --> - <ManagedDirectory>ProgramData\AUTOTRANSFER</ManagedDirectory> - <!-- GracePeriod: number of days after data in the "DONE" location expires, - which will trigger a summary email to the admin address. --> - <GracePeriod>5</GracePeriod> - <!-- DestinationDirectory: where files should be transferred to --> - <DestinationDirectory>\\fileserver.mydomain.xy\share\</DestinationDirectory> - <!-- TmpTransferDir: temporary directory relative to DestinationDirectory - to be used for running transfers --> - <TmpTransferDir>AUTOTRANSFER-TMP</TmpTransferDir> - - <!-- ServiceTimer: interval (in ms) for checking files and parameters --> - <ServiceTimer>1000</ServiceTimer> - - <!-- MaxCpuUsage: pause transfer if CPU usage is above this value (in %) --> - <MaxCpuUsage>25</MaxCpuUsage> - <!-- MinAvailableMemory: pause transfer if free RAM is below (in MB) --> - <MinAvailableMemory>512</MinAvailableMemory> - - <SpaceMonitoring> - <DriveToCheck> - <DriveName>C:</DriveName> - <SpaceThreshold>20000</SpaceThreshold> - </DriveToCheck> - <DriveToCheck> - <DriveName>D:</DriveName> - <SpaceThreshold>120000</SpaceThreshold> - </DriveToCheck> - </SpaceMonitoring> - <!-- StorageNotificationDelta: how often to send storage notification mails - in case one of the drives is below the threshold (in minutes) --> - <StorageNotificationDelta>5</StorageNotificationDelta> - - <BlacklistedProcesses> - <ProcessName>calc</ProcessName> - <ProcessName>notepad</ProcessName> - <ProcessName>wordpad</ProcessName> - </BlacklistedProcesses> - - <!-- SendAdminNotification: send email to user on finished transfers --> - <SendTransferNotification>true</SendTransferNotification> - <EmailFrom>admin@mydomain.xy</EmailFrom> - - <!-- SendAdminNotification: notify admins via email of certain events --> - <SendAdminNotification>true</SendAdminNotification> - <!-- AdminNotificationDelta: how long to wait (in minutes) after sending an - admin notification before sending the next one --> - <AdminNotificationDelta>60</AdminNotificationDelta> - - - <!-- OPTIONAL CONFIGURATION SETTINGS --> - - <!-- SmtpHost: SMTP server hostname --> - <SmtpHost>smtp.mydomain.xy</SmtpHost> - <!-- SmtpPort: SMTP server port, defaults to 25 if omitted --> - <SmtpPort>25</SmtpPort> - <!-- SmtpUserCredential: SMTP user name if authentication required --> - <SmtpUserCredential /> - <!-- SmtpPasswortCredential: SMTP password if authentication required --> - <SmtpPasswortCredential /> - - <!-- EmailPrefix: prefix label for email subjects --> - <EmailPrefix>[Core Facility] </EmailPrefix> - <AdminEmailAdress>admin@mydomain.xy</AdminEmailAdress> - <!-- AdminDebugEmailAdress: an email address where to send certain debug - messages to, e.g. on completed transfers. Can be empty. --> - <AdminDebugEmailAdress>admin@mydomain.xy</AdminDebugEmailAdress> - - <!-- GraceNotificationDelta: minimum time (in minutes) between two emails - about expired folders in the grace location (default: 720 (12h)) --> - <GraceNotificationDelta>720</GraceNotificationDelta> - - <!-- InterPacketGap: RoboCopy parameter to limit the bandwidth --> - <InterPacketGap>0</InterPacketGap> - - <!-- EnforceInheritedACLs: whether to enforce ACL inheritance when moving - files and directories, see this page for details (DEFAULT: true) - https://support.microsoft.com/en-us/help/320246 --> - <EnforceInheritedACLs>false</EnforceInheritedACLs> - - <!-- OPTIONAL CONFIGURATION SETTINGS --> -</ServiceConfig> \ No newline at end of file diff --git a/Resources/status-example.xml b/Resources/status-example.xml deleted file mode 100644 index 51c6b289db46f2b06e885c153fd157ea819a5f1f..0000000000000000000000000000000000000000 --- a/Resources/status-example.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<ServiceStatus xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> - <LastStatusUpdate>0001-01-01T00:00:00</LastStatusUpdate> - <LastStorageNotification>0001-01-01T00:00:00</LastStorageNotification> - <LastAdminNotification>0001-01-01T00:00:00</LastAdminNotification> - <LimitReason /> - <CurrentTransferSrc /> - <CurrentTargetTmp /> - <ServiceSuspended>false</ServiceSuspended> - <TransferInProgress>false</TransferInProgress> - <CleanShutdown>true</CleanShutdown> - <CurrentTransferSize>0</CurrentTransferSize> -</ServiceStatus> \ No newline at end of file diff --git a/Scripts/Make-Package.ps1 b/Scripts/Make-Package.ps1 index 6ffddf7fe31c5c625c1481642103fce22597c8df..e0d96c24d85aec47c404b12c77af4e297e7c8dda 100644 --- a/Scripts/Make-Package.ps1 +++ b/Scripts/Make-Package.ps1 @@ -2,10 +2,20 @@ $ResourceDir = "..\ATxService\Resources" $RsrcDirCommon = "..\Resources" -function Highlight([string]$Message, [string]$Color = "Cyan") { +function Highlight([string]$Message, [string]$Color = "Cyan", $Indent = $False) { + if ($Indent) { + Write-Host -NoNewline " " + } Write-Host -NoNewline "[" Write-Host -NoNewline -F $Color $Message Write-Host -NoNewline "]" + if ($Indent) { + Write-Host + } +} + +function RelToAbs([string]$RelPath) { + Join-Path -Resolve $(Get-Location) $RelPath } @@ -27,20 +37,20 @@ catch { $PkgDir = $BuildDate -replace ':','-' -replace ' ','_' $PkgDir = "build_" + $PkgDir -$BinariesDirService = "..\ATxService\bin\$($BuildConfiguration)" -$BinariesDirTrayApp = "..\ATxTray\bin\$($BuildConfiguration)" -$BinariesDirCfgTest = "..\ATxConfigTest\bin\$($BuildConfiguration)" +$BinariesDirService = RelToAbs "..\ATxService\bin\$($BuildConfiguration)" +$BinariesDirTrayApp = RelToAbs "..\ATxTray\bin\$($BuildConfiguration)" +$BinariesDirCfgTest = RelToAbs "..\ATxConfigTest\bin\$($BuildConfiguration)" Write-Host -NoNewline "Creating package " Highlight $PkgDir "Red" Write-Host " using binaries from:" -Write-Host $(Highlight $BinariesDirService "Green") -Write-Host $(Highlight $BinariesDirTrayApp "Green") -Write-Host $(Highlight $BinariesDirCfgTest "Green") -Write-Host "" +Highlight $BinariesDirService "Green" $True +Highlight $BinariesDirTrayApp "Green" $True +Highlight $BinariesDirCfgTest "Green" $True +Write-Host if (Test-Path $PkgDir) { - Write-Host "Removing existing package dir [$($PkgDir)]..." + Write-Host "Removing existing package dir [$($PkgDir)]...`n" Remove-Item -Recurse -Force $PkgDir } @@ -52,10 +62,10 @@ Copy-Item -Exclude *.pdb -Recurse "$($BinariesDirService)\*" $tgt Copy-Item -Exclude *.pdb -Recurse "$($BinariesDirTrayApp)\*" $tgt -EA Ignore Copy-Item -Exclude *.pdb -Recurse "$($BinariesDirCfgTest)\*" $tgt -EA Ignore # provide an up-to-date version of the example config file: -Copy-Item "$($RsrcDirCommon)\configuration-example.xml" $tgt +$example = New-Item -ItemType Container -Path $PkgDir -Name "conf-example" +Copy-Item "$($RsrcDirCommon)\conf\config.common.xml" $example +Copy-Item "$($RsrcDirCommon)\conf\host-specific.template.xml" $example -Copy-Item "$($RsrcDirCommon)\configuration-example.xml" "$($PkgDir)\configuration.xml" -Copy-Item "$($RsrcDirCommon)\status-example.xml" "$($PkgDir)\status.xml" Copy-Item "$($ResourceDir)\BuildDate.txt" "$($PkgDir)\AutoTx.log" Copy-Item "$($ResourceDir)\BuildConfiguration.txt" $($PkgDir) try { @@ -74,8 +84,11 @@ Copy-Item "Install-Service.ps1" $PkgDir Write-Host -NoNewline "Done creating package " Highlight $PkgDir -Write-Host -NoNewline " using config " -Highlight $BuildConfiguration -Write-Host -NoNewline " based on commit " -Highlight $BuildCommit +Write-Host +Highlight "configuration: $($BuildConfiguration)" -Indent $True +Highlight "commit: $($BuildCommit)" -Indent $True +Write-Host + +Write-Host -NoNewline "Location: " +Highlight "$(RelToAbs $PkgDir)" Write-Host \ No newline at end of file diff --git a/Updater/Update-Service.ps1 b/Updater/Update-Service.ps1 index 7e3333b9eca94049b33fa821f2b7792f32ccc767..f12c233122a539362d94b4740420ca09adf5df10 100644 --- a/Updater/Update-Service.ps1 +++ b/Updater/Update-Service.ps1 @@ -52,6 +52,7 @@ function ServiceIsBusy { } } + function Stop-TrayApp() { try { Stop-Process -Name "ATxTray" -Force -ErrorAction Stop @@ -137,7 +138,12 @@ function Start-MyService { function Get-WriteTime([string]$FileName) { try { - $TimeStamp = (Get-Item "$FileName").LastWriteTime + $TimeStamp = (Get-Item "$FileName" -EA Stop).LastWriteTime + } + catch [System.Management.Automation.ItemNotFoundException] { + Write-Verbose "File [$($FileName)] can't be found!" + throw [System.Management.Automation.ItemNotFoundException] ` + "File not found: $($FileName)." } catch { $ex = $_.Exception.Message @@ -157,11 +163,16 @@ function File-IsUpToDate([string]$ExistingFile, [string]$UpdateCandidate) { Log-Debug "File [$($ExistingFile)] is up-to-date." Return $True } + Write-Verbose "File [$($UpdateCandidate)] is newer than [$($ExistingFile)]." Return $False } function Create-Backup { + # Rename a file using a time-stamp suffix like "2017-12-04T16.41.35" while + # preserving its original suffix / extension. + # + # Return $True if the backup was created successfully, $False otherwise. Param ( [Parameter(Mandatory=$True)] [ValidateScript({Test-Path -PathType Leaf $_})] @@ -171,31 +182,28 @@ function Create-Backup { $FileWithoutSuffix = [io.path]::GetFileNameWithoutExtension($FileName) $FileSuffix = [io.path]::GetExtension($FileName) $BaseDir = Split-Path -Parent $FileName - + # assemble a timestamp string like "2017-12-04T16.41.35" $BakTimeStamp = Get-Date -Format s | foreach {$_ -replace ":", "."} $BakName = "$($FileWithoutSuffix)_pre-$($BakTimeStamp)$($FileSuffix)" Log-Info "Creating backup of [$($FileName)] as [$($BaseDir)\$($BakName)]." + try { - Rename-Item "$FileName" "$BaseDir\$BakName" -ErrorAction Stop + Rename-Item "$FileName" "$BaseDir\$BakName" } catch { - $ex = $_.Exception.Message - Log-Error "Backing up [$($FileName)] as [$($BakName)] FAILED!`n$($ex)" - Exit + Log-Error "Backing up [$($DstFile)] FAILED:`n> $($_.Exception.Message)" + Return $False } + Return $True } function Update-File { - # Check the given $SrcFile if a file with the same name is existing in - # $DstPath. If $SrcFile is newer, stop the service, create a backup of the - # file in $DstPath and finally copy the file from $SrcFile to $DstPath. + # Use the given $SrcFile to update the file with the same name in $DstPath, + # creating a backup of the original file before replacing it. # # Return $True if the file was updated, $False otherwise. - # - # WARNING: the function TERMINATES the script on any error! - # Param ( [Parameter(Mandatory=$True)] [ValidateScript({[IO.Path]::IsPathRooted($_)})] @@ -207,106 +215,204 @@ function Update-File { ) $DstFile = "$($DstPath)\$(Split-Path -Leaf $SrcFile)" - if (-Not (Test-Path "$DstFile")) { - Log-Info "File not existing in destination, NOT UPDATING: [$($DstFile)]" - Return $False - } + Write-Verbose "Trying to update [$($DstFile)] with [$($SrcFile)]..." - if (File-IsUpToDate -ExistingFile $DstFile -UpdateCandidate $SrcFile) { + if (-Not (Create-Backup -FileName $DstFile)) { Return $False } - Stop-MyService "Found newer file at $($SrcFile), updating..." - try { - Create-Backup -FileName $DstFile + Copy-Item -Path $SrcFile -Destination $DstPath + Log-Info "Updated config file [$($DstFile)]." } catch { - Log-Error "Backing up $($DstFile) FAILED!`n$($_.Exception.Message)" - Exit + Log-Error "Copying [$($SrcFile)] FAILED:`n> $($_.Exception.Message)" + Return $False + } + Return $True +} + + +function Update-Configuration { + # Update the common and host-specific configuration files with their new + # versions, stopping the service if necessary. + # The function DOES NOT do any checks, it simply runs the necessary update + # commands - meaning everything else (do the files exist, is an update + # required) has to be checked beforehand!! + # + # Return $True if all files were updated successfully. + $NewComm = Join-Path $UpdPathConfig "config.common.xml" + $NewHost = Join-Path $UpdPathConfig "$($env:COMPUTERNAME).xml" + Write-Verbose "Updating configuration files:`n> $($NewComm)`n> $($NewHost)" + + Stop-MyService "Updating configuration using files at [$($UpdPathConfig)]." + + $Ret = Update-File $NewComm $ConfigPath + # only continue if the first update worked: + if ($Ret) { + $Ret = Update-File $NewHost $ConfigPath } + Return $Ret +} + +function NewConfig-Available { + # Check the configuration update path and the given $DstPath for both + # configuration files (common and host-specific) and compare their + # respective file write-time. + # + # Return $True if the update path contains any newer file, $False otherwise. + Param ( + [Parameter(Mandatory=$True)] + [ValidateScript({(Get-Item $_).PSIsContainer})] + [String]$DstPath + ) + + # old and new common configuration + $OComm = Join-Path $DstPath "config.common.xml" + $NComm = Join-Path $UpdPathConfig "config.common.xml" + + # old and new host-specific configuration + $OHost = Join-Path $DstPath "$($env:COMPUTERNAME).xml" + $NHost = Join-Path $UpdPathConfig "$($env:COMPUTERNAME).xml" + + $Ret = $True try { - Copy-Item -Path $SrcFile -Destination $DstPath -ErrorAction Stop - Log-Info "Updated config file '$($DstFile)'." + $Ret = ( + $(File-IsUpToDate -ExistingFile $OHost -UpdateCandidate $NHost) -And + $(File-IsUpToDate -ExistingFile $OComm -UpdateCandidate $NComm) + ) } catch { - Log-Error "Copying $($SrcFile) FAILED!`n$($_.Exception.Message)" - Exit + Log-Error $("Checking for new configuration files failed:" + "$($_.Exception.Message)") + Return $False } + + if ($Ret) { + Write-Verbose "Configuration is up to date, no new files available." + Return $False + } + Log-Info "New configuration files found!" Return $True } -function Update-Configuration { - $RetOr = $False - # common config files first: - ForEach ($NewConfig in Get-ChildItem $UpdPathConfigCommon) { - $ret = Update-File $NewConfig.FullName $ConfigPath - $RetOr = $RetOr -Or $ret - } - # then host specific config files: - ForEach ($NewConfig in Get-ChildItem $UpdPathConfig) { - $ret = Update-File $NewConfig.FullName $ConfigPath - $RetOr = $RetOr -Or $ret - } - if (-Not ($RetOr)) { - Log-Debug "No (new) configuration file(s) found." +function Config-IsValid { + # Check if the new configuration provided at $UpdPathConfig validates with + # the appropriate "AutoTxConfigTest" binary (either the existing one in the + # service installation directory (if the service binaries won't be updated) + # or the new one at the $UpdPathBinaries location in case the service itself + # will be updated as well). + # + # Returns an array with two elements, the first one being $True in case the + # configuration was successfully validated ($False otherwise) and the second + # one containing the output of the configuration test tool as a string. + Param ( + [Parameter(Mandatory=$True)] + [ValidateScript({(Test-Path $_ -PathType Leaf)})] + [String]$ConfigTest, + + [Parameter(Mandatory=$True)] + [ValidateScript({(Test-Path $_ -PathType Container)})] + [String]$ConfigPath + ) + Write-Verbose "Running [$($ConfigTest) $($ConfigPath)]..." + $Summary = & $ConfigTest $ConfigPath + $Ret = $? + # pipe through Out-String to preserve line breaks: + $Summary = "$("=" * 80)`n$($Summary | Out-String)`n$("=" * 80)" + + if ($Ret) { + Log-Debug "Validated config files at [$($ConfigPath)]:`n$($Summary)" + Return $Ret, $Summary + } + Log-Error "Config at [$($ConfigPath)] FAILED VALIDATION:`n$($Summary)" + Return $Ret, $Summary +} + + +function Find-InstallationPackage { + # Try to locate the latest installation package using the pattern defined + # in the updater configuration. + Write-Verbose "Looking for installation package using pattern: $($Pattern)" + $PkgDir = Get-ChildItem -Path $UpdPathBinaries -Directory -Name | + Where-Object {$_ -match $Pattern} | + Sort-Object | + Select-Object -Last 1 + + if ([string]::IsNullOrEmpty($PkgDir)) { + Log-Error "couldn't find installation package matching '$($Pattern)'!" + Exit } - Return $RetOr + $PkgDir = "$($UpdPathBinaries)\$($PkgDir)" + Write-Verbose "Found update installation package: [$($PkgDir)]" + Return $PkgDir } function Copy-ServiceFiles { + # Copy the files from an update package to the service installation + # directory, overwriting existing ones. + # + # Returns $True for success, $False otherwise. + Write-Verbose "Copying service binaries from [$($UpdPackage)]..." try { - Write-Verbose "Looking for source package using pattern: $($Pattern)" - $PkgDir = Get-ChildItem -Path $UpdPathBinaries -Directory -Name | - Where-Object {$_ -match $Pattern} | - Sort-Object | - Select-Object -Last 1 - - if ([string]::IsNullOrEmpty($PkgDir)) { - Write-Host "ERROR: couldn't find package matching '$($Pattern)'!" - Exit - } - Write-Verbose "Found update source package: [$($PkgDir)]" - - Stop-MyService "Trying to update service using package [$($PkgDir)]." - Copy-Item -Recurse -Force -ErrorAction Stop ` - -Path "$($UpdPathBinaries)\$($PkgDir)\$($ServiceName)\*" ` + Copy-Item -Recurse -Force ` + -Path "$($UpdPackage)\$($ServiceName)\*" ` -Destination "$InstallationPath" } catch { - Log-Error "Updating service binaries FAILED!`n$($_.Exception.Message)" - Exit + Log-Error "Updating service binaries FAILED:`n> $($_.Exception.Message)" + Return $False } - Log-Info "Updated service binaries with [$($PkgDir)]." + Log-Info "Updated service binaries with [$($UpdPackage)]." + Return $True } function Update-ServiceBinaries { - $MarkerFile = "$($UpdPathMarkerFiles)\$($env:COMPUTERNAME)" - if (Test-Path "$MarkerFile" -Type Leaf) { - Log-Debug "Found marker [$($MarkerFile)], not updating service." + # Stop the tray application and the service, update the service binaries and + # create a marker file indicating the service on this host has been updated. + # + # Returns $True if binaries were updated successfully and the marker file + # has been created, $False otherwise. + Stop-TrayApp + Stop-MyService "Trying to update service using package [$($UpdPackage)]." + $Ret = Copy-ServiceFiles + if (-Not $Ret) { Return $False } - Stop-TrayApp - Copy-ServiceFiles + + $MarkerFile = "$($UpdPathMarkerFiles)\$($env:COMPUTERNAME)" try { - New-Item -Type File "$MarkerFile" -ErrorAction Stop | Out-Null + New-Item -Type File "$MarkerFile" | Out-Null Log-Debug "Created marker file [$($MarkerFile)]." } catch { - Log-Error "Creating [$($MarkerFile)] FAILED!`n$($_.Exception.Message)" - Exit + Log-Error "Creating [$($MarkerFile)] FAILED:`n> $($_.Exception.Message)" + Return $False + } + Return $True +} + + +function ServiceUpdate-Requested { + # Check for a host-specific marker file indicating whether the service + # binaries on this host should be updated. + $MarkerFile = "$($UpdPathMarkerFiles)\$($env:COMPUTERNAME)" + if (Test-Path "$MarkerFile" -Type Leaf) { + Log-Debug "Found marker [$($MarkerFile)], not updating service." + Return $False } + Write-Verbose "Marker [$($MarkerFile)] missing, service should be updated!" Return $True } function Upload-LogFiles { $Dest = "$($UploadPathLogs)\$($env:COMPUTERNAME)" - New-Item -Force -Type Directory $Dest + New-Item -Force -Type Directory $Dest | Out-Null try { Copy-Item -Force -ErrorAction Stop ` -Path "$($LogPath)\AutoTx.log" ` @@ -321,7 +427,7 @@ function Upload-LogFiles { function Get-HostDescription() { $Desc = $env:COMPUTERNAME - $ConfigXml = "$($InstallationPath)\configuration.xml" + $ConfigXml = "$($ConfigPath)\$($Desc).xml" try { [xml]$XML = Get-Content $ConfigXml -ErrorAction Stop # careful, we need a string comparison here: @@ -413,6 +519,7 @@ function Log-Debug([string]$Message) { ################################################################################ +$ErrorActionPreference = "Stop" try { . $UpdaterSettings @@ -442,8 +549,7 @@ Log-Debug "$($Me) started..." # first check if the service is installed and running at all $ServiceRunningBefore = ServiceIsRunning $ServiceName -$UpdPathConfig = "$($UpdateSourcePath)\Configs\$($env:COMPUTERNAME)" -$UpdPathConfigCommon = "$($UpdateSourcePath)\Configs\_COMMON_" +$UpdPathConfig = "$($UpdateSourcePath)\Configs" $UpdPathMarkerFiles = "$($UpdateSourcePath)\Service\UpdateMarkers" $UpdPathBinaries = "$($UpdateSourcePath)\Service\Binaries" $UploadPathLogs = "$($UpdateSourcePath)\Logs" @@ -453,7 +559,6 @@ Exit-IfDirMissing $LogPath "log files" Exit-IfDirMissing $ConfigPath "configuration files" Exit-IfDirMissing $UpdateSourcePath "update source" Exit-IfDirMissing $UpdPathConfig "configuration update" -Exit-IfDirMissing $UpdPathConfigCommon "common configuration update" Exit-IfDirMissing $UpdPathMarkerFiles "update marker" Exit-IfDirMissing $UpdPathBinaries "service binaries update" Exit-IfDirMissing $UploadPathLogs "log file target" @@ -463,29 +568,99 @@ Exit-IfDirMissing $UploadPathLogs "log file target" # the logfiles are uploaded no matter if one of the other tasks fails and # terminates the entire script: Upload-LogFiles -$ConfigUpdated = Update-Configuration -$ServiceUpdated = Update-ServiceBinaries -$msg = "" -if ($ConfigUpdated) { - $msg += "The configuration files were updated.`n" -} -if ($ServiceUpdated) { - $msg += "The service binaries were updated.`n" -} +try { + $UpdItems = @() + $ConfigShouldBeUpdated = NewConfig-Available $ConfigPath + $ServiceShouldBeUpdated = ServiceUpdate-Requested + if (-Not ($ConfigShouldBeUpdated -Or $ServiceShouldBeUpdated)) { + Log-Debug "No update action found to be necessary." + Exit + } + + # define where the configuration is located that should be tested: + $ConfigToTest = $ConfigPath + if ($ConfigShouldBeUpdated) { + $ConfigToTest = $UpdPathConfig + $UpdItems += "configuration files" + } + + # define which configuration checker executable to use for testing: + $ConftestExe = "$($InstallationPath)\AutoTxConfigTest.exe" + if ($ServiceShouldBeUpdated) { + $UpdPackage = Find-InstallationPackage + $ConftestExe = "$($UpdPackage)\$($ServiceName)\AutoTxConfigTest.exe" + $UpdItems += "service binaries" + } + + # now we're all set and can run the config test: + $ConfigValid, $ConfigSummary = Config-IsValid $ConftestExe $ConfigToTest + + + # if we don't have a valid configuration we complain and terminate: + if (-Not ($ConfigValid)) { + Log-Error "Configuration not valid for service, $($Me) terminating!" + Send-MailReport -Subject "Update failed, configuration invalid!" ` + -Body $("An update action was found to be necessary, however the" + "configuration didn't`npass the validator.`n`nThe following" + "summary was generated by the configuration checker:" + "`n`n$($ConfigSummary)") + Exit + } + + + # reaching this point means + # (1) something needs to be updated (config, service or both) + # AND + # (2) the config validates with the corresponding service version + Write-Verbose "Required update items:`n> - $($UpdItems -join "`n> - ")`n" + + if ($ConfigShouldBeUpdated) { + $ConfigUpdated = Update-Configuration + if (-Not $ConfigUpdated) { + $msg = "Updating the configuration failed, $($Me) terminating!" + Log-Error $msg + Send-MailReport -Subject "updated failed!" -Body $msg + Exit + } + } + + if ($ServiceShouldBeUpdated) { + $ServiceUpdated = Update-ServiceBinaries + if (-Not $ServiceUpdated) { + $msg = "Updating the service binaries failed, $($Me) terminating!" + Log-Error $msg + Send-MailReport -Subject "updated failed!" -Body $msg + Exit + } + } + + $UpdSummary = "Updated $($UpdItems -join " and ")." + + -if ($msg -ne "") { if ($ServiceRunningBefore) { - Log-Debug "Update action occurred, finishing up..." + Log-Debug "$($UpdSummary) Trying to start the service again..." Start-MyService } else { - Log-Debug "Not starting the service as it was not running before." + Log-Debug "$($UpdSummary) Leaving the service stopped, as before." + } + + $UpdDetails = $("An $($Me) run completed successfully. Updated items:" + "`n> - $($UpdItems -join "`n> - ")") + if ($ConfigUpdated) { + $UpdDetails += "`n`nConfig validation summary:`n$($ConfigSummary)" } - Send-MailReport -Subject "Config and / or service has been updated!" ` - -Body $msg -} else { - Log-Debug "No update action found to be necessary." } +catch { + $UpdDetails = $("Unexpected problem, check logs! $($Me) terminating." + "`n`n$($_.Exception.Message)") + $UpdSummary = "ERROR, unhandled problem occurered!" + Log-Error $UpdDetails +} + + +Send-MailReport -Subject "$UpdSummary" -Body "$UpdDetails" Upload-LogFiles diff --git a/Updater/UpdaterConfig-Example.inc.ps1 b/Updater/UpdaterConfig-Example.inc.ps1 index 900387003490db11ccdf29775e55b8d8079992f0..0417c19620b660cd718e220c23060f8f63ee2c94 100644 --- a/Updater/UpdaterConfig-Example.inc.ps1 +++ b/Updater/UpdaterConfig-Example.inc.ps1 @@ -2,7 +2,7 @@ $ServiceName = "AutoTx" $InstallationPath = "C:\Tools\$($ServiceName)" -$ConfigPath = "$($InstallationPath)" +$ConfigPath = "$($InstallationPath)\conf" $LogPath = "$($InstallationPath)" $UpdateSourcePath = "\\fileserver.mydomain.xy\share\_AUTOTX_"