uprocessauto.pas 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // SPDX-License-Identifier: GPL-3.0-only
  2. unit UProcessAuto;
  3. {$mode objfpc}{$H+}
  4. { This unit allows to receive line by line the output of a process
  5. and to send lines to its input in response.
  6. Note:
  7. - the process will freeze if it expects an input that is not provided.
  8. - if the process draw only part of a line, like with a progress bar, this
  9. won't be received in the events.
  10. }
  11. interface
  12. uses
  13. Classes, SysUtils;
  14. type
  15. TReceiveLineEvent = procedure(ALine: RawByteString) of object;
  16. TSendLineMethod = procedure(const ALine: RawByteString) of object;
  17. TBusyEvent = procedure(var ASleep: boolean) of object;
  18. var
  19. AutomationEnvironment: TStringList;
  20. function RunProcessAutomation(AExecutable: string; AParameters: array of string;
  21. out ASendLine: TSendLineMethod;
  22. AOnReceiveOutput: TReceiveLineEvent;
  23. AOnReceiveError: TReceiveLineEvent;
  24. AOnBusy: TBusyEvent): integer;
  25. implementation
  26. uses process, Pipes, math;
  27. const
  28. LineEndingStr: string = LineEnding;
  29. type
  30. { TAutomatedProcess }
  31. TAutomatedProcess = class(TProcess)
  32. constructor Create(AOwner: TComponent); override;
  33. procedure SendLine(const ALine: RawByteString);
  34. end;
  35. function RunProcessAutomation(AExecutable: string; AParameters: array of string;
  36. out ASendLine: TSendLineMethod;
  37. AOnReceiveOutput: TReceiveLineEvent;
  38. AOnReceiveError: TReceiveLineEvent;
  39. AOnBusy: TBusyEvent): integer;
  40. type
  41. TReceiveBuffer = record
  42. Data: RawByteString;
  43. Length: integer;
  44. OnReceive: TReceiveLineEvent;
  45. end;
  46. procedure InitBuffer(out Buffer: TReceiveBuffer; ASize: integer; AOnReceive: TReceiveLineEvent);
  47. begin
  48. Buffer.Data := '';
  49. setlength(Buffer.Data, ASize);
  50. Buffer.Length:= 0;
  51. Buffer.OnReceive:= AOnReceive;
  52. end;
  53. procedure ParseBuffer(var Buffer: TReceiveBuffer);
  54. var
  55. startIdx,idx, count: integer;
  56. line: RawByteString;
  57. begin
  58. startIdx := 1;
  59. idx := startIdx;
  60. while idx <= Buffer.Length do
  61. begin
  62. //find LineEnding
  63. if (Buffer.Data[idx] = LineEndingStr[1]) and
  64. (idx+length(LineEndingStr)-1 <= Buffer.Length) and
  65. (copy(Buffer.Data,idx,length(LineEndingStr)) = LineEndingStr) then
  66. begin
  67. line := copy(Buffer.Data, startIdx, idx-startIdx);
  68. Buffer.OnReceive(line);
  69. inc(idx, length(LineEndingStr));
  70. startIdx := idx;
  71. continue;
  72. end;
  73. inc(idx);
  74. end;
  75. if startIdx > 1 then
  76. begin
  77. count := Buffer.Length-startIdx+1;
  78. if count > 0 then
  79. move(Buffer.Data[startIdx], Buffer.Data[1], Buffer.Length-startIdx+1);
  80. dec(Buffer.Length, startIdx-1);
  81. end;
  82. end;
  83. function Receive(AInput: TInputPipeStream; var Buffer: TReceiveBuffer): boolean;
  84. var
  85. receivedCount: integer;
  86. begin
  87. receivedCount := AInput.NumBytesAvailable;
  88. if receivedCount > 0 then
  89. begin
  90. if Buffer.Length+receivedCount > length(Buffer.Data) then
  91. setlength(Buffer.Data, max(length(Buffer.Data)*2, Buffer.Length+receivedCount));
  92. AInput.Read(Buffer.Data[Buffer.Length+1], receivedCount);
  93. inc(Buffer.Length, receivedCount);
  94. ParseBuffer(Buffer);
  95. result := true;
  96. end else
  97. result := false;
  98. end;
  99. var
  100. p: TAutomatedProcess;
  101. Output, Error: TReceiveBuffer;
  102. i: integer;
  103. shouldSleep: Boolean;
  104. begin
  105. result := 0;
  106. p := TAutomatedProcess.Create(nil);
  107. ASendLine := @p.SendLine;
  108. try
  109. for i := 1 to GetEnvironmentVariableCount do
  110. p.Environment.Add(GetEnvironmentString(I));
  111. for i := 0 to AutomationEnvironment.Count-1 do
  112. p.Environment.Values[AutomationEnvironment.Names[i]] := AutomationEnvironment.Values[AutomationEnvironment.Names[i]];
  113. p.Executable:= AExecutable;
  114. for i := 0 to high(AParameters) do
  115. p.Parameters.Add(AParameters[i]);
  116. p.Execute;
  117. InitBuffer(Output, p.PipeBufferSize, AOnReceiveOutput);
  118. InitBuffer(Error, p.PipeBufferSize, AOnReceiveError);
  119. while p.Running do
  120. begin
  121. if not Receive(p.Output, Output) and
  122. not Receive(p.Stderr, Error) then
  123. begin
  124. shouldSleep := true;
  125. AOnBusy(shouldSleep);
  126. if shouldSleep then sleep(15);
  127. end;
  128. end;
  129. Receive(p.Output, Output);
  130. Receive(p.Stderr, Error);
  131. result := p.ExitCode;
  132. finally
  133. p.Free;
  134. end;
  135. end;
  136. { TAutomatedProcess }
  137. constructor TAutomatedProcess.Create(AOwner: TComponent);
  138. begin
  139. inherited Create(AOwner);
  140. Options:= [poNoConsole,poUsePipes];
  141. PipeBufferSize := 65536;
  142. end;
  143. procedure TAutomatedProcess.SendLine(const ALine: RawByteString);
  144. begin
  145. if length(ALine)>0 then
  146. Input.Write(ALine[1],length(ALine));
  147. Input.Write(LineEndingStr[1],length(LineEndingStr));
  148. end;
  149. initialization
  150. AutomationEnvironment := TStringList.Create;
  151. finalization
  152. AutomationEnvironment.Free;
  153. end.