[fpc-devel] simpleipc issues
Ondrej Pokorny
lazarus at kluug.net
Mon Sep 14 15:56:39 CEST 2015
I've developed a "single/multiple instances" feature for the Lazarus
IDE. For this feature I need an IPC. First I tried to use simpleipc but
I have struggled on bugs and missing functionality.
Particularly what I found:
1.) BUG: You can register multiple servers on Windows with the same name
(with StartServer procedure).
2.) Small issue: simpleipc uses exceptions if something fails (e.g. in
StartServer procedure). I would prefer using a function with boolean
result (true = OK, false = fail).
3.) MISSING: Multiple clients (from different processes) talk to one server.
4.) MISSING: Client is able to receive a response on a request.
5.) MISSING: (Optionally, not default) client sends a request to a
server that isn't running. The server is able to handle these requests
when it is started.
So I developed "advancedipc.pas" that does what I need. It can do
everything that is in simpleirc but a little bit differently (I changed
the exception behavior and also method names). The question is now what
to do?
1.) Include advancedipc.pas into FCL as a standalone unit.
2.) Make advancedipc.pas backwards compatible to simpleipc.pas and use
the new code in simpleipc.pas.
I attached the advancedipc.pas file.
The discussion about "multiple instances" is here:
http://bugs.freepascal.org/view.php?id=8051
Ondrej
-------------- next part --------------
{
*** Please use appropriate header ***
License standard LCL (GPL/LGPL ?)
Author: Ondrej Pokorny
Abstract:
Unit implementing two-way (request/response) IPC between 1 server and more clients
**********************************************************************}
unit AdvancedIPC;
{$mode objfpc}{$H+}
interface
uses
{$IFDEF UNIX}
baseunix,
{$endif}
sysutils, Classes;
const
HEADER_VERSION = 1;
type
TMessageHeader = packed record
HeaderVersion: Integer;
FileLock: Byte;//0 = unlocked, 1 = locked
MsgType: Integer;
MsgLen: Integer;
MsgVersion: Integer;
end;
TFileHandle = Classes.THandle;
TReleaseHandleStream = class(THandleStream)
public
destructor Destroy; override;
end;
TIPCBase = class
private
FGlobal: Boolean;
FFileName: string;
FServerName: string;
FMessageVersion: Integer;
protected
class function ServerNameToFileName(const aServerName: string; const aGlobal: Boolean): string;
function GetResponseFileName(const aMsgID: Integer): string;
function GetResponseFileName(const aRequestFileName: string): string;
function GetRequestPrefix: string;
function GetRequestFileName(const aMsgID: Integer): string;
function RequestFileNameToMsgID(const aFileName: string): Integer;
function GetUniqueRequest(out outFileName: string): Integer;
procedure SetServerName(const aServerName: string); virtual;
procedure SetGlobal(const aGlobal: Boolean); virtual;
function CanReadMessage(const aFileName: string; out outStream: TStream; out outMsgType, outMsgLen: Integer): Boolean;
procedure DoPostMessage(const aFileName: string; const aMsgType: Integer; const aStream: TStream);
property FileName: string read FFileName;
public
constructor Create; virtual;
public
class procedure FindRunningServers(const aServerNamePrefix: string;
const outServerNames: TStrings; const aGlobal: Boolean = False);
class function ServerIsRunning(const aServerName: string; const aGlobal: Boolean = False): Boolean;
property ServerName: string read FServerName write SetServerName;
property Global: Boolean read FGlobal write SetGlobal;
property MessageVersion: Integer read FMessageVersion write FMessageVersion;
end;
TIPCClient = class(TIPCBase)
var
FLastMsgFileName: string;
public
function PostRequest(const aMsgType: Integer; const aStream: TStream): Integer;//returns ID
function PeekResponse(const aStream: TStream; var outMsgType: Integer; const aTimeOut: Integer): Boolean;
procedure DeleteRequest;
function ServerRunning: Boolean;
end;
TIPCServer = class(TIPCBase)
private
FFileHandle: TFileHandle;
FActive: Boolean;
function FindFirstRequest(out outFileName: string; out outStream: TStream; out outMsgType, outMsgLen: Integer): Integer;
protected
procedure SetServerName(const aServerName: string); override;
procedure SetGlobal(const aGlobal: Boolean); override;
public
constructor Create; override;
destructor Destroy; override;
public
function PeekRequest(const aStream: TStream; var outMsgType: Integer): Boolean; overload;
function PeekRequest(const aStream: TStream; var outMsgID, outMsgType: Integer): Boolean; overload;
function PeekRequest(const aStream: TStream; var outMsgID, outMsgType: Integer; const aTimeOut: Integer): Boolean; overload;
procedure PostResponse(const aMsgID: Integer; const aMsgType: Integer; const aStream: TStream);
function FindHighestPendingRequestId: Integer;
function GetPendingRequestCount: Integer;
function StartServer(const aDeletePendingRequests: Boolean = True): Boolean;//returns true if unique and started
function StopServer(const aDeletePendingRequests: Boolean = True): Boolean;//returns true if stopped
procedure DeletePendingRequests;
property Active: Boolean read FActive;//true if started
end;
EICPException = class(Exception);
implementation
const
{$IFDEF UNIX}
GLOBAL_RIGHTS = S_IRUSR or S_IWUSR or S_IRGRP or S_IWGRP or S_IROTH or S_IWOTH;
{$ELSE}
GLOBAL_RIGHTS = 0;
{$ENDIF}
{ TIPCBase }
function TIPCBase.CanReadMessage(const aFileName: string; out
outStream: TStream; out outMsgType, outMsgLen: Integer): Boolean;
var
xFileHandle: TFileHandle;
xHeader: TMessageHeader;
begin
outStream := nil;
outMsgType := -1;
outMsgLen := 0;
Result := FileExists(aFileName);
if not Result then
Exit;
xFileHandle := FileOpen(aFileName, fmOpenRead or fmShareExclusive);
Result := xFileHandle <> feInvalidHandle;
if not Result then
Exit;
outStream := TReleaseHandleStream.Create(xFileHandle);
Result := (outStream.Size >= SizeOf(xHeader));
if not Result then
begin
FreeAndNil(outStream);
Exit;
end;
outStream.ReadBuffer(xHeader{%H-}, SizeOf(xHeader));
Result :=
(xHeader.HeaderVersion = HEADER_VERSION) and (xHeader.FileLock = 0) and
(xHeader.MsgVersion = MessageVersion) and
(outStream.Size = Int64(SizeOf(xHeader))+Int64(xHeader.MsgLen));
if not Result then
begin
FreeAndNil(outStream);
Exit;
end;
outMsgType := xHeader.MsgType;
outMsgLen := xHeader.MsgLen;
end;
constructor TIPCBase.Create;
begin
inherited Create;
end;
function TIPCBase.GetUniqueRequest(out outFileName: string): Integer;
begin
Randomize;
repeat
Result := Random(High(Integer));
outFileName := GetRequestFileName(Result);
until not FileExists(outFileName);
end;
class function TIPCBase.ServerIsRunning(const aServerName: string;
const aGlobal: Boolean): Boolean;
var
xServerFileHandle: TFileHandle;
xFileName: String;
begin
xFileName := ServerNameToFileName(aServerName, aGlobal);
Result := FileExists(xFileName);
if Result then
begin//+ check -> we should not be able to access the file
xServerFileHandle := FileCreate(xFileName, fmOpenReadWrite or fmShareExclusive, GLOBAL_RIGHTS);
Result := (xServerFileHandle=feInvalidHandle);
if not Result then
FileClose(xServerFileHandle);
end;
end;
class function TIPCBase.ServerNameToFileName(const aServerName: string;
const aGlobal: Boolean): string;
begin
Result := GetTempDir(aGlobal)+aServerName;
end;
procedure TIPCBase.SetGlobal(const aGlobal: Boolean);
begin
if FGlobal = aGlobal then Exit;
FGlobal := aGlobal;
FFileName := ServerNameToFileName(FServerName, FGlobal);
end;
procedure TIPCBase.DoPostMessage(const aFileName: string;
const aMsgType: Integer; const aStream: TStream);
var
xHeader: TMessageHeader;
xStream: TFileStream;
begin
xHeader.HeaderVersion := HEADER_VERSION;
xHeader.FileLock := 1;//locking
xHeader.MsgType := aMsgType;
xHeader.MsgLen := aStream.Size-aStream.Position;
xHeader.MsgVersion := MessageVersion;
xStream := TFileStream.Create(aFileName, fmCreate or fmShareExclusive, GLOBAL_RIGHTS);
try
xStream.WriteBuffer(xHeader, SizeOf(xHeader));
xStream.CopyFrom(aStream, 0);
xStream.Position := 0;//unlocking
xHeader.FileLock := 0;
xStream.WriteBuffer(xHeader, SizeOf(xHeader));
finally
xStream.Free;
end;
end;
function TIPCBase.RequestFileNameToMsgID(const aFileName: string): Integer;
begin
if Length(aFileName) > 8 then
Result := StrToIntDef('$'+Copy(aFileName, Length(aFileName)-7, 8), -1)
else
Result := -1;
end;
class procedure TIPCBase.FindRunningServers(const aServerNamePrefix: string;
const outServerNames: TStrings; const aGlobal: Boolean);
var
xRec: TRawByteSearchRec;
begin
if FindFirst(ServerNameToFileName(aServerNamePrefix+'*', aGlobal), faAnyFile, xRec) = 0 then
begin
repeat
if (Pos('_', xRec.Name) = 0) and//file that we found is not pending a message
ServerIsRunning(xRec.Name)
then
outServerNames.Add(xRec.Name);
until FindNext(xRec) <> 0;
end;
FindClose(xRec);
end;
function TIPCBase.GetRequestFileName(const aMsgID: Integer): string;
begin
Result := GetRequestPrefix+IntToHex(aMsgID, 8);
end;
function TIPCBase.GetRequestPrefix: string;
begin
Result := FFileName+'_';
end;
function TIPCBase.GetResponseFileName(const aMsgID: Integer): string;
begin
Result := GetResponseFileName(GetRequestFileName(aMsgID));
end;
function TIPCBase.GetResponseFileName(const aRequestFileName: string): string;
begin
Result := aRequestFileName+'_r';
end;
procedure TIPCBase.SetServerName(const aServerName: string);
var
I: Integer;
begin
if FServerName = aServerName then Exit;
for I := 1 to Length(aServerName) do
if not (aServerName[I] in ['a'..'z', 'A'..'Z', '0'..'9']) then
raise EICPException.Create('You cannot use the "_" character in server name.');
FServerName := aServerName;
FFileName := ServerNameToFileName(FServerName, FGlobal);
end;
{ TIPCClient }
procedure TIPCClient.DeleteRequest;
begin
if DeleteFile(FLastMsgFileName) then
FLastMsgFileName := '';
end;
function TIPCClient.PeekResponse(const aStream: TStream;
var outMsgType: Integer; const aTimeOut: Integer): Boolean;
var
xStart: QWord;
xStream: TStream;
xMsgLen: Integer;
xFileResponse: string;
begin
aStream.Size := 0;
outMsgType := -1;
Result := False;
xStart := GetTickCount64;
repeat
xFileResponse := GetResponseFileName(FLastMsgFileName);
if CanReadMessage(xFileResponse, xStream, outMsgType, xMsgLen) then
begin
aStream.CopyFrom(xStream, xMsgLen);
xStream.Free;
aStream.Position := 0;
DeleteFile(xFileResponse);
Exit(True);
end
else if aTimeOut > 20 then
Sleep(10);
until (GetTickCount64-xStart > aTimeOut);
end;
function TIPCClient.PostRequest(const aMsgType: Integer; const aStream: TStream
): Integer;
begin
Result := GetUniqueRequest(FLastMsgFileName);
DeleteFile(GetResponseFileName(FLastMsgFileName));//delete old response, if there is any
DoPostMessage(FLastMsgFileName, aMsgType, aStream);
end;
function TIPCClient.ServerRunning: Boolean;
begin
Result := ServerIsRunning(ServerName);
end;
{ TReleaseHandleStream }
destructor TReleaseHandleStream.Destroy;
begin
FileClose(Handle);
inherited Destroy;
end;
{ TIPCServer }
procedure TIPCServer.DeletePendingRequests;
var
xRec: TRawByteSearchRec;
xDir: string;
begin
xDir := ExtractFilePath(FFileName);
if FindFirst(GetRequestPrefix+'*', faAnyFile, xRec) = 0 then
begin
repeat
DeleteFile(xDir+xRec.Name);
until FindNext(xRec) <> 0;
end;
FindClose(xRec);
end;
constructor TIPCServer.Create;
begin
inherited Create;
FFileHandle := feInvalidHandle;
end;
destructor TIPCServer.Destroy;
begin
if FActive then
StopServer;
inherited Destroy;
end;
function TIPCServer.FindFirstRequest(out outFileName: string; out
outStream: TStream; out outMsgType, outMsgLen: Integer): Integer;
var
xRec: TRawByteSearchRec;
begin
outFileName := '';
outStream := nil;
outMsgType := -1;
outMsgLen := 0;
Result := -1;
if FindFirst(GetRequestPrefix+'*', faAnyFile, xRec) = 0 then
begin
repeat
Result := RequestFileNameToMsgID(xRec.Name);
if Result >= 0 then
begin
outFileName := GetRequestFileName(Result);
if not CanReadMessage(outFileName, outStream, outMsgType, outMsgLen) then
Result := -1;
end;
until (Result >= 0) or (FindNext(xRec) <> 0);
end;
FindClose(xRec);
end;
function TIPCServer.FindHighestPendingRequestId: Integer;
var
xRec: TRawByteSearchRec;
xMsgID, xHighestId: LongInt;
begin
xHighestId := -1;
Result := -1;
if FindFirst(GetRequestPrefix+'*', faAnyFile, xRec) = 0 then
begin
repeat
xMsgID := RequestFileNameToMsgID(xRec.Name);
if xMsgID > xHighestId then
begin
xHighestId := xMsgID;
Result := xMsgID;
end;
until FindNext(xRec) <> 0;
end;
FindClose(xRec);
end;
function TIPCServer.GetPendingRequestCount: Integer;
var
xRec: TRawByteSearchRec;
begin
Result := 0;
if FindFirst(GetRequestPrefix+'*', faAnyFile, xRec) = 0 then
begin
repeat
if RequestFileNameToMsgID(xRec.Name) >= 0 then
Inc(Result);
until FindNext(xRec) <> 0;
end;
FindClose(xRec);
end;
function TIPCServer.PeekRequest(const aStream: TStream; var outMsgID,
outMsgType: Integer): Boolean;
var
xStream: TStream;
xMsgLen: Integer;
xMsgFileName: string;
begin
aStream.Size := 0;
outMsgType := -1;
xMsgFileName := '';
outMsgID := FindFirstRequest(xMsgFileName, xStream, outMsgType, xMsgLen);
Result := outMsgID >= 0;
if Result then
begin
aStream.CopyFrom(xStream, xMsgLen);
aStream.Position := 0;
xStream.Free;
DeleteFile(xMsgFileName);
end;
end;
function TIPCServer.PeekRequest(const aStream: TStream; var outMsgID,
outMsgType: Integer; const aTimeOut: Integer): Boolean;
var
xStart: QWord;
begin
Result := False;
xStart := GetTickCount64;
repeat
if PeekRequest(aStream, outMsgID, outMsgType) then
Exit(True)
else if aTimeOut > 20 then
Sleep(10);
until (GetTickCount64-xStart > aTimeOut);
end;
function TIPCServer.PeekRequest(const aStream: TStream; var outMsgType: Integer
): Boolean;
var
xMsgID: Integer;
begin
Result := PeekRequest(aStream, xMsgID{%H-}, outMsgType);
end;
procedure TIPCServer.PostResponse(const aMsgID: Integer;
const aMsgType: Integer; const aStream: TStream);
begin
DoPostMessage(GetResponseFileName(aMsgID), aMsgType, aStream);
end;
procedure TIPCServer.SetGlobal(const aGlobal: Boolean);
begin
if Active then
raise EICPException.Create('You cannot change the global property when the server is active.');
inherited SetGlobal(aGlobal);
end;
procedure TIPCServer.SetServerName(const aServerName: string);
begin
if Active then
raise EICPException.Create('You cannot change the server name when the server is active.');
inherited SetServerName(aServerName);
end;
function TIPCServer.StartServer(const aDeletePendingRequests: Boolean): Boolean;
begin
FFileHandle := FileCreate(FFileName, fmCreate or fmShareExclusive, GLOBAL_RIGHTS);
Result := (FFileHandle<>feInvalidHandle);
FActive := Result;
if Result and aDeletePendingRequests then
DeletePendingRequests;
end;
function TIPCServer.StopServer(const aDeletePendingRequests: Boolean): Boolean;
begin
if not FActive then
Exit(True);
if FFileHandle<>feInvalidHandle then
FileClose(FFileHandle);
DeleteFile(FFileName);
FFileName := '';
if aDeletePendingRequests then
DeletePendingRequests;
FActive := False;
end;
end.
More information about the fpc-devel
mailing list