Synchronicity: 一个文件夹同步应用程序
用于同步文件夹的 Windows 服务
在我们开始之前...
我想警告你,这是一篇非常长的文章,坦白说,我对整个事情几乎完全感到厌倦。代码和这篇文章已经耗费了我三周的时间,现在我很难再提起工作的欲望。我还必须提到,我实际上还没有将这段代码部署到预期的服务器(主要是因为我刚刚提到的原因),你可以肯定,需要进行一些调整来修复很可能是小的但令人烦恼的问题(希望不要太多)。因此,请享受这篇文章,如果你愿意,请下载代码并使用由此产生的应用程序。我希望没有人会因为代码没有在其预期环境中进行100%测试而感到不满。
引言
我在家运行自己的网络服务器,你可能会猜到,我经常对其进行更新。更新过程包括将新/更改的文件复制到共享文件夹,然后使用终端服务登录到服务器并将文件复制到相应的inetpub子文件夹。你可能会猜到,这已经成为一个麻烦,因为有许多子文件夹(其中又包含子文件夹,等等),而且为了完成操作而实际登录到服务器有点麻烦。我当时想,肯定有更好的方法。
警告:下面显示的一些代码自作为文章内容添加以来可能已发生变化。我尝试更新所有内容,但在如此规模的文章中,我可能遗漏了一两件事。请耐心等待,如果文章中出现任何奇怪的地方,请务必查看实际代码(在zip文件中)。
注意:屏幕截图位于本文末尾,因为将它们放在我讨论GUI的地方更有意义。
解决方案
我需要一个应用程序,它能够同步文件夹,而无需任何比复制文件更繁琐的操作。是的,我知道像Microsoft Sync Toy和第三方产品Sure-Sync这样的程序,但作为一名程序员,我认为这会是一个相当直接的应用程序,可以自己编写。毕竟,它不需要经过实战测试供公众使用,而且我希望对它的工作方式有一定程度的控制。此外,编写代码比安装别人的代码更有趣。本文(及其描述的代码)就是结果。
特色技术
本文将演示以下技术
- 多线程(和线程池)
- 扩展方法
- WCF(使用命名管道)
- 系统托盘应用程序
- 进程间通信(使用WCF)
- Windows 服务
- Windows Forms
- 自定义事件
- 无模式窗口
- 强制管理员模式
- 文件 I/O
- 以编程方式启动/停止Windows服务
- 以编程方式安装/卸载Windows服务
初步目标
本质上,这个应用程序会以某种方式检测何时有新文件夹或文件被创建或复制到源文件夹中,然后立即将它们复制到目标文件夹。同时,我也不希望这个应用程序耗尽CPU或占用内存。虽然我目前个人不需要监视多个文件夹,但我认为你们中的一些人可能需要,所以内存消耗和CPU使用率是一个合理的问题。
每个好的应用程序都始于设计,经过几次失败的尝试后,我决定我也应该如此。毋庸置疑,这个应用程序应该采用Windows服务的形式(毕竟,即使用户未登录,我也希望它能运行),所以这是解决方案的主要重点。解决方案中的所有其他项目都将以明确支持Windows服务开发为目的而创建。
由于调试Windows服务有时会非常痛苦,我决定包含某种“测试控制台”应用程序将大大减轻调试任务的负担,让我可以在无需经历无数次安装/测试/卸载循环的情况下测试核心服务代码。
既然我现在解决方案中有两个应用程序有效地使用相同的代码,那么很明显我需要一个库程序集来包含大部分核心代码。
接下来,我决定如果有一种方法可以配置应用程序而无需手动编辑XML文件,那会很好。这个应用程序将是可选的,并且在运行时会局限于系统托盘。
最后,我认为测试控制台/服务应该能够将状态消息传达给配置应用程序,所以我需要一个WCF服务来执行该任务。此外,我决定它应该使用命名管道,因为所有程序集都将在同一台计算机上运行。像核心代码一样,它将由所有需要它的应用程序使用(我最后一次统计是三个),因此它也应该有自己的程序集。
后期添加
就像我这样宏大的计划经常发生的那样,总是在最后一刻出现意想不到的丑陋问题。在我的案例中,系统托盘应用程序的一项功能需要管理员权限,但只有这一个功能需要此类权限。在搜索Google甚至在这里发布了一个关于它的问题后,我得出结论,完成任务的唯一方法是在此解决方案中包含另一个程序集。它所做的只是尝试启动/停止Windows服务。
它应该如何工作
这个想法是让Windows服务(或测试控制台应用程序)每隔X分钟等待一次,然后检查暂存文件夹(用户将新/修改的文件放在此处)。如果发现新/修改的文件(通过我编写的一些巧妙的比较代码),这些文件将被复制到目标文件夹。文件复制完成后,Windows服务会尝试通知系统托盘应用程序,该应用程序(如果正在运行)将更新一个列表框控件,显示过去24小时的活动。
以下对象描述按创建顺序排列,主要是因为我无法想出更好的逻辑排列方式。
SynchroLib - 核心代码
在设计核心代码组件时,我最初的想法是使用我的`FileSystemWatcher`文章中的代码,但在考虑到我可能希望同时监视多个文件夹后,我担心代码会变成一个维护怪物。因此,我决定采用不同的方法。
`SynchroLib`程序集包含所有与实际操作文件夹和文件相关的代码。为了简洁起见,下面所有路径名示例都将基于以下两个示例路径
- 目标路径(文件最终将被复制到此处)- C:\inetpub\mywebsite
- 源路径(新文件将在此处暂存)- E:\Staging\mywebsite
SyncItemCollection 对象
我总是创建派生自集合的对象,因为我讨厌一直输入`List
为此,此对象包含`SyncItem`线程管理方法。它所做的只是启动每个`SyncItem`对象的同步过程。我曾一度计划使用类似于这篇文章中代码的`SmartThreadPool`,但后来决定我并不真正需要线程池管理的额外复杂性。因此,如果实现了`__USE_THREADPOOL__`编译器定义,则使用`SmartThreadPool`代码。您所要做的就是在SynchroLib项目的构建属性中定义它,然后将`SmartThreadPool` DLL包含在项目的程序集引用中。
在不使用`SmartThreadPool`的情况下,此对象中的代码范围如下。首先,我们有从设置文件加载项的`XElement`属性
//................................................................................
public XElement XElement
{
get
{
XElement value = new XElement("SyncItems");
foreach(SyncItem item in this)
{
value.Add(item.XElement);
}
return value;
}
set
{
if (value != null)
{
foreach(XElement element in value.Elements())
{
this.Add(new SyncItem(element));
}
}
}
}
然后,我们有启动`SyncItem`线程的方法
//--------------------------------------------------------------------------------
public void StartUpdate()
{
if (this.Count > 0)
{
foreach(SyncItem item in this)
{
item.Start();
}
}
}
当您查看文件中的实际代码时,您会发现实现线程池所需的代码量相对较大,您可能会随之同意,虽然在技术上“更酷”,但它比完成工作所需的代码量要多。但是,请记住,您可以通过简单的编译器定义将其打开,所以如果那让您感到兴奋,请随时使用。
SyncItem 对象
每个同步文件夹都由一个`SyncItem`对象表示,该对象包含以下数据属性
- 名称 - 这是您可以赋予`SyncItem`的英文名称,仅用于在浏览状态消息时更容易识别。
- SyncFromFolder - 这是用户暂存要同步的文件(也称为源文件夹)的文件夹。
- SyncToFolder - 这是文件同步到的文件夹(也称为目标文件夹)。
- BackupBeforeSync - 如果要同步的文件在同步前进行了备份,则为 True。
- DeleteAfterSync - 如果要同步的文件在同步后要从暂存文件夹中删除,则为 True。
我继续我与 Linq-to-XML 的恋情,并提供了一些易于使用的属性,方便设置/获取对象中的数据
//................................................................................
public XElement XElement
{
get
{
XElement value = new XElement("SyncItem"
,new XElement("Name", this.Name)
,new XElement("SyncFromPath", this.SyncFromPath)
,new XElement("SyncToPath", this.SyncToPath)
,new XElement("BackupPath", this.BackupFolder)
,new XElement("Enabled", this.Enabled)
,new XElement("SyncSubFolders", this.SyncSubfolders)
,new XElement("BackupBeforeSync", this.BackupBeforeSync)
,new XElement("DeleteAfterSync", this.DeleteAfterSync)
);
return value;
}
set
{
this.Name = value.GetValue("Name", Guid.NewGuid().ToString("N"));
this.SyncFromPath = value.GetValue("SyncFromPath", "");
this.SyncToPath = value.GetValue("SyncToPath", "");
this.BackupPath = value.GetValue("BackupPath", "");
this.Enabled = value.GetValue("Enabled", true);
this.SyncSubfolders = value.GetValue("SyncSubFolders", true);
this.BackupBeforeSync = value.GetValue("BackupBeforeSync", false);
this.DeleteAfterSync = value.GetValue("DeleteAfterSync", false);
}
}
这些属性由适当的构造函数重载支持
//--------------------------------------------------------------------------------
public SyncItem(XElement value)
{
this.XElement = value;
Init();
}
Init 方法(从所有构造函数调用)在所有适当的属性都已正确配置后创建并启动线程
//--------------------------------------------------------------------------------
public void Init()
{
if (this.SyncThread != null)
{
this.SyncThread.Abort();
this.SyncThread = null;
}
this.SyncThread = new Thread(new ThreadStart(SyncFiles));
this.SyncThread.IsBackground = true;
if (this.CanStartSync)
{
this.ToFilesList = new FileInfoList(this.SyncFromPath, this.SyncToPath);
this.ToFilesList.GetFiles(this.SyncToPath, this.SyncSubfolders);
}
}
对象初始化后,会扫描目标文件夹以查找现有文件,并维护此文件列表,直到应用程序生命周期结束。
由于同步可能是一个漫长的过程,同步操作在线程中执行。以下方法启动线程。
//--------------------------------------------------------------------------------
public void Start()
{
Debug.WriteLine("{0} STARTED ====================", this.Name);
if (this.SyncThread == null || this.SyncThread.ThreadState != System.Threading.ThreadState.Unstarted)
{
this.SyncThread = new Thread(new ThreadStart(SyncFiles));
this.SyncThread.IsBackground = true;
}
this.SyncThread.Start();
}
您可能已经注意到if语句包含一个`ThreadState`检查。原因是线程一旦停止,就不能重新启动,所以我们必须检查线程状态,看是否需要重新创建线程,或者我们是否可以使用现有的线程。
实际的线程委托方法如下。它是一个非常简单的方法,指示现有文件列表更新自身,完成后,会为任何可能正在侦听的监听者触发一个事件。
//--------------------------------------------------------------------------------
private void SyncFiles()
{
if (this.CanStartSync)
{
try
{
DateTime before = DateTime.Now;
this.ToFilesList.Update(this.SyncFromPath, this.SyncSubfolders);
DateTime after = DateTime.Now;
TimeSpan elapsed = after - before;
int updates = this.ToFilesList.Updates;
FileInfoEvent(this, new FileInfoArgs(updates, elapsed));
}
catch (ThreadAbortException ex)
{
if (ex != null) {}
}
catch (Exception)
{
throw;
}
}
}
请注意,我们只是吞掉`ThreadAbortException`(因为它很可能是因为我们告诉它中止而中止的),我们只是重新抛出任何其他异常。您可能已经注意到,在尝试同步文件之前,我们检查`CanStartSync`属性。此属性对数据执行一些健全性检查,如果一切正常则返回true
//................................................................................
public bool CanStartSync
{
get
{
bool canSync = false;
if (Directory.Exists(this.SyncFromPath) &&
Directory.Exists(this.SyncToPath) &&
this.SyncFromPath.ToLower() != this.SyncToPath.ToLower() &&
this.Enabled)
{
canSync = true;
}
return canSync;
}
}
我们发送的事件包含执行的更新数量,以及处理更新所花费的时间。
public class FileInfoArgs : EventArgs
{
public int UpdateCount { get; set; }
public TimeSpan Elapsed { get; set; }
//--------------------------------------------------------------------------------
public FileInfoArgs(int count, TimeSpan elapsed)
{
this.UpdateCount = count;
this.Elapsed = elapsed;
}
}
public delegate void FileInfoHandler(object sender, FileInfoArgs e);
FileInfoEx 对象
这个对象之所以存在,仅仅是因为您不能从`FileInfo`派生一个新对象。我需要一种方法来剥离检索到的对象的根文件夹,因为我们需要能够比较文件名,但只在文件夹名称应该匹配的层次结构位置进行比较。您无法通过完全限定路径来比较文件名,也不能使用`FileInfo.Name`属性,因为同名文件可能存在于多个子文件夹中。例如,这个文件名
C:\inetpub\mywebsite\myfile.aspx
...与此文件不同
C:\inetpub\mywebsite\thisfolder\myfile.aspx
因此,为了使文件具有可比性,我们必须剥离我所称的它们的根文件夹名称,以便集合中的名称看起来像这样
mywebsite\myfile.aspx
既然我必须创建`FileInfoEx`对象,将文件比较代码放在其中是完全合理的。它由一个方法组成,该方法根据指定的文件比较标志确定指定的`FileInfoEx`项是否与正在比较的项匹配。
//--------------------------------------------------------------------------------
public bool Equal(FileInfoEx fileB, FileCompareFlags flags)
{
// assume no matches
FileCompareFlags equalFlags = 0;
// first, we compare the unrooted name (the filename without the
// root from/to path)
if ((flags & FileCompareFlags.UnrootedName) == FileCompareFlags.UnrootedName)
{
equalFlags = (this.FileName == fileB.FileName) ? FileCompareFlags.UnrootedName : 0;
}
// and then we compare the actual FileInfo properties
equalFlags |= this.FileInfoObj.EqualityFlags(fileB.FileInfoObj, flags);
// if the flags that are set here are equal to the flags specified, this
// method will return true
return (equalFlags == flags);
}
`FileCompareFlags`枚举器提供了一种定义`FileInfoEx`项必须有多么“相等”才能在应用程序看来“相等”的方法。由于可以比较一个或多个属性的相等性,因此该枚举器利用`[Flags]`属性,并如下所示
[Flags]
public enum FileCompareFlags {All = 0,
FullName = 1,
Created = 2,
LastAccess = 4,
LastWrite = 8,
Length = 16,
CreatedUTC = 32,
LastAccessUTC = 64,
LastWriteUTC = 128,
Attributes = 256,
Extension = 512,
UnrootedName = 1024};
文件比较的最终组件是一个扩展方法,它比较实际的`FileInfo`属性(以及几个支持方法)
//--------------------------------------------------------------------------------
private static bool FlagIsSet(FileCompareFlags flags, FileCompareFlags flag)
{
bool isSet = ((flags & flag) == flag);
return isSet;
}
//--------------------------------------------------------------------------------
public static bool Equal(this FileInfo fileA, FileInfo fileB, FileCompareFlags flags)
{
bool isEqual = (fileA.EqualityFlags(fileB, flags) == flags);
return isEqual;
}
//--------------------------------------------------------------------------------
public static FileCompareFlags EqualityFlags(this FileInfo fileA, FileInfo fileB, FileCompareFlags flags)
{
FileCompareFlags equalFlags = FileCompareFlags.All;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.Attributes) &&
fileA.Attributes == fileB.Attributes) ? FileCompareFlags.Attributes : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.Created) &&
fileA.CreationTime == fileB.CreationTime) ? FileCompareFlags.Created : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.CreatedUTC) &&
fileA.CreationTimeUtc == fileB.CreationTimeUtc) ? FileCompareFlags.CreatedUTC : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.Extension) &&
fileA.Extension == fileB.Extension) ? FileCompareFlags.Extension : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.LastAccess) &&
fileA.LastAccessTime == fileB.LastAccessTime) ? FileCompareFlags.LastAccess : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.LastAccessUTC) &&
fileA.LastAccessTimeUtc == fileB.LastAccessTimeUtc) ? FileCompareFlags.LastAccessUTC : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.LastWrite) &&
fileA.LastWriteTime == fileB.LastWriteTime) ? FileCompareFlags.LastWrite : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.LastWriteUTC) &&
fileA.LastWriteTimeUtc == fileB.LastWriteTimeUtc) ? FileCompareFlags.LastWriteUTC : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.Length) &&
fileA.Length == fileB.Length) ? FileCompareFlags.Length : 0;
equalFlags |= (FlagIsSet(flags, FileCompareFlags.FullName) &&
fileA.FullName == fileB.FullName) ? FileCompareFlags.FullName : 0;
return equalFlags;
}
上述方法使用指定的标志并比较相应属性是否相等。如果指定属性相等,则其标志值将与临时相等标志变量进行按位或运算,并最终与指定的相等标志值进行比较。如果两个相等标志值匹配,则文件被认为是“相等”的,并且混乱随之而来。
核心代码将基本上每隔X分钟(时间由用户指定)检查一次文件夹,并检索源文件夹中所有文件/文件夹的列表,将其与目标文件夹的内容进行比较,然后将任何新的或更改的文件复制到适当的目标文件夹/子文件夹。
尽管定时线程在两个应用程序中都需要,而且代码就在那里,但在这里讨论它更有意义。
FileInfoList 对象
此对象表示`FileInfoEx`对象的集合,并负责实际的同步过程,该过程通过调用`Update`方法开始。我们首先需要做的是在暂存文件夹(`SyncFromPath`)中构建文件列表。
//-------------------------------------------------------------------------------- public void Update(string path, bool incSubs) { FileInfoList newList = new FileInfoList(m_syncFromPath, m_syncToPath); newList.GetFiles(path, incSubs);
然后我们通过调用`NewOrChanged`方法找到所有需要删除的文件(看!我们正在使用LINQ!)
var newerList = (from item in newList where NewOrChanged(item) select item).ToList(); newList.Clear(); this.Updates = newerList.Count;
最后,我们遍历新/更改文件列表,并备份(如果需要)、删除我们正在替换的文件,最后复制新版本。
foreach (FileInfoEx item in newerList) { bool backupFirst = false; try { // build our file names string sourceName = System.IO.Path.Combine(m_syncFromPath, item.FileName); string targetName = System.IO.Path.Combine(m_syncToPath, item.FileName); // assume the path hasn't been verified bool pathVerified = false; // if the target file already exists if (File.Exists(targetName)) { // back it up if necessary if (backupFirst) { // copy to backup folder } // delete it File.Delete(targetName); // since the file exists, the path must exist as well pathVerified = true; } if (!pathVerified) { VerifyPath(System.IO.Path.GetDirectoryName(targetName)); } File.Copy(sourceName, targetName); } catch (Exception ex) { throw new Exception("Exception encountered while updating files", ex); } } }
SyncSettings 对象
因为我有两个应用程序使用相同的设置,所以我认为手动创建一个设置对象会更容易/方便,而这个对象就是结果。我没有从头开始编写整个代码,而是参考了我之前的文章(在应用程序之间共享用户设置[^]),并提取了AppSettingsBase类。这个类有很多已经编写好的实用属性和方法,例如一个创建所需应用程序数据文件夹的方法
//--------------------------------------------------------------------------------
protected string CreateAppDataFolder(string folderName)
{
string appDataPath = "";
string dataFilePath = "";
folderName = folderName.Trim();
if (folderName != "")
{
try
{
// Set the directory where the file will come from. The folder name
// returned will be different between XP and Vista. Under XP, the default
// folder name is "C:\Documents and Settings\All Users\Application Data\[folderName]"
// while under Vista, the folder name is "C:\Program Data\[folderName]".
appDataPath = System.Environment.GetFolderPath(this.SpecialFolder);
}
catch (Exception)
{
throw;
}
if (folderName.Contains("\\"))
{
string[] path = folderName.Split('\\');
int folderCount = 0;
int folderIndex = -1;
for (int i = 0; i < path.Length; i++)
{
string folder = path[i];
if (folder != "")
{
if (folderIndex == -1)
{
folderIndex = i;
}
folderCount++;
}
}
if (folderCount != 1)
{
throw new Exception("Invalid folder name specified (this function only creates the root app data folder for the application).");
}
folderName = path[folderIndex];
}
}
if (folderName == "")
{
throw new Exception("Processed folder name resulted in an empty string.");
}
try
{
dataFilePath = System.IO.Path.Combine(appDataPath, folderName);
if (!Directory.Exists(dataFilePath))
{
Directory.CreateDirectory(dataFilePath);
}
}
catch (Exception)
{
throw;
}
return dataFilePath;
}
然后我创建了SyncSettings类,继承自AppSettingsBase,并添加了支持我的应用程序特定数据所需的方法。首先,我需要提供一个属性,可以用于从XElement对象获取/设置数据。
//................................................................................
public override XElement XElement
{
get
{
return new XElement(this.SettingsKeyName
,new XElement("SyncMinutes", this.SyncMinutes.ToString())
,new XElement("NormalizeTime", this.NormalizeTime.ToString())
,new XElement("UseHeuristics", this.UseHeuristics.ToString())
,new XElement("HeuristicTime", this.HeuristicTime.ToString())
,new XElement("HeuristicEvents", this.HeuristicEvents.ToString())
);
}
set
{
if (value != null)
{
this.SyncMinutes = value.GetValue("SyncMinutes", 5);
this.NormalizeTime = value.GetValue("NormalizeTime", true);
this.UseHeuristics = value.GetValue("UseHeuristics", true);
this.HeuristicTime = value.GetValue("HeuristicTime", 30);
this.HeuristicEvents = value.GetValue("HeuristicEvents", 6);
}
}
}
然后是SyncItems集合
public SyncItemCollection SyncItems { get; set; }
指定了一些常量
private const string APP_DATA_FOLDER = "PaddedwallSync";
private const string APP_DATA_FILENAME = "Settings.xml";
private const string FILE_COMMENT = "Synch Settings";
private const string SETTINGS_KEYNAME = "Settings";
private const string SYNC_ITEMS_KEYNAME = "SyncItems";
在派生类的构造函数中初始化了一些基类属性
public SyncSettings(XElement defaultSettings)
: base()
{
this.SyncItems = new SyncItemCollection();
this.SpecialFolder = System.Environment.SpecialFolder.CommonApplicationData;
this.DefaultSettings = defaultSettings;
this.IsDefault = false;
this.FileName = APP_DATA_FILENAME;
this.SettingsKeyName = SETTINGS_KEYNAME;
this.SettingsFileComment = FILE_COMMENT;
this.DataFilePath = CreateAppDataFolder(APP_DATA_FOLDER);
this.FullyQualifiedPath = System.IO.Path.Combine(this.DataFilePath, this.FileName);
...
}
最后,我添加了适当的`Load`和`Save`方法
//--------------------------------------------------------------------------------
public override void Load()
{
if (File.Exists(this.FullyQualifiedPath))
{
try
{
XDocument doc = XDocument.Load(this.FullyQualifiedPath);
XElement root = doc.Element("ROOT");
if (root != null)
{
XElement settings = root.Element(this.SettingsKeyName);
if (settings != null)
{
this.XElement = settings;
}
if (SyncItems != null)
{
this.SyncItems.Clear();
}
this.SyncItems = new SyncItemCollection(root.Element("SyncItems"));
}
}
catch (Exception ex)
{
throw new Exception("Exception encountered while loading settings file", ex);
}
}
}
//--------------------------------------------------------------------------------
public override void Save()
{
try
{
if (File.Exists(this.FullyQualifiedPath))
{
File.Delete(this.FullyQualifiedPath);
}
XDocument doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"),
new XComment(this.SettingsFileComment));
XElement root = new XElement("ROOT");
root.Add(this.XElement);
root.Add(this.SyncItems.XElement);
doc.Add(root);
doc.Save(this.FullyQualifiedPath);
}
catch (Exception ex)
{
throw new Exception("Exception encountered while saving settings file", ex);
}
}
其他扩展方法
我喜欢扩展方法。它们允许您扩展没有源代码或无法继承的类。对于这个应用程序,我需要两个这样的方法。第一个方法涉及让我们比较两个DateTime对象,但允许我们指定要比较的属性(或多个属性)。在这个应用程序中,我们只需要比较分钟,这在没有额外代码的情况下很容易做到,但那有什么乐趣呢?我;我已经发布了一个关于这段代码的提示技巧(这里[^]),但由于我讨厌点击来查找与文章相关的内容,所以我认为在这里介绍这段代码是值得的。我们首先定义一个带有Flags属性的枚举器,以便我们可以一次设置多个枚举器
[Flags]
public enum DatePartFlags {Ticks = 0,
Year = 1,
Month = 2,
Day = 4,
Hour = 8,
Minute = 16,
Second = 32,
Millisecond = 64 };
我使用`Ticks`作为第一个序数,这样如果程序员愿意,他可以比较整个`DateTime`而无需指定所有其他属性。接下来,我实现了一个辅助方法来帮助确定是否设置了标志
//--------------------------------------------------------------------------------
private static bool FlagIsSet(DatePartFlags flags, DatePartFlags flag)
{
bool isSet = ((flags & flag) == flag);
return isSet;
}
最后,我实现了`Equal`方法
//--------------------------------------------------------------------------------
public static bool Equal(this DateTime now, DateTime then, DatePartFlags flags)
{
bool isEqual = false;
if (flags == DatePartFlags.Ticks)
{
isEqual = (now == then);
}
else
{
DatePartFlags equalFlags = DatePartFlags.Ticks;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Year) &&
now.Year == then.Year) ? DatePartFlags.Year : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Month) &&
now.Month == then.Month) ? DatePartFlags.Month : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Day) &&
now.Day == then.Day) ? DatePartFlags.Day : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Hour) &&
now.Hour == then.Hour) ? DatePartFlags.Hour : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Minute) &&
now.Minute == then.Minute) ? DatePartFlags.Minute : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Second) &&
now.Second == then.Second) ? DatePartFlags.Second : 0;
equalFlags |= (FlagIsSet(flags, DatePartFlags.Millisecond) &&
now.Millisecond == then.Millisecond) ? DatePartFlags.Millisecond : 0;
isEqual = (flags == equalFlags);
}
return isEqual;
}
为了确定相等性,我们传递所需的标志和要比较的DateTime对象。在该方法中,我们创建一个新的标志枚举器,并检查每个属性(由传入的枚举器指示),并根据需要将匹配的标志应用于内部枚举器。完成属性比较后,我们将生成的内部标志枚举器与传入的枚举器进行比较。如果它们相同,则所有适当的属性都相等,并且我们匹配成功!
SynchroWCF - 通信组件
为了实现控制台和服务组件与设置应用程序之间的通信,并且由于所有这些组件都将存在于同一台机器上,我选择使用带有命名管道绑定的WFC。我有两个要求——我希望在没有配置文件的情况下实例化主机和客户端,并且我希望从主机发布事件,以便实例化它的应用程序可以显示从客户端发送的状态消息。服务本身非常简单,因为我们只有两个方法。WCF服务不支持方法重载(所有方法都必须有唯一的名称)真是遗憾——否则它会提供一个更清晰的接口。
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
[ServiceContract]
public interface ISynchroService
{
[OperationContract]
void SendStatusMessage(string msg);
[OperationContract]
void SendStatusMessageEx(string msg, DateTime datetime);
}
就类本身而言,我不得不做一些对于典型的普通 WCF 服务而言不寻常的事情。请记住,我不需要存储`ServiceHost`对象接收到的任何东西,但我 *确实* 想将接收到的数据传递给托管服务的应用程序。为了实现这一点,我需要通过包含`InstanceContextMode`属性来增强`ServiceBehavior`属性。通过将其设置为`Single`,我可以从 ServiceHost 发送事件。
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, IncludeExceptionDetailInFaults=true)]
public class SynchroService : ISynchroService
{
public event SynchroHostEventHandler SynchroHostEvent = delegate { };
public SynchroService()
{
}
//--------------------------------------------------------------------------------
public void SendStatusMessage(string msg)
{
SynchroHostEvent(this, new SynchroHostEventArgs(msg, DateTime.Now));
}
//--------------------------------------------------------------------------------
public void SendStatusMessageEx(string msg, DateTime datetime)
{
SynchroHostEvent(this, new SynchroHostEventArgs(msg, datetime));
}
}
为了使这段代码既可用作客户端又可用作主机,我创建了静态的`SvcGlobals`对象。在该对象中,我提供了操作`ServiceHost`(用于服务器)和`ChannelFactory`(用于客户端)对象的方法。
我首先定义一些变量,这样所生成的对象就能“同心同德”地工作。
public static class SvcGlobals
{
private static NetNamedPipeBinding m_Binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
public static Uri m_baseAddress = new Uri("net.pipe:///SynchroService");
并在构造函数中加入了一些初始化代码
//--------------------------------------------------------------------------------
static SvcGlobals()
{
SvcHost = null;
}
接下来,我编写了几个用于实例化和关闭主机的方法
//--------------------------------------------------------------------------------
public static bool CreateServiceHost()
{
bool available = (SvcHost != null);
if (SvcHost == null)
{
try
{
SynchroService svc = new SynchroService();
SvcHost = new ServiceHost(svc, m_baseAddress);
SvcHost.AddServiceEndpoint(typeof(ISynchroService), m_binding, "");
SvcHost.Open();
available = (SvcHost != null);
}
catch (Exception ex)
{
throw new Exception("Exception encountered while creating SvcHost", ex);
}
}
return available;
}
//--------------------------------------------------------------------------------
public static void CloseServiceHost()
{
try
{
if (SvcHost != null && SvcHost.State == CommunicationState.Opened)
{
SvcHost.Close();
}
}
catch (Exception ex)
{
throw new Exception("Exception encountered while closing SvcHost", ex);
}
}
最后,还有几个与客户端相关的方法
//--------------------------------------------------------------------------------
public static bool CreateServiceClient()
{
bool available = (SvcClient != null);
if (SvcClient == null)
{
try
{
ChannelFactory<ISynchroService> factory = new ChannelFactory<ISynchroService>(m_binding, new EndpointAddress(m_baseAddress.AbsoluteUri));
SvcClient = factory.CreateChannel();
available = (SvcClient != null);
}
catch (Exception ex)
{
throw new Exception("Exception encountered while creating SvcClient", ex);
}
}
return available;
}
//--------------------------------------------------------------------------------
public static void ResetServiceClient()
{
SvcClient = null;
}
如您所见,实现 WCF 代码的影响非常小,唯一非典型的编码是支持我希望从服务发送到应用程序的事件。
SynchroSetup - 系统托盘应用程序
此应用程序允许用户配置和监视Windows服务(甚至测试控制台应用程序)。主窗体如下所示
最突出的功能是列表框,其中显示了从Windows服务接收到的所有状态消息。按钮用于以下操作
关闭 - 终止应用程序
最小化 - 将应用程序最小化到系统托盘
配置 - 允许用户配置Windows服务
启动/重启服务 - 启动(或重启)Windows服务(如果已安装)
停止服务 - 停止Windows服务(如果已安装)
如果 Windows 服务组件未安装,则“启动/重启服务”和“停止服务”按钮将被禁用。
配置
当用户点击“配置”按钮时,会显示“配置”窗体
此表单允许用户配置基本设置,以及添加、编辑或删除同步项。
同步分钟
此字段表示同步事件之间的时间间隔(分钟)。最小可能值为5,最大为60。
标准化时间
当服务启动其计时循环时,它会立即运行一个同步事件,然后将下一个事件时间计算为等于当前时间加上指定的同步分钟数的未来某个时间点。如果选中此复选框,服务将在第一个同步事件之后尝试将后续同步事件对齐到小时的偶数分钟。如果您坐在那里观察状态消息,这会使同步事件更具可预测性。
同步项列表
此控件显示所有当前指定的同步项。虽然可以显示无限数量的同步项,但您必须注意,拥有的越多,处理所有项所需的时间可能就越长,并且Windows服务在处理过程中消耗的内存也越多。明智的做法是在机器上指定不超过五个,并且在这样做时,请考虑为完成同步事件而将处理的文件数量。在我的情况下,我将只有一两个同步项。
添加按钮
“添加”按钮允许用户向列表中添加新的同步项。显示以下表单
由于大多数字段都是自解释的,我将本节限制在更有趣的控件上。首先,我们有同步项名称。请注意,它会自动为您填充。当表单初始化时,它会通过遍历现有名称,递增并附加一个计数器到“同步项”来生成自己的默认名称,直到找到一个列表中不存在的名称。这使您不必手动输入唯一的名称。
第二个最有趣的控件是标题为“悬停此处显示当前列表”的标签。当您将鼠标悬停在该标签上时,会显示一个无模式表单,其中包含当前名称列表。这允许您选择一个作为新名称的模板,或者只是查看已指定的内容,这样您就无需手动输入相同的名称。请看下面
编辑按钮
此按钮允许您编辑同步项列表中当前选定的同步项。该表单与“添加”按钮使用的表单相同,但由于用户正在编辑而不是添加新项,因此初始化方式略有不同。
删除按钮
此按钮允许用户删除同步项列表中当前选定的同步项。
安装服务按钮
我不知道你怎么想,但安装/卸载Windows服务真是个麻烦事,我讨厌一遍又一遍地做,尤其是在开发服务时。这个按钮允许用户无需使用命令窗口即可安装Windows服务。如果安装尝试成功,按钮文本将变为“卸载服务”。要使用此按钮安装/卸载服务,您必须已经指定了InstallUtil.EXE应用程序的位置。
SynchroServiceStarter - 权限问题,笨蛋
这个应用程序存在的唯一目的就是启动或停止Windows服务。我不得不创建这个应用程序,以缓解设置应用程序所需的权限提升(而我想要避免这样做)。代码看起来像这样
//--------------------------------------------------------------------------------
static void Main(string[] args)
{
try
{
// If this application isn't running as administrator, set the
// appropriate error code, and display a message at the console.
if (!Globals.RunningAsAdministrator())
{
Console.WriteLine("You must run this application as administrator.");
SetExitCode(SSSExitCodes.NotAdminMode);
return;
}
// If the service isn't installed, set the appropriate exit code and
// display a message at the console.
if (!Globals.IsServiceInstalled())
{
SetExitCode(SSSExitCodes.ServiceNotFound);
Console.WriteLine("Service not found.");
return;
}
// If we have arguments, try to start or stop the service
if (args != null &&
args.Length == 1 &&
args[0].Length > 1 &&
(args[0][0] == '-' || args[0][0] == '/'))
{
SetExitCode(SSSExitCodes.Success);
ServiceControllerStatus currentStatus = Globals.SynchroService.Status;
switch (args[0].Substring(1).ToLower())
{
case "start":
Globals.StartService();
if (!Globals.IsServiceInstalledWithStatus(ServiceControllerStatus.Running))
{
SetExitCode(SSSExitCodes.ServiceNotStarted);
Console.WriteLine("Service found, but could not be started.");
}
else
{
Console.WriteLine("Service found, and started.");
}
break;
case "stop":
Globals.StopService();
if (!Globals.IsServiceInstalledWithStatus(ServiceControllerStatus.Stopped))
{
SetExitCode(SSSExitCodes.ServiceNotStopped);
Console.WriteLine("Service found, but could not be stopped.");
}
else
{
Console.WriteLine("Service found, and stopped.");
}
break;
default:
SetExitCode(SSSExitCodes.InvalidParameters);
Console.WriteLine("Service found, but no appropriate commandline parameters specified. Expecting either '-start' or '-stop'");
break;
}
}
else
{
SetExitCode(SSSExitCodes.NoParameters);
Console.WriteLine("Service found, but could not be stopped.");
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("Exception: {0}", ex.Message);
SetExitCode(SSSExitCodes.Exception);
}
}
//--------------------------------------------------------------------------------
static void SetExitCode(SSSExitCodes code)
{
Environment.ExitCode = (int)code;
}
SharedAppObjects - 用于共享,嗯,对象
当SynchroServiceStarter应用程序的需求扇了我一巴掌时,我决定需要一个程序集,它能让我在启动器应用程序和配置应用程序中正确设置和解释退出代码。这个程序集就是结果。除了`SSSExitCodes`枚举器和一个包含将整数转换为指定枚举器类型序数的单个方法的静态类之外,它实际上什么都没有。
public enum SSSExitCodes { Success=0,
NotAdminMode,
ServiceNotFound,
ServiceNotStarted,
ServiceNotStopped,
Exception,
InvalidParameters,
NoParameters,
Unexpected };
public static class SharedAppObjects
{
//--------------------------------------------------------------------------------
/// <summary>
/// Casts the specified integer to an appropriate enum. If all else fails,
/// the enum will be returned as the specified default ordinal.
/// </summary>
/// <param name="value">The integer value representing an enumeration element</param>
/// <param name="deafaultValue">The default enumertion to be used if the specified "value" does not exist in the enumeration definition</param>
/// <returns></returns>
public static T IntToEnum<T>(int value, T defaultValue)
{
T enumValue = (Enum.IsDefined(typeof(T), value)) ? (T)(object)value : defaultValue;
return enumValue;
}
}
在开发过程中,我开始纠结于WCF服务无法跨越权限边界进行通信的事实,并开始尝试不同的IPC(进程间通信)方法,最后决定只能接受SynchroSetup应用程序必须以管理员模式运行的事实。因此,我决定,虽然ServiceStarter应用程序并非总是必需的,但我还是将该应用程序中的大部分代码移到了这个库中。
我们首先看到一个确定服务是否安装的方法。这个方法只是检索正在运行的服务列表并遍历它们以查看是否能找到我们的服务。如果找到了,我们会从列表中获取实例以供以后使用。
//--------------------------------------------------------------------------------
public static bool IsServiceInstalled()
{
bool installed = false;
// get the list of currently installed Windows services
ServiceController[] services = ServiceController.GetServices();
// look for the name
foreach (ServiceController service in services)
{
if (service.ServiceName == m_serviceName)
{
SynchroService = new ServiceController(m_serviceName);
installed = true;
break;
}
}
return installed;
}
我还需要一个重载来检查服务是否已安装并具有特定状态
//--------------------------------------------------------------------------------
public static bool IsServiceInstalled(ServiceControllerStatus status)
{
bool installed = (IsServiceInstalled() && SynchroService.Status == status) ? true : false;
return installed;
}
最后,有一个实际启动和/或停止服务的方法。
//--------------------------------------------------------------------------------
public static void StartStopService(bool starting)
{
try
{
if (IsServiceInstalled())
{
TimeSpan timeout = TimeSpan.FromMilliseconds(SERVICE_TIMEOUT);
if (SynchroService.Status == ServiceControllerStatus.Running)
{
SynchroService.Stop();
SynchroService.WaitForStatus(ServiceControllerStatus.Stopped, timeout);
}
if (starting)
{
SynchroService.Start();
SynchroService.WaitForStatus(ServiceControllerStatus.Running, timeout);
}
}
else
{
throw new Exception("SynchroService object was null.");
}
}
catch (Exception ex)
{
throw new Exception(string.Format("Service could not be {0}:\n\n{1}",
(restart) ? "started/restarted" : "stopped",
ex.Message));
}
}
我还另外提供了两个方法,分别用于启动和停止服务,每个方法都调用上面带有适当 true/false 参数的方法。
SynchroConsole - 测试程序
此应用程序用于测试/调试核心代码,如果需要,实际上可以用来代替Windows服务。它提供了一个简单的用户界面,仅在运行其处理循环时显示状态消息。由于这只是一个测试控制台,我将提供一个屏幕截图,但除了引起您对特定功能的注意之外,不会详细介绍应用程序本身。
此应用程序不仅测试循环/同步代码,还测试服务和系统托盘中配置应用程序之间的通信。确保WCF通信正常工作的最明显方法是注意窗体右上角。如果WCF连接成功,将显示一个标签,上面写着“已连接”。如果应用程序找不到配置应用程序的服务主机,则标签将显示“服务器未找到”。
此外,状态消息将反映连接的缺失。控制台应用程序将在每个同步事件中尝试重新连接,因此用户可以随意打开和关闭配置应用程序,而不会对控制台应用程序产生任何不良影响。
SynchroService - Windows 服务
此服务是本文存在的主要原因,旨在安装在需要同步的计算机上,并与配置应用程序一起安装(安装测试控制台是可选的)。
服务被安装为手动启动,因为在初始安装时,您很可能没有指定任何同步项(这就是我为用户提供从配置应用程序安装方法的原因)。服务还以LocalSystem帐户安装,以便我可以同步到受保护的文件夹(如Inetpub)。以下是Installer类的构造函数
public SyncSvcInstaller()
{
InitializeComponent();
try
{
// create and configure our installer and processinstaller objects
m_serviceInstaller = new ExtendedServiceInstaller();
// If you don't want the service to start automatically, change the
// following line to whatever start mode is appropriate.
m_serviceInstaller.StartType = ServiceStartMode.Manual;
//TODO: Change these three items to more accurately reflect the service's purpose
m_serviceInstaller.DisplayName = "Synchronicity Service";
m_serviceInstaller.ServiceName = "Synchronicity Service";
m_serviceInstaller.Description = "Synchronizes files between specified folder pairs";
m_processInstaller = new ServiceProcessInstaller();
m_processInstaller.Account = ServiceAccount.LocalSystem;
// add our installers to the list
this.Installers.Add(m_serviceInstaller);
this.Installers.Add(m_processInstaller);
// perform any other preparatory steps necessary to make your service
// function properly.
}
catch (IndexOutOfRangeException oorEx)
{
Console.WriteLine(oorEx.Message);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
接下来,我们来看一下服务本身。由于大部分核心代码都位于已描述的其他程序集中,因此它并没有太多内容。当然,服务最重要的部分围绕着决定何时运行同步事件的线程。服务类开始(正如你可能猜到的那样)声明一些必要的变量
public partial class SynchroService :ServiceBase
{
Thread m_updateThread = null;
DatePartFlags m_equalityFlags = DatePartFlags.Minute | DatePartFlags.Second;
SyncItemCollection m_syncItems = new SyncItemCollection();
SyncSettings m_settings = new SyncSettings(null);
System.Threading.ThreadState m_threadState = System.Threading.ThreadState.Unstarted;
AppLog m_log = new AppLog("SynchroService", AppLog.LogLevel.Verbose);
`m_updateThread`成员是坐着旋转并最终启动更新处理的线程。`m_threadState`成员允许我们在服务本身处于暂停状态时有效地“暂停”线程。列出的最后一个成员支持日志功能,并且是有些老旧的代码。当我编写它时,我还是.Net编程的新手,而且由于它有效,并且由于我已经在这段代码上花费了太多时间,我现在没有兴趣回去调整它。事实上,我甚至不会在本文中讨论它,因为坦率地说,它不值得讨论。我满足于“它有效”的知识。
接下来,我们有 OnStart 方法
//--------------------------------------------------------------------------------
protected override void OnStart(string[] args)
{
m_log.SendToLog("Starting SynchroService...", AppLog.LogLevel.Verbose);
this.m_settings.Load();
this.m_updateThread = new Thread(new ThreadStart(UpdateThread));
this.m_updateThread.IsBackground = true;
this.m_updateThread.Start();
}
我们在这里所做的唯一事情就是启动线程。线程会一直运行,直到服务停止。线程委托如下所示
//--------------------------------------------------------------------------------
private void UpdateThread()
{
m_threadState = System.Threading.ThreadState.Running;
try
{
DateTime temp;
DateTime now;
DateTime then = new DateTime(0);
TimeSpan interval = new TimeSpan(0, 0, this.m_settings.SyncMinutes, 0, 0);
bool waiting = true;
while (true)
{
// if the service is running
if (m_threadState == System.Threading.ThreadState.Running)
{
temp = DateTime.Now;
now = new DateTime(temp.Year, temp.Month, temp.Day, temp.Hour, temp.Minute, 0, 0);
if (!waiting)
{
int difference = (this.m_settings.NormalizeTime) ? now.Minute % m_settings.SyncMinutes : 0;
then = now.Add(interval.Subtract(new TimeSpan(0, 0, difference, 0, 0)));
waiting = true;
}
if (now.Equal(then, m_equalityFlags) || then.Ticks == 0)
{
waiting = false;
CheckForFiles();
}
else
{
Thread.Sleep(1000);
}
}
else
{
Thread.Sleep(1000);
}
}
}
catch (ThreadAbortException)
{
// we don't care if the thread aborted because we probably aborted it.
}
catch (Exception ex)
{
m_log.SendErrorToLog(ex.Message);
}
}
线程方法每秒检查一次是否该检查文件以进行更新。如果是时候检查更新,则调用`CheckForFiles()`方法
//--------------------------------------------------------------------------------
private void CheckForFiles()
{
string text = string.Format("Checking {0} sync item{1}",
this.m_settings.SyncItems.Count,
(this.m_settings.SyncItems.Count > 1) ? "s" : "");
UpdateActivityList(text, DateTime.Now);
m_settings.SyncItems.StartUpdate();
}
因为我从控制台应用程序中获取了代码,所以我使用了`UpdateActivityList`方法并对其进行了修改以用于此服务
//--------------------------------------------------------------------------------
private void UpdateActivityList(string text, DateTime date)
{
string dateFormat = "dd MMM yyyy HH:mm";
string errorText = "";
DateTime now = DateTime.Now;
text = string.Format("{0} - {1}", date.ToString(dateFormat), text);
try
{
if (SvcGlobals.CreateServiceClient())
{
SvcGlobals.SvcClient.SendStatusMessageEx(text, date);
}
}
catch (EndpointNotFoundException)
{
errorText = "No endpoint found.";
}
catch (CommunicationObjectFaultedException)
{
errorText = "WCF client object is faulted.";
}
catch (Exception ex)
{
errorText = ex.Message;
}
if (!string.IsNullOrEmpty(errorText))
{
m_log.SendErrorToLog(errorText);
m_log.SendToLog(text, AppLog.LogLevel.Noise);
}
}
最终评论
虽然编码既有趣又有时具有挑战性,但我在过程中发现并认识到了一些事情。
- 要启动/停止 Windows 服务,您需要管理员权限。我早就知道这一点,但我不知道的是,无法在应用程序中提升权限,仅针对该应用程序的某个部分。
- 如果您想通过WCF在Windows服务和桌面应用程序之间进行通信,则桌面应用程序必须以管理员身份运行,并且似乎与您使用的绑定无关。
- 如果你想使用`Process`类以管理员身份启动应用程序,你必须设置`Process.StartInfo.Verb="runas"`和`Process.StartInfo.UseShellExecute=true`。这(设置`UseShellExecute=true`)反过来又阻止了你重定向任何输出。
- 徒劳无功?ServiceStarter 应用程序的编写是为了避免 SynchroSetup 应用程序需要管理员权限。然而,结果是 SynchroSetup 仍然需要管理员权限才能通过 WCF 与 SynchroService 应用程序通信。
- 有时,你就是得说“F*CK!”,然后找到一个合适的变通方法(这不是一个新教训,但在撰写本文时得到了极大的加强)。
既然Windows服务(我们今天在这里的全部原因)实际上是专门为我编写的,而且我将在我旁边的计算机上运行它,所以我决定我将接受系统托盘应用程序必须以管理员身份运行的要求,并且只在我需要时才运行它。如果我将其设置为以管理员身份运行,运行它将总是弹出UAC,因此将其放在Windows启动文件夹中只会让我烦透了。唉,它只在配置服务时才真正需要,而且说实话,我写所有这些代码已经感到厌倦了。
历史
- 2011年2月5日 - 原始版本