Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Monitoring file activity on removable devices

3.88/5 (12 votes)
19 Oct 2008CPOL6 min read 1   2.4K  
It logs file creation, deletion and rename on removable devices in a file.

snapshotusbmonitor.jpg

Introduction

The application maintains a log of file activity such as file rename, deletion and creation on removable drives. Details of each activity are recorded along with the date and time on which it occurs. A user needs to specify the path of the file (txt) where the log is to be stored and click Start Logging. The application looks for any removal devices attached at the start and also caters for any new arriving devices. It retrieves the drive letter(s) for all the attached or newly arrived drives automatically and activates the respective FileSystemWatcher object which we will explain in the upcoming sections.

Background

USBs have become a ubiquitous means of transporting information. However it can lead to unwanted transfer of valuable data, files, manuals etc. Monitoring file activity on removal drives becomes necessary sometimes especially in organizations having sensitive data. Regarding the issue, we could not find a lot of relevant literature or applications on the internet. Therefore we decided to develop a small application of our own that would allow a user to perform the task. The application is meant for both a simple user who just wants to monitor file activity on removable drives as well as programmers and developers who after studying the article will be able to alter the program according to their needs (e.g. making the window invisible, running it on windows startup, monitoring remotely etc).

Details of the application 

In the following passages we will describe the main features of the application.

System.IO.FileSystemWatcher Class

According to MSDN, FileSystemWatcher “listens to the file system change notifications and raises events when a directory, or file in a directory, changes.” More information can be found here. We will briefly explain the important features of the FileSystemWatcher class in simplest of the terms.

Path - The path on which the activity will be monitored. By default the sub directories are included but if you want to change it you can set IncludeSubdirectories property to false.

NotifyFilter - This specifies the types of changes to monitor. For this application the filters of our interest are LastAccess, LastWrite, FileName and DirectoryName. The programmer can add or remove any of the available filters.

EventHandlers - Some action needs to be performed every time an event related to file activity is triggered. These actions are contained in separate functions whose references need to be registered with the corresponding properties. These properties are Changed, Created, Renamed and Delete. These references need to be held in special signatures called delegates. Read this for more information on Events and Delegates. However if you are not fully comfortable with delegates it is sufficient to know that you just need to associate a function with an event in a special format (typically the sender and the function itself). It will be clearer when we take a look at the implementation in the next section.

Filter - Specifies the type of files to be monitored. By default it monitors all types of files.

EnableRaisingEvents - This property starts the FileSystemWatcher activity when it is true.

watcherArray Class 

To see full implementation of this class, please download the source code. It is basically a class that contains an array of 26 FileSystemWatcher objects, one for each drive (0 corresponds to A, 1 to B,…, 25 to Z). In the constructor the function initializeArray() is called.

initializeArray() 

In the beginning we check for existing removable drives and store the values in the bool array called drives. (0 corresponds to A, 1 to B,…, 25 to Z).
bool * drives = DrivesFunctions::CheckExistingRemovableDrives();

DrivesFunctions class is explained very briefly in the next section.

Next it initializes all the objects in the array with different values depending on the drives array. This is done through Path and EnableRaisingEvents properties of each FileSystemWatcher object in the array.

for(int i=0; i<26; i++)
{
	watcherArray[i] = gcnew FileSystemWatcher();
	if(drives[i])
	{
		watcherArray[i]->Path = StringManipulations::AsciiToChar(i+65);
		watcherArray[i]->EnableRaisingEvents = true;
	}
	else
	{
		watcherArray[i]->Path = "";
		watcherArray[i]->EnableRaisingEvents = false;
	} //for loop continues...

Then notifyFilters property is set. This is same for all the objects of the array.

watcherArray[i]->NotifyFilter = static_cast<notifyfilters />(NotifyFilters::LastAccess |NotifyFilters::LastWrite
 | NotifyFilters::FileName | NotifyFilters::DirectoryName);</notifyfilters />
Finally the functions are registered for each type of event. For our purposes we did not include Changed property since that introduced redundancy. However it can be activated if desired by any programmer by following similar approach as follows.
	watcherArray[i]->Created += gcnew FileSystemEventHandler(this,&WatcherArray::onCreated);
	watcherArray[i]->Deleted += gcnew FileSystemEventHandler(this,&WatcherArray::onDeleted);
	watcherArray[i]->Renamed += gcnew RenamedEventHandler(this,&WatcherArray::onRenamed);
}//for loop ends 
onCreated, onDeleted and onRenamed are the functions that contain the implementations to be executed on file creation, deletion and rename respectively. In our case the implementation comprises writing the information on the log file.

