{ $Project$ $Workfile$ $Revision$ $DateUTC$ $Id$ This file is part of the Indy (Internet Direct) project, and is offered under the dual-licensing agreement described on the Indy website. (http://www.indyproject.org/) Copyright: (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved. } { $Log$ } { Rev 1.9 9/30/2004 5:04:18 PM BGooijen Self was not initialized Rev 1.8 6/11/2004 9:36:14 AM DSiders Added "Do not Localize" comments. Rev 1.7 2004.02.07 5:03:02 PM czhower .net fixes. Rev 1.6 2004.02.03 5:43:44 PM czhower Name changes Rev 1.5 1/21/2004 2:42:46 PM JPMugaas InitComponent Rev 1.4 1/3/2004 12:59:54 PM JPMugaas These should now compile with Kudzu's change in IdCoreGlobal. Rev 1.3 4/12/2003 9:21:32 PM GGrieve give up on Indy10 for the moment Rev 1.2 10/15/2003 9:53:42 PM GGrieve DotNet changes Rev 1.1 23/6/2003 22:33:54 GGrieve update for indy10 IOHandler model Rev 1.0 11/13/2002 07:53:58 AM JPMugaas 05/09/2002 Grahame Grieve Fixed SingleThread Timeout Issues + WaitForConnection 23/01/2002 Grahame Grieve Fixed for network changes to TIdTCPxxx wrote DUnit testing, increased assertions change OnMessageReceive, added VHandled parameter 07/12/2001 Grahame Grieve Various fixes for cmSingleThread mode 05/11/2001 Grahame Grieve Merge into Indy 03/09/2001 Grahame Grieve Prepare for Indy } { ============================================================================== Warning: this code is currently broken in Indy 10. The extensive changes to the IOHandler architecture mean that the way this unit works - doing asynchronous IO in a single connection - can no longer work without causing access violations whenever the socket is closed This code needs to be re-written to resolve these issues somehow, but no clear design has emerged at this point ============================================================================== } { Indy HL7 Minimal Lower Layer Protocol TIdHL7 Original author Grahame Grieve This code was donated by HL7Connect.com For more HL7 open source code see http://www.hl7connect.com/tools This unit implements support for the Standard HL7 minimal Lower Layer protocol. For further details, consult the HL7 standard (www.hl7.org). Before you can use this component, you must set the following properties: CommunicationMode Address (if you want to be a client) Port isListener and hook the appropriate events (see below) This component will operate as either a server or a client depending on the configuration } (* note: Events are structurally important for this component. However there is a bug in SyncObjs for Linux under Kylix 1 and 2 where TEvent.WaitFor cannot be used with timeouts. If you compile your own RTL, then you can fix the routine like this: function TEvent.WaitFor(Timeout: LongWord): TWaitResult; {$IFDEF LINUX} var ts : TTimeSpec; begin ts.tv_sec := timeout div 1000; ts.tv_nsec := (timeout mod 1000) * 1000000; if sem_timedwait(FSem, ts) = 0 then result := wrSignaled else result := wrTimeOut; {$ENDIF} and then disable this define: this is a serious issue - unless you fix the RTL, this component does not function properly on Linux at the present time. This may be fixed in a future version *) { TODO : use Server.MaxConnections } unit IdHL7; interface {$i IdCompilerDefines.inc} uses Classes, IdBaseComponent, IdContext, IdException, IdGlobal, IdTCPClient, IdTCPConnection, IdTCPServer, SysUtils; const MSG_START = #$0B; {do not localize} MSG_END = #$1C#$0D; {do not localize} type EHL7CommunicationError = class(EIdException) Protected FInterfaceName: String; Public constructor Create(AnInterfaceName, AMessage: String); property InterfaceName: String Read FInterfaceName; end; THL7CommunicationMode = (cmUnknown, // not valid - default setting must be changed by application cmAsynchronous, // see comments below for meanings of the other parameters cmSynchronous, cmSingleThread); TSendResponse = (srNone, // internal use only - never returned srError, // internal use only - never returned srNoConnection, // you tried to send but there was no connection srSent, // you asked to send without waiting, and it has been done srOK, // sent ok, and response returned srTimeout); // we sent but there was no response (connection will be dropped internally TIdHL7Status = (isStopped, // not doing anything isNotConnected, // not Connected (Server state) isConnecting, // Client is attempting to connect isWaitReconnect, // Client is in delay loop prior to attempting to connect isConnected, // connected OK isUnusable // Not Usable - stop failed ); const { default property values } DEFAULT_ADDRESS = ''; {do not localize} DEFAULT_PORT = 0; DEFAULT_TIMEOUT = 30000; DEFAULT_RECEIVE_TIMEOUT = 30000; NULL_IP = '0.0.0.0'; {do not localize} DEFAULT_CONN_LIMIT = 1; DEFAULT_RECONNECT_DELAY = 15000; DEFAULT_COMM_MODE = cmUnknown; DEFAULT_IS_LISTENER = True; MILLISECOND_LENGTH = (1 / (24 * 60 * 60 * 1000)); type // the connection is provided in these events so that applications can obtain information about the // the peer. It's never OK to write to these connections TMessageArriveEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String) of object; TMessageReceiveEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String; var VHandled: Boolean; var VReply: String) of object; TReceiveErrorEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String; AException: Exception; var VReply: String; var VDropConnection: Boolean) of object; TIdHL7 = class; TIdHL7ConnCountEvent = procedure (ASender : TIdHL7; AConnCount : integer) of object; TIdHL7ClientThread = class(TThread) Protected FClient: TIdTCPClient; FCloseEvent: TIdLocalEvent; FOwner: TIdHL7; procedure Execute; Override; procedure PollStack; Public constructor Create(aOwner: TIdHL7); destructor Destroy; Override; end; TIdHL7 = class(TIdBaseComponent) Protected FLock: TIdCriticalSection; FStatus: TIdHL7Status; FStatusDesc: String; // these queues hold messages when running in singlethread mode FMsgQueue: TList; FHndMsgQueue: TList; FAddress: String; FCommunicationMode: THL7CommunicationMode; FConnectionLimit: Word; FIPMask: String; FIPRestriction: String; FIsListener: Boolean; FObject: TObject; FPreStopped: Boolean; FPort: Word; FReconnectDelay: LongWord; FTimeOut: Cardinal; FReceiveTimeout: LongWord; FOnConnect: TNotifyEvent; FOnDisconnect: TNotifyEvent; FOnConnCountChange : TIdHL7ConnCountEvent; FOnMessageArrive: TMessageArriveEvent; FOnReceiveMessage: TMessageReceiveEvent; FOnReceiveError: TReceiveErrorEvent; FIsServer: Boolean; // current connection count (server only) (can only exceed 1 when mode is not // asynchronous and we are listening) FConnCount: Integer; FServer: TIdTCPServer; // if we are a server, and the mode is not asynchronous, and we are not listening, then // we will track the current server connection with this, so we can initiate sending on it FServerConn: TIdTCPConnection; // A thread exists to connect and receive incoming tcp traffic FClientThread: TIdHL7ClientThread; FClient: TIdTCPClient; // these fields are used for handling message response in synchronous mode FWaitingForAnswer: Boolean; FWaitStop: TDateTime; FMsgReply: String; FReplyResponse: TSendResponse; FWaitEvent: TIdLocalEvent; procedure SetAddress(const AValue: String); procedure SetConnectionLimit(const AValue: Word); procedure SetIPMask(const AValue: String); procedure SetIPRestriction(const AValue: String); procedure SetPort(const AValue: Word); procedure SetReconnectDelay(const AValue: LongWord); procedure SetTimeOut(const AValue: LongWord); procedure SetCommunicationMode(const AValue: THL7CommunicationMode); procedure SetIsListener(const AValue: Boolean); function GetStatus: TIdHL7Status; function GetStatusDesc: String; procedure InternalSetStatus(const AStatus: TIdHL7Status; ADesc: String); procedure CheckServerParameters; procedure StartServer; procedure StopServer; procedure DropServerConnection; procedure ServerConnect(AContext: TIdContext); procedure ServerExecute(AContext: TIdContext); procedure ServerDisconnect(AContext: TIdContext); procedure CheckClientParameters; procedure StartClient; procedure StopClient; procedure DropClientConnection; procedure HandleIncoming(const AMsg : String; AConnection: TIdTCPConnection); function HandleMessage(const AMsg: String; AConn: TIdTCPConnection; var VReply: String): Boolean; procedure InitComponent; override; Public destructor Destroy; Override; procedure EnforceWaitReplyTimeout; function Going: Boolean; // for the app to use to hold any related object property ObjTag: TObject Read FObject Write FObject; // status property Status: TIdHL7Status Read GetStatus; property StatusDesc: String Read GetStatusDesc; function Connected: Boolean; property IsServer: Boolean Read FIsServer; procedure Start; procedure PreStop; // call this in advance to start the shut down process. You do not need to call this procedure Stop; procedure WaitForConnection(AMaxLength: Integer); // milliseconds // asynchronous. function AsynchronousSend(AMsg: String): TSendResponse; property OnMessageArrive: TMessageArriveEvent Read FOnMessageArrive Write FOnMessageArrive; // synchronous function SynchronousSend(AMsg: String; var VReply: String): TSendResponse; property OnReceiveMessage: TMessageReceiveEvent Read FOnReceiveMessage Write FOnReceiveMessage; procedure CheckSynchronousSendResult(AResult: TSendResponse; AMsg: String); // single thread procedure SendMessage(AMsg: String); // you can't call SendMessage again without calling GetReply first function GetReply(var VReply: String): TSendResponse; function GetMessage(var VMsg: String): TObject; // return nil if no messages // if you don't call SendReply then no reply will be sent. procedure SendReply(AMsgHnd: TObject; AReply: String); Published // basic properties property Address: String Read FAddress Write SetAddress; // leave blank and we will be server property Port: Word Read FPort Write SetPort Default DEFAULT_PORT; // milliseconds - message timeout - how long we wait for other system to reply property TimeOut: LongWord Read FTimeOut Write SetTimeOut Default DEFAULT_TIMEOUT; // milliseconds - message timeout. When running cmSingleThread, how long we wait for the application to process an incoming message before giving up property ReceiveTimeout: LongWord Read FReceiveTimeout Write FReceiveTimeout Default DEFAULT_RECEIVE_TIMEOUT; // server properties property ConnectionLimit: Word Read FConnectionLimit Write SetConnectionLimit Default DEFAULT_CONN_LIMIT; // ignored if isListener is false property IPRestriction: String Read FIPRestriction Write SetIPRestriction; property IPMask: String Read FIPMask Write SetIPMask; // client properties // milliseconds - how long we wait after losing connection to retry property ReconnectDelay: LongWord Read FReconnectDelay Write SetReconnectDelay Default DEFAULT_RECONNECT_DELAY; // message flow // Set this to one of 4 possibilities: // // cmUnknown // Default at start up. You must set a value before starting // // cmAsynchronous // Send Messages with AsynchronousSend. does not wait for // remote side to respond before returning // Receive Messages with OnMessageArrive. Message may // be response or new message // The application is responsible for responding to the remote // application and dropping the link as required // You must hook the OnMessageArrive Event before setting this mode // The property IsListener has no meaning in this mode // // cmSynchronous // Send Messages with SynchronousSend. Remote applications response // will be returned (or timeout). Only use if IsListener is false // Receive Messages with OnReceiveMessage. Only if IsListener is // true // In this mode, the object will wait for a response when sending, // and expects the application to reply when a message arrives. // In this mode, the interface can either be the listener or the // initiator but not both. IsListener controls which one. // note that OnReceiveMessage must be thread safe if you allow // more than one connection to a server // // cmSingleThread // Send Messages with SendMessage. Poll for answer using GetReply. // Only if isListener is false // Receive Messages using GetMessage. Return a response using // SendReply. Only if IsListener is true // This mode is the same as cmSynchronous, but the application is // assumed to be single threaded. The application must poll to // find out what is happening rather than being informed using // an event in a different thread property CommunicationMode: THL7CommunicationMode Read FCommunicationMode Write SetCommunicationMode Default DEFAULT_COMM_MODE; // note that IsListener is not related to which end is client. Either end // may make the connection, and thereafter only one end will be the initiator // and one end will be the listener. Generally it is recommended that the // listener be the server. If the client is listening, network conditions // may lead to a state where the client has a phantom connection and it will // never find out since it doesn't initiate traffic. In this case, restart // the interface if there isn't traffic for a period property IsListener: Boolean Read FIsListener Write SetIsListener Default DEFAULT_IS_LISTENER; // useful for application property OnConnect: TNotifyEvent Read FOnConnect Write FOnConnect; property OnDisconnect: TNotifyEvent Read FOnDisconnect Write FOnDisconnect; // this is called whenever OnConnect and OnDisconnect are called, and at other times, but only when server // it will be called after OnConnect and before OnDisconnect property OnConnCountChange : TIdHL7ConnCountEvent read FOnConnCountChange write FOnConnCountChange; // this is called when an unhandled exception is generated by the // hl7 object or the application. It allows the application to // construct a useful return error, log the exception, and drop the // connection if it wants property OnReceiveError: TReceiveErrorEvent Read FOnReceiveError Write FOnReceiveError; end; implementation uses {$IFDEF USE_VCL_POSIX} {$IFDEF DARWIN} CoreServices, {$ENDIF} PosixSysSelect, PosixSysTime, {$ENDIF} IdGlobalProtocols, IdResourceStringsProtocols; type TQueuedMessage = class(TIdInterfacedObject) Private FEvent: TIdLocalEvent; FMsg: String; FTimeOut: LongWord; FReply: String; procedure Wait; Public constructor Create(aMsg: String; ATimeOut: LongWord); destructor Destroy; Override; end; { TQueuedMessage } constructor TQueuedMessage.Create(aMsg: String; ATimeOut: LongWord); begin assert(aMsg <> '', 'Attempt to queue an empty message'); {do not localize} assert(ATimeout <> 0, 'Attempt to queue a message with a 0 timeout'); {do not localize} inherited Create; FEvent := TIdLocalEvent.Create(False, False); FMsg := aMsg; FTimeOut := ATimeOut; end; destructor TQueuedMessage.Destroy; begin assert(self <> NIL); FreeAndNil(FEvent); inherited; end; procedure TQueuedMessage.Wait; begin assert(Assigned(Self)); assert(Assigned(FEvent)); FEvent.WaitFor(FTimeOut); end; { EHL7CommunicationError } constructor EHL7CommunicationError.Create(AnInterfaceName, AMessage: String); begin // assert(AInterfaceName <> '', 'Attempt to create an exception for an unnamed interface') // assert(AMessage <> '', 'Attempt to create an exception with an empty message') // actually, we do not enforce either of these conditions, though they should both be true, // since we are already raising an exception FInterfaceName := AnInterfaceName; if FInterfaceName <> '' then {do not localize} begin inherited Create('[' + AnInterfaceName + '] ' + AMessage) end else begin inherited Create(AMessage); end end; { TIdHL7 } procedure TIdHL7.InitComponent; begin inherited; raise EIdException.create(RSHL7Broken); {do not localize} // partly redundant initialization of properties FIsListener := DEFAULT_IS_LISTENER; FCommunicationMode := DEFAULT_COMM_MODE; FTimeOut := DEFAULT_TIMEOUT; FReconnectDelay := DEFAULT_RECONNECT_DELAY; FReceiveTimeout := DEFAULT_RECEIVE_TIMEOUT; FConnectionLimit := DEFAULT_CONN_LIMIT; FIPMask := NULL_IP; FIPRestriction := NULL_IP; FAddress := DEFAULT_ADDRESS; FPort := DEFAULT_PORT; FOnReceiveMessage := NIL; FOnConnect := NIL; FOnDisconnect := NIL; FObject := NIL; // initialise status FStatus := IsStopped; FStatusDesc := RSHL7StatusStopped; // build internal infrastructure Flock := TIdCriticalSection.Create; FConnCount := 0; FServer := NIL; FServerConn := NIL; FClientThread := NIL; FClient := NIL; FMsgQueue := TList.Create; FHndMsgQueue := TList.Create; FWaitingForAnswer := False; FMsgReply := ''; {do not localize} FReplyResponse := srNone; FWaitEvent := TIdLocalEvent.Create(False, False); end; destructor TIdHL7.Destroy; begin assert(Assigned(Self)); try if Going then begin Stop; end; finally FreeAndNil(FMsgQueue); FreeAndNil(FHndMsgQueue); FreeAndNil(FWaitEvent); FreeAndNil(FLock); inherited; end; end; {========================================================== Property Servers ==========================================================} procedure TIdHL7.SetAddress(const AValue: String); begin assert(Assigned(Self)); // we don't make any assertions about AValue - will be '' if we are a server if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Address'])); {do not localize??} end; FAddress := AValue; end; procedure TIdHL7.SetConnectionLimit(const AValue: Word); begin assert(Assigned(Self)); // no restrictions on AValue if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['ConnectionLimit'])); {do not localize??} end; FConnectionLimit := AValue; end; procedure TIdHL7.SetIPMask(const AValue: String); begin assert(Assigned(Self)); // to do: enforce that AValue is a valid Subnet mask if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IP Mask'])); {do not localize??} end; FIPMask := AValue; end; procedure TIdHL7.SetIPRestriction(const AValue: String); begin assert(Assigned(Self)); // to do: enforce that AValue is a valid IP address range if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IP Restriction'])); {do not localize??} end; FIPRestriction := AValue; end; procedure TIdHL7.SetPort(const AValue: Word); begin assert(Assigned(Self)); assert(AValue <> 0, 'Attempt to use Port 0 for HL7 Communications'); {do not localize} if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Port'])); {do not localize} end; FPort := AValue; end; procedure TIdHL7.SetReconnectDelay(const AValue: LongWord); begin assert(Assigned(Self)); // any value for AValue is accepted, although this may not make sense if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Reconnect Delay'])); {do not localize} end; FReconnectDelay := AValue; end; procedure TIdHL7.SetTimeOut(const AValue: LongWord); begin assert(Assigned(Self)); assert(FTimeout > 0, 'Attempt to configure TIdHL7 with a Timeout of 0'); {do not localize} // we don't fucntion at all if timeout is 0, though there is circumstances where it's not relevent if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Time Out'])); {do not localize??} end; FTimeOut := AValue; end; procedure TIdHL7.SetCommunicationMode(const AValue: THL7CommunicationMode); begin assert(Assigned(Self)); Assert((AValue >= Low(THL7CommunicationMode)) and (AValue <= High(THL7CommunicationMode)), 'Value for TIdHL7.CommunicationMode not in range'); {do not localize} // only could arise if someone is typecasting? if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Communication Mode'])); {do not localize} end; FCommunicationMode := AValue; end; procedure TIdHL7.SetIsListener(const AValue: Boolean); begin assert(Assigned(Self)); // AValue isn't checked if Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IsListener'])); {do not localize} end; FIsListener := AValue; end; function TIdHL7.GetStatus: TIdHL7Status; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try Result := FStatus; finally FLock.Leave; end; end; function TIdHL7.Connected: Boolean; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try Result := FStatus = IsConnected; finally FLock.Leave; end; end; function TIdHL7.GetStatusDesc: String; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try Result := FStatusDesc; finally FLock.Leave; end; end; procedure TIdHL7.InternalSetStatus(const AStatus: TIdHL7Status; ADesc: String); begin assert(Assigned(Self)); Assert((AStatus >= Low(TIdHL7Status)) and (AStatus <= High(TIdHL7Status)), 'Value for TIdHL7.CommunicationMode not in range'); {do not localize} // ADesc is allowed to be anything at all assert(Assigned(FLock)); FLock.Enter; try FStatus := AStatus; FStatusDesc := ADesc; finally FLock.Leave; end; end; {========================================================== Application Control ==========================================================} procedure TIdHL7.Start; var LStatus: TIdHL7Status; begin assert(Assigned(Self)); LStatus := GetStatus; if LStatus = IsUnusable then begin raise EHL7CommunicationError.Create(Name, RSHL7NotFailedToStop); end; if LStatus <> IsStopped then begin raise EHL7CommunicationError.Create(Name, RSHL7AlreadyStarted); end; if FCommunicationMode = cmUnknown then begin raise EHL7CommunicationError.Create(Name, RSHL7ModeNotSet); end; if FCommunicationMode = cmAsynchronous then begin if not Assigned(FOnMessageArrive) then begin raise EHL7CommunicationError.Create(Name, RSHL7NoAsynEvent); end; end; if (FCommunicationMode = cmSynchronous) and IsListener then begin if not Assigned(FOnReceiveMessage) then begin raise EHL7CommunicationError.Create(Name, RSHL7NoSynEvent); end; end; FIsServer := (FAddress = ''); if FIsServer then begin StartServer end else begin StartClient; end; FPreStopped := False; FWaitingForAnswer := False; end; procedure TIdHL7.PreStop; procedure JolList(l: TList); var i: Integer; begin for i := 0 to l.Count - 1 do begin TQueuedMessage(l[i]).FEvent.SetEvent; end; end; begin assert(Assigned(Self)); if FCommunicationMode = cmSingleThread then begin assert(Assigned(FLock)); assert(Assigned(FMsgQueue)); assert(Assigned(FHndMsgQueue)); FLock.Enter; try JolList(FMsgQueue); JolList(FHndMsgQueue); finally FLock.Leave; end; end; FPreStopped := True; end; procedure TIdHL7.Stop; begin assert(Assigned(Self)); if not Going then begin raise EHL7CommunicationError.Create(Name, RSHL7AlreadyStopped); end; if not FPreStopped then begin PreStop; IndySleep(10); // give other threads a chance to clean up end; if FIsServer then begin StopServer end else begin StopClient; end; end; {========================================================== Server Connection Maintainance ==========================================================} procedure TIdHL7.EnforceWaitReplyTimeout; begin Stop; Start; end; function TIdHL7.Going: Boolean; var LStatus: TIdHL7Status; begin assert(Assigned(Self)); LStatus := GetStatus; Result := (LStatus <> IsStopped) and (LStatus <> IsUnusable); end; procedure TIdHL7.WaitForConnection(AMaxLength: Integer); var LStopWaiting: TDateTime; begin LStopWaiting := Now + (AMaxLength * ((1 / (24 * 60)) / (60 * 1000))); while not Connected and (LStopWaiting > Now) do IndySleep(50); end; procedure TIdHL7.CheckSynchronousSendResult(AResult: TSendResponse; AMsg: String); begin case AResult of srNone: raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalsrNone); srError: raise EHL7CommunicationError.Create(Name, AMsg); srNoConnection: raise EHL7CommunicationError.Create(Name,RSHL7ErrNotConn); srSent: // cause this should only be returned asynchronously raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalsrSent); srOK:; // all ok srTimeout: raise EHL7CommunicationError.Create(Name,RSHL7ErrNoResponse); else raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalUnknownVal + IntToStr(Ord(AResult))); {do not localize} end; end; procedure TIdHL7.CheckServerParameters; begin assert(Assigned(Self)); if (FCommunicationMode = cmAsynchronous) or not FIsListener then begin FConnectionLimit := 1; end; if (FPort < 1) then // though we have already ensured that this cannot happen begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7InvalidPort, [FPort])); end; end; procedure TIdHL7.StartServer; begin assert(Assigned(Self)); CheckServerParameters; FServer := TIdTCPServer.Create(NIL); try FServer.DefaultPort := FPort; Fserver.OnConnect := ServerConnect; FServer.OnExecute := ServerExecute; FServer.OnDisconnect := ServerDisconnect; FServer.Active := True; InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected); except on e: Exception do begin InternalSetStatus(IsStopped, IndyFormat(RSHL7StatusFailedToStart, [e.message])); FreeAndNil(FServer); raise; end; end; end; procedure TIdHL7.StopServer; begin assert(Assigned(Self)); try FServer.Active := False; FreeAndNil(FServer); InternalSetStatus(IsStopped, RSHL7StatusStopped); except on e: Exception do begin // somewhat arbitrary decision: if for some reason we fail to shutdown, // we will stubbornly refuse to work again. InternalSetStatus(IsUnusable, IndyFormat(RSHL7StatusFailedToStop, [e.message])); FServer := NIL; raise end; end; end; procedure TIdHL7.ServerConnect(AContext: TIdContext); var LNotify : Boolean; LConnCount : integer; LValid : Boolean; begin assert(Assigned(Self)); assert(Assigned(AContext)); assert(Assigned(FLock)); FLock.Enter; try LNotify := FConnCount = 0; inc(FConnCount); LConnCount := FConnCount; // it would be better to stop getting here in the case of an invalid connection // cause here we drop it - nasty for the client. To be investigated later LValid := FConnCount <= FConnectionLimit; if (FConnCount = 1) and (FCommunicationMode <> cmAsynchronous) and not IsListener then begin FServerConn := AContext.Connection; end; if LNotify then begin InternalSetStatus(IsConnected, RSHL7StatusConnected); end; finally FLock.Leave; end; if LValid then begin if LNotify and Assigned(FOnConnect) then begin FOnConnect(self); end; if Assigned(FOnConnCountChange) and (FConnectionLimit <> 1) then begin FOnConnCountChange(Self, LConnCount); end; end else begin // Thread exceeds connection limit AContext.Connection.Disconnect; end; end; procedure TIdHL7.ServerDisconnect(AContext: TIdContext); var LNotify: Boolean; LConnCount : integer; begin assert(Assigned(Self)); assert(Assigned(AContext)); assert(Assigned(FLock)); FLock.Enter; try dec(FConnCount); LNotify := FConnCount = 0; LConnCount := FConnCount; if AContext.Connection = FServerConn then begin FServerConn := NIL; end; if LNotify then begin InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected); end; finally FLock.Leave; end; if Assigned(FOnConnCountChange) and (FConnectionLimit <> 1) then begin FOnConnCountChange(Self, LConnCount); end; if LNotify and Assigned(FOnDisconnect) then begin FOnDisconnect(self); end; end; procedure TIdHL7.ServerExecute(AContext: TIdContext); var s : String; begin assert(Assigned(Self)); assert(Assigned(AContext)); try // 1. prompt the network for content. AContext.Connection.IOHandler.ReadLn(MSG_START); // throw this content away if Assigned(AContext.Connection.IOHandler) then begin s := AContext.Connection.IOHandler.ReadLn(MSG_END); if length(s) > 0 then begin HandleIncoming(s, AContext.Connection); end; end; except try // well, there was some network error. We aren't sure what it // was, and it doesn't matter for this layer. we're just going // to make sure that we start again. // to review: what happens to the error messages? AContext.Connection.Disconnect; except end; end; end; procedure TIdHL7.DropServerConnection; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try if Assigned(FServerConn) then FServerConn.Disconnect; finally FLock.Leave; end; end; {========================================================== Client Connection Maintainance ==========================================================} procedure TIdHL7.CheckClientParameters; begin assert(Assigned(Self)); if (FPort < 1) then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7InvalidPort, [FPort])); end; end; procedure TIdHL7.StartClient; begin assert(Assigned(Self)); CheckClientParameters; FClientThread := TIdHL7ClientThread.Create(self); InternalSetStatus(isConnecting, RSHL7StatusConnecting); end; procedure TIdHL7.StopClient; var LFinished: Boolean; LWaitStop: LongWord; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try FClientThread.Terminate; FClientThread.FClient.Disconnect; FClientThread.FCloseEvent.SetEvent; finally FLock.Leave; end; LWaitStop := Ticks + 5000; repeat LFinished := (GetStatus = IsStopped); if not LFinished then begin IndySleep(10); end; until LFinished or (Ticks > LWaitStop); if GetStatus <> IsStopped then begin // for some reason the client failed to shutdown. We will stubbornly refuse to work again InternalSetStatus(IsUnusable, IndyFormat(RSHL7StatusFailedToStop, [RSHL7ClientThreadNotStopped])); end; end; procedure TIdHL7.DropClientConnection; begin assert(Assigned(Self)); assert(Assigned(FLock)); FLock.Enter; try if Assigned(FClientThread) and Assigned(FClientThread.FClient) then begin FClientThread.FClient.Disconnect; end else begin // This may happen validly because both ends are trying to drop the connection simultaineously end; finally FLock.Leave; end; end; { TIdHL7ClientThread } constructor TIdHL7ClientThread.Create(aOwner: TIdHL7); begin assert(Assigned(AOwner)); FOwner := aOwner; FCloseEvent := TIdLocalEvent.Create(True, False); inherited Create(False); FreeOnTerminate := True; end; destructor TIdHL7ClientThread.Destroy; begin assert(Assigned(Self)); assert(Assigned(FOwner)); assert(Assigned(FOwner.FLock)); FreeAndNil(FCloseEvent); try FOwner.FLock.Enter; try FOwner.FClientThread := NIL; FOwner.InternalSetStatus(isStopped, RSHL7StatusStopped); finally FOwner.FLock.Leave; end; except // it's really vaguely possible that the owner // may be dead before we are. If that is the case, we blow up here. // who cares. end; inherited; end; procedure TIdHL7ClientThread.PollStack; var LBuffer: String; begin assert(Assigned(Self)); LBuffer := ''; repeat // we don't send here - we just poll the stack for content // if the application wants to terminate us at this point, // then it will disconnect the socket and we will get thrown // out // we really don't care at all whether the disconnect was clean or ugly // but we do need to suppress exceptions that come from // indy otherwise the client thread will terminate try FClient.IOHandler.ReadLn(MSG_START); // we toss this content if Assigned(FClient.IOHandler) then begin LBuffer := FClient.IOHandler.ReadLn(MSG_END); if LBuffer <> '' then begin FOwner.HandleIncoming(LBuffer, FClient); end; end; except try // well, there was some network error. We aren't sure what it // was, and it doesn't matter for this layer. we're just going // to make sure that we start again. // to review: what happens to the error messages? FClient.Disconnect; except end; end; until Terminated or not FClient.Connected; end; procedure TIdHL7ClientThread.Execute; var LRecTime: TDateTime; begin assert(Assigned(Self)); try FClient := TIdTCPClient.Create(NIL); try FClient.Host := FOwner.FAddress; FClient.Port := FOwner.FPort; repeat // try to connect. Try indefinitely but wait Owner.FReconnectDelay // between attempts. Problems: how long does Connect take? repeat FOwner.InternalSetStatus(IsConnecting, rsHL7StatusConnecting); try FClient.Connect; except on e: Exception do begin LRecTime := Now + ((FOwner.FReconnectDelay / 1000) * {second length} (1 / (24 * 60 * 60))); //not we can take more liberties with the time and date output because it's only //for human consumption (probably in a log FOwner.InternalSetStatus(IsWaitReconnect, IndyFormat(rsHL7StatusReConnect, [DateTimeToStr(LRecTime), e.message])); {do not localize??} end; end; if not Terminated and not FClient.Connected then begin FCloseEvent.WaitFor(FOwner.FReconnectDelay); end; until Terminated or FClient.Connected; if Terminated then begin exit; end; FOwner.FLock.Enter; try FOwner.FClient := FClient; FOwner.InternalSetStatus(IsConnected, rsHL7StatusConnected); finally FOwner.FLock.Leave; end; if Assigned(FOwner.FOnConnect) then begin FOwner.FOnConnect(FOwner); end; try PollStack; finally FOwner.FLock.Enter; try FOwner.FClient := NIL; FOwner.InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected); finally FOwner.FLock.Leave; end; if Assigned(FOwner.FOnDisconnect) then begin FOwner.FOnDisconnect(FOwner); end; end; if not Terminated then begin // we got disconnected. ReconnectDelay applies. FCloseEvent.WaitFor(FOwner.FReconnectDelay); end; until terminated; finally FreeAndNil(FClient); end; except on e: Exception do // presumably some comms or indy related exception // there's not really anyplace good to put this???? end; end; {========================================================== Internal process management ==========================================================} procedure TIdHL7.HandleIncoming(const AMsg : String; AConnection: TIdTCPConnection); var LReply: String; begin assert(Assigned(Self)); assert(AMsg <> '', 'Attempt to handle an empty Message'); {do not localize} assert(Assigned(AConnection)); try // process any messages in the buffer (may get more than one per packet) if HandleMessage(AMsg, AConnection, LReply) then begin if LReply <> '' then begin AConnection.IOHandler.Write(MSG_START + LReply + MSG_END); end; end else begin AConnection.Disconnect; end; except // well, we need to suppress the exception, and force a reconnection // we don't know why an exception has been allowed to propagate back // to us, it shouldn't be allowed. so what we're going to do, is drop // the connection so that we force all the network layers on both // ends to reconnect. // this is a waste of time if the error came from the application but // this is not supposed to happen try AConnection.Disconnect; except // nothing - suppress end; end; end; function TIdHL7.HandleMessage(const AMsg: String; AConn: TIdTCPConnection; var VReply: String): Boolean; var LQueMsg: TQueuedMessage; LIndex: Integer; begin assert(Assigned(Self)); assert(AMsg <> '', 'Attempt to handle an empty Message'); {do not localize} assert(Assigned(FLock)); VReply := ''; Result := True; try case FCommunicationMode of cmUnknown: begin raise EHL7CommunicationError.Create(Name, RSHL7ImpossibleMessage); end; cmAsynchronous: begin FOnMessageArrive(self, AConn, Amsg); end; cmSynchronous, cmSingleThread: begin if IsListener then begin if FCommunicationMode = cmSynchronous then begin Result := False; FOnReceiveMessage(self, AConn, AMsg, Result, VReply) end else begin LQueMsg := TQueuedMessage.Create(AMsg, FReceiveTimeout); LQueMsg._AddRef; try FLock.Enter; try FMsgQueue.Add(LQueMsg); finally FLock.Leave; end; LQueMsg.wait; // no locking. There is potential problems here. To be reviewed VReply := LQueMsg.FReply; finally FLock.Enter; try LIndex := FMsgQueue.IndexOf(LQueMsg); if LIndex > -1 then FMsgQueue.Delete(LIndex); finally FLock.Leave; end; LQueMsg._Release; end; end end else begin FLock.Enter; try if FWaitingForAnswer then begin FWaitingForAnswer := False; FMsgReply := AMsg; FReplyResponse := srOK; if FCommunicationMode = cmSynchronous then begin assert(Assigned(FWaitEvent)); FWaitEvent.SetEvent; end; end else begin // we could have got here by timing out, but this is quite unlikely, // since the connection will be dropped in that case. We will report // this as a spurious message raise EHL7CommunicationError.Create(Name, RSHL7UnexpectedMessage); end; finally FLock.Leave; end; end end; else begin raise EHL7CommunicationError.Create(Name, RSHL7UnknownMode); end; end; except on e: Exception do if Assigned(FOnReceiveError) then begin FOnReceiveError(self, AConn, AMsg, e, VReply, Result) end else begin Result := False; end; end; end; {========================================================== Sending ==========================================================} // this procedure is not technically thread safe. // if the connection is disappearing when we are attempting // to write, we can get transient access violations. Several // strategies are available to prevent this but they significantly // increase the scope of the locks, which costs more than it gains function TIdHL7.AsynchronousSend(AMsg: String): TSendResponse; begin assert(Assigned(self)); assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize} assert(Assigned(FLock)); Result := srNone; // just to suppress the compiler warning FLock.Enter; try if not Going then begin raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWorking, [RSHL7SendMessage])) end else if GetStatus <> isConnected then begin Result := srNoConnection end else begin if FIsServer then begin if Assigned(FServerConn) then begin FServerConn.IOHandler.Write(MSG_START + AMsg + MSG_END); Result := srSent end else begin raise EHL7CommunicationError.Create(Name, RSHL7NoConnectionFound); end end else begin FClient.IOHandler.Write(MSG_START + AMsg + MSG_END); Result := srSent end; end; finally FLock.Leave; end end; function TIdHL7.SynchronousSend(AMsg: String; var VReply: String): TSendResponse; begin assert(Assigned(self)); assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize} assert(Assigned(FLock)); Result := srError; FLock.Enter; try FWaitingForAnswer := True; FWaitStop := Now + (FTimeOut * MILLISECOND_LENGTH); FReplyResponse := srTimeout; FMsgReply := ''; finally FLock.Leave; end; try Result := AsynchronousSend(AMsg); if Result = srSent then begin assert(Assigned(FWaitEvent)); FWaitEvent.WaitFor(FTimeOut); end; finally FLock.Enter; try FWaitingForAnswer := False; if Result = srSent then begin Result := FReplyResponse; end; if Result = srTimeout then begin if FIsServer then DropServerConnection else DropClientConnection; end; VReply := FMsgReply; finally FLock.Leave; end; end; end; procedure TIdHL7.SendMessage(AMsg: String); begin assert(Assigned(self)); assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize} assert(Assigned(FLock)); if FWaitingForAnswer then raise EHL7CommunicationError.Create(Name, RSHL7WaitForAnswer); FLock.Enter; try FWaitingForAnswer := True; FWaitStop := Now + (FTimeOut * MILLISECOND_LENGTH); FMsgReply := ''; FReplyResponse := AsynchronousSend(AMsg); finally FLock.Leave; end; end; function TIdHL7.GetReply(var VReply: String): TSendResponse; begin assert(Assigned(self)); assert(Assigned(FLock)); FLock.Enter; try if FWaitingForAnswer then begin if FWaitStop < Now then begin Result := srTimeout; VReply := ''; FWaitingForAnswer := False; FReplyResponse := srError; end else begin Result := srNone; end; end else begin Result := FReplyResponse; if Result = srSent then begin Result := srTimeOut; end; VReply := FMsgReply; FWaitingForAnswer := False; FReplyResponse := srError; end; finally FLock.Leave; end; end; function TIdHL7.GetMessage(var VMsg: String): TObject; begin assert(Assigned(self)); assert(Assigned(FLock)); assert(Assigned(FMsgQueue)); FLock.Enter; try if FMsgQueue.Count = 0 then begin Result := NIL; end else begin Result := FMsgQueue[0]; TQueuedMessage(Result)._AddRef; VMsg := TQueuedMessage(Result).FMsg; FMsgQueue.Delete(0); FHndMsgQueue.Add(Result); end; finally FLock.Leave; end; end; procedure TIdHL7.SendReply(AMsgHnd: TObject; AReply: String); var qm: TQueuedMessage; begin assert(Assigned(self)); assert(Assigned(AMsgHnd)); assert(AReply <> '', 'Attempt to send an empty reply'); {do not localize} assert(Assigned(FLock)); FLock.Enter; try qm := AMsgHnd as TQueuedMessage; qm.FReply := AReply; qm._Release; FHndMsgQueue.Delete(FHndMsgQueue.IndexOf(AMsgHnd)); finally FLock.Leave; end; qm.FEvent.SetEvent; end; end.