Unit Dispatcher;

{ Copyright (C) 2006-2016 by Shleps
  This library is a part of ChaosWarehouse project.

  It is free software; you can redistribute it and/or modify it
  under the terms of the GNU Library General Public License v2

  This program is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  for more details.

  Usage example:

  Format of network commands
  any number of commands separated by #10 or #13#10
  disconnect#10




applicable commands are

<worker name>::Run
<worker name>::Stop
<worker name>::Step
<worker name>::Close
<worker name>::Open
<worker name>::Drop
<worker name>::Quit

<worker name>::QueueCommand:<command>
<worker name>::QueueFunction:<command>
<worker name>::Announce Worker:<name>:<role>:<connection>:<connection addr>|<comma separated command list>
<worker name>::Denounce Worker:<worker name>
<worker name>::Stat:<sender>:<comma separated stat list>
<worker name>::system:<shell command>
<worker name>::shell:<shell command>');
<worker name>::test_integral:<X1>:<X2>:<dX>:<portion N>');
<worker name>::meta_integral:<X1>:<X2>:<dX>:<portions>');
<worker name>::reducesum:<name>:<value>:<portionN>');




ToDo:
  implement parallel dispatch+
  implement RqDir connection method
  fix high cpu load while waiting of exec in Linux
  implement performance counters in interface and timestamps on packets
  implement "pause-continue" with saving current queue to disk and restart application

  add sDB functions
  Add "dispatcher+" role

}

{$mode objfpc}{$H+}
{$DEFINE GUI}
Interface

Uses
  Classes, SysUtils, FileUtil, Process, Pipes, //LCLType,// LResources, Forms, Controls,
  //Graphics, Dialogs, StdCtrls, ExtCtrls, Buttons, ShellCtrls, ComCtrls,
 // MaskEdit, Spin, Grids, LCLType, ValEdit,
  {$IFDEF LINUX}CThreads,{$ENDIF} Types, blcksock, synsock, synautil, synamisc,
  sDB, eventlog, ShlepsUtils;

Type


  TUDPDaemon = class(TThread)
  private
  public
    Sock: TUDPBlockSocket;
    Buf: TStringList;
    Constructor Create;
    procedure Execute;override;
    procedure EnqueueCommands;
  end;

  TTCPDaemon = class(TThread)
  private
  public
    Sock:TTCPBlockSocket;
    Constructor Create;
    Destructor Destroy; override;
    procedure Execute; override;
  end;

  TTCPThrd = class(TThread)
  private
  public
    Sock:TTCPBlockSocket;
    CSock: TSocket;
    Buf:TStringList;
    Constructor Create (hsock:tSocket);
    procedure Execute; override;
    procedure EnqueueCommands;
  end;


Var
  LogLevel:Longint; //ToDo: implement fill from GUI and config
  Log:TStringList;
  CurrentCommand:String;
  I:Longint; //ToDo: Remove to local => need for init history lists
  DoMultipleSteps:Boolean;
  Run,AlreadyRun:Boolean;
  Quitting:Boolean; //Flag for "finish process rest of commands and quit" mode
  QueueOpenButtonDown:Boolean;
  TurboButtonDown:Boolean;
  PureTimeButtonDown:Boolean;
  WorkersChanged:Boolean; // notification for GUI to extra update
  IDNumerator:Longint;
  PC:Longint; // counter for "every Nth command"

  Waiters,WaiterValues,Locks:TStringList;

  CommandQueue,RTQueue:TStringList;
  HistoryC:TStringList;
  HistoryT:TStringList;
  HistoryR:TStringList;
  HistoryLength:Longint;
  HistoryIndex:Longint; //history round buffer origin
  OtherGUI:TStringList; //container for non-list values to send to remotte guis

  Workers:TStringList;
  Roles:TStringList;
  Connections:TStringList;
  ConnectionAddrs:TStringList;
  QueueOpens:TStringList;
  QueueRuns:TStringList;
  QueueLengths:TStringList;
  AvgSPCs:TStringList;    // Average time for execution per worker
  ETEs:TStringList;       // Estimate time of execution per worker
  RemoteGUIs:TStringList; // List of connected remote GUIs to update with current state of command queue, history etc

  T1,T2,T3:TDateTime;
  AvgCompList:TStringList;
  AvgCompPointer:Longint;
  AvgCompCount:Longint;
  AvgSum:Extended;
  QWasEmpty:Boolean; //flag to avoid affect of empty queue to average

  ICommands:TStringList;
  Aliases:TStringList;
  RoundRobin:Longint;

  PacketTimeOut:Longint;
  UDPDaemon:TUDPDaemon;
  TCPDaemon:TTCPDaemon;
  TCPThrd:TTCPThrd;
  W2DTCPSocket:TTCPBlockSocket;
  D2WTCPSocket:TTCPBlockSocket;
  SendUDPSocket:TUDPBlockSocket;
  ErrCode:Longint;
  StatusList:TStringList;// Command ID=Status
  ResultList:TStringList;// Result of Command

Procedure LogMessage(Level:Longint;Msg:String);
Function ExecShellCmd(CLI:String):String;
Function IsRTCommand(Command:String):Boolean;
Procedure SendStat;
Procedure ProcessRTCommand;
Procedure ProcessCommand;
Function ExecCommand(Command:String):String;
Procedure QueueCommand(Command:String);
Function QueueFunction(Command:String):String;
Procedure QueueMultipleCommands(Commands:TStringList);
Procedure RegisterWorker(ICommands:TStringList);

Procedure SendDataTo(WorkerName,Command:String);

Function CreateWaiter(Name,Command:String):String;
Function CreateLock(Name:String):String;
Function LockWaiter(WaiterName,LockName:String):String;
Function UnLock(LockName:String):String;
Procedure GracefulClose;

Implementation
Uses
  {$ifdef GUI} Dispatcher_iface, unit1, CADengine,
  {$endif} Config;

//==============================================================================================
//============================== S e r v i c e   r o u t i n e s ===============================
//==============================================================================================

Procedure LogMessage(Level:Longint;Msg:String);
Begin
  If Level>LogLevel Then Exit;
  Log.Add(Msg);
  // ToDo: implement logging to file and log buffer flush
  If DispatcherForm<>Nil then Begin
    DispatcherForm.LogMemo.Lines.Add(Msg);
    If DispatcherForm.LogMemo.Lines.Count>DispatcherForm.LogDepthEdit.Value Then
       DispatcherForm.LogMemo.Lines.Delete(0);
  End;
End;

{Function RunHiddenCommand(Cmd:String):Longint;
commented to enable tui
Var Process:TProcess; ProcessOut:TInputPipeStream;
Begin
  Process:=TProcess.Create(Nil);
  Try
    Process.Options:=[poUsePipes,poNoConsole,poStderrToOutPut];
    Process.ShowWindow:=swoNone;
    Process.CommandLine:=OSPath(Cmd);
    Try
      Process.Execute;
      Repeat Application.ProcessMessages;Until Not Process.Active;
 //     Process.WaitOnExit;
      ProcessOut:=Process.Output;
      Result:=Process.ExitStatus;
    Finally Process.Free; End;
  Except
  End;
End;}

Function RunWinCommand(Cmd:String):Longint;
Var Process:TProcess; ProcessOut:TInputPipeStream;
Begin
  Process:=TProcess.Create(Nil);
  Try
    Process.Options:=[poNewConsole,poNewProcessGroup,poStdErrToOutPut];
    Process.ShowWindow:=swoShow;
    Process.CommandLine:=OSPath(Cmd);
    Try
      Process.Execute;
      ProcessOut:=Process.Output;
      Result:=Process.ExitStatus;
    Finally Process.Free; End;
  Except

  End;
End;


Function ExecShellCmd(CLI:String):String;
Var I,Avail:Longint;
  Process:TProcess; ProcessOut:TInputPipeStream;
  CB:String;b,pb,Buf,RequestedOP,AttributeName,AttributeType:String;
Begin
        LogMessage(3,CLI);{StopWaitingResponce:=False;}
        Process:=TProcess.Create(Nil);
        Process.Options:=[poUsePipes,poNoConsole,poStdErrToOutPut];
        Process.ShowWindow:=swoShow;
        Process.CommandLine:=CLI;
        Try
          Buf:='';
          Process.Execute;
          ProcessOut:=Process.Output;
          Repeat
            Avail:=ProcessOut.NumBytesAvailable;
            If Avail>0 Then Begin
              If Avail>254 Then Repeat
                SetLength(cb,254);
                ProcessOut.Read(CB[1],254);
                Buf:=Buf+CB;
                Avail:=Avail-254;
              Until Avail<=254;
              SetLength(CB,Avail);
              ProcessOut.Read(CB[1],Avail);
              Buf:=Buf+CB;
            End;
            Sleep(10);
          // Commented to implement TUI
          //  Application.ProcessMessages;
          Until ((Not Process.Active) And (ProcessOut.NumBytesAvailable=0)) {Or (StopWaitingResponce)};
        Except LogMessage(0,'Error running '+CLI);End;
        Process.CloseOutput;Process.Free;
        LogMessage(3,FilterEnter(Buf));
    {    If StopWaitingResponce Then Result:='cancelled' Else} //Result:=FilterEnter(Buf);
        Result:=Buf;
End;


Function IsRTCommand(Command:String):Boolean;
Begin
  //ToDo: need more reliable detection with parsing of 1st and 2nd fields
   Result:=(Pos(':Run',Command)<>0) Or
           (Pos(':Stop',Command)<>0) Or
           (Pos(':Step',Command)<>0) Or
           (Pos(':Open',Command)<>0) Or
           (Pos(':Close',Command)<>0) Or
           (Pos(':Drop',Command)<>0) Or
           (Pos(':Quit',Command)<>0) Or
           (Pos(':Stat:',Command)<>0) Or
           (Pos(':Announce Worker:',Command)<>0) Or
           (Pos(':Denounce Worker:',Command)<>0) Or
           (Pos(':Connect GUI:',Command)<>0) Or
           (Pos(':Disconnect GUI:',Command)<>0) Or
           (Pos(':Update GUI:',Command)<>0) Or
           (Pos(':Command Status:',Command)<>0) Or
           (Pos(':Create Waiter:',Command)<>0) Or
           (Pos(':Create Lock:',Command)<>0) Or
           (Pos(':Lock Waiter:',Command)<>0) Or
           (Pos(':UnLock:',Command)<>0)
           ;
end;

//==============================================================================================
//============================== S o c k e t s    n e t w o r k i n g  =========================
//==============================================================================================

Procedure SendUDPString2Disp(S:String);
Var SendSock:TUDPBlockSocket;
  B,IP,Port:String;
Begin
  B:=ConfigFile.Values['Dispatcher Addr'];
  IP:=GetNextToken(B,Port,':');
  LogMessage(4,'Sending UDP packet to '+ConfigFile.Values['Dispatcher Addr']);
  SendSock.Connect(IP,Port);
  SendSock.SendString(AnsiString(S));
end;

Constructor TUDPDaemon.Create;
Var B,IP,Port:String;
begin
  inherited create(false);
  Sock:=TUDPBlockSocket.Create;
  Buf:=TStringList.Create;
  With Sock Do Begin
    CreateSocket;
    EnableReuse(True);
    ConvertLineEnd:=True;
    B:=ConfigFile.Values['Connection Addr'];
    IP:=GetNextToken(B,Port,':');
    If (IP='*')Or(IP='') Then IP:=GetHostName;
    If (Port='*')Or(Port='') Then Port:='0';
    Bind(IP,Port);
    GetSins;
    IP:=Sock.GetLocalSinIP;
    FreeOnTerminate:=True;
    If Port='0' Then ConfigFile.Values['MyName']:=ConfigFile.Values['MyName']+'_'+IntToStr(Sock.GetLocalSinPort);
   // DispatcherForm.Caption:=ConfigFile.Values['MyName']; // to be moved to GUI part
    ConfigFile.Values['Connection Addr']:=IP+':'+IntToStr(Sock.GetLocalSinPort);
  End;
