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;
}
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);
}
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.
- 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.
- 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.
- 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.
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);
if(usbWatcher)
usbWatcher->stopWatcherOnDrive(driveName);
break;
default:
break;
}
Recommendations and Suggestions
Please email your comments and suggestions at abdulazizrehan@gmail.com