Introduction
Like many CPians, I'm a long term member of the
http://setiathome.ssl.berkeley.edu/index.html project. I joined way back in June of 1999 and, at the time of writing (September of 2003), have managed to process about 23400 work units.
In principle it's simple. You download a client program, fill in a few dialog boxes and forget it. The client program downloads a block of data to be processed, processes it and uploads the results. Then it downloads the next block of data and repeats the process.
The SETI people know their user base though :) Most users will be content with downloading a screensaver version of the client program and let it crunch away. Some of us however want to crunch as much as possible. The SETI people provide a command line client that does the same work as the screensaver version. As a result, there is a whole 'follow on' market of add ons to ensure that our computers are crunching the maximum possible number of SETI workunits per day.
This is my humble and rather belated contribution to the 'follow on' market that achieves the goals I wanted to achieve. My goals were:
- I wanted to run SETI as a service process without requiring any user intervention. We have 3 computers here at my home. One for me, one for my wife and one for the kids. All of them run Windows 2000 so reboots are relatively infrequent. However, reboots DO occur and I didn't want to have to run around each and every computer to be sure SETI was running on them.
I could have achieved this goal by putting each instance of SETI onto the start menu but that has the disadvantage that the program then runs on the currently logged in users desktop. Users sometimes close programs they know nothing about!
More importantly, I didn't want to have to ensure that someone had logged on to each computer (and remained logged on) just to know that SETI was running on each.
- I wanted to be able to run multiple instances of SETI on a multi CPU computer.
- I wanted to be able to set the process affinity of a SETI instance on a multi CPU computer.
- I'm lazy. I didn't want to have to write an administration client that would instruct the service on how to run multiple instances.
The first goal was achieved by using the service wizard in Visual C++ to create a service which runs SETI even if no one is logged in and ensures that SETI continues to run even if it terminates unexpectedly.
The remaining goals are covered in this article. It should be noted that even though I wrote this project to cater for SETI there's nothing to prevent it's being used for any similar project. Do be aware that seti.exe
is hard-wired into the service and thus a recompile with the correct target exe name would be needed.
Running multiple instances of SETI
The SETI command line client assumes the data files it's processing are in the same directory as the executable. When the client starts running it creates a lock file. When it stops running it removes the lock file. If you attempt to start a second instance of the client in the same directory the second instance detects the lock file and throws up thousands of error messages to tell you it's already running.
The solution is obvious. Create one directory for each instance of SETI. However, SetiService
needs to know which directories to use. But I already told you I'm lazy. I didn't want to write an administration client to set registry entries instructing SetiService
which directories to use. Instead I opted for the following set of rules.
SetiService
assumes that any directories it's to process (using the SETI client) are subdirectories of the directory in which SetiService
is located.
SetiService
only looks one level below the directory it finds itself in.
SetiService
(obviously) checks that there's a copy of the client executable in each subdirectory.
For each directory that meets the above criteria
SetiService
creates a thread that, in turn, spawns a copy of the SETI client.
The relevant code, added to the CServiceModule::Run()
function created for you by the service wizard, looks like this.
if ((hStop = CreateEvent(NULL, TRUE, FALSE, NULL)) != HANDLE(NULL))
{
TCHAR tszTemp[_MAX_PATH],
tszDrive[_MAX_DRIVE],
tszPath[_MAX_PATH],
tszFileName[_MAX_FNAME],
tszExtension[_MAX_EXT];
CString csPath,
csSetiPath;
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
GetModuleFileName(NULL, tszTemp, sizeof(tszTemp));
_splitpath(tszTemp, tszDrive, tszPath, tszFileName, tszExtension);
csPath = tszDrive;
csPath += tszPath;
csSetiPath = csPath;
csPath += _T("*.*");
if ((hFind = FindFirstFile(csPath,&FindFileData)) !=INVALID_HANDLE_VALUE)
{
do
{
if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if (
_tcscmp(FindFileData.cFileName, _T(".")) != 0
&&
_tcscmp(FindFileData.cFileName, _T("..")) != 0
)
{
runInfo *sRun = new runInfo;
sRun->m_csWorkingFolder = csSetiPath;
sRun->m_csWorkingFolder += FindFileData.cFileName;
sRun->m_csExeName = sRun->m_csWorkingFolder;
sRun->m_csExeName += _T("\\seti.exe");
sRun->me = this;
sRun->dwProcessor = 0;
sRun->m_hArray[1] = hStop;
if (_access(sRun->m_csExeName, 0) == 0)
{
CString csTemp;
for (int i = 0; i < 32; i++)
{
csTemp.Format(
_T("%s\\%d"), sRun->m_csWorkingFolder, i
);
if (_access(csTemp, 0) == 0)
sRun->dwProcessor |= 0x1 << i;
}
_beginthread(RunSETI, 0, LPVOID(sRun));
}
else
delete sRun;
}
}
} while (FindNextFile(hFind, &FindFileData));
FindClose(hFind);
LogEvent(_T("Finished directory search loop"));
}
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
SetEvent(hStop);
}
else
LogEvent(_T("Unable to create Stop Event, exiting"));
The code first attempts to create an unnamed event which is used later to signal each thread to exit. If we can't create the event the service terminates.
We then get the full path and filename for the service itself using the GetModuleFileName
API. This returns the fully qualified name of the currently running exe file. We split this into its component pieces and then build up a search string (in csPath
) and another string which simply refers to the directory in which SetiService
is currently running.
We then traverse that directory looking for subdirectories. Each directory is then tested to see that it's not the current directory (.) or the parent directory (..) and then to be sure it contains a copy of the SETI client executable. If the directory passes all those tests a thread is launched, passing the runInfo
structure we initialised for that directory.
Ignore the for
loop for now.
The thread procedure looks like this.
void RunSETI(LPVOID data)
{
{
runInfo *me = (runInfo *) data;
BOOL bStop = FALSE;
_ASSERTE(me);
STARTUPINFO si;
PROCESS_INFORMATION pi;
restart:
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
me->me->LogEvent(_T("About to start %s"), me->m_csExeName);
if (CreateProcess(me->m_csExeName, NULL, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL,
me->m_csWorkingFolder, &si, &pi))
{
me->m_hArray[0] = pi.hProcess;
if (me->dwProcessor)
{
me->me->LogEvent("Attempting to set %s to processor %d",
me->m_csExeName,
me->dwProcessor);
SetProcessAffinityMask(pi.hProcess, me->dwProcessor);
}
while (bStop == FALSE)
{
switch (WaitForMultipleObjects(2, me->m_hArray,
FALSE, INFINITE))
{
case WAIT_TIMEOUT:
continue;
case WAIT_OBJECT_0:
goto restart;
case WAIT_OBJECT_0 + 1:
bStop = TRUE;
break;
}
}
TerminateProcess(me->m_hArray[0], 1);
delete me;
}
else
me->me->LogEvent(_T("Attempting to start %s returned code %d"),
me->m_csExeName,
GetLastError());
}
}
This is straightforward enough. We initialise a couple of structures, set some flags and create a running instance of the SETI client. About the only thing of interest in the
CreateProcess
call is that we specify the working directory for the instance.
The thread then sleeps until one of two things happens.
SetiService
is stopped.
- The SETI client stops running.
When
SetiService
is stopped each thread is closed down after terminating the instance of the SETI client it spawned (and is monitoring).
If the SETI client stops running SetiService
starts it again.
Specifying the processor to run an instance on
On a multiprocessor machine you can specify the processor(s) on which a process will run by calling the SetProcessAffinityMask()
API. The API takes a process handle specifying the process to modify and a 32 bit bitmask specifying which processors to use. Set bit 0 and the process is allowed to run on processor 0. Set bit 1 and... well you get the idea.
This is where we come back to the for
loop earlier that I said to ignore.
CString csTemp;
for (int i = 0; i < 32; i++)
{
csTemp.Format(_T("%s\\%d", sRun->m_csWorkingFolder, i);
if (_access(csTemp, 0) == 0)
sRun->dwProcessor |= 0x1 << i;
}
The
runInfo
structure has a
DWORD
field initialised to 0. The code simply checks for the existence of numbered files in the range of 0 to 31 in the same directory as the SETI client executable. If a file exists the correspondingly numbered bit of the
DWORD
is set. Note that the code only cares that the file exists - it can be a zero length file. Note also that there is no extension.
When the thread procedure launches the SETI client instance it checks the value of the DWORD
.
if (me->dwProcessor)
{
me->me->LogEvent("Attempting to set %s to processor %d", me->m_csExeName,
me->dwProcessor);
SetProcessAffinityMask(pi.hProcess, me->dwProcessor);
}
If the value is non zero it's used as the argument to
SetProcessAffinityMask()
. Thus, it's very easy to control which processors each instance of the SETI client is permitted to run on. If, on the other hand, you specify a process affinity but there's only one cpu available the
SetProcessAffinityMask()
call is ignored.
Changing a running instance of SETI without restarting the service
The service polls each directory once a minute. It looks for 2 things. The first thing is a change in processor affinity. It recalculates the processor affinity for this directory and if it sees a change it modifies the process affnity for the SETI instance.
The other thing it looks for is the presence (or absence) of a disable
file. If this file appears in the directory after the service is started it will kill the SETI instance, conversely, if the file was there and it disappears the service restarts the SETI instance.
The service only does this if the instance was enabled to run when the service starts (it checks for the disable
file when it starts up and terminates the thread if the disable
file exists). Pretty obviously, if the instance was disabled then the thread monitoring that directory would have terminated and would not notice a future change.
This extra level of control makes it fairly easy to control SETI instances running on other machines. You can disable or enable a specific instance of SETI by the creation or deletion of a specific file and expect the service to notice within a minute or so. I did it this way so that I didn't have to write an admin client (remember I'm lazy) and could control SetiService using only network access to the file system.
Installing the service
If you actually want to use the service installation is very easy. Simply copy
setisrv.exe
to a directory and register it as a service using the following command line
setisrv /service
Then create one or more subdirectories in the directory where you installed
setisrv.exe
and move your SETI installations to those directories, one per directory. Don't forget to rename your SETI client executable to
seti.exe
. (The standard downloaded clients have a long file name which includes platform information etc).
Uninstalling the service
Run this command line.
setisrv /unregserver
Then delete the directory that contained setiservice and all child directories.
Notes
- By default the service is installed to run using the
LocalSystem
account. It'll run quite happily like this but not even the administrator will be able to kill an errant SETI process launched that way. If you want to be able to control the SETI processes using the task manager you need to change the account SetiService
runs under. This is done via the Services applet in control panel.
- SETI processes launched by
SetiService
do not appear on your desktop. Once launched you cannot interact with them in any way. Because of this it's necessary to manually launch a new instance of SETI the very first time you run it in order to enter account information and begin processing SETI data. Once you've done that however, SETI will pick up user account details from a file in it's working directory and will require no further manual intervention.
- I've only tested the multiprocessor support on a dual processor Athlon MP system. Since the code does nothing Athlon specific it ought to work on any version of the multiprocessor kernel.
Version History
Version 1 - 17 September 2003.
Version 2 - 06 OCtober 2003. Added some explanations about setting process affinity.
Version 3 - 18 December 2003. Updated version that supports runtime changes to processor affinity and allows for enabling/disabling a specific instance of SETI.