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

Nincsenek megjegyzések:

Megjegyzés küldése