end;

procedure TUDPDaemon.EnqueueCommands;
Var I:Longint;
Begin
  If Buf.Count>0 Then Begin
    QueueMultipleCommands(Buf);
    Buf.Clear;
  End;
End;

procedure TUDPDaemon.Execute;
Var S,S1,B,IP,Port,ReceivedCmd:String;
  PosD:Longint;
begin
  With Sock Do Begin
{  original for UDP
    While Not Terminated Do Begin
      S1:=Sock.RecvPacket(PacketTimeout);
      If S1<>'' Then S:=S+S1;
      Repeat
        PosD:=Pos(#10,S);
        If PosD>0 Then Begin
          Buf.Add(Copy(S,1,PosD-1));
          S:=Copy(S,PosD+1,Length(S)-PosD);
        End;
      Until PosD=0;
      Synchronize(@EnqueueCommands);
    End;
  end;}

{// copied from TCP template}
  Repeat
    If Terminated Then Break;
    S1:=RecvPacket(PacketTimeOut);
    If S1<>'' Then S:=S+S1;
    PosD:=Pos(#10,S);
    If PosD>0 Then Repeat
      PosD:=Pos(#10,S);
      If PosD>0 Then Begin
        ReceivedCmd:=Copy(S,1,PosD-1);
        If ReceivedCmd<>'disconnect' Then Buf.Add(ReceivedCmd);
        S:=Copy(S,PosD+1,Length(S)-PosD);
      End;
      {$ifdef GUI}  Synchronize(@EnqueueCommands);
      {$else}  EnqueueCommands;
      {$endif}

    Until PosD=0;
  Until False; // ReceivedCmd='disconnect'; - for TCP only
  end;

end;

Function DetectIP:String;
Var Sock:TTCPBlockSocket;
  ln:string;
  iplist:TStringList;
Begin
    Sock:=TTCPBlockSocket.Create;


sock.ResetLastError;
ln:=sock.Localname;
Result:=Sock.ResolveName(LN);
Result:=Sock.ResolveName('localhost');
Sock.Bind(sock.localname,'4055');
    IPList:=TStringList.Create;
    Sock.ResolveNameToIP(ln,IPList);
//    Writeln(IPList.Text);
ln:=intToStr(sock.LastError);
//ShowMessage(sock.GetErrorDescEx);
    sock.getsins;
    Result:=Sock.GetLocalSinIP;
    sock.CloseSocket;
    Sock.Free;
end;

Constructor TTCPDaemon.Create;
Var B,IP,IP2,Port:String;
    IPList:TStringList;
begin
  inherited create(false);
//  ShowMessage(DetectIP);
  Sock:=TTCPBlockSocket.Create;
  FreeOnTerminate:=True;{test change on 20150204}
  With Sock Do Begin
    CreateSocket;
    SetLinger(True,10);
    B:=ConfigFile.Values['Connection Addr'];
    IP:=GetNextToken(B,Port,':');
    If (IP='*')Or(IP='') Then IP:=GetHostName;
//    IP2:=sock. .ResolveName(GetHostName);
//    If (IP='*')Or(IP='') Then IP:='0.0.0.0';
    If (Port='*')Or(Port='') Then Port:='0';
    Bind(IP,Port);
    LogMessage(0,GetErrorDescEx);
    GetSins;
    IP:=Sock.GetLocalSinIP;
    If Port='0' Then ConfigFile.Values['MyName']:=ConfigFile.Values['MyName']+'_'+IntToStr(Sock.GetLocalSinPort);
    ConfigFile.Values['Connection Addr']:=IP+':'+IntToStr(Sock.GetLocalSinPort);
  End;
end;

Destructor TTCPDaemon.Destroy;
begin
  Sock.free;
end;

procedure TTCPDaemon.Execute;
var
  ClientSock:TSocket;
  B,IP,Port:String;
begin
  With Sock Do Begin
    Listen;
    Repeat
      If Terminated Then Break;
      If CanRead(100) Then Begin
        ClientSock:=Accept;
        If LastError=0 then TTCPThrd.Create(ClientSock);
      End;
    Until false;
  End;
end;

Constructor TTCPThrd.Create(Hsock:TSocket);
begin
  inherited create(false);
  Csock := Hsock;
  FreeOnTerminate:=True;
  Buf:=TStringList.Create;
  Sock:=TTCPBlockSocket.Create;
  Sock.Socket:=CSock;
end;

procedure TTCPThrd.EnqueueCommands;
Var I:Longint;
Begin
  If Buf.Count>0 Then Begin
    QueueMultipleCommands(Buf);
    Buf.Clear;
  End;
End;

procedure TTCPThrd.Execute;
var
  s,s1,IP,ID,Date,tcY,tcM,tcD,tcH,tcMN,filepath: string;
  posd,idn,module,I,ErrCode:Longint;
  ReceivedCmd:String;
begin
    S:='';S1:='';
    With Sock Do Begin
      Sock.ConvertLineEnd:=True;
      GetSins;
      Repeat
        If Terminated Then Begin Sock.CloseSocket;Exit;End;
        S1:=RecvPacket(PacketTimeOut);
        If S1<>'' Then S:=S+S1;
        If LastError<>0 Then Begin Sock.CloseSocket;Exit;End; //was commented
        PosD:=Pos(#10,S);
        If PosD>0 Then Repeat
          PosD:=Pos(#10,S);
          If PosD>0 Then Begin
            ReceivedCmd:=Copy(S,1,PosD-1);
            If ReceivedCmd<>'disconnect' Then Buf.Add(ReceivedCmd);
            S:=Copy(S,PosD+1,Length(S)-PosD);
          End;
        {$ifdef GUI}  Synchronize(@EnqueueCommands);
        {$else}  EnqueueCommands;
        {$endif}
          If Terminated Then Begin Sock.CloseSocket;Exit;End
        Until PosD=0;
      Until ReceivedCmd='disconnect';
    End;
    Sock.CloseSocket;
end;

//==============================================================================================
//============================== B u i l d i n g   t r a n s p o r t   n e t ===================
//==============================================================================================

Procedure QueueCommand(Command:String);
Var B,PB,ID,SeenBy,Sender,Worker,ServiceField:String;
    ParsedServiceField:TStringList;
Begin
  If (Command='')Or(Pos(';',Command)=1)Or(Pos('#',Command)=1) Then Begin
    CommandQueue.Add(Command);
    Exit;
  End; //newly added
  Worker:=GetNextToken(Command,PB,':');
  B:=PB;
  If (Worker<>ConfigFile.Values['MyName']) And (Worker<>'local') And (Worker<>'self') Then Begin
    ServiceField:=GetNextToken(B,PB,':');
    If ServiceField='' Then Begin //I am the sender - fill the service feld initially
      ServiceField:='Sender='+ConfigFile.Values['MyName'];
    end Else Begin //I am forwarder or addressee - parse the service field
      ParsedServiceField:=TStringList.Create;
      ParsedServiceField.Delimiter:=',';
      ParsedServiceField.DelimitedText:=ServiceField;
      Sender:=ParsedServiceField.Values['Sender'];
      If ParsedServiceField.IndexOfName('SeenBy')<0 Then Begin
        ParsedServiceField.Add('SeenBy='+ConfigFile.Values['MyName']);
      End Else Begin
        SeenBy:=ParsedServiceField.Values['SeenBy'];
        SeenBy:=SeenBy+' '+ConfigFile.Values['MyName'];
        ParsedServiceField.Values['SeenBy']:=SeenBy;
      End;
      ID:=ParsedServiceField.Values['ID'];
      ServiceField:=ParsedServiceField.DelimitedText;
      ParsedServiceField.Free;
    end;
    Command:=Worker+':'+ServiceField+':'+PB;
  end;
  If IsRTCommand(Command) Then RTQueue.Add(Command) Else CommandQueue.Add(Command);
 End;

Function QueueFunction(Command:String):String;
Var B,PB,ID,SeenBy,Sender,Worker,ServiceField:String;
    ParsedServiceField:TStringList;
Begin
  If (Command='')Or(Pos(';',Command)=1)Or(Pos('#',Command)=1) Then Begin
    CommandQueue.Add(Command);
    Exit;
  End; //newly added
  //здесь генерация ID
  IDNumerator:=(IDNumerator+1) Mod MaxInt;
  Worker:=GetNextToken(Command,PB,':');
  B:=PB;
  If (Worker=ConfigFile.Values['MyName']) Or (Worker='local') Or (Worker='self') Then Begin
    Result:=''; //если выполняем локально - ответ не генерируется
  End Else Begin
    ServiceField:=GetNextToken(B,PB,':');
    If ServiceField='' Then Begin //I am the sender - fill the service field initially
      ID:=ConfigFile.Values['MyName']+'/'+IntToStr(IDNumerator);
      ServiceField:='ID='+ID+',Sender='+ConfigFile.Values['MyName'];
      StatusList.Add(ID+'=sent');
      ResultList.Add('');
      Result:=ID;
    End Else Begin //I am forwarder or addressee - parse the service field
      ParsedServiceField:=TStringList.Create;
      ParsedServiceField.Delimiter:=',';
      ParsedServiceField.DelimitedText:=ServiceField;
      Sender:=ParsedServiceField.Values['Sender'];
      If ParsedServiceField.IndexOfName('SeenBy')<0 Then Begin
        ParsedServiceField.Add('SeenBy='+ConfigFile.Values['MyName']);
      End Else Begin
        SeenBy:=ParsedServiceField.Values['SeenBy'];
        SeenBy:=SeenBy+' '+ConfigFile.Values['MyName'];
        ParsedServiceField.Values['SeenBy']:=SeenBy;
      End;
      ID:=ParsedServiceField.Values['ID'];
      ServiceField:=ParsedServiceField.DelimitedText;
      ParsedServiceField.Free;
    end;
    Command:=Worker+':'+ServiceField+':'+PB;
  end;
  CommandQueue.Add(Command);
End;

Procedure QueueMultipleCommands(Commands:TStringList);
Var B,PB,ID,Command,SeenBy,Sender,Worker,ServiceField:String;
    ParsedServiceField:TStringList;
    I:Longint;
Begin
  If Commands.Count>0 Then
    For I:=0 To Commands.Count-1 Do Begin
      Command:=Commands[I];
      If (Command='')Or(Pos(';',Command)=1)Or(Pos('#',Command)=1) Then Begin CommandQueue.Add(Command);Continue;End; //newly added
      Worker:=GetNextToken(Command,PB,':');
      B:=PB;
      If (Worker<>ConfigFile.Values['MyName']) And (Worker<>'local') And (Worker<>'self') Then Begin
        ServiceField:=GetNextToken(B,PB,':');
        If ServiceField='' Then Begin //I am the sender - fill the service feld initially
          ServiceField:='Sender='+ConfigFile.Values['MyName'];
        end Else Begin //I am forwarder or addressee - parse the service field
          ParsedServiceField:=TStringList.Create;
          ParsedServiceField.Delimiter:=',';
          ParsedServiceField.DelimitedText:=ServiceField;
          Sender:=ParsedServiceField.Values['Sender'];
          If ParsedServiceField.IndexOfName('SeenBy')<0 Then Begin
            ParsedServiceField.Add('SeenBy='+ConfigFile.Values['MyName']);
          End Else Begin
            SeenBy:=ParsedServiceField.Values['SeenBy'];
            SeenBy:=SeenBy+' '+ConfigFile.Values['MyName'];
            ParsedServiceField.Values['SeenBy']:=SeenBy;
          End;
          ID:=ParsedServiceField.Values['ID'];
          ServiceField:=ParsedServiceField.DelimitedText;
          ParsedServiceField.Free;
        end;
        Command:=Worker+':'+ServiceField+':'+PB;
      end;
    If IsRTCommand(Command) Then RTQueue.Add(Command) Else CommandQueue.Add(Command);
  End;
end;

Procedure CommandStatus(ID,Status,Result:String);
Var I,N:Longint;
Begin
  N:=StatusList.IndexOfName(ID);
  If N<0 Then Exit;
  StatusList[N]:=ID+'='+Status;
  ResultList[N]:=Result;
{  For I:=1 To DispatcherForm.HistoryStringGrid.RowCount-1 Do Begin
    If Pos('ID='+ID,DispatcherForm.HistoryStringGrid.Cells[0,I])<>0 Then Begin
      DispatcherForm.HistoryStringGrid.Cells[1,I]:=Result;
      DispatcherForm.CommandResult.Text:=Result;
      Exit;
    end;
  end;}
end;

Function WaitForID(ID:String):String;
Var I,N:Longint;
Begin
  N:=StatusList.IndexOfName(ID);   //StatusList - StringList of 'ID=Status'
  If N<0 Then Begin
    LogMessage(1,'Error: no result for command with ID '+ID);
    Result:='Error: no result for command with ID '+ID;
    Exit;
  End;
  While StatusList.ValueFromIndex[N]<>'Ok' Do Begin
  //  Application.ProcessMessages;   commented to conver 2 tui
    //Sleep(10);
  end;
  Result:=ResultList[N];
  StatusList.Delete(N);
  ResultList.Delete(N);
end;

{Register this worker/slave dispatcher on master dispatcher on start}
Procedure RegisterWorker(ICommands:TStringList);
Var ICommandList:TStringList;
    I,Index:Longint;
    WorkerName,B,PB,IP,Port:String;
Begin
  PacketTimeOut:=StrToInt(ConfigFile.Values['PacketTimeOut']);
  WorkerName:=ConfigFile.Values['MyName'];
  If WorkerName='*' Then Begin
    //autodetect local host name
    WorkerName:=GetHostName;
    ConfigFile.Values['MyName']:=WorkerName;
  End;
  ICommandList:=TStringList.Create;
  ICommandList.AddStrings(ICommands);
  ICommandList.Insert(0,'self::Run');
  ICommandList.Insert(1,'self::Stop');
  ICommandList.Insert(2,'self::Step');
  ICommandList.Insert(3,'self::Open');
  ICommandList.Insert(4,'self::Close');
  ICommandList.Insert(5,'self::Drop');
  ICommandList.Insert(6,'self::Quit');
  ICommandList.Insert(7,'self::Stat');
  ICommandList.Insert(8,'self::Announce Worker:<name>:<role>:<connection>|<comma separated commands>');
  ICommandList.Insert(9,'self::Denounce Worker:<name>');
  ICommandList.Insert(10,'self::Connect GUI:<GUI name>');
  ICommandList.Insert(11,'self::Disconnect GUI:<GUI name>');
  ICommandList.Insert(12,'self::Update GUI:<comma separated info>');
  ICommandList.Insert(13,'self::Command Status:<ID>');
  ICommandList.Insert(14,'self::QueueCommand:<command>');
  ICommandList.Insert(15,'self::system:<shell command>');
  ICommandList.Insert(16,'self::system_o:<shell command>');
  ICommandList.Insert(17,'self::shell:<shell command>');
  ICommandList.Insert(18,'self::Create Waiter:<WaiterName>:<delayed command>');
  ICommandList.Insert(19,'self::Create Lock:<LockName>');
  ICommandList.Insert(20,'self::Lock Waiter:<WaiterName>:<LockName>');
  ICommandList.Insert(21,'self::Unlock:<LockName>');
  ICommandList.Insert(22,'self::test_integral:<X1>:<X2>:<dX>:<portion N>');
  ICommandList.Insert(23,'self::meta_integral:<X1>:<X2>:<dX>:<portions>');
  ICommandList.Insert(24,'self::reducesum:<name>:<portion value>:<portion N>');

  If (LowerCase(ConfigFile.Values['Role'])='dispatcher') And (ConfigFile.Values['Dispatcher Name']='') Then Begin
      // I am root dispatcher
      // registering commands locally
      Workers.Add(WorkerName);
      Roles.Add(ConfigFile.Values['Role']);
      Connections.Add(ConfigFile.Values['Connection']);// should be replaced to autodetect of proper method to connect to this worker
      ConnectionAddrs.Add(ConfigFile.Values['Connection addr']);
      QueueOpens.Add('Open');
      QueueRuns.Add('Run');
      QueueLengths.Add('0');
      AvgSPCs.Add('0');
      ETEs.Add('0');
      Workers.Objects[Workers.IndexOf(WorkerName)]:=ICommandList;
      If LowerCase(ConfigFile.Values['Connection'])='tcp' Then Begin
        If LowerCase(ConfigFile.Values['TCPListen'])='enabled' Then Begin
          TCPDaemon:=TTCPDaemon.Create;
          LogMessage(4,'Local address set to '+ConfigFile.Values['Connection Addr']);
        End;
      End;
      If LowerCase(ConfigFile.Values['Connection'])='udp' Then Begin
        If LowerCase(ConfigFile.Values['UDPListen'])='enabled' Then Begin
          UDPDaemon:=TUDPDaemon.Create;
          LogMessage(4,'Local address set to '+ConfigFile.Values['Connection Addr']);
        End;
      End;
    End Else Begin
    // I am additional dispatcher
    // or I am worker/viewer - not dispatcher
    // inserting info about our dispatcher
    Workers.Add(ConfigFile.Values['Dispatcher Name']);
    Roles.Add('Dispatcher');
    Connections.Add(ConfigFile.Values['Dispatcher Connection']);
    ConnectionAddrs.Add(ConfigFile.Values['Dispatcher Addr']);
    QueueOpens.Add('Open');
    QueueRuns.Add('Run');
    QueueLengths.Add('0');
    AvgSPCs.Add('0');
    ETEs.Add('0');
//following line uncommented in D3
//    Workers.Objects[Workers.IndexOf(WorkerName)]:=ICommandList;// - will be received from dispatcher later
    If LowerCase(ConfigFile.Values['Connection'])='tcp' Then Begin
      If LowerCase(ConfigFile.Values['TCPListen'])='enabled' Then Begin
        TCPDaemon:=TTCPDaemon.Create;
        LogMessage(4,'Local address set to '+ConfigFile.Values['Connection Addr']);
      End;
    End;
    If LowerCase(ConfigFile.Values['Connection'])='udp' Then Begin
      If LowerCase(ConfigFile.Values['UDPListen'])='enabled' Then Begin
        UDPDaemon:=TUDPDaemon.Create;
        LogMessage(4,'Local address set to '+ConfigFile.Values['Connection Addr']);
      End;
    End;
  end;
  If LowerCase(ConfigFile.Values['Dispatcher Connection'])='tcp' Then Begin
    //Establish TCP Connection To Dispatcher
    B:=ConfigFile.Values['Dispatcher Addr'];
    IP:=GetNextToken(B,Port,':');
    LogMessage(4,'Connecting to '+ConfigFile.Values['Dispatcher Addr']);
    W2DTCPSocket:=TTCPBlockSocket.Create;
    Index:=Workers.IndexOf(ConfigFile.Values['Dispatcher Name']);
    If Index<0 Then Begin LogMessage(0,'Dispatcher not in worker list: '+ConfigFile.Values['Dispatcher Name']);Exit;End;
    Connections.Objects[Index]:=W2DTCPSocket; //W2D socket enlisted in common list of sockets
//    W2DTCPSocket.Connect('192.168.77.128',Port);
    W2DTCPSocket.Connect(IP,Port);

//    If W2DTCPSocket.LastError <> 0 Then Begin LogMessage(1,'Error connecting to '+IP+':'+Port);Exit;End;
  End;
  If LowerCase(ConfigFile.Values['Dispatcher Connection'])='udp' Then Begin
    //Establish UDP Connection To Dispatcher
    B:=ConfigFile.Values['Dispatcher Addr'];
    IP:=GetNextToken(B,Port,':');
    LogMessage(4,'Connecting to '+ConfigFile.Values['Dispatcher Addr']);

    Index:=Workers.IndexOf(ConfigFile.Values['Dispatcher Name']);
    If Index<0 Then Begin LogMessage(0,'Dispatcher not in worker list: '+ConfigFile.Values['Dispatcher Name']);Exit;End;
    Connections.Objects[Index]:=SendUDPSocket; //W2D socekt enlisted in common list of sockets
    SendUDPSocket.Connect(IP,Port);
    If SendUDPSocket.LastError <> 0 Then Begin LogMessage(1,'Error connecting to '+IP+':'+Port);Exit;End;
  End;
    // registering at external dispatcher
    If ConfigFile.Values['Dispatcher Name']<>'' Then     // if there is another Dispatcher
      ///urgent - was UrgeCommand
        QueueCommand(ConfigFile.Values['Dispatcher Name']+'::Announce Worker:'+ConfigFile.Values['MyName']+':'+ConfigFile.Values['Role']+':'
       +ConfigFile.Values['Connection']+':'+ConfigFile.Values['Connection Addr']+'|'+ICommandList.CommaText);
End;

{ another worker sent us announce}
Procedure RegisterExternalWorker(WorkerName,Role,Connection,ConnAddr:String;ICommands:TStringList);
Var ICommandList,IPList:TStringList;
    I,J,Index:Longint;
    CurrentWorker,B,IP,IPAddr,Port:String;
Begin
  { to register new worker
       check if it is not already on list
          if it's name=myname replace connection with "self"
       if it is already on list - modify attributes
       common part
       TCP connection: create socket
                       connect with worker
                       put socket to connections.objects
       fill local GUI
       if I am dispatcher - forward annouce to my direct neibourghs
           replacing connection to via in case of retransmission to another dispatcher (except if we registering another dispatcher)
           without replacement in case of retransmission to worker

  }
  Index:=Workers.IndexOf(WorkerName);
  If Index<0 Then Begin
    // data about really new worker, never mentioned earlier
    Workers.Add(WorkerName);
    Roles.Add(Role);
    If WorkerName=ConfigFile.Values['MyName'] Then Begin Connection:='self';ConnAddr:='';End;
    Connections.Add(Connection);
    ConnectionAddrs.Add(ConnAddr);
    QueueOpens.Add('Open');
    QueueRuns.Add('Run');
    QueueLengths.Add('0');
    AvgSPCs.Add('0');
    ETEs.Add('0');
    Index:=Workers.IndexOf(WorkerName);
    ICommandList:=TStringList.Create;
    ICommandList.AddStrings(ICommands);
    Workers.Objects[Index]:=ICommandList;
  End Else Begin
    // Worker is already on the list - just update it's parameters and _exit_
    Roles[Index]:=Role;
    If (Connection='via') And (ConnAddr=ConfigFile.Values['MyName']) Then Exit; //dispatcher received echo about his already known worker
    If (Role='worker')Or(Role='viewer') Then Begin //don't update info about our direct dispatcher
      Connections[Index]:=Connection;
      ConnectionAddrs[Index]:=ConnAddr;
    End;
    ICommandList:=TStringList.Create;
    ICommandList.AddStrings(ICommands);
    Workers.Objects[Index]:=ICommandList;
    Exit;
  end;
  If (LowerCase(Connection)='tcp')And(WorkerName<>ConfigFile.Values['MyName']) Then Begin
    //Establish TCP Connection To new worker
    B:=ConnAddr;
    IP:=GetNextToken(B,Port,':');
    LogMessage(4,'Connecting to '+ConnAddr);
    D2WTCPSocket:=TTCPBlockSocket.Create;
    D2WTCPSocket.GetSins;
    D2WTCPSocket.ConvertLineEnd:=True;
    If Index<0 Then Begin LogMessage(0,'Command to unexisting worker:'+WorkerName);Exit;End;
    Connections.Objects[Index]:=D2WTCPSocket;
    IPList:=TStringList.Create;
    D2WTCPSocket.ResolveNameToIP(IP,IPList);
    IPAddr:=IPList[0];
    IPList.Free;
    (Connections.Objects[Index] As TTCPBlockSocket).Connect(IPAddr,Port);
    If D2WTCPSocket.LastError <> 0 Then Begin LogMessage(1,'Error connection to '+IPAddr+':'+Port);Exit;End;
  End;
  If LowerCase(Connection)='udp' Then Begin
    //UDP don't need Connections
    B:=ConnAddr;
    IP:=GetNextToken(B,Port,':');
    LogMessage(4,'Connecting to '+ConnAddr);
    SendUDPSocket.GetSins;
    SendUDPSocket.ConvertLineEnd:=True;
    If Index<0 Then Begin LogMessage(0,'Command to unexisting worker:'+WorkerName);Exit;End;
    Connections.Objects[Index]:=SendUDPSocket;
//    (Connections.Objects[Index] As TUDPBlockSocket).Connect(IP,Port); //that's wrong coonnect should be right before sending    but need to check if peer is up before start
//    If SendUDPSocket.LastError <> 0 Then Begin LogMessage(1,'Error connection to '+IP+':'+Port);Exit;End;
  End;

  If LowerCase(ConfigFile.Values['Role'])='worker' Then Exit;
  // if I'am dispatcher - send update of worker table to my neighbourghs
  If WorkerName=ConfigFile.Values['MyName'] Then Exit; //I'm the noob - nothing to send;
  If Workers.Count>0 Then
    For I:=0 To Workers.Count-1 Do Begin
      CurrentWorker:=Workers[I];
      If (LowerCase(Connections[I])<>'via') Then Begin
        If (CurrentWorker=WorkerName) Then  // don't return registration to sender
      //    QueueCommand(Workers[I]+'::Announce Worker:'+WorkerName+':'+Role+':'+Connection+':'+ConnAddr+'|'+(Workers.Objects[I] As TStringList).CommaText)
        Else
          If ((LowerCase(Roles[I])='dispatcher') And (Workers[I]<>ConfigFile.Values['MyName'])) Then  // to all dispatchers except myself with replacement to via
            If Connection='via' Then
              QueueCommand(CurrentWorker+'::Announce Worker:'+WorkerName+':'+Role+':'+Connection+':'+ConnAddr+'|'+(Workers.Objects[Index] As TStringList).CommaText)
            Else
              QueueCommand(CurrentWorker+'::Announce Worker:'+WorkerName+':'+Role+':via:'+ConfigFile.Values['MyName']+'|'+(Workers.Objects[Index] As TStringList).CommaText)
          Else
            If (LowerCase(Roles[I])='worker')Or(LowerCase(Roles[I])='viewer') Then // only to directly connected workers
              If LowerCase(ConfigFile.Values['AllowDirect'])='yes' Then
                QueueCommand(CurrentWorker+'::Announce Worker:'+WorkerName+':'+Role+':'+Connection+':'+ConnAddr+'|'+(Workers.Objects[Index] As TStringList).CommaText)
              Else
                QueueCommand(CurrentWorker+'::Announce Worker:'+WorkerName+':'+Role+':via:'+ConfigFile.Values['MyName']+'|'+(Workers.Objects[Index] As TStringList).CommaText);
      End;
    End;
  // if I'am the closest dispatcher - also send to the newbie full list of workers
  If WorkerName=ConfigFile.Values['MyName'] Then Exit; //I'm the noob - nothing to send;
  J:=Workers.IndexOf(WorkerName);
  If J<0 Then Begin
    LogMessage(1,'Error - worker is not on the list:'+WorkerName);
    Exit;
  End;
  If Connections[J]<>'via' Then Begin
    For I:=0 To Workers.Count-1 Do Begin
      CurrentWorker:=Workers[I];
      If ((LowerCase(Roles[I])='dispatcher') And (Workers[I]<>ConfigFile.Values['MyName'])) Then // except myself
        QueueCommand(WorkerName+'::Announce Worker:'+Workers[I]+':'+Roles[I]+':via:'+ConfigFile.Values['MyName']+'|'+(Workers.Objects[I] As TStringList).CommaText)
      Else
        If LowerCase(ConfigFile.Values['AllowDirect'])='yes' Then
          QueueCommand(WorkerName+'::Announce Worker:'+Workers[I]+':'+Roles[I]+':'+Connections[I]+':'+ConnectionAddrs[I]+'|'+(Workers.Objects[I] As TStringList).CommaText)
        Else
          QueueCommand(WorkerName+'::Announce Worker:'+Workers[I]+':'+Roles[I]+':via:'+ConfigFile.Values['MyName']+'|'+(Workers.Objects[I] As TStringList).CommaText);
//          QueueCommand(CurrentWorker+'::Announce Worker:'+WorkerName+':'+Role+':via:'+ConfigFile.Values['MyName']+'|'+(Workers.Objects[Index] As TStringList).CommaText);
    End;
  end;
End;


Procedure UnRegisterExternalWorker(WorkerName:String);
Var I:Longint;
Begin
  I:=Workers.IndexOf(WorkerName);
  If I>0 Then Begin
    Workers.Objects[I].Free;
    Workers.Delete(I);
    Roles.Delete(I);
    Connections.Delete(I);
    ConnectionAddrs.Delete(I);
    QueueOpens.Delete(I);
    QueueRuns.Delete(I);
    QueueLengths.Delete(I);
    AvgSPCs.Delete(I);
    ETEs.Delete(I);
    // if I'am dispatcher - send updated worker table to "other Dispatchers"??? and all my workers
    For I:=0 To Workers.Count-1 Do Begin
      If ((LowerCase(Roles[I])='dispatcher') And (Workers[I]<>ConfigFile.Values['MyName'])) // to all dispatchers except myself
          Or ((LowerCase(Roles[I])='worker') And (LowerCase(Connections[I])<>'via')) // only to directly connected workers
      Then
        // was     UrgeCommand
        QueueCommand(Workers[I]+'::Denounce Worker:'+WorkerName);
    End;
  end;
end;

Procedure ProcessStat(SenderName:String; StatList:TStringList);
Var I,N:Longint;B,PB,WorkerName,Open,Run,QL,AvgSPC,ETE:String;
Begin
  If StatList.Count>0 Then
    For I:=0 To StatList.Count-1 Do Begin
      B:=StatList[I];
      WorkerName:=GetNextToken(B,Pb,':');
      B:=PB;
      Open:=GetNextToken(B,Pb,':');
      B:=PB;
      Run:=GetNextToken(B,Pb,':');
      B:=PB;
      QL:=GetNextToken(B,Pb,':');
      B:=PB;
      AvgSPC:=GetNextToken(B,Pb,':');
      ETE:=PB;
      If WorkerName<>ConfigFile.Values['MyName'] Then Begin // don't tell me about myself
        N:=Workers.IndexOf(WorkerName);
        If (N>=0)And(N+1<=QueueOpens.Count) Then Begin
          // Protection from stat arrived for worker, that not registered yet
          // was (N>=0)And(N+1<=DispatcherForm.WorkersGrid.RowCount-1)
          QueueOpens[N]:=Open;
          QueueRuns[N]:=Run;
          QueueLengths[N]:=QL;
          AvgSPCs[N]:=AvgSPC;
          ETEs[N]:=ETE;
        End;
      End;
    end;
end;
//==============================================================================================
//==============================  Distributed  synchronization  ================================
//==============================================================================================


Function CreateWaiter(Name,Command:String):String;
Var LockList:TStringList;
Begin
  If Waiters.IndexOfName(Name)>=0 Then
    Result:='Error: waiter already exists:'+Name
  Else Begin
    LockList:=TStringList.Create;
    Waiters.AddObject(Name+'='+Command,LockList);
    WaiterValues.Add(Name+'=');
    Result:='Ok';
  end;
end;

Function CreateLock(Name:String):String;
Var WaiterList:TStringList;
Begin
  If Locks.IndexOf(Name)>=0 Then
    Result:='Error: Lock already exists:'+Name
  Else Begin
    WaiterList:=TStringList.Create;
    Locks.AddObject(Name,WaiterList);
    Result:='Ok';
  End;
end;

Function LockWaiter(WaiterName,LockName:String):String;
Var WaiterList,LockList,LockValueList:TStringList;
    M,N:Longint;
Begin
  M:=Waiters.IndexOfName(WaiterName);
  N:=Locks.IndexOf(LockName);
  If M<0 Then Begin Result:='Error: Waiter is not on the list:'+WaiterName;Exit;End;
  If N<0 Then Begin Result:='Error: Lock is not on the list:'+LockName;Exit;End;
  LockList:=Waiters.Objects[M] As TStringList;
  LockList.Add(LockName);
  WaiterList:=Locks.Objects[N] As TStringList;
  WaiterList.Add(WaiterName);
  Result:='Ok';
end;

Function Unlock(LockName:String):String;
Var WaiterList,LockList:TStringList;
    Command:String;
    M,N,X,I:Longint;
Begin
  Result:='';
  M:=Locks.IndexOf(LockName);
  If M<0 Then Begin Result:='Error: there were no lock with name '+LockName+'.';Exit;End;
  WaiterList:=Locks.Objects[M] As TStringList;
  If WaiterList.Count>0 Then
    For I:=WaiterList.Count-1 DownTo 0 Do Begin
      N:=Waiters.IndexOfName(WaiterList[I]);//
      If N<0 Then Begin Result:='Error: there were no waiter with name '+WaiterList[I]+'.';Exit;End;
      LockList:=Waiters.Objects[N] As TStringList;
      X:=LockList.IndexOf(LockName);
      If X>=0 Then Begin
        //there is the lock in list - removing
        LockList.Delete(X);
        If LockList.Count=0 Then Begin
          //no more lock left on this waiter - need to activate waiter's command and remove waiter
          Command:=Waiters.ValueFromIndex[N];
          If WaiterValues.ValueFromIndex[N]='' Then QueueCommand(Command)
          Else QueueCommand(Command+':'+WaiterValues.ValueFromIndex[N]);
          LockList.Free;
          Waiters.Delete(N);
          WaiterValues.Delete(N);//assume waiters and values lists are synchronous
        end;
      End Else Result:='Error: there were no lock '+LockName+' on '+WaiterList[I]+' waiter''s list.';
      WaiterList.Delete(I);
    end;
  WaiterList.Free;
  Locks.Delete(M);
  If Result='' then Result:='Ok';
end;

Function Test_Integral(A,B,Delta,Portion:String):String;
Var XA,XB,X,dX,Sum:Extended;
Begin
    XA:=StrToFloat(A);
    XB:=StrToFloat(B);
    dX:=StrToFloat(Delta);
    Sum:=0;
    X:=XA;
    Repeat
      Sum:=Sum+X;// f(x)=x
      X:=X+dX;
    Until X>XB;
    Result:=FloatToStr(Sum*dX);
    If Portion<>'' Then QueueCommand('head::ReduceSum:integral:'+Result+':'+Portion);
End;

Function Meta_Integral(A,B,Delta,Portions:String):String;
Var XA,XB,X1,X2,Sum:Extended;
    I,N:Longint;
Begin
  CreateWaiter('integral','self::IntegralValue');
  XA:=StrToFloat(A);
  XB:=StrToFloat(B);
  N:=StrToInt(Portions);
  For I:=0 To N-1 Do Begin
    X1:=XA+I*(XB-XA)/N;
    X2:=XA+(I+1)*(XB-XA)/N;
    CreateLock('portion'+IntToStr(I));
    LockWaiter('integral','portion'+IntToStr(I));
    QueueCommand('*::test_integral:'+FloatToStr(X1)+':'+FloatToStr(X2)+':'+Delta+':portion'+IntToStr(I));
  End;
End;

Function ReduceSum(WaiterName,Value,Lockname:String):String;
Var I,N:Longint;Sum:String;SumF,ValF:Extended;
Begin
  N:=WaiterValues.IndexOfName(WaiterName);
  Sum:=WaiterValues.ValueFromIndex[N];If Sum='' Then Sum:='0';
  SumF:=StrToFloat(Sum);
  ValF:=StrToFloat(Value);
  SumF:=SumF+ValF;
  Sum:=FloatToStr(SumF);
  WaiterValues.ValueFromIndex[N]:=Sum;
  Unlock(LockName);
  Result:=Sum;
End;

//==============================================================================================
//============================== Q u e u e    e x e c u t i o n ================================
//==============================================================================================

Procedure SendDataTo(WorkerName,Command:String);
Var Index,ErrCode:Longint;
    TCPSocket:TTCPBlockSocket;
    UDPSocket:TUDPBlockSocket;
    Forwarder,B,IP,Port:String;
Begin
  Index:=Workers.IndexOf(WorkerName);
  If Index<0 Then Begin LogMessage(0,'Command to unexisting worker:'+WorkerName);Exit;End;

  If LowerCase(Connections[Index])='tcp' Then Begin
     LogMessage(4,'Sending to '+WorkerName+' via TCP '+Command);
     TCPSocket:=(Connections.Objects[Index] As TTCPBlockSocket);
     ErrCode:=TCPSocket.LastError;
     If ErrCode<>0 Then Begin LogMessage(1,'E2gog '+IntToStr(ErrCode)+' sending to '+TCPSocket.GetRemoteSinIP+':'+IntToStr(TCPSocket.GetRemoteSinPort)+' '+Command);Exit;End;
     TCPSocket.SendString(AnsiString(Command+#10));
     ErrCode:=TCPSocket.LastError;
     If ErrCode<>0 Then Begin LogMessage(1,'E2gog '+IntToStr(ErrCode)+' sending to '+TCPSocket.GetRemoteSinIP+':'+IntToStr(TCPSocket.GetRemoteSinPort)+' '+Command);Exit;End;
   End;
   If LowerCase(Connections[Index])='udp' Then Begin
     LogMessage(4,'Sending to '+WorkerName+' via UDP '+Command);
     //LogMessage(1,'Not implemented Sending to '+WorkerName+' via UDP '+Command);
     UDPSocket:=SendUDPSocket;
     SendUDPSocket.ConvertLineEnd:=True;
     B:=ConnectionAddrs[Index];
     IP:=GetNextToken(B,Port,':');
     SendUDPSocket.Connect(IP,Port);
     ErrCode:=UDPSocket.LastError;
     If ErrCode<>0 Then Begin LogMessage(1,'E2gog '+IntToStr(ErrCode)+' sending to '+UDPSocket.GetRemoteSinIP+':'+IntToStr(UDPSocket.GetRemoteSinPort)+' '+Command);Exit;End;
     SendUDPSocket.SendString(AnsiString(Command+#10));
     ErrCode:=UDPSocket.LastError;
     If ErrCode<>0 Then Begin LogMessage(1,'E2gog '+IntToStr(ErrCode)+' sending to '+UDPSocket.GetRemoteSinIP+':'+IntToStr(UDPSocket.GetRemoteSinPort)+' '+Command);Exit;End;
   end;
   If LowerCase(Connections[Index])='via' Then Begin
     Forwarder:=ConnectionAddrs[Index];
     SendDataTo(Forwarder,Command);//рекурсивное прохождение таблицы via
     {или надо разбираться, если я worker - тупо посылаю диспетчеру,
      если я диспетчер - хожу по таблице, пока не найду прямой адресации вместо via и пересылаю
     }
   end;

end;

Function ConnectGUI(GUIName:String):String;
Begin
  If RemoteGUIs.IndexOf(GUIName)<0 Then Begin
    RemoteGUIs.Add(GUIName);
    Result:='Ok';
  end Else Result:='Error: remote GUI '+GUIName+' was already on the list.';
End;

Function DisconnectGUI(GUIName:String):String;
Begin
  If RemoteGUIs.IndexOf(GUIName)>=0 Then Begin
    RemoteGUIs.Delete(RemoteGUIs.IndexOf(GUIName));
    Result:='Ok'
  end Else Result:='Error: remote GUI '+GUIName+' was not on the list.';

End;

Function ReceiveGUIUpdate(Name,Data:String):String;
Begin
  {$ifdef GUI}
  // console version shouldn't receive information for displaying GUI
  Case LowerCase(Name) of
  'workers': RemoteWorkers.CommaText:=Data;
  'roles':   RemoteRoles.CommaText:=Data;
  'connections': RemoteConnections.CommaText:=Data;
  'connectionaddrs': RemoteConnectionAddrs.CommaText:=Data;
  'queueopens': RemoteQueueOpens.CommaText:=Data;
  'queueruns':  RemoteQueueRuns.CommaText:=Data;
  'queuelengths': RemoteQueueLengths.CommaText:=Data;
  'avgspcs': RemoteAvgSPCs.CommaText:=Data;
  'etes': RemoteETEs.CommaText:=Data;
  'queue': RemoteCommandQueue.CommaText:=Data;
  'historyc': RemoteHistoryC.CommaText:=Data;
  'historyt': RemoteHistoryT.CommaText:=Data;
  'historyr': RemoteHistoryR.CommaText:=Data;
  'other':    RemoteOther.CommaText:=Data;
  end;
  {$endif}
End;


Function ExecCommand(Command:String):String;
{ this procedure starts parsing of command, process interpreter internal command and routes
 external commands to the main module.}
var B,PB,Sender,Worker,ServiceField,Obj,Cmd,Name,Role,Conn,ConnAddr,Attr,Key,AddrType,Addr,Value,Path,InputFile,OutputFile,FileName:String;
    CursorName,SourceAddr,CtxName1,ConceptID1,CtxName2,ConceptID2,RelIntName,Aspect1,Aspect2,AttrName,Addr1,Addr2:String;
    IntA,IntB,Delta,Portions,Low,LockName:String;

    Done:Boolean;
    DBName,CTXName,Module,Depth,CacheSize,CachePolicy,NModule,NDepth,NCacheSize,NCachePolicy,ConceptID:String;
    ID,Status,ReturnResult:String;
    CommandList:TStringList;
    TCPSocket:TTCPBlockSocket;
    I,Index,Index2:Longint;  Pattern:String;
    ParsedServiceField:TStringList;
Begin
  { command is either alias in form "<alias>:parameters" converted to "<full command>+' '+Parameters"
   or <worker>:<2nd field>:<cmd>
    process aliasing
    process internal dispatcher unit commands
    process application command
    forward unexecuted commands
  }

  // Replacing Aliases with actual commands
  If (Pos('#',Command)=1)Or(Pos(';',Command)=1)Or(Command='') Then Exit;
  If Aliases.Count>0 Then Begin
    Pattern:=Copy(Command,1,Pos(':',Command)-1);
    Index:=Aliases.IndexOfName(Pattern);
    LogMessage(4,'Using alias index '+IntToStr(Index));
    If (Index<>-1) Then Begin
      Command:=Aliases.ValueFromIndex[Index]+' '+Copy(Command,Pos(':',Command)+1,Length(Command)-Pos(':',Command));
    end;
  End;
  Worker:=GetNextToken(Command,PB,':');
  LogMessage(4,'running '+Command);
  Done:=False;
  If (LowerCase(Worker)='self')Or(LowerCase(Worker)='local')Or(LowerCase(Worker)=lowercase(ConfigFile.Values['MyName'])) Then Begin
    B:=PB;
    ServiceField:=GetNextToken(B,PB,':');
    ParsedServiceField:=TStringList.Create;
    ParsedServiceField.Delimiter:=',';
    ParsedServiceField.DelimitedText:=ServiceField;
    Sender:=ParsedServiceField.Values['Sender'];
    ID:=ParsedServiceField.Values['ID'];
    ParsedServiceField.Free;
    B:=PB;
    Cmd:=GetNextToken(B,PB,':');
    Attr:=PB;
    If LowerCase(Command)='clearconfig' Then Begin ConfigFile.Clear;Done:=True;End;
    If LowerCase(Command)='loadconfig' Then Begin
      If Attr='' Then Begin
        Config.LoadConfigFile;
        Result:='default config loaded';
      End Else Begin
        Config.LoadConfigPart(Attr);
        Result:=Attr+' config loaded';
      End;
      Done:=True;
    End;
    If LowerCase(Cmd)='run' Then Begin
//    ('self::Run');
      DoMultipleSteps:=True;
      Run:=True;
      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
      QueueRuns[Index]:='Run';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='stop' Then Begin
//    ('self::Stop');
      DoMultipleSteps:=False;
      Run:=False;
      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
      QueueRuns[Index]:='Stop';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='step' Then Begin
//    ('self::Step');
      DoMultipleSteps:=False;
      Run:=True;
      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
      QueueRuns[Index]:='Stop';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='open' Then Begin
//    ('self::Open');
      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
      QueueOpens[Index]:='Open';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='close' Then Begin
//    ('self::Close');
      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
      QueueOpens[Index]:='Closed';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='drop' Then Begin
//    ('self::Drop');
      //ToDo: implement queue drop
//      Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
//      QueueOpens[Index]:='Open';
      SendStat;
      Done:=True;
    End;
    If LowerCase(Cmd)='quit' Then Begin
//    ('self::Quit');
      // ToDo: implement quit after queue completion
      Quitting:=True;
      Done:=True;
    End;

    If (LowerCase(Cmd)='shell') Then Begin
     //parsing of string with replacement of SDB objects field names to their values should be added here
//      Result:=ExecShellCmd(PB);
      Result:=ExecShellCmd('bash -c "'+PB+'"');
      Done:=True;
    End;
    If (LowerCase(Cmd)='system') Then Begin
     //parsing of string with replacement of SDB objects field names to their values should be added here
      Result:=IntToStr(RunWinCommand(PB));
      Done:=True;
    End;
    If (LowerCase(Cmd)='system_o') Then Begin
      Result:=ExecShellCmd(PB);
      Done:=True;
    End;

    If LowerCase(Cmd)='queuecommand' Then Begin
//    ('self::QueueCommand:<command>');
      QueueCommand(Attr);
      Done:=True;
    End;
    If LowerCase(Cmd)='announce worker' Then Begin
//    ('self::Announce Worker:<name>:<role>:<connection>:<connection addr>|<comma separated command list>');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      Role:=GetNextToken(B,PB,':');
      B:=PB;
      Conn:=GetNextToken(B,PB,':');
      B:=PB;
      ConnAddr:=GetNextToken(B,PB,'|');
      CommandList:=TStringList.Create;
      CommandList.CommaText:=PB;
      RegisterExternalWorker(Name,Role,Conn,ConnAddr,CommandList);
      WorkersChanged:=True;
      CommandList.Free;
      Done:=True;
    End;
    If LowerCase(Cmd)='denounce worker' Then Begin
//    ('self::Denounce Worker:<name>');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      UnRegisterExternalWorker(Name);
      WorkersChanged:=True;
      Done:=True;
    End;
    If LowerCase(Cmd)='connect gui' Then Begin
//    ('self::Connect GUI:<GUI name>');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      ConnectGUI(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='disconnect gui' Then Begin
//    ('self::Disconnect GUI:<GUI name>');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      DisconnectGUI(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='update gui' Then Begin
//    ('self::Update GUI:data name:<comma separated data>');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      ReceiveGUIUpdate(Name,B);
      Done:=True;
    End;
    If LowerCase(Cmd)='command status' Then Begin
//    ('self::Command Status:<CommandID>:<Status>:<Result>');
     // Name:=GetNextToken(Attr,PB,':');
     // B:=PB;
      ID:=GetNextToken(Attr,PB,':');
      B:=PB;
      Status:=GetNextToken(B,PB,':');
      ReturnResult:=PB;
      CommandStatus(ID,Status,ReturnResult);
      ID:='';
      Result:=ReturnResult;
      Done:=True;
    End;

    If LowerCase(Cmd)='stat' Then Begin
//    ('self::Stat:<sender>:<stat list>');
     // Name:=GetNextToken(Attr,PB,':');
     // B:=PB;
      Worker:=GetNextToken(Attr,PB,':');
      CommandList:=TStringList.Create;
      CommandList.CommaText:=PB;
      ProcessStat(Worker,CommandList);
      CommandList.Free;
      ID:='';
      Result:=ReturnResult;
      Done:=True;
    End;

    If LowerCase(Cmd)='create waiter' Then Begin
    //    ('self::Create Waiter:<Name>:<Command>');
      Name:=GetNextToken(Attr,PB,':');
      Path:=PB;
      Result:=CreateWaiter(Name,Path);
      Done:=True;
    End;

    If LowerCase(Cmd)='create lock' Then Begin
    //    ('self::Create Lock:<Name>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=CreateLock(Name);
      Done:=True;
    End;

    If LowerCase(Cmd)='lock waiter' Then Begin
    //    ('self::Lock Waiter:<WaiterName>:<LockName>');
      Name:=GetNextToken(Attr,PB,':');
      Path:=PB;
      Result:=LockWaiter(Name,Path);
      Done:=True;
    End;
    If LowerCase(Cmd)='unlock' Then Begin
    //    ('self::UnLock:<LockName>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=Unlock(Name);
      Done:=True;
    End;

    If LowerCase(Cmd)='test_integral' Then Begin
    //    ('self::test_integral:A:B:delta:PortionN');
      intA:=GetNextToken(Attr,PB,':');
      B:=PB;
      intB:=GetNextToken(B,PB,':');
      B:=PB;
      Delta:=GetNextToken(B,PB,':');
      Path:=PB;
      Result:=Test_Integral(intA,intB,Delta,Path);
      Done:=True;
    End;

    If LowerCase(Cmd)='meta_integral' Then Begin
    //    ('self::meta_integral:A:B:delta:portions');
      intA:=GetNextToken(Attr,PB,':');
      B:=PB;
      intB:=GetNextToken(B,PB,':');
      B:=PB;
      Delta:=GetNextToken(B,PB,':');
      Portions:=PB;
      Result:=Meta_Integral(intA,intB,Delta,Portions);
      Done:=True;
    End;

    If LowerCase(Cmd)='reducesum' Then Begin
    //    ('self::reducesum:WaiterName:LockName:value');
      Name:=GetNextToken(Attr,PB,':');
      B:=PB;
      LockName:=GetNextToken(B,PB,':');
      Path:=PB;
      Result:=ReduceSum(Name,LockName,Path);
      Done:=True;
    End;

    If LowerCase(Cmd)='sdb_createdb' Then Begin
//    ('self::sDB_CreateDB:<Name>:<Path>');
      Name:=GetNextToken(Attr,PB,':');
      Path:=PB;
      Result:=sDB_CreateDB(Name,Path);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_opendb' Then Begin
//    ('self::sDB_OpenDB:<Path>');
      Path:=Attr;//GetNextToken(Attr,PB,':');
      Result:=sDB_OpenDB(Path);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_closedb' Then Begin
//    ('self::sDB_CloseDB:<Name>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=sDB_CloseDB(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_deletedb' Then Begin
//    ('self::sDB_DeleteDB:<Path>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=sDB_DeleteDB(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_createcursor' Then Begin
//    ('self::sDB_CreateCursor:<Name>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=sDB_CreateCursor(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_deletecursor' Then Begin
//    ('self::sDB_DeleteCursor:<Name>');
      Name:=GetNextToken(Attr,PB,':');
      Result:=sDB_DeleteCursor(Name);
      Done:=True;
    End;
    If LowerCase(Cmd)='sdb_setcursor' Then Begin
//    ('self::sDB_SetCursor:<CursorName>:<DBName>:<Addr>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      DBName:=GetNextToken(B,PB,':');
      B:=PB;
      Result:=sDB_SetCursor(CursorName,DBName,B);
      Done:=True;
    End;

    If LowerCase(Cmd)='sdb_createctx' Then Begin
//    ('self::sDB_CreateCtx:<DBName>:<CTXName>:<Module>:<Depth>:<CachePolicy>');
      DBName:=GetNextToken(Attr,PB,':');
      B:=PB;
      CTXName:=GetNextToken(B,PB,':');
      B:=PB;
      Module:=GetNextToken(B,PB,':');
      B:=PB;
      Depth:=GetNextToken(B,PB,':');
      B:=PB;
      CacheSize:=GetNextToken(B,PB,':');
      B:=PB;
      CachePolicy:=GetNextToken(B,PB,':');
      B:=PB;
      NModule:=GetNextToken(B,PB,':');
      B:=PB;
      NDepth:=GetNextToken(B,PB,':');
      B:=PB;
      NCacheSize:=GetNextToken(B,PB,':');
      NCachePolicy:=PB;
      Result:=sDB_CreateCtx(DBName,CtxName,Module,Depth,CacheSize,CachePolicy,NModule,NDepth,NCacheSize,NCachePolicy);
      Done:=True;
    end;
    If LowerCase(Cmd)='sdb_deletectx' Then Begin
//    ('self::sDB_DeleteCtx:<DBName>:<CTXName>);
      DBName:=GetNextToken(Attr,PB,':');
      CTXName:=PB;
      Result:=sDB_DeleteCtx(DBName,CtxName);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_fqwritename' Then Begin
//    ('self::sDB_FQWriteName:<DBName>:<CtxName>:<Name>:<ConcepID>');
      DBName:=GetNextToken(Attr,PB,':');
      B:=PB;
      CTXName:=GetNextToken(B,PB,':');
      B:=PB;
      Name:=GetNextToken(B,PB,':');
      B:=PB;
      Result:=sDB_FQWriteName(DBName,CtxName,Name,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crcreateconcept' Then Begin
//    ('self::sDB_CRCreateConcept:<Cursorame>:<InternalName>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CRCreateConcept(CursorName,B);
      Done:=True;
    end;
    If LowerCase(Cmd)='sdb_fqflushcacheobject' Then Begin
//    ('self::sDB_FQFlushCacheObject:<DBName>:<CTX>:<ConceptID>);
      DBName:=GetNextToken(Attr,PB,':');
      B:=PB;
      CTXName:=GetNextToken(B,PB,':');
      B:=PB;
      ConceptID:=B;
      Result:=sDB_FQFlushCacheObject(DBName,CtxName,ConceptID);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_fqdeleteconcept' Then Begin
//    ('self::oDB_FQDeleteConcept:<DBName>:<CTXName>:<ConceptID>');
      DBName:=GetNextToken(Attr,PB,':');
      B:=PB;
      CTXName:=GetNextToken(B,PB,':');
      ConceptID:=PB;
      Result:=sDB_FQDeleteConcept(DBName,CtxName,ConceptID);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cafindconcept' Then Begin
//    ('self::sDB_CAFindConcept:<CursorName>:<Addr>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CAFindConcept(CursorName,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cacreateslink' Then Begin
//    ('self::sDB_CACreateSLink:<CursorName>:<SourceAddr>:<Key>:<AddrType>:<Addr>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      SourceAddr:=GetNextToken(B,PB,':');
      B:=PB;
      Key:=GetNextToken(B,PB,':');
      B:=PB;
      AddrType:=GetNextToken(B,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      Result:=sDB_CACreateSLink(CursorName,SourceAddr,Key,AddrType,Addr);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cacreatealink' Then Begin
//    ('self::sDB_CACreateALink:<CursorName>:<SourceAddr>:<Key>:<AddrType>:<Addr>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      SourceAddr:=GetNextToken(B,PB,':');
      B:=PB;
      Key:=GetNextToken(B,PB,':');
      B:=PB;
      AddrType:=GetNextToken(B,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      Result:=sDB_CACreateALink(CursorName,SourceAddr,Key,AddrType,Addr);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cacreatebinrel' Then Begin
//    ('self::sDB_CACreateBinRel:<CursorName>:<Addr1>:<Addr2>:<RelIntName>:<Aspect1>:<Aspect2>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr1:=GetNextToken(B,PB,':');
      B:=PB;
      Addr2:=GetNextToken(B,PB,':');
      B:=PB;
      RelIntName:=GetNextToken(B,PB,':');
      B:=PB;
      Aspect1:=GetNextToken(B,PB,':');
      B:=PB;
      Aspect2:=GetNextToken(B,PB,':');
      Result:=sDB_CACreateBinRel(CursorName,Addr1,Addr2,RelIntName,Aspect1,Aspect2);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crcreatebinrel' Then Begin
//    ('self::sDB_FQCreateBinRel:<CursorName1>:<CursorName2>:<RelIntName>:<Aspect1>:<Aspect2>');
      Addr1:=GetNextToken(B,PB,':');
      B:=PB;
      Addr2:=GetNextToken(B,PB,':');
      B:=PB;
      RelIntName:=GetNextToken(B,PB,':');
      B:=PB;
      Aspect1:=GetNextToken(B,PB,':');
      B:=PB;
      Aspect2:=GetNextToken(B,PB,':');
      Result:=sDB_CRCreateBinRel(Addr1,Addr2,RelIntName,Aspect1,Aspect2);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crsearchbyname' Then Begin
//    ('self::sDB_CRSearchByName:<CursorName>:<Name>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CRSearchByName(CursorName,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crsearchbychain' Then Begin
//    ('self::sDB_CRSearchByChain:<CursorName>:<Chain>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CRSearchByChain(CursorName,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crfilteroutbychain' Then Begin
//    ('self::sDB_CRFilterOutByChain:<CursorName>:<Chain>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CRFilterOutByChain(CursorName,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_crfilterkeepbychain' Then Begin
//    ('self::sDB_CRFilterKeepByChain:<CursorName>:<Chain>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Result:=sDB_CRFilterKeepByChain(CursorName,B);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_casetattrvalue' Then Begin
//    ('self::sDB_CASetAttrValue:<CursorName>:<ConceptAddr>:<AttrName>:<Value>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      B:=PB;
      AttrName:=GetNextToken(B,PB,':');
      B:=PB;
      Result:=sDB_CASetAttrValue(CursorName,Addr,AttrName,B);
      Done:=True;
    End;

    If LowerCase(Cmd)='sdb_cawritevalueplus' Then Begin
//    ('self::sDB_CAWriteValuePlus:<CursorName>:<Addr>:<AttrName>:<Value>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      B:=PB;
      AttrName:=GetNextToken(B,PB,':');
      B:=PB;
      Result:=sDB_CAWriteValuePlus(CursorName,Addr,AttrName,B);
      Done:=True;
    End;

    If LowerCase(Cmd)='sdb_crwritevalueplus' Then Begin
//    ('self::sDB_CRWriteValuePlus:<CursorName>:<AttrName>:<Value>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      AttrName:=GetNextToken(B,PB,':');
      B:=PB;
      Result:=sDB_CRWriteValuePlus(CursorName,AttrName,B);
      Done:=True;
    End;

    If LowerCase(Cmd)='sdb_caimportobject' Then Begin
//    ('self::sDB_CAImportObject:<CursorName>:<Addr>:<FileName>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      FileName:=PB;
      Result:=sDB_CAImportObject(CursorName,Addr,FileName);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_caaddtogroup' Then Begin
//    ('self::sDB_CAAddToGroup:<CursorName>:<GroupAddr>');
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      Result:=sDB_CAAddToGroup(CursorName,Addr);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cractivatemethod' Then Begin
//    ('self::sDB_CRActivateMethod:<CursorName>:<MethodName>);
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      Result:=sDB_CRActivateMethod(CursorName,Addr);
      Done:=True;
    end;

    If LowerCase(Cmd)='sdb_cractivateprototypemethod' Then Begin
//    ('self::sDB_CRActivatePrototypeMethod:<CursorName>:<MethodName>);
      CursorName:=GetNextToken(Attr,PB,':');
      B:=PB;
      Addr:=GetNextToken(B,PB,':');
      Result:=sDB_CRActivatePrototypeMethod(CursorName,Addr);
      Done:=True;
    end;

{    If LowerCase(Cmd)='odb_flushcachedobject' Then Begin
//    ('self::oDB_FlushCachedObject:<DBName>:<CTXName>:<ObjectID>');
      DBName:=GetNextToken(Attr,PB,':');
      B:=PB;
      CTXName:=GetNextToken(B,PB,':');
      ObjectID:=PB;
//      Result:=oDB_FlushCachedObject(DBName,CtxName,ObjectID);
      Done:=True;
    end;}

    // user workload should be included below
    // if command not already done by Dispatcher2 unit - send it to the main app
    If Not(Done) Then Result:=Unit1.ExecCommand(Obj+':'+Cmd+':'+PB);
    If Result<>'' Then LogMessage(4,'('+Command+')='+Result);
    If ID<>'' Then ExecCommand(Sender+'::Command Status:'+ID+':Ok:'+Result);
    //update GUI state
    {
    DispatcherForm.sDBListBoxClick(Nil);
    DispatcherForm.CTXListBoxDblClick(Nil);
    DispatcherForm.CursorStringGridDblClick(Nil);
    DispatcherForm.CTXCacheListBoxDblClick(Nil);
    DispatcherForm.CTXCacheListBoxSelectionChange(Nil,False);
    DispatcherForm.CursorStringGridSelection(Nil,0,DispatcherForm.CursorStringGrid.Selection.Top);}
    Exit;
  End;

  // else - if command not specified to be run on this instance
  If (LowerCase(ConfigFile.Values['Role'])='worker') Then Begin
    // I'm worker - routing commands to my dispatcher
    // need to add if command could be sent directly and flag "Enable direct talk"

    SendDataTo(ConfigFile.Values['Dispatcher Name'],Command);
    Exit;
  End;
  If (LowerCase(ConfigFile.Values['Role'])='dispatcher')And(ConfigFile.Values['Dispatcher name']=Worker) Then Begin
    // I'm secondary dispatcher - and doing registration in more root dispatcher
    SendDataTo(ConfigFile.Values['Dispatcher Name'],Command);
    Exit;
  End;
    // I'm dispatcher - forwarding and parallelizing commands
    If Worker='*' Then Begin
      //Scheduling / load balancing
      Case LowerCase(ConfigFile.Values['Balancer']) of
      'roundrobin': Begin  //Round-robin
            RoundRobin:=(RoundRobin+1) Mod Workers.Count;
            // Skip dispatchers and viewers while there are any workers exists
            While ((Roles[RoundRobin]='dispatcher')Or(Roles[RoundRobin]='viewer'))And(Roles.IndexOf('worker')>-1) Do Begin
              RoundRobin:=(RoundRobin+1) Mod Workers.Count;
            end;
            // Skip Closed and suspended workers. WARNING! dispatcher will hang if all workers are closed or stopped
            While (QueueOpens[RoundRobin]<>'Open') Or (QueueRuns[RoundRobin]<>'Run') Do
            RoundRobin:=(RoundRobin+1) Mod Workers.Count;
            Worker:=Workers[RoundRobin];
      End;
      'ete': Begin  // Lowest ETE
            Index:=0;
            Low:=ETEs[0];
            If Workers.Count>1 Then
              For I:=0 To ETEs.Count-1 Do Begin
                If (((Roles[I]='dispatcher')And(Roles.IndexOf('worker')<0))Or((Roles[I]='worker')))
                    And (QueueOpens[I]='Open') And (QueueRuns[I]='Run') Then
                  If (ETEs[I]<Low) Then Begin
                    Index:=I;Low:=ETEs[I];
                end;
              end;
            Worker:=Workers[Index];
      End;
      'ql': Begin  // Lowest Queue Length
              Index:=0;
              Low:=ETEs[0];
              If Workers.Count>1 Then
                For I:=0 To ETEs.Count-1 Do Begin
                  If (((Roles[I]='dispatcher')And(Roles.IndexOf('worker')<0))Or((Roles[I]='worker')))
                      And (QueueOpens[I]='Open') And (QueueRuns[I]='Run') Then
                    If (ETEs[I]<Low) Then Begin
                      Index:=I;Low:=ETEs[I];
                    end;
                end;
              Worker:=Workers[Index];
      End
      End;
      Delete(Command,1,1);
      Insert(Worker,Command,0);
    End;
    If Worker=ConfigFile.Values['MyName'] Then // There are no any worker - running it myself
    ExecCommand(Command) Else
    SendDataTo(Worker,Command);
End;

Procedure SendStat;
Var K,N:Longint;
    StatList:TStringList;
Begin //update perf stat
      PC:=0;
      N:=Workers.IndexOf(ConfigFile.Values['MyName']);
      If N>=0 Then Begin
        If LowerCase(Roles[N])='dispatcher' Then Begin
          // dispatcher sends stat of it's workers to every other dispatcher (if they are not closed or stopped)
          StatList:=TStringList.Create;
          For K:=0 To Workers.Count-1 Do
            If (Roles[K]='worker') And (Connections[K]='via') Then
              StatList.Add(Workers[K]+':'+QueueOpens[K]+':'+QueueRuns[K]+':'+QueueLengths[K]+':'+AvgSPCs[K]+':'+ETEs[K]);
          For K:=0 To Workers.Count-1 Do
            If (LowerCase(Roles[K])='dispatcher') And (Workers[K]<>ConfigFile.Values['MyName']) And
               (LowerCase(QueueOpens[K])='open') And (LowerCase(QueueRuns[K])='run') Then
                 ExecCommand(Workers[K]+'::Stat:'+StatList.CommaText);
          // dispatcher sends stat of all workers to every it's worker (if they are not closed or stopped)
          StatList.Clear;
          For K:=0 To Workers.Count-1 Do
            StatList.Add(Workers[K]+':'+QueueOpens[K]+':'+QueueRuns[K]+':'+QueueLengths[K]+':'+AvgSPCs[K]+':'+ETEs[K]);
          For K:=0 To Workers.Count-1 Do
            If (LowerCase(Roles[K])='worker') And (Connections[K]<>'via') And
               (LowerCase(QueueOpens[K])='open') And (LowerCase(QueueRuns[K])='run') Then
              ExecCommand(Workers[K]+'::Stat:'+StatList.CommaText);
          StatList.Free;
        End Else Begin
          // worker sends stat only to it's direct dispatcher
          If N>-1 Then Begin
            ExecCommand(ConfigFile.Values['Dispatcher Name']+'::Stat:'+ConfigFile.Values['MyName']+
            ':'+ConfigFile.Values['MyName']+':'+QueueOpens[N]+':'+QueueRuns[N]+':'+QueueLengths[N]+':'+AvgSPCs[N]+':'+ETEs[N]);
          end;
        End;
      End;
End;

Procedure ProcessRTCommand;
Var DirtyTime,PureTime,UsedTime:TDateTime; H,M,S,ms:Word;
    I,J,K,N,Index:Longint;
    CurrentCommand,ComResult:String;StatList:TStringList;
Begin
  If RTQueue.Count>0 Then Begin
    CurrentCommand:=RTQueue[0];
    RTQueue.Delete(0);
    ComResult:=ExecCommand(CurrentCommand);
    CurrentCommand:='';
  end;
End;

Procedure ProcessCommand;
Var DirtyTime,PureTime,UsedTime:TDateTime; H,M,S,ms:Word;
    I,J,Index:Longint;
    CurrentCommand,ComResult:String;
Begin
  If Run And (CommandQueue.Count>0) Then
  Begin
    CurrentCommand:=CommandQueue[0];
    CommandQueue.Delete(0);
    HistoryIndex:=(HistoryIndex+1) Mod HistoryC.Count; //вместо копирования вверх всей таблицы - кольцевой буфер
    HistoryC[HistoryIndex]:=CurrentCommand;
    HistoryR[HistoryIndex]:='';
    HistoryT[HistoryIndex]:='';
    T1:=Now;
    ComResult:=ExecCommand(CurrentCommand);
    T3:=T2;
    T2:=Now;
    PureTime:=T2-T1;
    If QWasEmpty Then DirtyTime:=PureTime Else DirtyTime:=T2-T3;
    QWasEmpty:=False;
    If PureTimeButtonDown Then UsedTime:=PureTime Else UsedTime:=DirtyTime;
    DecodeTime(UsedTime,H,M,S,ms);
    HistoryR[HistoryIndex]:=ComResult;
    HistoryT[HistoryIndex]:=IntToStr(H)+':'+IntToStr(M)+':'+IntToStr(S)+'.'+IntToStr(ms);
  End;
    Index:=Workers.IndexOf(ConfigFile.Values['MyName']);
    If Index>-1 Then Begin
      QueueLengths[Index]:=IntToStr(CommandQueue.Count);
      // Computing average msecs per command
      If (CurrentCommand<>'') Then Begin
        If AvgCompList.Count<AvgCompCount Then AvgCompList.Add(IntToStr(Round(mSecsPerDay*UsedTime)))
        Else Begin
          AvgCompList[AvgCompPointer]:=IntToStr(Round(mSecsPerDay*UsedTime));
          AvgCompPointer:=(AvgCompPointer+1) Mod AvgCompCount;
        End;
        AvgSum:=0;
        For J:=0 To AvgCompList.Count-1 Do
          AvgSum:=AvgSum+StrToFloat(AvgCompList[J]);
        AvgSPCs[Index]:=IntToStr(Round(AvgSum/AvgCompList.Count));
        ETEs[Index]:=FloatToStr(Round(CommandQueue.Count*AvgSum/AvgCompList.Count/1000));
      End;
    end;
    PC:=(PC+1) Mod StrToInt(ConfigFile.Values['StatEvery']);
    // need to update stat if update period > StatInterval
    // or every StatEvery commands
{    If (mSecsPerDay*(Now-T3)>StrToFloat(ConfigFile.Values['StatInterval'])) Then Begin QWasEmpty:=True;PC:=0;T3:=Now;End;
    If (PC=0)Or((mSecsPerDay*(UsedTime))>=StrToFloat(ConfigFile.Values['StatInterval'])) Then
      SendStat;}
    If Not DoMultipleSteps Then Run:=False;
End;




{ ToDo: переделать в не гуи
procedure TDispatcherForm.FormClose(Sender: TObject;  var CloseAction: TCloseAction);
Var I:Longint;Socket:TTCPBlockSocket;
begin
  //Send "I'm leaving" notification if I'm worker
  If LowerCase(ConfigFile.Values['Role'])='worker' Then Begin
    SendDataTo(ConfigFile.Values['Dispatcher Name'],ConfigFile.Values['Dispatcher Name']+'::Denounce worker:'+ConfigFile.Values['MyName']);
    SendDataTo(ConfigFile.Values['Dispatcher Name'],'disconnect');
  End;
  For I:=0 To Workers.Count-1 Do Begin
    If LowerCase(Connections[I])='tcp' Then Begin
      Socket:=Connections.Objects[I] As TTCPBlockSocket;
      Socket.Free;
    end;
  End;
end;
}



{ ToDo: переделать в не гуи
procedure TDispatcherForm.HistoryLengthEditChange(Sender: TObject);
begin
  HistoryStringGrid.RowCount:=HistoryLengthEdit.Value+1;
end;
}

Procedure GracefulClose;
Var I:Longint;Socket:TTCPBlockSocket;
begin
  //Send "I'm leaving" notification if I'm worker
  If LowerCase(ConfigFile.Values['Role'])='worker' Then Begin
    Dispatcher.SendDataTo(ConfigFile.Values['Dispatcher Name'],ConfigFile.Values['Dispatcher Name']+'::Denounce Worker:'+ConfigFile.Values['MyName']);
    Dispatcher.SendDataTo(ConfigFile.Values['Dispatcher Name'],'disconnect');
  End;
  For I:=0 To Workers.Count-1 Do Begin
    If LowerCase(Connections[I])='tcp' Then Begin
      Socket:=Connections.Objects[I] As TTCPBlockSocket;
      Socket.Free;
    end;
  End;
end;

Initialization
  RTQueue:=TStringList.Create;
  CommandQueue:=TStringList.Create;
  HistoryLength:=10;
  HistoryC:=TStringList.Create;
  HistoryT:=TStringList.Create;
  HistoryR:=TStringList.Create;
  OtherGUI:=TStringList.Create;

  For I:=0 To HistoryLength-1 Do Begin
    HistoryC.Add('');
    HistoryT.Add('');
    HistoryR.Add('');
  End;

  DoMultipleSteps:=True;
  Run:=True;AlreadyRun:=False;
  Quitting:=False;
  RemoteGUIs:=TStringList.Create;
  TurboButtonDown:=False;
  QueueOpenButtonDown:=True;
  PureTimeButtonDown:=False;
  WorkersChanged:=False;
  Workers:=TStringList.Create;
  Roles:=TStringList.Create;
  Connections:=TStringList.Create;
  ConnectionAddrs:=TStringList.Create;
  QueueOpens:=TStringList.Create;
  QueueRuns:=TStringList.Create;
  QueueLengths:=TStringList.Create;
  AvgSPCs:=TStringList.Create;
  ETEs:=TStringList.Create;
  Aliases:=TStringList.Create;
  StatusList:=TStringList.Create;
  ResultList:=TStringList.Create;
  Waiters:=TStringList.Create;
  WaiterValues:=TStringList.Create;
  Locks:=TStringList.Create;
  Log:=TStringList.Create;
  IDNumerator:=0;
  SendUDPSocket:=TUDPBlockSocket.Create;
  SendUDPSocket.CreateSocket;
  SendUDPSocket.EnableReuse(True);
  AvgCompCount:=30;
  AvgCompPointer:=0;
  AvgCompList:=TStringList.Create;
  T1:=Now;T2:=T1;T3:=T1;

  Aliases.Clear;
  LogMessage(0,'Loading default config');
  Config.LoadConfigFile;
  //Verify all needed parameters are configured, otherwise append config with default value and write it.
  If ConfigFile.IndexOfName('MyName')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "MyName=". Consider "MyName=*" ');
    Exit;
  End;
  If ConfigFile.IndexOfName('Role')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "Role=". Consider "workser" or "dispatcher"');
    Exit;
  End;
  If ConfigFile.IndexOfName('PacketTimeOut')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "PacketTimeOut=". ');
    Exit;
  End;
  If ConfigFile.IndexOfName('Connection')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "Connection=". ');
    Exit;
  End;
  If ConfigFile.IndexOfName('Connection Addr')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "Connection Addr=". ');
    Exit;
  End;
  If ConfigFile.IndexOfName('AllowDirect')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "AllowDirect=". ');
    Exit;
  End;
  If ConfigFile.IndexOfName('StatEvery')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "StatEvery=". ');
    Exit;
  End;
  If ConfigFile.IndexOfName('StatInterval')<0 Then Begin
    LogMessage(0,'Config file error: missing mandatory parameter "StatInterval=". ');
    Exit;
  End;
  If LowerCase(ConfigFile.Values['Connection'])='tcp' Then Begin
    If ConfigFile.IndexOfName('TCPListen')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "TCPListen=". ');
      Exit;
    End;
  end;
  If LowerCase(ConfigFile.Values['Connection'])='udp' Then Begin
    If ConfigFile.IndexOfName('UDPListen')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "UDPListen=". ');
      Exit;
    End;
  end;
  If LowerCase(ConfigFile.Values['Role'])='worker' Then Begin
    If ConfigFile.IndexOfName('Dispatcher Name')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "Dispatcher Name=". ');
      Exit;
    End;
    If ConfigFile.IndexOfName('Dispatcher Connection')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "Dispatcher Connection=". ');
      Exit;
    End;
    If ConfigFile.IndexOfName('Dispatcher Addr')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "Dispatcher Addr=". ');
      Exit;
    End;
  end;
  If LowerCase(ConfigFile.Values['Role'])='dispatcher' Then Begin
    If ConfigFile.IndexOfName('Balancer')<0 Then Begin
      LogMessage(0,'Config file error: missing mandatory parameter "Balancer=". Consider "roundrobin", "ete" or "ql"');
      Exit;
    End;
  end;
 ICommands:=TStringList.Create;
 ICommands.Add('self::ClearConfig');
 ICommands.Add('self::LoadConfig:<FileName>');
 // moved from Pencil
 ICommands.Add('self::CAD_UpdateDraw');
 ICommands.Add('self::CAD_UpdateCoordBase');
 ICommands.Add('self::CAD_UpdateScale');
 ICommands.Add('self::CAD_PeekViewPosition');
 ICommands.Add('self::CAD_UpdateAll');
 ICommands.Add('self::CAD_Scene2GUI');
 ICommands.Add('self::CAD_GUI2Scene');
 ICommands.Add('self::CAD_LoadSceneFile:<FileName>');
 ICommands.Add('self::CAD_SaveSceneFile:<FileName>');
 ICommands.Add('self::CAD_ReloadSource:<callout>');
 ICommands.Add('self::CAD_Teleport:<location>');
 ICommands.Add('self::CAD_Hover:<Steps>:<location>');
 ICommands.Add('self::CAD_Fly:<Steps>:<location>');
 ICommands.Add('self::CAD_CreateObject:Name=<name> ...');
 ICommands.Add('self::CAD_CreateObjectPort:<ObjName>:<Style>');
 ICommands.Add('self::CAD_ArrangePortsByRect:<ObjName>:<Cols>:<Rows>');
 ICommands.Add('self::CAD_ArrangePortsByRectAll:<Cols>:<Rows>');
 ICommands.Add('self::CAD_ArrangePortsByLines:<ObjName>:<Ix>:<Iy>:<Ox>:<Oy>');
 ICommands.Add('self::CAD_CreateCallout:Name=<name> ...');
 ICommands.Add('self::CAD_CreateConnection:StartObj= StartPort= EndObj= EndPort=');
 ICommands.Add('self::CAD_CreateNPConnection:StartObj= EndObj=');
 ICommands.Add('self::CAD_SetSource:<Item Name>:Src');
 ICommands.Add('self::CAD_SetText:<Name>:Text');
 ICommands.Add('self::CAD_GetAttr:<Item Name>:<Attr name>');
 ICommands.Add('self::CAD_SetAttr:<Item Name>:<Attr name>:<Attr Value>');
 ICommands.Add('self::CAD_ModAttr:<Item Name>:<Attr name>:<Attr delta>');
 ICommands.Add('self::CAD_FixItem:<Item Name>');
 ICommands.Add('self::CAD_FixAll');
 ICommands.Add('self::CAD_Select:<Item Name>');
 ICommands.Add('self::CAD_UnSelect:<Item Name>');
 ICommands.Add('self::CAD_UnSelectAll');
 ICommands.Add('self::CAD_Copy');
 ICommands.Add('self::CAD_Paste');
 ICommands.Add('self::CAD_DeleteSelection');
 ICommands.Add('self::CAD_DeleteItem:<Item Name>');
 ICommands.Add('self::CAD_MoveTo:<x>:<y>');
 ICommands.Add('self::CAD_CreateGroup:<Group Name>');
 ICommands.Add('self::CAD_AddToGroup:<group>:<item>');
 ICommands.Add('self::CAD_DelFromGroup:<group>:<item>');

 ICommands.Add('self::sDB_CreateDB:<Name>:<Path>');
 ICommands.Add('self::sDB_OpenDB:<Path>');
 ICommands.Add('self::sDB_CloseDB:<Name>');
 ICommands.Add('self::sDB_DeleteDB:<Path>');
 ICommands.Add('self::sDB_CreateCursor:<Name>');
 ICommands.Add('self::sDB_SetCursor:<CursorName>:<DBName>:<Addr>');
 ICommands.Add('self::sDB_DeleteCursor:Name');
 ICommands.Add('self::sDB_FQCreateCtx:<DBName>:<CTXName>:<Module>:<Depth>:<CacheSize>:<CachePolicy>');
 ICommands.Add('self::sDB_CreateCtx:<CTXName>:<Module>:<Depth>:<CacheSize>:<CachePolicy>:<NModule>:<NDepth>:<NCacheSize>:<NCachePolicy>');
 ICommands.Add('self::sDB_FQDeleteCtx:<DBName>:<CTXName>');
 ICommands.Add('self::sDB_DeleteCtx:<CTXName>');
 ICommands.Add('self::sDB_FQWriteName:<DBName>:<CtxName>:<Name>:<ConceptID>');
 ICommands.Add('self::sDB_FQFlushCacheObject:<DBName>:<CTX>:<ConceptID>');
 ICommands.Add('self::sDB_FQCreateConcept:<DBName>:<CTXName>:<InternalName>');
 ICommands.Add('self::sDB_CRCreateConcept:<CursorName>:<InternalName>');
 ICommands.Add('self::sDB_FQDeleteConcept:<DBName>:<CTXName>:<ConceptID>');
 ICommands.Add('self::sDB_DeleteConcept:<ConceptID>');
 ICommands.Add('self::sDB_CRCreateSLink:<CursorName>:<SourceAddr>:<Key>:<AddrType>:<Addr>');
 ICommands.Add('self::sDB_CRCreateALink:<CursorName>:<SourceAddr>:<Key>:<AddrType>:<Addr>');
 ICommands.Add('self::sDB_FQCreateBinRel:<CursorName>:<Addr1>:<Addr2>:<RelIntName>:<Aspect1>:<Aspect2>');
 ICommands.Add('self::sDB_CRCreateBinRel:<CursorName1>:<CursorName2>:<RelIntName>:<Aspect1>:<Aspect2>');
 ICommands.Add('self::sDB_CAWriteValuePlus:<CursorName>:<Addr>:<AttrName>:<Value>');
 ICommands.Add('self::sDB_CRWriteValuePlus:<CursorName>:<Key>:<Value>');
 ICommands.Add('self::sDB_CRFindConcept:<CursorName>:<Addr>');
 ICommands.Add('self::sDB_CRSearchByName:<CursorName>:<Name>');
 ICommands.Add('self::sDB_CRSearchByChain:<CursorName>:<Chain>');
 ICommands.Add('self::sDB_CRFilterOutByChain:<CursorName>:<Chain>');
 ICommands.Add('self::sDB_CRFilterKeepByChain:<CursorName>:<Chain>');
 ICommands.Add('self::sDB_CASetAttrValue:<CursorName>:<Addr>:<AttrName>:<Value>');
 ICommands.Add('self::sDB_CRImportObject:<CursorName>:<Addr>:<FileName>');
 ICommands.Add('self::sDB_CRAddToGroup:<CursorName>:<GroupAddr>');
 ICommands.Add('self::sDB_CRActivateMethod:<CursorName>:<MethodName>');
 ICommands.Add('self::sDB_CRActivatePrototypeMethod:<CursorName>:<MethodName>');
 If Config.ConfigFile.Values['Aliases']<>'' Then Begin
   Try
     LogMessage(0,'Loading alisases from '+Config.ConfigFile.Values['Aliases']);
     Aliases.LoadFromFile(Config.ConfigFile.Values['Aliases']);
   Except
     LogMessage(0,'Error reading alisases from '+Config.ConfigFile.Values['Aliases']);
   end;
 end;
 If Aliases.Count>0 Then Begin
   For I:=0 To Aliases.Count-1 Do
     ICommands.Add(Aliases[I]);
 End;
 RegisterWorker(ICommands);
 ICommands.Free;


End.

