Before We Start...
I'd like to warn you that this is a VERY long article, and to be quite frank, I'm almost totally bored with the whole thing. The code and this article article have so far consumed three full weeks of my time, and it's beginning to be difficult to muster the desire to work on it. I also have to mention that I haven't actually deployed this code to the intended server (mostly for the reason I've just stated), and you can bet that there will have to be some tweaks needed to fix what will most likely be small but annoying issues (hopefully, not too many of them). Therefore, enjoy the article, and if you feel like it, download the code and play with the applications that result. I hope nobody takes it personally that the code isn't 100% tested in its intended environment.
Introduction
I run my own web server at home, and as you might guess, I make frequent updates to it. The update process involves copying new/changed files to a shared folder, and then using terminal services to log onto the box and copy the files to the appropriate inetpub sub-folder. As you might guess, this has become a bit of a bother because there are a number of sub-folders (that have sub-folders, and so on), and it's kind of a pain to have to actually log onto the server in order to complete the operation. Surely, I thought, there had to be a better way.
Caveat: Some of the code shown below may have changed since being added as article content. I tried to update everything, but as is likely in an article of this size, I may have missed one or two things. Be patient, and make sure you review the actual code (in the zip file) if something looks funny in the article.
NOTICE: Screen shots are at the end of this article because it made more sense to put them where I talk about the GUI.
Solution
I needed an app that would sync up folders that didn't involve anything more tedious than just copying the files somewhere. Yes, I'm aware of programs like Microsoft Sync Toy and a third-party product called Sure-Sync, but being a programmer, I figured this would be a fairly straightforward application to write myself. After all, it doesn't have to be battle-tested for public consumption, and I wanted a certain degree of control over precisely how it works. Besides, it's more fun to write code than it is to install someone else's. This article (and the code it describes) is the result.
Featured Technologies
The following technologies will be demonstrated in this article:
- Multi-threading (and thread pooling)
- Interprocess communication (using WCF)
- Start/stop a Windows service programatically
- Install/uninstall a Windows service programatically
Preliminary Goals
Essentially, this application would somehow detect when new folders or files were created or copied into a source folder, and then immediately copy them to the target folder. At the same time, I didn't want this app to suck all the life out of the CPU or hog memory. While I don't personally have a need to watch multiple folders at this time, I figured some of you might, so memory consumption and CPU usage are a reasonable concern.
Every good application starts out with a design, and after a few false starts, I decided I should as well. It probably goes without saying that this app should take the form of a Windows service (afterall, I want it to run even when a user isn't logged on), so that's the primary thrust of the solution. All other projects in the solution will be created with the express purpose of supporting the development of the Windows service.
Since debugging a Windows service can be downright painful at times, I decided including some sort of "test console" application would make the task of debugging far less burdensome, allowing me to test the core service code without actually having to go through the install/test/uninstall cycle a billion times.
Since I now have two applications in the solution that effectively use the same code, it became obvious that I needed a library assembly that would contain the bulk of the core code.
Next, I decided it would be nice if there was a way to configure the app without having to manually edit XML files. This app would be optional and when running would confine itself to the system tray.
Finally, I thought that the test console/service should be able to communicate status messages to the configuration app, so I need a WCF service to perform that task. Further, I decided that it should use named pipes because all of the assemblies would be running on the same box. Like the core code, it would be used by all of the apps that needed it (three by my last count), and so it deserved its own assembly as well.
Late Addition
As it often is with grandios schemes such as mine, something unexpected always seems to rear its ugly head at the last possible minute. In my case, one of the features of the system tray application required administrator privileges, but only that one single feature required such privileges. After scouring google and even posting a question here about it, I came to the conclusion that the only way to accomplish the task was to include yet another assembly in this solution. All it does is attempts to start/stop the Windows service.
How It's Supposed to Work
The idea is for the Windows service (or test console app) to wait every X minutes to pass, and then check the staging folder (where new/modified files will be placed by the user). If new/modified files are found (via some clever comparison code written by yours truly), those files are copied to the target folder. After the files have been copied, the Windows service attempts to notify the systray application which (if running) will update a listbox control showing the last 24 hours worth of activity.
The object descriptions below are listed in the order they were created, mostly because I couldn't come up with any better way to logically list them.
SynchroLib - The Core Code
In designing the core code component, my initial thought was to make use of the code found in my FileSystemWatcher
article, but after considering that I might want to watch several folders at one time, I was concerned that the code would turn into a maintenance monster. I therefore decided to take a different approach.
The SynchroLib
assembly contains all of the code associated with actually manipulating the folders and files. In the interest of brevity, all path name examples below will be based on the two following example paths:
- Target path (where the files will ultimately be copied) - C:\inetpub\mywebsite
- Source path (where the new files will be staged) - E:\Staging\mywebsite
The SyncItemCollection Object
I create objects derived from collections all the time because I hate typing List<MyObject>
all the time, and if the collection needs some special handling (like this one happens to need), it can be abstracted to this class instead of living in the main application.
To that end, this object contains the SyncItem
thread management method. All it does is starts the synchronizing process for each SyncItem
object. At one point, I had planned on using the SmartThreadPool
similar to the code in this article, but decided later on that I didn't really need the added complexity of thread pool management. As a result, the SmartThreadPool
code is used if the __USE_THREADPOOL__
compiler definition is implemented. All you have to do is define it in the SynchroLib project's Build properties, and then include the SmartThreadPool
DLL in the project's assembly references.
Without the use of the SmartThreadPool
, the following is the extent of the code found in this object. First, we have the XElement
property that loads the collection with items from the settings file:
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));
}
}
}
}
And then, we have the method that kicks off the SyncItem
threads:
public void StartUpdate()
{
if (this.Count > 0)
{
foreach(SyncItem item in this)
{
item.Start();
}
}
}
When you look at the actual code in the file, you'll see the comparatively extensive amount of code needed to implement the thread pool, and you might subsequently agree that while technologically "cooler", it's more code than is necessary to get the job done. However, remember that you can switch it on with a simple compiler definition, so if that's what blows up your skirt, by all means, be my guest.
The SyncItem Object
Each synchronized folder is represented by a SyncItem
object, and this object contains the following data properties:
- Name - This is the English name you can give to the
SyncItem
, and is only used for easier identification while perusing the status messages.
- SyncFromFolder - This is the folder where the user stages files to be synchronized (aka the source folder).
- SyncToFolder - This is the folder where files are synchronized to (aka the target folder).
- BackupBeforeSync - True if the files being synchronizeded are backed up before they are synchronized.
- DeleteAfterSync - True if the files being synchronized are to be deleted from the staging folder after synchronization has occurred.
I continued my love affair with Linq-to-XML, and provided some easy-to-use properties for convenient setting/getting of the data in the object:
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);
}
}
These properties are supported by an appropriate constructor overload:
public SyncItem(XElement value)
{
this.XElement = value;
Init();
}
The Init method (called from all constructors) creates and starts the thread if all of the approriate properties have been properly configured:
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);
}
}
When the object is initialized, the target folder is scanned for existing files, and this list of files is maintained for the life of the application.
Because synchronizing could become a lengthy process, the act of synchronizing is performed within a thread. The following method starts the thread.
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();
}
You may have noticed the if statement includes a ThreadState
check. The reason is that once a thread has been stopped, it cannot be restarted, so we have to check the thread state to see if the thread needs to be recreated, or if we can go with what we have.
The actual thread delegate method follows. It's a very simple method that tells the list of existing files to update itself, and once completed, an event is fired for anyone who might be listening.
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;
}
}
}
Notice that we simply eat the ThreadAbortException (because it most likely aborted because we told it to), and we simply re-throw any other exception. You may have noticed that before trying to sync files, we check the CanStartSync
property. This property performs some sanity checks on the data, and returns true if everything is copacetic:
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;
}
}
The event that we send contains the number of updates performed, and how long it took to process the updates.
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);
The FileInfoEx Object
This object exists solely because you can't derive a new object from FileInfo
. I needed a way to strip the root folder off the retrieved object because we need to be able to compare file names, but only at the hierarchy location where the folder names are supposed to match. There's no way you can get a compare file names with their fully qualified paths, and you can't use the FileInfo.Name
property because a file with the same name could exist in more than one sub-folder. For instance, this file name:
C:\inetpub\mywebsite\myfile.aspx
...isn't the same file as this one:
C:\inetpub\mywebsite\thisfolder\myfile.aspx
So, to make the files comparable, we have to strip off what I call their root folder names so that the names in the collection look like this:
mywebsite\myfile.aspx
Since I had to create the FileInfoEx
object, it made perfect sense to put the file comparison code in it. This is comprised of a single method that determines if the specified FileInfoEx
item matches the one being compared against according to the specified file comparison flags:
public bool Equal(FileInfoEx fileB, FileCompareFlags flags)
{
FileCompareFlags equalFlags = 0;
if ((flags & FileCompareFlags.UnrootedName) == FileCompareFlags.UnrootedName)
{
equalFlags = (this.FileName == fileB.FileName) ? FileCompareFlags.UnrootedName : 0;
}
equalFlags |= this.FileInfoObj.EqualityFlags(fileB.FileInfoObj, flags);
return (equalFlags == flags);
}
The FileCompareFlags
enumerator provides a way to define just how equal a FileInfoEx
item has to be to be "equal" in the eyes of the application. Since one or more properties can be compared for equality, the enumerator utilizes the [Flags]
attribute, and looks like this:
[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};
The final component of file comparison is an extension method that compares the actual FileInfo
properties (as well as a couple of support methods:
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;
}
The methods above use the indicated flag(s) and compares the appropriate property for equality. If a specified property is equal, its flag value is OR'd to an interim equality flags variable, and eventually compared to the specified equality flags value. If the two equality flag values match, then the file is considered "equal" to each other, and chaos ensues.
The core code would essentially check the folder every X-number of minutes (the time would be specified by the user), and would retrieve a list of ALL of the files/folders in the source folder, compare them with the contents of the target folder, and copy anything new or changed into the appropriate target folder/sub-folder.
Even though the timing thread was needed in both apps, and that's where that code is, it makes more sense to talk about it here.
The FileInfoList Object
This object represents a collection of FileInfoEx objects, and is responsible for the actual synchronizing process, which begins by calling the Update
method. The first thing we need to do is to build a list of files in the staging folder (SyncFromPath
).
public void Update(string path, bool incSubs)
{
FileInfoList newList = new FileInfoList(m_syncFromPath, m_syncToPath);
newList.GetFiles(path, incSubs);
Then we find all of the files that need to be deleted by calling the NewOrChanged
method (Look! We're using LINQ!):
var newerList = (from item in newList
where NewOrChanged(item)
select item).ToList();
newList.Clear();
this.Updates = newerList.Count;
Finally, we interate through the list of new/changed files, and backup (if necessary), delete the file we're replacing, and finally copythe new version.
foreach (FileInfoEx item in newerList)
{
bool backupFirst = false;
try
{
string sourceName = System.IO.Path.Combine(m_syncFromPath, item.FileName);
string targetName = System.IO.Path.Combine(m_syncToPath, item.FileName);
bool pathVerified = false;
if (File.Exists(targetName))
{
if (backupFirst)
{
}
File.Delete(targetName);
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);
}
}
}
The SyncSettings Object
Because I had two applications using the same settings, I figured it would be easier/convenient to manually create a settings object, and this object is the result. Instead of writing the entire thing from scratch, I referred back to my earlier article here (Share User Settings Between Applications[^]), and pulled out the AppSettingsBase class. This class has a bunch of handy properties and methods that have already been written, such as a method that creates the necessary application data folder:
protected string CreateAppDataFolder(string folderName)
{
string appDataPath = "";
string dataFilePath = "";
folderName = folderName.Trim();
if (folderName != "")
{
try
{
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;
}
I then creating the SyncSettings class, inherited from AppSettingsBase, and added the methods necessary to support my app-specific data. First, I needed to provide a property that could be used to get/set the data from a XElement object:
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);
}
}
}
And then for the SyncItems collection:
public SyncItemCollection SyncItems { get; set; }
Specified some constants:
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";
Initialzed some base class properties in the derived class' constructor:
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);
...
}
And finally, I added appropriate Load
and Save
methods:
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);
}
}
Other Extension Methods
I like extension methods. They allow you to extend classes that you don't have the source code for, or that cannot be inherited. For this application, I needed two such methods. The first one involves letting us compare two dateTime objects, but allows us to specify WHICH property (or properties) we want to compare. We ony need to compare minutes in this appication, and that would be a simple thing to do without any additional code, but where's the fun in that? I;'ve already posted a tip trick regarding this code (here[^]), but since I hate clicking around to find stuff relating to an article, I figureed it would be worth going through that code here. We begin by defining an enumerator with the Flags attribute so we can set more than one enumerator at a time:
[Flags]
public enum DatePartFlags {Ticks = 0,
Year = 1,
Month = 2,
Day = 4,
Hour = 8,
Minute = 16,
Second = 32,
Millisecond = 64 };
I used Ticks
as the first ordinal so that if the programmer wanted to, he could compare the entire DateTime
without having to speciay all of the other attributes. Next, I implemented a helper method to assist in determining whether or not a flag was set:
private static bool FlagIsSet(DatePartFlags flags, DatePartFlags flag)
{
bool isSet = ((flags & flag) == flag);
return isSet;
}
Finally, I implemented the Equal
method:
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;
}
To determine equality, we pass the desired flags and the DateTime object to be compared against. In the method, we create a new flags enumerator, and proceed to check each property (indicated by the passed-in enumerator), and apply matching flags as appropriate to the inner enumerator. When we're done comparied properties, we then compare the resulting inner flags enumerator against the one that was passed it. If they're the same, then all of the appropriate properties are equal, and we have a match!
SynchroWCF - The Communications Component
To enable communications between the console and service components, and the setup application, and since all of these compoenents would exist on the same machine, I elected to use WFC with a named pipe binding. I had two requirements - I wanted to instantiate the host and the client without a config file, and I wanted to post events from the host so that the application instantiating it could display the status messages send from the client. The service itself is a very simple affair because we only have two methods in it. It's really a shame that WCF services don't support method overloading (all method MUST have a unique name) - it would have presented a much cleaner interface.
[ServiceContract]
public interface ISynchroService
{
[OperationContract]
void SendStatusMessage(string msg);
[OperationContract]
void SendStatusMessageEx(string msg, DateTime datetime);
}
As far as the class itself is concerned, I had to do something that is out of the ordinary for your typical vanilla WCF service. Remember, I ddon't need to store anything being recieved by the ServiceHost
object, but I *DO* want to pass the data being recieved on to the application hosting the service. In order to pull this off, I needed to beef up the ServiceBehavior
attribute by including the InstanceContextMode
property. By setting this to Single<code>, I can send events from the 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));
}
}
In order to make this code usable as either a client or a host, I created the static SvcGlobals
object. In that object, I provide methods to manipulate the ServiceHost
(for the server) and ChannelFactory
(for the client) objects.
I start off by defining some variables so that the resulting objects are singing out of the same hymnal, so to speak.
public static class SvcGlobals
{
private static NetNamedPipeBinding m_Binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
public static Uri m_baseAddress = new Uri("net.pipe://localhost/SynchroService");
And follow that up with some initialization code in the constructor:
static SvcGlobals()
{
SvcHost = null;
}
Next, I wrote a couple of methods to instantiate and close the host:
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);
}
}
And finally, there are a couple of client-related methods:
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;
}
As you can see, implementing the WCF code was very low impact, and the only atypical coding required was to support the events I wanted to send to the app from the service.
SynchroSetup - The System Tray App
This application allows the user to both configure and monitor the Windows service (or even the test console application). The main form looks like this:
The most prominent feature is the listbox, where all of the status message received from the Windows service are displayed. The buttons are for the following actions:
Close - Terminates the application
Minimize - Causes the application to be minimized to the system tray
Configure - Allows the user to configure the Windows service
Start/Restart Service - Starts (or restarts) the Windows service (if it's installed)
Stop Service - Stops the Windows service (if it's installed)
The Start/Restart and Stop Service buttons will be disabled if the Windows service component is not currently installed.
Configuration
When the user clicks the Configure button, the Configuration form is displayed:
This form allows the user to configure the basic settings, as well as add, edit, or delete sync items.
Sync Minutes
This field represents the number of minutes between synchronization events. The minimum possible value is 5, and the maximum is 60.
Normalize Time
When the service starts its timing loop, it immediately runs a sync event, and then calculates the next event time to be a point in the future equal to the current time plus whatever sync minutes were specified. If this checkbox is checked, the service will - after the first sync event - attempt to align subsequent sync events on even minutes of the hour. This makes the sync events more predictable if you're sitting there watching the status messages.
Sync Items List
This control displays all of the currently specified sync items. While an unlimited number of sync items can be displayed, you have to take note that the more you have, the longer it's likely to process all of them, and the more memory the Windows service will consume while doing so. The smart play would be to specify no more than five on the machine, and when doing so, consider the number of files that will be processed in order to complete a sync event. In my case, I will only have one or two sync items.
The Add Button
The Add button allows the user to add a new sync item to the list. The following form is displayed:
Since most of the fields are self-explanatory, I'll restrict this section to the more interesting controls. First, we have the Sync item name. Notice that it's automatically populated for you. When the form initializes, it comes up with its own default name by iterating through the existing names, incrementing and appending a counter to "Sync Item " until it finds a name that doesn't already exist in the list. This keeps you from having to manually type something unique.
The 2nd most interesting control is the label titled "Hover Here For Current List". When you hover your mouse over that label, a modeless form is displayed that contains a list of the current names. This allows you to either select one to use as a template for a new name, or just to see what's already been specified so that you don't enter an identical name manually. See below:
The Edit Button
This button allows you to edit the currently selected sync item in the sync item list. The form is the same as the one used for the Add button, but initializes a little differently due to the fact that the user is editing instead of adding a new item.
The Delete Button
This button allows the user to delete the currently selected sync item in the sync item list.
The Install Service Button
I don't know about you, but installing/uninstalling a Windows service is a pain in the butt, and I hate doing it over and over again, especially while developing the service. This button allows the user to install the Windows service without having to use a command window to do so. If the install attempt was successful, the button text will change to "Uninstall Service". In order to install/uninstall the service with this button, you must have already specified the location of the InstallUtil.EXE application.
SynchroServiceStarter - It's the Privileges, Stupid
This application exists for the sole purpose of either starting or stopping the Windows service. I had to create this application in order to mitigate the privilege escalation that would have been required for the setup app (and that I wanted to avoid doing). The code looks like this:
static void Main(string[] args)
{
try
{
if (!Globals.RunningAsAdministrator())
{
Console.WriteLine("You must run this application as administrator.");
SetExitCode(SSSExitCodes.NotAdminMode);
return;
}
if (!Globals.IsServiceInstalled())
{
SetExitCode(SSSExitCodes.ServiceNotFound);
Console.WriteLine("Service not found.");
return;
}
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 - For Sharing, ummmm, Objects
When the need for the SynchroServiceStarter application slapped me across the face, I decided that I needed an assembly that would allow me to correctly set and interpret the exit codes in both the starter app and the configuration app. This assembly is the result. There's really nothing in it except the SSSExitCodes
enumerator and a static class that holds a single method that converts integers to the specified enumerator type ordinal.
public enum SSSExitCodes { Success=0,
NotAdminMode,
ServiceNotFound,
ServiceNotStarted,
ServiceNotStopped,
Exception,
InvalidParameters,
NoParameters,
Unexpected };
public static class SharedAppObjects
{
public static T IntToEnum<T>(int value, T defaultValue)
{
T enumValue = (Enum.IsDefined(typeof(T), value)) ? (T)(object)value : defaultValue;
return enumValue;
}
}
A little further along in development, I started to obsess over the fact that a WCF service cannot communicate across privilege boundaries, and began playing with different approaches to IPC (inter-process communications), and decided that I'd just resign myself to the fact that the SynchroSetup app had to be run in admin mode. As a result, I decided that while running the ServiceStarter app wasn't going to always be necessary, so I moved pretty much all of the code that was in that application into this library.
The first thing we see is a method that determines if the service is installed. This method simply retrieves the list of running services and iterates through them to see if it can find our service. If it's found, we snag the instances from the list for use later.
public static bool IsServiceInstalled()
{
bool installed = false;
ServiceController[] services = ServiceController.GetServices();
foreach (ServiceController service in services)
{
if (service.ServiceName == m_serviceName)
{
SynchroService = new ServiceController(m_serviceName);
installed = true;
break;
}
}
return installed;
}
I also needed an overload to see if the service was installed and had a particular status:
public static bool IsServiceInstalled(ServiceControllerStatus status)
{
bool installed = (IsServiceInstalled() && SynchroService.Status == status) ? true : false;
return installed;
}
Finally, there's a method that actually starts and /or stops the service.
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));
}
}
I also have two additional methods that break out the starting and stopping of the service, with each one calling the method above with the appropriate true/false parameter.
SynchroConsole - The Test Program
This application is used to test/debug the core code, and if desired, can actually be used instead of the Windows service. It provides a simple user interface that merely displays status messages as it runs through its processing loop. Since this is just a test console, I'll go ahead and provide a screen shot, but won't bother going into the specifics of the application itself except to bring to your attention a specific feature.
This app test not only the loop/synchronization code, but also the communications between the service and the Configuration application that lives in the system tray. The most visible way to ensure that the WCF communications stuff is working is to take not of the upper right-hand corner of the form. If the WCF connection is made, a label will appear that reads "Connected". If the application can't find the configuration application's service host, the label will read "Server Not Found".
In addition, status messages will reflect the absence of a connection. The console application will attempt to reconnect at each sync event, so the user can turn the the configuration application on and off at will with no ill effects on the console application.
SynchroService - The Windows Service
This service is the primary reason this article exists, and is intended to be installed on the box on which the synchronization is desired, along with the configuration application (installing the test console is optional).
The service is installed to start manually because it's likely that upon initial installation, you won't have any sync items specified (and that's why I provided a method for the user to install from the configuration application. The service also installs as the LocalSystem account so that I can sync to protected folders (such as Inetpub). Here's a look at the Installer class's constructor:
public SyncSvcInstaller()
{
InitializeComponent();
try
{
m_serviceInstaller = new ExtendedServiceInstaller();
m_serviceInstaller.StartType = ServiceStartMode.Manual;
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;
this.Installers.Add(m_serviceInstaller);
this.Installers.Add(m_processInstaller);
}
catch (IndexOutOfRangeException oorEx)
{
Console.WriteLine(oorEx.Message);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Next, we want to look at the service itself. There's really not that much too it since most of the core code is located in other assemblies that have already been described. Of course, the most significant part of the service centers around the thread that determines when to run a sync event. The service class starts out (as you might guess) with some declaration of necessary variables:
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);
The m_updateThread member is the thread that sits and spins, and ultimate initialtes the update processing. The m_threadState member allows us to effectively "pause" the thread when the service itself is in a paused state. The last member listed supports logging functionality, and is somewhat ancient code. I was still fairly new to .Net coding when I wrote it, and since it works, and since I've already spent too much time on this code, I have no desire to go back and tweak it right now. In fact, I'm not even going to discuss it in this article because to put it bluntly, it's not worthy of discussion. I'm content in the knowledge that "it works".
Next, we have the OnStart method:
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();
}
The only thing we do here is start the thread. The thread runs until the service is stopped. The thread delegate looks like this:
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 (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)
{
}
catch (Exception ex)
{
m_log.SendErrorToLog(ex.Message);
}
}
The thread method checks once per second to see if it's time to check for files to update. If it's time to check for updates the CheckForFiles()
method is called:
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();
}
Because I took the code from the console application, I use the UpdateActivityList
method and massaged it for use in this service:
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);
}
}
Final Comments
While the coding was a fun and at times challenging, I made a few discoveries and realizations along the way.
- To start/stop a Windows Service, you need admin privileges. I already knew this, but what I wasn't aware of is that there is no way to elevate privileges in an application for just one part of that application.
- If you want to communicate through WCF bewteen a Windows service and a desktop application, the desktop application MUST be run as administrator, and it doesn't appear to matter which binding you use.
- If you want to spawn an application as administrator using the
Process
class, you MUST set Process.StartInfo.Verb="runas"
, and Process.StartInfo.UseShellExecute=true
. This (setting UseShellExecute=true
) in turn prevents you from redirecting any of the output.
- Wasted Effort? The ServiceStarter application was written to avoid making the SynchroSetup application require admin priviledges. It then turned out that SynchroSetup STILL need admin privileges to even communicate via WCF with the SynchroService application.
- Sometimes, you just gotta say "F*CK!", and then find a suitable workaround (not a new lesson, but one heavily reinforced while writing this article).
Since the Windows service (the whole reason we're here today) is actually written specifically for me, and I'll be running on a box right next to me, I decided that I would just live with the must-run-as-admin requirement for the system tray application, and only run it when I need it. If I set it to run as admin, running it would ALWAYS bring up the UAC, so putting it in the Windows Startup folder would just end up annoying the hell outa me. Ah well, it's only really needed to configure the service, and to be brutally honest, I'm getting bored writing all this code.
History
- 05 Feb 2011 - Original version