beginWatcherOnDrive() and stopWatcherOnDrive() 

These functions take the drive letter on which the FileSystemWatcher is to be started or stopped as an argument. As soon as a device is attached to the system, the overridden windows procedure function (described later) calls beginWatcherOnDrive after detecting the drive letter and checking if the device is removable. Similarly stopWatcherOnDrive stops the FileSystemWatcher once a drive is removed. Again these are done through Path and EnableRaisingEvents properties.
void beginWatcherOnDrive(char driveLetter)
{
	int index = driveLetter-65;
	if( index >=0 && index<26)
	{
		if(watcherArray[index]->Path == "")
			watcherArray[index]->Path = StringManipulations::AsciiToChar(int(driveLetter)) + ":\\";
		watcherArray[index]->EnableRaisingEvents = true;
	}
}
void stopWatcherOnDrive(char driveLetter)
{
	int index = driveLetter-65;
	if( index>=0 && index<26)
	{
		watcherArray[index]->EnableRaisingEvents = false;
	}
}

DrivesFunctions 

Explaining this class with static functions will require a different article altogether. If you're interested in knowing how they're implemented, please read about GetLogicalDrives() ,GetVolumeInformation() , and GetDriveType(). We will discuss the main functions of our DrivesFunctions class briefly.
  1. CheckExistingRemovableDrives() - This returns a bool array of 26 which holds true for the indexes on which the removable drives are present and false for the rest.

  2. EvaluateMask(DWORD Mask) - This returns the drive letter on which the removable device is connected or disconnected after evaluating the mask received from the WndProc function described later.

  3. DetermineDriveType(char Drive) - In this function we are just returning the type of the device (removable, fixed etc) through its drive letter. However more information can be extracted from this (see article above).

WndProc Method 

Hardware changes such as attaching a USB are conveyed to all the top level windows by the operating system through a function called WndProc. Information about the change (e.g. in our case device arrival or removal, drive letter etc) is sent to this function in the Message object. In order to read the information in this message we need to override the WndProc method. This is done in the Form’s functions declaration area.
//Overriding Windows Procedure
virtual void WndProc(System::Windows::Forms::Message %m) override
{
	switch (m.Msg){
		case WM_DEVICECHANGE:
			Main_OnDeviceChange(m.WParam,m.LParam);
	}
	Form::WndProc(m);
}
The Message class has several properties, three of which are of our interest. The first one is Msg which is an integer and tells us about the type of message that is received. In this case it is WM_DEVICECHANGE. The other two are WParam and LParam which contain the information about the type of device change and details about the drive respectively. To extract useful information about the drive from the LParam variable we have to perform a two step type casting.

First we need to get a pointer to DEV_BROADCAST_HDR structure through which we determine the structure received through the WM_DEVICECHANGE event.

PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lparam.ToPointer();

This is further type casted to a pointer to DEV_BROADCAST_VOLUME structure which contains the unit mask for the volume information.

PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
We can then call functions from our DeviceFunctions class mentioned in the previous section to know the details about the drive and enable or stop the corresponding FileSystemWatcher objects.
char driveType[15]="", driveName;
switch(wparam.ToInt32())
{
	case DBT_DEVICEARRIVAL:
		driveName = DrivesFunctions::EvaluateMask(lpdbv->dbcv_unitmask);
		strcpy(driveType,"");
		strcpy(driveType, DrivesFunctions::DetermineDriveType(driveName));
		if( strcmp(driveType,"Removable") == 0 )
			if(usbWatcher)
				usbWatcher->beginWatcherOnDrive(driveName);
		break;
	case DBT_DEVICEREMOVECOMPLETE:
		driveName = DrivesFunctions::EvaluateMask(lpdbv->dbcv_unitmask);
		//Drive letter determined, remove FileSystemWatcher
		if(usbWatcher)
			usbWatcher->stopWatcherOnDrive(driveName);
		break;
	default:
		break;
}

Recommendations and Suggestions 

Please email your comments and suggestions at abdulazizrehan@gmail.com

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)