Update your ISAPI app on the fly without bringing down IIS! Even while users are hitting your DLL (Views: 28)
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 |