2011. március 11., péntek
Update your ISAPI app on the fly without bringing down IIS! Even while users are hitting your DLL
Problem/Question/Abstract:
Everyone developing ISAPI apps runs into the problem when they have to update the customers ISAPI app you wrote... you have to stop their web server. Also when creating your ISAPI apps, you have to kill IIS or sometimes it wont die and you have to reboot. I have solved this problem !!
Answer:
This is an ISAPI loader application that loads your isapi app, when you have a new version of your app, then this application will unload your old one and load your new one.. even while users are hitting your application. It is thread safe.
Repeat: This app acts as a loader for your app.
This is how to use it..
First compile this app, it makes no difference what you call it when compiling.
After you compile it, rename the dll to the same name as your isapi dll and then change the extension of your isapi dll to .run, thats it. When the loader dll gets its first web request, then it will look for a file called .run that has the same name and will load it.
Example:
Say your original ISAPI DLL is called proposal.dll and it handles online proposals.
You would change its name to proposal.run and then the loader program must be named proposal.dll (it replaces yours)
When the loader gets its web request it will look for proposal.run, if it is found then it will load it and pass the request to it. (once it is loaded it stays loaded)
Now for the good part, its time to update your dll. All you have to do is name your new dll proposal.update
The loader will look for this file at a maximum of 1 time every 10 seconds and only during web requests so as not to decrease the performance under heavy load.
The loader will wait until the last request is finished (through a syncronization object) then it will put a hold on all next web requests while it unloades your current .run dll, it will make a backup of it with the .backup extension then will rename the .update to .run then it will load the new .run and then continue handling web requests all without a web surfer seeing a single interuption - except the time it takes for you dll to startup which may be very small and not noticable. The user will not get an error while updating.
Understand?
I will restate it in summary because it is very important.
Loader = make it the name your current isapi dll.
YourISAPI = change extension to .run
To update, copy your new dll with the extension .update
the loader will make your .update into .run and backup the previous .run.
PERFORMANCE
I paid close attention to this when I wrote it... That is why it does an update check at most of 1 time every 10 seconds.
I created a special version of the dll which wrapped the request call with a check to the cpu clock ticks, and commented out the line that calls the second DLL which only left the time for my code.
I am running a Pentium 733 and I timed it in clock ticks, I calculated the time as
Time/733,000,000
First load of a 582K application
9664208 clock ticks = 13.184 ms (ms is according to my calculations)
Largest time for checking if update exists, note this only happens at most of 1 time every 10 seconds.
143042 clock ticks = 0.195 ms (pretty small for a update check!)
Largest time without file check (under load this happens most often), remember file checks only happen 1 time per 10 seconds.
11980 clock ticks = 0.016 ms (I'd say that is insignificant !)
What I would really like to do is use shell notifications so I could remove the update check. When I have time I can do it, but am a little worried the notification somehow would not occur - just feel like it could be unreliable. Have to check it.
Here is the full source, only 1 unit and it is a library so it is your main project source, save it as ISAPILoader.dpr. If you have any trouble post a comment and I will answer it.
It compiles to a little 63K.
I would appreciate any ideas to make it better, thank you.
library ISAPILoader;
{ Author William Egge }
{ Company Eggcentric }
{ Website: http://www.eggcentric.com }
{ email egge@eggcentric.com }
{ original file name ISAPILoader.dpr }
{ Date created June 24, 2001 }
{ version 1.0 }
uses
Windows,
SysUtils,
syncObjs,
ISAPI2;
type
TGetExtensionVersion = function(VerInfo: PHSE_VERSION_INFO): BOOL; stdcall;
THttpExtensionProc = function(ECB: PEXTENSION_CONTROL_BLOCK): DWORD; stdcall;
TTerminateExtension = function(dwFlags: DWORD): BOOL; stdcall;
TISAPIProcs = record
Module: HModule;
GetExtensionVersion: TGetExtensionVersion;
HttpExtensionProc: THttpExtensionProc;
TerminateExtension: TTerminateExtension;
end;
{$R *.RES}
const
MinCheckElapse = 10000;
// Only check for updates if 10 seconds have passed from last check.
{
I would prefer to use Shell Notifications but I want to get this done tonight :-)
}
var
LastCheckTime: LongWord = 0; //
ImpISAPIProcs: TISAPIProcs = (Module: 0; GetExtensionVersion: nil;
HttpExtensionProc: nil; TerminateExtension: nil);
ProcsLoaded: Boolean = False;
Sync: TMultiReadExclusiveWriteSynchronizer;
SyncTime: TCriticalSection;
function DLLName: string;
var
FileName: array[0..MAX_PATH] of char;
begin
FillChar(FileName, SizeOf(FileName), #0);
GetModuleFileName(HInstance, FileName, SizeOf(FileName));
Result := FileName;
end;
procedure UnloadProcs;
begin
Sync.BeginWrite;
try
try
if Assigned(ImpISAPIProcs.TerminateExtension) then
ImpISAPIProcs.TerminateExtension(HSE_TERM_MUST_UNLOAD);
except
// Let it die !
end;
try
if ImpISAPIProcs.Module <> 0 then
FreeLibrary(ImpISAPIProcs.Module);
except
// Keep trying to unload it. Goal is to get it out whatever it takes.
end;
FillChar(ImpISAPIProcs, SizeOf(ImpISAPIProcs), 0); // set values to 0 and nil
ProcsLoaded := False;
finally
Sync.EndWrite;
end;
end;
procedure LoadProcs;
var
DummyStartupParam: HSE_VERSION_INFO;
UpdateName: string;
RunName: string;
BackupName: string;
ThisDLLName: string;
begin
// This does a force load, even if not needed. Current code should not call this unless needed.
Sync.BeginWrite;
try
// First unload current DLL if loaded;
// If your DLL misbehaves unloading then we have a problem because we may not be able to overwrite the old file.
UnloadProcs;
ThisDLLName := DLLName;
UpdateName := ChangeFileExt(ThisDLLName, '.update');
RunName := ChangeFileExt(ThisDLLName, '.run');
BackupName := ChangeFileExt(ThisDLLName, '.backup');
// First, is there an update? Yes - Backup run and rename it to run.
if FileExists(UpdateName) then
begin
if FileExists(RunName) then
begin
if FileExists(BackupName) then
DeleteFile(BackupName);
RenameFile(RunName, BackupName);
end;
RenameFile(UpdateName, RunName);
end;
// Now Load the Run name
if FileExists(RunName) then
begin
with ImpISAPIProcs do
begin
Module := LoadLibrary(PChar(RunName));
GetExtensionVersion := GetProcAddress(Module, 'GetExtensionVersion');
HttpExtensionProc := GetProcAddress(Module, 'HttpExtensionProc');
TerminateExtension := GetProcAddress(Module, 'TerminateExtension');
if Assigned(GetExtensionVersion) then
begin
if not GetExtensionVersion(@DummyStartupParam) then
begin
FreeLibrary(Module);
Module := 0;
GetExtensionVersion := nil;
HttpExtensionProc := nil;
TerminateExtension := nil;
end
else
ProcsLoaded := True;
end;
end;
end;
finally
Sync.EndWrite;
end;
end;
function GetISAPIProcs: TISAPIProcs;
begin
Sync.BeginRead;
try
if not ProcsLoaded then
begin
Sync.BeginWrite;
try
// Check again in case 2 threads tried to load at the same time.
if not ProcsLoaded then
LoadProcs;
finally
Sync.EndWrite;
end;
end;
SyncTime.Enter;
try
if (GetTickCount - LastCheckTime) >= MinCheckElapse then
begin
if FileExists(ChangeFileExt(DLLName, '.update')) then
LoadProcs;
LastCheckTime := GetTickCount;
end;
finally
SyncTime.Leave;
end;
Result := ImpISAPIProcs;
finally
Sync.EndRead;
end;
end;
//=========================
// ISAPI Interface
//=========================
function GetExtensionVersion(VerInfo: PHSE_VERSION_INFO): BOOL; stdcall;
begin
try
Sync := TMultiReadExclusiveWriteSynchronizer.Create;
SyncTime := TCriticalSection.Create;
VerInfo^.dwExtensionVersion := 1;
VerInfo^.lpszExtensionDesc :=
'ISAPI Loader by Eggcentric: http://www.eggcentric.com/';
Result := True;
except
Result := False; // Do not kill IIS on exceptions (no raise)
end;
end;
function HttpExtensionProc(ECB: PEXTENSION_CONTROL_BLOCK): DWORD; stdcall;
var
Error: string;
Bytes: DWord;
Procs: TISAPIProcs;
begin
Result := HSE_STATUS_ERROR;
// Make compiler happy, says return value could be undefiend - cannot figure out how.
try
Sync.BeginRead;
try
Procs := GetISAPIProcs;
if not Assigned(Procs.HttpExtensionProc) then
raise Exception.Create('HttpExtensionProc not loaded.');
Result := Procs.HttpExtensionProc(ECB);
finally
Sync.EndRead;
end;
except
on E: Exception do
begin
Result := HSE_STATUS_ERROR;
Error := E.ClassName + ': "' + E.Message + '"';
Bytes := Length(Error);
ECB^.dwHttpStatusCode := 500;
ECB^.WriteClient(ECB^.ConnID, @Error[1], Bytes, 0);
end;
end;
end;
function TerminateExtension(dwFlags: DWORD): BOOL; stdcall;
begin
Result := True;
try
try
UnloadProcs;
except
end;
Sync.Free;
SyncTime.Free;
except
// Do not kill IIS on exceptions. (no raise)
end;
end;
exports
GetExtensionVersion,
HttpExtensionProc,
TerminateExtension;
begin
end.
Component Download: http://www.eggcentric.com/Download/ISAPILoaderSource.zip
Feliratkozás:
Megjegyzések küldése (Atom)
Nincsenek megjegyzések:
Megjegyzés küldése