diff --git a/ATXSerializables/ATXSerializables.csproj b/ATXSerializables/ATXSerializables.csproj new file mode 100644 index 0000000000000000000000000000000000000000..0b6ec7c14655fd6bce4af74198b074db4b8e7b33 --- /dev/null +++ b/ATXSerializables/ATXSerializables.csproj @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{166D65D5-EE10-4364-8AA3-4D86BA5CE244}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>ATXSerializables</RootNamespace> + <AssemblyName>ATXSerializables</AssemblyName> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Configuration" /> + <Reference Include="System.Core" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="DriveToCheck.cs" /> + <Compile Include="ServiceConfig.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="ServiceStatus.cs" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/ATXSerializables/DriveToCheck.cs b/ATXSerializables/DriveToCheck.cs new file mode 100644 index 0000000000000000000000000000000000000000..7d52e74420d89dd81a53bf5505e69c8ee2df1a52 --- /dev/null +++ b/ATXSerializables/DriveToCheck.cs @@ -0,0 +1,18 @@ +using System.Xml.Serialization; + +namespace ATXSerializables +{ + /// <summary> + /// Helper class for the nested SpaceMonitoring sections. + /// </summary> + public class DriveToCheck + { + [XmlElement("DriveName")] + public string DriveName { get; set; } + + // the value is to be compared to System.IO.DriveInfo.TotalFreeSpace + // hence we use the same type (long) to avoid unnecessary casts later: + [XmlElement("SpaceThreshold")] + public long SpaceThreshold { get; set; } + } +} \ No newline at end of file diff --git a/ATXSerializables/Properties/AssemblyInfo.cs b/ATXSerializables/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a2d27ee401fca56259fa9f20ac24763690f4a4c --- /dev/null +++ b/ATXSerializables/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ATXSerializables")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Universitaet Basel")] +[assembly: AssemblyProduct("ATXSerializables")] +[assembly: AssemblyCopyright("Copyright © Universitaet Basel 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c1ed2607-6acb-4202-9354-65d19c5a06d1")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ATXSerializables/ServiceConfig.cs b/ATXSerializables/ServiceConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..512e426421acc5e1e134447e86f1ba30dd267fd8 --- /dev/null +++ b/ATXSerializables/ServiceConfig.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Xml.Serialization; + +namespace ATXSerializables +{ + /// <summary> + /// configuration class based on xml + /// </summary> + [Serializable] + public class ServiceConfig + { + [XmlIgnore] public string ValidationWarnings; + + public ServiceConfig() { + ValidationWarnings = ""; + // set values for the optional XML elements: + SmtpHost = ""; + SmtpPort = 25; + SmtpUserCredential = ""; + SmtpPasswortCredential = ""; + EmailPrefix = ""; + AdminEmailAdress = ""; + AdminDebugEmailAdress = ""; + GraceNotificationDelta = 720; + + InterPacketGap = 0; + + EnforceInheritedACLs = true; + } + + #region required configuration parameters + + /// <summary> + /// A human friendly name for the host, to be used in emails etc. + /// </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> + public string SourceDrive { get; set; } + + /// <summary> + /// The name of a directory on SourceDrive that is monitored for new files. + /// </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> + /// Target path to transfer files to. Usually a UNC location. + /// </summary> + public string DestinationDirectory { get; set; } + + /// <summary> + /// The name of a subdirectory in the DestinationDirectory to be used + /// to keep the temporary data of running transfers. + /// </summary> + public string TmpTransferDir { get; set; } + + public string EmailFrom { get; set; } + + public int ServiceTimer { get; set; } + + public int MaxCpuUsage { get; set; } + public int MinAvailableMemory { get; set; } + public int AdminNotificationDelta { get; set; } + public int StorageNotificationDelta { get; set; } + + /// <summary> + /// GracePeriod: number of days after data in the "DONE" location expires, + /// which will trigger a summary email to the admin address. + /// </summary> + public int GracePeriod { get; set; } + + public bool SendAdminNotification { get; set; } + public bool SendTransferNotification { get; set; } + public bool Debug { get; set; } + + [XmlArray] + [XmlArrayItem(ElementName = "DriveToCheck")] + public List<DriveToCheck> SpaceMonitoring { get; set; } + + [XmlArray] + [XmlArrayItem(ElementName = "ProcessName")] + public List<string> BlacklistedProcesses { get; set; } + + #endregion + + + #region optional configuration parameters + + public string SmtpHost { get; set; } + public string SmtpUserCredential { get; set; } + public string SmtpPasswortCredential { get; set; } + public int SmtpPort { get; set; } + + public string EmailPrefix { get; set; } + public string AdminEmailAdress { get; set; } + public string AdminDebugEmailAdress { get; set; } + + public int GraceNotificationDelta { get; set; } + + public int InterPacketGap { get; set; } + + /// <summary> + /// EnforceInheritedACLs: whether to enforce ACL inheritance when moving files and + /// directories, see https://support.microsoft.com/en-us/help/320246 for more details. + /// </summary> + public bool EnforceInheritedACLs { get; set; } + + #endregion + + + 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) { + var xs = new XmlSerializer(typeof(ServiceConfig)); + var reader = File.OpenText(file); + var config = (ServiceConfig) xs.Deserialize(reader); + reader.Close(); + ValidateConfiguration(config); + return config; + } + + 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!"); + + if (c.SourceDrive.Substring(1) != @":\") + throw new ConfigurationErrorsException("SourceDrive must be a drive " + + @"letter followed by a colon and a backslash, e.g. 'D:\'!"); + + // 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 + "')!"); + + + // spooling directories: IncomingDirectory + ManagedDirectory + if (c.IncomingDirectory.StartsWith(@"\")) + throw new ConfigurationErrorsException("IncomingDirectory must not start with a backslash!"); + if (c.ManagedDirectory.StartsWith(@"\")) + throw new ConfigurationErrorsException("ManagedDirectory must not start with a backslash!"); + + if (!Directory.Exists(c.DestinationDirectory)) + throw new ConfigurationErrorsException("can't find destination: " + c.DestinationDirectory); + + var tmpTransferPath = Path.Combine(c.DestinationDirectory, c.TmpTransferDir); + if (!Directory.Exists(tmpTransferPath)) + throw new ConfigurationErrorsException("temporary transfer dir doesn't exist: " + tmpTransferPath); + + if (c.ServiceTimer < 1000) + throw new ConfigurationErrorsException("ServiceTimer must not be smaller than 1000 ms!"); + + + // NON-CRITICAL stuff just adds messages to ValidationWarnings: + // DestinationDirectory + if (!c.DestinationDirectory.StartsWith(@"\\")) + c.ValidationWarnings += " - <DestinationDirectory> is not a UNC path!\n"; + } + + public string Summary() { + var msg = + "HostAlias: " + HostAlias + "\n" + + "SourceDrive: " + SourceDrive + "\n" + + "IncomingDirectory: " + IncomingDirectory + "\n" + + "MarkerFile: " + MarkerFile + "\n" + + "ManagedDirectory: " + ManagedDirectory + "\n" + + "GracePeriod: " + GracePeriod + "\n" + + "DestinationDirectory: " + DestinationDirectory + "\n" + + "TmpTransferDir: " + TmpTransferDir + "\n" + + "EnforceInheritedACLs: " + EnforceInheritedACLs + "\n" + + "ServiceTimer: " + ServiceTimer + "\n" + + "InterPacketGap: " + InterPacketGap + "\n" + + "MaxCpuUsage: " + MaxCpuUsage + "\n" + + "MinAvailableMemory: " + MinAvailableMemory + "\n"; + foreach (var processName in BlacklistedProcesses) { + msg += "BlacklistedProcess: " + processName + "\n"; + } + foreach (var driveToCheck in SpaceMonitoring) { + msg += "Drive to check free space: " + driveToCheck.DriveName + + " (threshold: " + driveToCheck.SpaceThreshold + ")" + "\n"; + } + if (string.IsNullOrEmpty(SmtpHost)) { + msg += "SmtpHost: ====== Not configured, disabling email! ======" + "\n"; + } else { + msg += + "SmtpHost: " + SmtpHost + "\n" + + "EmailFrom: " + EmailFrom + "\n" + + "AdminEmailAdress: " + AdminEmailAdress + "\n" + + "AdminDebugEmailAdress: " + AdminDebugEmailAdress + "\n" + + "StorageNotificationDelta: " + StorageNotificationDelta + "\n" + + "AdminNotificationDelta: " + AdminNotificationDelta + "\n" + + "GraceNotificationDelta: " + GraceNotificationDelta + "\n"; + } + return msg; + } + } +} diff --git a/ATXSerializables/ServiceStatus.cs b/ATXSerializables/ServiceStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..5bdc52f41e790961ba74026e49fc70f909b4aa10 --- /dev/null +++ b/ATXSerializables/ServiceStatus.cs @@ -0,0 +1,226 @@ +using System; +using System.IO; +using System.Xml.Serialization; + +namespace ATXSerializables +{ + [Serializable] + public class ServiceStatus + { + [XmlIgnore] string _storageFile; // remember where we came from + [XmlIgnore] private ServiceConfig _config; + [XmlIgnore] public string ValidationWarnings; + + private DateTime _lastStatusUpdate; + private DateTime _lastStorageNotification; + private DateTime _lastAdminNotification; + private DateTime _lastGraceNotification; + + private string _limitReason; + string _currentTransferSrc; + string _currentTargetTmp; + + bool _transferInProgress; + private bool _serviceSuspended; + private bool _cleanShutdown; + + private long _currentTransferSize; + + [XmlElement("LastStatusUpdate", DataType = "dateTime")] + public DateTime LastStatusUpdate { + get { return _lastStatusUpdate; } + set { _lastStatusUpdate = value; } + } + + [XmlElement("LastStorageNotification", DataType = "dateTime")] + public DateTime LastStorageNotification { + get { return _lastStorageNotification; } + set { + _lastStorageNotification = value; + Serialize(); + } + } + + [XmlElement("LastAdminNotification", DataType = "dateTime")] + public DateTime LastAdminNotification { + get { return _lastAdminNotification; } + set { + _lastAdminNotification = value; + Serialize(); + } + } + + [XmlElement("LastGraceNotification", DataType = "dateTime")] + public DateTime LastGraceNotification { + get { return _lastGraceNotification; } + set { + _lastGraceNotification = value; + Serialize(); + } + } + + public string LimitReason { + get { return _limitReason; } + set { + _limitReason = value; + // log("LimitReason was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + public string CurrentTransferSrc { + get { return _currentTransferSrc; } + set { + _currentTransferSrc = value; + // log("CurrentTransferSrc was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + public string CurrentTargetTmp { + get { return _currentTargetTmp; } + set { + _currentTargetTmp = value; + // log("CurrentTargetTmp was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + public bool ServiceSuspended { + get { return _serviceSuspended; } + set { + _serviceSuspended = value; + // log("ServiceSuspended was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + public bool TransferInProgress { + get { return _transferInProgress; } + set { + _transferInProgress = value; + // log("FilecopyFinished was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + /// <summary> + /// Indicates whether the service was cleanly shut down (false while the service is running). + /// </summary> + public bool CleanShutdown { + get { return _cleanShutdown; } + set { + _cleanShutdown = value; + Serialize(); + } + } + + public long CurrentTransferSize { + get { return _currentTransferSize; } + set { + _currentTransferSize = value; + // log("CurrentTransferSize was updated (" + value + "), calling Serialize()..."); + Serialize(); + } + } + + public ServiceStatus() { + _currentTransferSrc = ""; + _currentTargetTmp = ""; + _transferInProgress = false; + } + + public void Serialize() { + /* During de-serialization, the setter methods get called as well but + * we should not serialize until the deserialization has completed. + * As the storage file name will only be set after this, it is sufficient + * to test for this (plus, we can't serialize anyway without it). + */ + if (_storageFile == null) { + // log("File name for XML serialization is not set, doing nothing."); + return; + } + // update the timestamp: + LastStatusUpdate = DateTime.Now; + try { + var xs = new XmlSerializer(GetType()); + var writer = File.CreateText(_storageFile); + xs.Serialize(writer, this); + writer.Flush(); + writer.Close(); + } + catch (Exception ex) { + log("Error in Serialize(): " + ex.Message); + } + log("Finished serializing " + _storageFile); + } + + static void log(string message) { + // use Console.WriteLine until proper logging is there (running as a system + // service means those messages will disappear): + Console.WriteLine(message); + /* + using (var sw = File.AppendText(@"C:\Tools\AutoTx\console.log")) { + sw.WriteLine(message); + } + */ + } + + public static ServiceStatus Deserialize(string file, ServiceConfig config) { + ServiceStatus status; + + var xs = new XmlSerializer(typeof(ServiceStatus)); + try { + var reader = File.OpenText(file); + status = (ServiceStatus) xs.Deserialize(reader); + reader.Close(); + } + catch (Exception) { + // if reading the status XML fails, we return an empty (new) one + status = new ServiceStatus(); + } + status._config = config; + ValidateStatus(status); + // now set the storage filename: + status._storageFile = file; + return status; + } + + private static void ValidateStatus(ServiceStatus s) { + // CurrentTransferSrc + if (s.CurrentTransferSrc.Length > 0 + && !Directory.Exists(s.CurrentTransferSrc)) { + s.ValidationWarnings += " - found non-existing source path of an unfinished " + + "transfer: " + s.CurrentTransferSrc + "\n"; + s.CurrentTransferSrc = ""; + } + + // CurrentTargetTmp + var currentTargetTmpPath = Path.Combine(s._config.DestinationDirectory, + s._config.TmpTransferDir, + s.CurrentTargetTmp); + if (s.CurrentTargetTmp.Length > 0 + && !Directory.Exists(currentTargetTmpPath)) { + s.ValidationWarnings += " - found non-existing temporary path of an " + + "unfinished transfer: " + currentTargetTmpPath + "\n"; + s.CurrentTargetTmp = ""; + } + } + + public string Summary() { + return + "CurrentTransferSrc: " + CurrentTransferSrc + "\n" + + "CurrentTargetTmp: " + CurrentTargetTmp + "\n" + + "TransferInProgress: " + TransferInProgress + "\n" + + "CurrentTransferSize: " + CurrentTransferSize + "\n" + + "LastStatusUpdate: " + + LastStatusUpdate.ToString("yyyy-MM-dd HH:mm:ss") + "\n" + + "LastStorageNotification: " + + LastStorageNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n" + + "LastAdminNotification: " + + LastAdminNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n" + + "LastGraceNotification: " + + LastGraceNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n"; + } + } +} diff --git a/ATXService.sln b/ATXService.sln index d9493449da049d71ede790b80c9444cadabb7a0e..0c405b028eacc0c2d3d7e1da61edc528ed9646c7 100644 --- a/ATXService.sln +++ b/ATXService.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 12.0.40629.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATXProject", "AutoTx\ATXProject.csproj", "{5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATXSerializables", "ATXSerializables\ATXSerializables.csproj", "{166D65D5-EE10-4364-8AA3-4D86BA5CE244}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Release|Any CPU.Build.0 = Release|Any CPU + {166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Debug|Any CPU.Build.0 = Debug|Any CPU + {166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Release|Any CPU.ActiveCfg = Release|Any CPU + {166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE