Introduction
The full source code can be divided into a few parts:
- Command line processing
- Win32 service
- Windows hook
- Configuration file interpreter
This demo project runs as a Windows service application to override creation parameters of any specified window. As part of the service code, it reads entries from a configuration file. Each entry should have a window class name and its desired creation parameters. In the beginning of its execution, it installs a global hook procedure of type WH_SHELL
to monitor shell events. Whenever a top level window is created, it receives notification about it and adjusts the window according to the set of parameters defined by the user.
Some examples of what this demo program can do:
Making only 'Notepad' to be transparent:
Making only 'ExploreWClass' to be transparent:
Making 'SciCalc' to always start at the upper left corner of the desktop:
All the above can be done with the following few lines in the configuration file:
ExploreWClass -1 -1 -1 -1 200
Notepad -1 -1 -1 -1 200
SciCalc 10 10 260 0 -1
The above examples are trivial, much more can be done with this simple application. The configuration file interpreter within the service application can be reprogrammed to understand complex scripting language. For example, we can add clock and date information to an application's title with the same type of hook, or we can disable certain annoying pop-ups by destroying them every time they are created.
Command Line Processing
Files:
- Main.cpp
- CCmdLineArgs.cpp
- CCmdLineArgs.h
The program handles command line arguments in a very similar way to how GNU programs do. Arguments can be options or non-options. Option arguments begin with a '-'. For example:
helloworld.exe -i -r c:\hello\world\helloworld.exe
- helloworld.exe is the program file name.
- -i and -r are option arguments.
- c:\hello\world\helloworld.exe is a non-option argument.
Each short option can have a corresponding long option and vice versa. Long names can sometimes be easier to remember. Long options begin with a '--'. For example:
helloworld.exe --install --run
- helloworld.exe is the program file name.
- --install and --run are long options.
The way CCmdLineArgs
works is to call Parse
repeatedly with argc
and argv
until it returns CLA_DONE
. Each time Parse
is called, one argument is processed. If the argument is a short option, a unique character representing the option is returned; if the argument is a long option, a character can be similarly returned or a variable can be set.
The function Parse
is defined as follows:
static char Parse(int argc, char *const argv[],
const char *optstr, const lopt_entry *lopts);
The string optstr
defines all short options and the table lopt_entry
should hold information about all long options.
optstr
contains a string of characters with each of them representing a valid short option. A character in optstr
can be appended with a single colon, ':' or a double colon, '::'. Single colon means the option requires an argument, whereas double colon means the option can be with or without an argument. An argument to an option and the option itself must be written together. For example:
helloworld.exe -ic:\hello\world\helloworld.exe
- helloworld.exe is the program file name.
- -i is an option and c:\hello\world\helloworld.exe is the argument to the option -i.
lopt_entry
is defined as follows:
typedef struct lopt_entry_
{
const char *name;
int has_arg;
int *flag;
int val;
} lopt_entry;
name
is the long option name.
has_arg
determines whether the long option requires an argument, and has three possible values, CLA_ARG
, CLA_NOARG
and CLA_OPTARG
.
flag
is the variable to set by Parse
. If NULL
, Parse
will return val
as a character, otherwise the variable *flag
will be set to val
.
Examples:
If you want your program helloworld.exe to accept the following few arguments:
helloworld.exe -1 -2 -3 -4 -5 -6 -7 -8hello
the Parse
function should be something like the following:
char ret = Parse(argc, argv, "12345678:", NULL);
Win32 Service
Files:
- Main.cpp
- CWinServ.cpp
- CWinServ.cpp
The program is capable of installing and uninstalling itself as a service, and executing service code when the Service Control Manager starts it.
- To install, use the options -i or --install.
- To uninstall, use the options -u or --uninstall.
- To run, do not specify any arguments or use the options -r or --run.
There are two excellent articles that explain how a simple service application can be created.
I assume readers have read through the above articles and now have a good understanding of the construction of a simple service application. The implementation in CWinServ.cpp and CWinServ.h is similar, except that it is done with a static class.
I have made the service to accept the following few user controls:
SERVICE_CONTROL_PAUSE |
The list of target classes and their parameters will be cleared, overriding will be paused. |
SERVICE_CONTROL_CONTINUE |
The list will be reconstructed, overriding will continue. |
SERVICE_CONTROL_STOP |
Service will terminate. |
SERVICE_CONTROL_SHUTDOWN |
Service will terminate. |
Windows Hook
Files:
To monitor the system for events, we need to install a hook procedure. The events being monitored can be associated to either a specific thread or all threads in the same desktop as the calling thread.
In this example application, we want windows of specific classes to be created with our own window creation parameters. The WH_SHELL
hook can be used for this purpose. This type of Windows hook enables us to monitor shell events such as creation and destruction of top-level windows. More information can be found in the Platform SDK Documentation, search for the keyword "ShellProc
".
This global hook procedure must be in a separate DLL. Since the hook-installing application must have the handle to the DLL module, the installing and uninstalling functions are placed within the same DLL as the hook procedure. Otherwise, a shared variable can be exported from the DLL to hold the handle to the DLL module.
Window Stations and Desktops
Service applications, by default, are running in a different window station from the user's. To find out what window stations are currently running, use EnumWindowStations
. In my case, there are:
- WinSta0
- Service-0x0-3e7$
- Service-0x0-3e4$
- Service-0x0-3e5$
- SAWinSta
- __X78B95_89_lW
For every window station, there are desktops. To find out what they are, use EnumDesktops
. The following desktops are in my "WinSta0" window station.
- Default
- Disconnect
- Winlogon
As pointed out earlier, the user's window station and desktop are "WinSta0" and "Default" respectively. Recall that a global hook procedure receives only notification about events happening in the same desktop. Therefore, we have to switch the window station and desktop of our service thread to the same as the user's. The following portion of the source code does exactly that:
HWINSTA hWinStaUser = OpenWindowStation("WinSta0", FALSE, MAXIMUM_ALLOWED);
if (SetProcessWindowStation(hWinStaUser)) {
HDESK hDeskUser = OpenDesktop("Default", 0, FALSE, MAXIMUM_ALLOWED);
if (SetThreadDesktop(hDeskUser)) {
if (InstallHook()) {
......
......
UninstallHook();
serv_stat.dwWin32ExitCode = 0;
}
}
if (hDeskUser) {
CloseDesktop(hDeskUser);
}
}
if (hWinStaUser) {
CloseWindowStation(hWinStaUser);
}
At any point in time, we have no idea where our hook procedure is in the hook chain. If an application decides to install the same type of hook, it is likely to be after our service code, which is during startup. Therefore, our hook procedure has a high chance of always staying at the end of the hook chain. Although hook procedures should always call CallNextHookEx
, they can always choose not to. To ensure that the service always works, we should always check that our hook procedure is called whenever a shell event occurs. This, however is not implemented in this sample application.
Configuration File Format
For the sake of this sample application, the file format has been designed to be trivially simple. Each line in the configuration file should correspond one-to-one with each member in the structure below:
typedef struct SHWP_STRUCT_ {
char szClassName[256];
int x, y, cx, cy, alpha;
} SHWP_STRUCT, *LPSHWP_STRUCT;
A valid line in the configuration file is:
ExploreWClass -1 -1 -1 -1 200
ExploreWClass
will be the class name. A value of -1 means the parameter should be ignored. The last one sets the desired alpha value to 200.
Note that the implementation does not incorporate any error-checking mechanism to detect errors in the configuration file.
Others
The main part of the service code is the following:
while (serv_stat.dwCurrentState != SERVICE_STOPPED) {
if (serv_stat.dwCurrentState == SERVICE_RUNNING) {
Sleep(5000);
RefreshEntries();
}
}
The configuration file is read every five seconds. Therefore, lines added to or removed from the file will take effect after five seconds in the worst case.
Note that by default each process using a DLL has its own copy of all global and static variables. Here, at least two processes are linked to the DLL, one being the service and the other being the thread that calls our hook procedure whenever a shell event occurs. Of course, our configuration entries read from file must be shared across these instances. For that, I used the data_seg
pragma to define a shared data segment. The permission for the segment is set with the linker to be /SECTION:.SHARED,RWS.
#pragma data_seg(".SHARED")
SHWP_STRUCT entries[NUM_OF_ENTRIES] = { 0 };
#pragma data_seg()
References
- Arguments, Options, and the Environment.
- Window Stations and Desktops Overview, Microsoft Platform SDK Documentation.
- Hook Overview, Microsoft Platform SDK Documentation.
- Creating a Simple Win32 Service in C++ by Thompson Nigel.
- Five Steps to Writing Windows Services in C by Yevgeny Menaker.