UnTerminal.pas 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. {
  2. UnTerminal 0.9b
  3. ===============
  4. Por Tito Hinostroza 07/11/2016
  5. * Se reemplaza el campo sendCRLF por LineDelimSend, y pasa a ser un enumerado, para tener
  6. más flexibilidad en la configuración.
  7. * Se crea la propiedad LineDelimRecv, para poder configurar el tipo de delimitador de
  8. línea que se debe reconocer al recibir.
  9. * Se modifica TTermVT100.AddData, para que reconozca los diversos delimitadores de
  10. línea.
  11. * Se crean los campos TTermVT100.bhvLF y TTermVT100.bhvCR, para poder flexibilizar el
  12. comportamiento de los caracteres de salto ed línea.
  13. * Se reordenan los campos en la declaración de TConsoleProc.
  14. Description
  15. ===========
  16. Lazarus Unit for controlling console process, with Prompt Detection.
  17. This unit can process the standard input/output of console process, and support ANSI
  18. escape sequences, using a virtual VT100 terminal. It includes
  19. routines for detect the prompt, and consequently the states of BUSY and READY.
  20. In the current version it's not supported to read the standard error stream.
  21. For to start a process it's necessary to create an object TConsoleProc:
  22. p := TConsoleProc.Create(StatusBar1.Panels[1]);
  23. ...
  24. p.Free;
  25. }
  26. unit UnTerminal;
  27. {$mode objfpc}{$H+}
  28. interface
  29. uses Classes, SysUtils, Process, ExtCtrls, Dialogs, Graphics, ComCtrls,
  30. LCLProc, LCLType, types, Strutils, TermVT;
  31. const
  32. UBLOCK_SIZE = 2048; //Tamaño de bloque de lectura de salida de proceso
  33. type
  34. //Posibles estados de la conexión
  35. TEstadoCon = (
  36. ECO_CONNECTING, //Iniciado y Conectando
  37. ECO_ERROR_CON, //Iniciado y Con error de conexión
  38. ECO_BUSY, //Iniciado y conectado, pero ejecutando algún proceso
  39. ECO_READY, //Iniciado y conectado, libre para aceptar comandos.
  40. ECO_STOPPED //Proceso no iniciado. Puede que haya datos pendientes en el "buffer"
  41. );
  42. //Tipos de reconocimiento del prompt en una línea
  43. TPrompMatch = (
  44. prmExactly, //prompt es la línea entera
  45. prmAtBegin, //prompt aparece al inicio de la línea
  46. prmAtEnd, //prompt aparece al final de la línea
  47. prmAtAnyPos //prompt aparece en cualquier parte de la línea
  48. );
  49. //Tipo de delimitador de línea a enviar
  50. TUtLineDelSend = (
  51. LDS_CRLF, //Envía los caracteres CR y LF
  52. LDS_CR, //Envía solo CR
  53. LDS_LF //Envía solo LF
  54. );
  55. //Tipo de delimitador de línea a recibir
  56. TUtLineDelRecv = (
  57. LDR_CRLF, //El salto de línea es CR-LF (o LF-CR)
  58. LDR_CR, //El salto de línea es este caracter. Se ignora LF
  59. LDR_LF, //El salto de línea es este caracter. Se ignora CR
  60. LDR_CR_LF //El salto de línea es este CR o LF
  61. );
  62. {Evento. Pasa la cantidad de bytes que llegan y la columna y fila final de la matriz Lin[] }
  63. TEvProcState = procedure(nDat: integer; pFinal: TPoint) of object;
  64. TEvReadData = procedure(nDat: integer; const lastLine: string) of object;
  65. TEvGetPrompt = procedure(prmLine: string; pIni: TPoint; HeightScr: integer) of object;
  66. TEvChkForPrompt = function(const lin: string): boolean of object;
  67. TEvLinCompleted = procedure(const lin: string) of object;
  68. TEvRecSysComm = procedure(info: string; pIni: TPoint) of object;
  69. TEvRefreshAll = procedure(const grilla: TtsGrid) of object;
  70. TEvInitScreen = procedure(const grilla: TtsGrid; fIni, fFin: integer) of object;
  71. TEvRefreshLine = procedure(const grilla: TtsGrid; fIni, HeightScr: integer) of object;
  72. TEvRefreshLines= procedure(const grilla: TtsGrid; fIni, fFin, HeightScr: integer) of object;
  73. TEvAddNewLine = procedure(HeightScr: integer) of object;
  74. { TConsoleProc }
  75. //Clase que define un proceso
  76. TConsoleProc = class
  77. private
  78. bolsa : array[0..UBLOCK_SIZE] of char; //buffer para almacenar salidas(tiene un caracter más)
  79. nLeidos : LongInt;
  80. lstTmp : TStringList;
  81. cAnim : integer; //contador para animación de ícono de estado
  82. angA : integer; //contador para animación de ícono de estado
  83. LoopList: TStringList; //lista de salida para cuando se usa RunInLoop().
  84. function ChangeState(estado0: TEstadoCon): boolean; //Cambia el State actual
  85. procedure ProcLoop(const lin: string);
  86. procedure SetLineDelimRecv(AValue: TUtLineDelRecv);
  87. protected
  88. panel : TStatusPanel; //referencia a panel para nostrar estado
  89. curPanel : TStatusPanel; //para nostrar posición de cursor de editor de salida
  90. lastState : TEstadoCon; //Estado anterior
  91. txtState : string; //Cadena que describe el estado actual de la conexión
  92. clock : TTimer; //temporizador para leer salida del proceso
  93. FLineDelimSend: TUtLineDelSend;
  94. FLineDelimRecv: TUtLineDelRecv;
  95. function ContainsPrompt(const linAct: string; var pos1, pos2: integer;
  96. posIni: integer=1): boolean;
  97. function ContainsPromptL(const linAct: string; var pos1, pos2: integer
  98. ): boolean;
  99. function EsPrompt(const cad: string): boolean;
  100. function GetAnchoTerminal: integer;
  101. procedure SetAnchoTerminal(AValue: integer);
  102. function ReadData: boolean;
  103. //respuesta a eventos de term
  104. procedure termAddLine;
  105. procedure termRefreshLines(fIni, fFin: integer);
  106. procedure termRecSysComm(info: string);
  107. procedure termLineCompleted(const lineCompleted: string);
  108. public //Eventos
  109. //Eventos de cambio de estado
  110. OnConnecting : TEvProcState; //indica que se inicia el proceso y trata de conectar
  111. OnBusy : TEvProcState; //indica que está esperando prompt
  112. OnStopped : TEvProcState; //indica que se terminó el proceso
  113. OnGetPrompt : TEvGetPrompt; //indica que llegó el prompt
  114. OnChangeState: TEvRecSysComm; //cambia de estado
  115. //Eventos de llegada de datos
  116. OnRefreshAll : TEvRefreshAll; //Para refrescar todo el contenido del terminal. No recomendable.
  117. OnInitScreen : TEvInitScreen; //indica que se debe agregar líneas de texto
  118. OnRefreshLine : TEvRefreshLine; //indica que se deben refrescar una línea
  119. OnRefreshLines: TEvRefreshLines; //indica que se deben refrescar un grupo de líneas
  120. OnAddLine : TEvAddNewLine; //inidca que se debe agregar una línea a la salida
  121. //Eventos de llegada de datos, opcionales.
  122. OnLineCompleted:TEvLinCompleted; {Cuando se ha terminado de escribir una línea en el terminal.
  123. No funcionará si es que se producen saltos en el cursor}
  124. OnLinePrompt : TEvLinCompleted; //Cuando llega la línea del prompt
  125. //Eventos adicionales
  126. OnChkForPrompt: TEvChkForPrompt; //Permite incluir una rutina externa para verificación de prompt.
  127. OnFirstReady : TEvGetPrompt; //La primera vez que de detcta el prompt
  128. OnReadData : TEvReadData; //Cuando llega una trama de datos por el terminal
  129. OnRecSysComm : TEvRecSysComm; {indica que llegó información del sistema remoto (usuario,
  130. directorio actual, etc) Solo para conex. Telnet}
  131. public
  132. //datos del proceso
  133. State : TEstadoCon; //Estado de la conexión
  134. ClearOnOpen : boolean; //Para limpiar pantalla al llamar a Open()
  135. p : TProcess; //el proceso a manejar
  136. //manejo del prompt
  137. detecPrompt: boolean; //activa la detección de prompt.
  138. promptIni : string; //cadena inicial del prompt
  139. promptFin : string; //cadena final del prompt
  140. promptMatch: TPrompMatch; //tipo de coincidencia
  141. HayPrompt : boolean; //bandera, indica si se detectó el prompt en la última línea
  142. msjError : string; //guarda el mensaje de error
  143. term : TTermVT100; //Terminal
  144. property LineDelimSend: TUtLineDelSend read FLineDelimSend write FLineDelimSend; //Tipo de delimitador de línea
  145. property LineDelimRecv: TUtLineDelRecv read FLineDelimRecv write SetLineDelimRecv; //Tipo de delimitador de línea para recibir
  146. procedure Start; //inicia proceso
  147. procedure Open(progPath, progParam: string); //Inicia conexión
  148. function Close: boolean; //Termina la conexión
  149. procedure RefreshConnection(Sender: TObject); //Refresca la conexión
  150. function RunInLoop(TimeoutSegs: integer=-1): boolean;
  151. function RunInLoop(progPath, progParam: string; TimeoutSegs: integer = -1): boolean;
  152. function RunInLoop(progPath, progParam: string; TimeoutSegs: integer;
  153. var progOut: TStringList): boolean;
  154. procedure ClearTerminal;
  155. property TerminalWidth: integer read GetAnchoTerminal write SetAnchoTerminal;
  156. procedure Send(const txt: string);
  157. procedure SendLn(txt: string); //Envía datos por el "stdin"
  158. procedure SendFile(name: string); //Envía el contenido de un archivo
  159. procedure SendVT100Key(var Key: Word; Shift: TShiftState); //Envía una tecla con secuencia de escape
  160. //control de barra de estado
  161. procedure RefPanelEstado;
  162. procedure DrawStatePanel(c: TCanvas; const Rect: TRect); virtual;
  163. function LastLine: string; inline; //devuelve la última línea
  164. procedure AutoConfigPrompt; virtual;
  165. public //Constructor y destructor
  166. constructor Create(PanControl: TStatusPanel); virtual; //Constructor
  167. destructor Destroy; override; //Limpia los buffers
  168. end;
  169. implementation
  170. //uses FormConfig; //se necesita acceder a las propiedades de prompt
  171. const
  172. STA_NAME_CONNEC = 'Connecting';
  173. STA_NAME_ERR_CON = 'Connection Error';
  174. STA_NAME_BUSY = 'Busy';
  175. STA_NAME_READY = 'Ready';
  176. STA_NAME_STOPPED = 'Stopped';
  177. MSG_ERR_NO_APP_DEF = 'No Application specified for connection.';
  178. MSG_FAIL_START_APP = 'Fail Starting Application: ';
  179. MSG_NO_PRMP_FOUND = 'Prompt Not Found for to configure in Terminal.';
  180. MSG_ERR_TIMEOUT = 'Timeout in process.';
  181. {
  182. STA_NAME_CONNEC = 'Conectando';
  183. STA_NAME_ERR_CON = 'Error en conexión';
  184. STA_NAME_BUSY = 'Ocupado';
  185. STA_NAME_READY = 'Disponible';
  186. STA_NAME_STOPPED = 'Detenido';
  187. MSG_ERR_NO_APP_DEF = 'No se especificó aplicativo para conexión.';
  188. MSG_FAIL_START_APP = 'Fallo al iniciar aplicativo: ';
  189. MSG_NO_PRMP_FOUND = 'No se encuentra un prompt en el terminal para configurarlo.';
  190. //}
  191. function Explode(delimiter:string; str:string):TStringDynArray;
  192. var
  193. p,cc,dsize:integer;
  194. begin
  195. cc := 0;
  196. dsize := length(delimiter);
  197. while true do begin
  198. p := pos(delimiter,str);
  199. if p > 0 then begin
  200. inc(cc);
  201. setlength(result,cc);
  202. result[cc-1] := copy(str,1,p-1);
  203. delete(str,1,p+dsize-1);
  204. end else break;
  205. end;
  206. inc(cc);
  207. setlength(result,cc);
  208. result[cc-1] := str;
  209. end;
  210. function TConsoleProc.ChangeState(estado0: TEstadoCon): boolean;
  211. {Cambia el estado de la conexión y actualiza un panel con información sobre el estado}
  212. begin
  213. lastState := State; //pasa State actual a anterior
  214. State := estado0; //fija State actual
  215. if lastState <> State then begin //indica si hubo cambio
  216. //hubo cambio de State
  217. Result := true;
  218. case State of
  219. ECO_CONNECTING: begin
  220. txtState := STA_NAME_CONNEC;
  221. RefPanelEstado; //fuerza a redibujar panel con el nuevo State
  222. if OnConnecting<>nil then OnConnecting(0,term.CurXY);
  223. end;
  224. ECO_ERROR_CON: begin
  225. txtState := STA_NAME_ERR_CON;
  226. RefPanelEstado; //fuerza a redibujar panel con el nuevo State
  227. // if OnErrorConex <> nil then OnErrorConex(nLeidos, pErr);
  228. end;
  229. ECO_BUSY: begin
  230. txtState := STA_NAME_BUSY;
  231. RefPanelEstado; //fuerza a redibujar panel con el nuevo State
  232. if OnBusy <> nil then OnBusy(nLeidos, term.CurXY);
  233. end;
  234. ECO_READY: begin
  235. txtState := STA_NAME_READY;
  236. RefPanelEstado; //fuerza a redibujar panel con el nuevo State
  237. if OnGetPrompt <> nil then OnGetPrompt(LastLine, term.CurXY, term.height);
  238. end;
  239. ECO_STOPPED: begin
  240. txtState := STA_NAME_STOPPED;
  241. RefPanelEstado; //fuerza a redibujar panel con el nuevo State
  242. if OnStopped <> nil then OnStopped(nLeidos, term.CurXY);
  243. end;
  244. end;
  245. if OnChangeState<>nil then OnChangeState(txtState, term.CurXY);
  246. end;
  247. end;
  248. function TConsoleProc.LastLine: string; inline;
  249. //Devuelve la línea donde se encuentra el cursor. Salvo que haya, saltos en el cursor,
  250. //devolverá siempre los últimos caracteres recibidos.
  251. begin
  252. Result := term.buf[term.CurY];
  253. end;
  254. procedure TConsoleProc.RefPanelEstado; //Refresca el estado del panel del StatusBar asociado.
  255. begin
  256. if panel = nil then exit; //protección
  257. //fuerza a llamar al evento OnDrawPanel del StatusBar
  258. panel.StatusBar.InvalidatePanel(panel.Index,[ppText]);
  259. //y este a us vez debe llamar a DrawStatePanel()
  260. end;
  261. procedure TConsoleProc.DrawStatePanel(c: TCanvas; const Rect: TRect);
  262. {Dibuja un ícono y texto, de acuerdo al estado de la conexión. Este código está pensado
  263. para ser usado en el evento OnDrawPanel() de una barra de estado}
  264. var
  265. p1,p2: Tpoint;
  266. procedure Torta(c: Tcanvas; x1,y1,x2,y2: integer; a1,a2: double); //dibuja una torta
  267. var x3,y3,x4,y4: integer;
  268. xc, yc: integer;
  269. begin
  270. xc := (x1+x2) div 2; yc := (y1+y2) div 2;
  271. x3:=xc + round(1000*cos(a1));
  272. y3:=yc + round(1000*sin(a1));
  273. x4:=xc + round(1000*cos(a2));
  274. y4:=yc + round(1000*sin(a2));
  275. c.pie(x1,y1,x2,y2,x3,y3,x4,y4);
  276. end;
  277. procedure Circulo(c: Tcanvas; xc,yc: integer; n: integer); //dibuja un círculo
  278. const r = 2;
  279. begin
  280. case n of
  281. 5: c.Brush.Color:=$B0FFB0;
  282. 4: c.Brush.Color:=$40FF40;
  283. 3: c.Brush.Color:=$00E000;
  284. 2: c.Brush.Color:=$00CC00;
  285. 1: c.Brush.Color:=$00A000;
  286. 0: c.Brush.Color:=$008000;
  287. else
  288. c.Brush.Color:=clWhite;
  289. end;
  290. c.Pen.Color:=c.Brush.Color;
  291. c.Ellipse(xc-r, yc-r+1, xc+r, yc+r+1);
  292. end;
  293. begin
  294. if State in [ECO_CONNECTING, ECO_BUSY] then begin //estados de espera
  295. c.Pen.Width:=0; //restaura ancho
  296. Circulo(c,Rect.Left+5,Rect.Top+5, angA);
  297. inc(angA);if angA>7 then angA:=0;
  298. Circulo(c,Rect.Left+9,Rect.Top+3, angA);
  299. inc(angA);if angA>7 then angA:=0;
  300. Circulo(c,Rect.Left+13,Rect.Top+5, angA);
  301. inc(angA);if angA>7 then angA:=0;
  302. Circulo(c,Rect.Left+15,Rect.Top+9, angA);
  303. inc(angA);if angA>7 then angA:=0;
  304. Circulo(c,Rect.Left+13,Rect.Top+13, angA);
  305. inc(angA);if angA>7 then angA:=0;
  306. Circulo(c,Rect.Left+9,Rect.Top+15, angA);
  307. inc(angA);if angA>7 then angA:=0;
  308. Circulo(c,Rect.Left+5,Rect.Top+13, angA);
  309. inc(angA);if angA>7 then angA:=0;
  310. Circulo(c,Rect.Left+3,Rect.Top+9, angA);
  311. inc(angA);if angA>7 then angA:=0;
  312. end else if State = ECO_ERROR_CON then begin //error de conexión
  313. //c´rculo rojo
  314. c.Brush.Color:=clRed;
  315. c.Pen.Color:=clRed;
  316. c.Ellipse(Rect.Left+2, Rect.Top+2, Rect.Left+16, Rect.Top+16);
  317. //aspa blanca
  318. c.Pen.Color:=clWhite;
  319. c.Pen.Width:=2;
  320. p1.x := Rect.Left+5; p1.y := Rect.Top+5;
  321. p2.x := Rect.Left+12; p2.y := Rect.Top+12;
  322. c.Line(p1,p2);
  323. p1.x := Rect.Left+5; p1.y := Rect.Top+12;
  324. p2.x := Rect.Left+12; p2.y := Rect.Top+5;
  325. c.Line(p1,p2);
  326. end else if State = ECO_READY then begin //disponible
  327. c.Brush.Color:=clGreen;
  328. c.Pen.Color:=clGreen;
  329. c.Ellipse(Rect.Left+2, Rect.Top+2,Rect.Left+16, Rect.Top+16);
  330. c.Pen.Color:=clWhite;
  331. c.Pen.Width:=2;
  332. p1.x := Rect.Left+6; p1.y := Rect.Top+7;
  333. p2.x := Rect.Left+8; p2.y := Rect.Top+12;
  334. c.Line(p1,p2);
  335. p1.x := Rect.Left+12; p1.y := Rect.Top+5;
  336. // p2.x := Rect.Left+12; p2.y := Rect.Top+5;
  337. c.Line(p2,p1);
  338. end else begin //estados detenido
  339. //círculo gris
  340. c.Brush.Color:=clGray;
  341. c.Pen.Color:=clGray;
  342. c.Ellipse(Rect.Left+2, Rect.Top+2, Rect.Left+16, Rect.Top+16);
  343. //aspa blanca
  344. c.Pen.Color:=clWhite;
  345. c.Pen.Width:=2;
  346. p1.x := Rect.Left+5; p1.y := Rect.Top+5;
  347. p2.x := Rect.Left+12; p2.y := Rect.Top+12;
  348. c.Line(p1,p2);
  349. p1.x := Rect.Left+5; p1.y := Rect.Top+12;
  350. p2.x := Rect.Left+12; p2.y := Rect.Top+5;
  351. c.Line(p1,p2);
  352. end;
  353. c.Font.Color:=clBlack;
  354. c.TextRect(Rect, 19 + Rect.Left, 2 + Rect.Top, txtState);
  355. end;
  356. function TConsoleProc.GetAnchoTerminal: integer;
  357. //Devuelve el ancho del terminal
  358. begin
  359. Result := term.width;
  360. end;
  361. procedure TConsoleProc.SetAnchoTerminal(AValue: integer);
  362. //Fija el ancho del terminal
  363. begin
  364. if term.width=AValue then Exit;
  365. term.width := AValue;
  366. end;
  367. procedure TConsoleProc.Start;
  368. {Inicia el proceso y verifica si hubo error al lanzar el proceso. Los parámetros del
  369. proceso, deben haberse fijado antes en el proceso.}
  370. begin
  371. //ejecutamos
  372. ChangeState(ECO_CONNECTING); //importante para
  373. try
  374. p.Execute;
  375. if not p.Running then begin
  376. //Falló al iniciar
  377. ChangeState(ECO_STOPPED);
  378. Exit;
  379. end;
  380. //Se inició, y esperamos a que RefreshConnection() procese los datos recibidos
  381. except
  382. if trim(p.Executable) = '' then
  383. msjError := MSG_ERR_NO_APP_DEF
  384. else
  385. msjError := MSG_FAIL_START_APP + p.Executable;
  386. ChangeState(ECO_ERROR_CON); //genera evento
  387. end;
  388. end;
  389. procedure TConsoleProc.Open(progPath, progParam: string);
  390. //Rutina principal para iniciar un programa.
  391. begin
  392. term.Clear;
  393. if trim(progPath) = '' then exit; //protección
  394. //Inicia la salida de texto, refrescando todo el terminal
  395. if ClearOnOpen then ClearTerminal;
  396. if p.Running then p.Terminate(0);
  397. // Vamos a lanzar el proceso
  398. p.CommandLine := progPath + ' ' + progParam;
  399. // p.Executable := progPath;
  400. // p.Parameters.Clear;
  401. // p.Parameters.Add(progParam);
  402. // Definimos comportamiento de 'TProccess'. Es importante direccionar los errores.
  403. p.Options := [poUsePipes, poStderrToOutPut, poNoConsole];
  404. Start; //puede dar error
  405. end;
  406. function TConsoleProc.Close: boolean;
  407. //Cierra la conexión actual. Si hay error devuelve False.
  408. var c: integer;
  409. begin
  410. Result := true;
  411. //verifica el proceso
  412. if p.Running then p.Terminate(0);
  413. //espera hasta 100 mseg
  414. c := 0;
  415. while p.Running and (c<20) do begin
  416. sleep(5);
  417. inc(c);
  418. end;
  419. if c>= 20 then exit(false); //sale con error
  420. //Pasa de Runnig a Not Running
  421. ChangeState(ECO_STOPPED);
  422. //Puede que quede datos en el "stdout"
  423. ReadData; //lee lo que queda
  424. end;
  425. procedure TConsoleProc.ClearTerminal;
  426. {Reinicia el terminal iniciando en (1,1) y limpiando la grilla}
  427. begin
  428. term.Clear; //limpia grilla y reinicia cursor
  429. //genera evento para reiniciar salida
  430. if OnInitScreen<>nil then OnInitScreen(term.buf, 1, term.height);
  431. end;
  432. function TConsoleProc.ContainsPrompt(const linAct: string; var pos1, pos2: integer;
  433. posIni: integer = 1): boolean;
  434. //Verifica si una cadena de texto contiene al prompt, usando los valores actuales
  435. //de promptIni y promptFin.
  436. //Si la cadena contiene al prompt, devuelve TRUE y actualiza los valores pos1 y pos2
  437. //que son los límites inicial y final del prompt, dentro de la cadema.
  438. //posIni, es la posición inicial (inclusivo) desde donde se buscará.
  439. //Si la salida del proceso va a ir a un editor con resaltador de sintaxis, esta rutina debe
  440. //ser similar a la del resaltador para que haya sincronía en lo que se ve. No se separra esta
  441. //rutina en otra unidad para que esta unidad no tenga dependencias y se pueda usar como
  442. //librería. Además la detección del prompt para el proceso, es diferente de la deteción
  443. //para un resaltador de sintaxis.
  444. var
  445. lar: Integer;
  446. begin
  447. Result := FALSE; //valor por defecto
  448. lar := length(promptIni);
  449. pos1 := posEx(promptIni, linAct, posIni);
  450. if (lar>0) and (pos1>0) then begin
  451. //puede ser
  452. if promptFin = '' then begin
  453. //no hace falta validar más
  454. pos2:=pos1+lar-1; //límite final
  455. Result := true;
  456. exit; //no hace falta explorar más
  457. end;
  458. //hay que validar la existencia del fin del prompt
  459. pos2 :=posEx(promptFin,linAct, posIni);
  460. if pos2>0 then begin //encontró
  461. pos2 := pos2 + length(promptFin)-1;
  462. Result := true;
  463. exit;
  464. end;
  465. end;
  466. end;
  467. function TConsoleProc.ContainsPromptL(const linAct: string; var pos1, pos2: integer): boolean;
  468. //Similar a ContainsPrompt(), pero devuelve la última ocurrencia.
  469. var
  470. p1,p2: Integer;
  471. hay: Boolean;
  472. begin
  473. hay := ContainsPrompt(linAct, p1, p2, 1);
  474. if not hay then exit(false); //no existe
  475. //existe el prompt, busca otro más adelante
  476. repeat
  477. pos1 := p1; pos2 := p2; //guarda valores
  478. hay := ContainsPrompt(linAct, p1, p2, p1+1);
  479. until not hay;
  480. exit(true); //hay valores
  481. end;
  482. function TConsoleProc.EsPrompt(const cad: string): boolean;
  483. //Indica si la línea dada, es el prompt, de acuerdo a los parámetros dados. Esta función
  484. //se pone aquí, porque aquí se tiene fácil acceso a las configuraciones del prompt.
  485. var
  486. pos2: integer;
  487. pos1: integer;
  488. begin
  489. if detecPrompt then begin //si hay detección activa
  490. Result := false;
  491. //contiene al prompt, pero hay que ver la posición
  492. case promptMatch of
  493. prmExactly : begin
  494. if not ContainsPrompt(cad, pos1, pos2) then exit;
  495. if (pos1 = 1) and (pos2=length(cad)) then exit(true);
  496. end;
  497. prmAtBegin : begin
  498. if not ContainsPrompt(cad, pos1, pos2) then exit;
  499. if (pos1 = 1) then exit(true);
  500. end;
  501. prmAtEnd : begin
  502. if not ContainsPromptL(cad, pos1, pos2) then exit;
  503. if (pos2=length(cad)) then exit(true);
  504. end;
  505. prmAtAnyPos: begin
  506. if not ContainsPrompt(cad, pos1, pos2) then exit;
  507. exit(true);
  508. end;
  509. end;
  510. end else begin
  511. Result := false;
  512. end;
  513. end;
  514. function TConsoleProc.ReadData: boolean;
  515. {Verifica la salida del proceso. Si llegan datos los pasa a "term" y devuelve TRUE.
  516. Lee en un solo bloque si el tamaño de los datos, es menor que UBLOCK_SIZE, en caso
  517. contrario lee varios bloques. Actualiza "nLeidos", "HayPrompt". }
  518. var nDis : longint;
  519. nBytes : LongInt;
  520. begin
  521. // pIni := LeePosFin;
  522. Result := false; //valor por defecto
  523. nLeidos := 0;
  524. HayPrompt := false;
  525. if P.Output = nil then exit; //no hay cola
  526. repeat
  527. //vemos cuantos bytes hay "en este momento"
  528. nDis := P.Output.NumBytesAvailable;
  529. if nDis = 0 then break; //sale del lazo
  530. if nDis < UBLOCK_SIZE then begin
  531. //leemos solo los que hay, sino se queda esperando
  532. nBytes := P.Output.Read(bolsa, nDis);
  533. bolsa[nBytes] := #0; //marca fin de cadena
  534. term.AddData(@bolsa); //puede generar eventos
  535. nLeidos += nBytes;
  536. end else begin
  537. {Leemos bloque de UBLOCK_SIZE bytes. bolsa[] tiene en realidad un tamaño de
  538. UBLOCK_SIZE+1, así que dejará al menos un byte libre, para poner 0x00}
  539. nBytes := P.Output.Read(bolsa, UBLOCK_SIZE);
  540. bolsa[nBytes] := #0; //marca fin de cadena
  541. term.AddData(@bolsa); //puede generar eventos
  542. nLeidos += nBytes;
  543. end;
  544. {aquí también se puede detetar el prompt, con más posibilidad de detectar los
  545. posibles "Prompt" intermedios}
  546. Result := true; //hay datos
  547. until not P.Running or (nBytes = 0);
  548. if not Result then exit;
  549. {Terminó de leer, aquí detectamos el prompt, porque es casi seguro que llegue
  550. al final de la trama.
  551. Ver si la línea actual, es realmente el prompt, es la forma más segura. Se probó
  552. viendo si la línea actual empezaba con el prompt, pero daba casos (sobretodo en
  553. conexiones lentas) en que llegaba una trama con pocos caracteres, de modo que se
  554. generaba el evento de llegada de prompt dos veces (tal vez más) en una misma línea}
  555. if OnChkForPrompt <> nil then begin
  556. //Hay rutina de verificación externa
  557. HayPrompt := OnChkForPrompt(LastLine);
  558. end else begin
  559. if EsPrompt(LastLine) then
  560. HayPrompt:=true;
  561. end;
  562. if OnReadData<>nil then OnReadData(nLeidos, LastLine);
  563. if HayPrompt then begin
  564. //Genera el evento. Este evento se generará siempre que se detecte el prompt en la
  565. //última línea sin ver el estado: El cambio de estado es otro procesamiento.
  566. if OnLinePrompt<>nil then OnLinePrompt(LastLine);
  567. end;
  568. end;
  569. procedure TConsoleProc.RefreshConnection(Sender: TObject);
  570. {Refresca el estado de la conexión. Verifica si hay datos de salida del proceso, para
  571. generar los eventos respectivos que capturan la salida. Es llamado autométicamente
  572. por un timer, cuando está disponible, pero en aplicaciones de consola, puede que sea
  573. necesario llamarlo manualmente, o usar el método }
  574. begin
  575. if State = ECO_STOPPED then Exit; //No está corriendo el proceso.
  576. if p.Running then begin
  577. //Se está ejecutando
  578. if ReadData then begin //actualiza "HayPrompt"
  579. if State in [ECO_READY, ECO_BUSY] then begin
  580. if HayPrompt then begin
  581. ChangeState(ECO_READY);
  582. end else begin
  583. ChangeState(ECO_BUSY);
  584. end;
  585. end else begin
  586. //Se está esperando conseguir la conexión (State = ECO_CONNECTING)
  587. //Puede que se detenga aquí con un mensaje de error en lugar del prompt
  588. if HayPrompt then begin
  589. //se consiguió conectar por primera vez
  590. if OnFirstReady<>nil then OnFirstReady('',term.CurXY, term.height);
  591. // State := ECO_READY; //para que pase a ECO_BUSY
  592. // SendLn(COMAN_INIC); //envía comandos iniciales (lanza evento Ocupado)
  593. ChangeState(ECO_READY);
  594. end;
  595. end;
  596. end;
  597. end else begin //terminó
  598. ChangeState(ECO_STOPPED);
  599. ReadData; //lee por si quedaban datos en el buffer
  600. end;
  601. //actualiza animación
  602. inc(cAnim);
  603. if (cAnim mod 4) = 0 then begin
  604. if State in [ECO_CONNECTING, ECO_BUSY] then begin //estados de espera
  605. inc(angA);if angA>7 then angA:=0;
  606. RefPanelEstado;
  607. end;
  608. cAnim := 0;
  609. end;
  610. end;
  611. procedure TConsoleProc.Send(const txt: string);
  612. {Envía una cadena como como flujo de entrada al proceso.
  613. Es importante agregar el caracter #13#10 al final. De otra forma no se leerá el "stdin"}
  614. begin
  615. if p = NIL then exit;
  616. if not p.Running then exit;
  617. p.PipeBufferSize:=20000;
  618. p.Input.Size:=20000;
  619. p.Input.Write(txt[1], length(txt)); //pasa el origen de los datos
  620. //para que se genere un cambio de State aunque el comando sea muy corto
  621. if State = ECO_READY then ChangeState(ECO_BUSY);
  622. end;
  623. procedure TConsoleProc.SendLn(txt: string);
  624. {Envía un comando al proceso. Incluye el salto de línea al final de la línea.
  625. También puede recibir cadneas de varias líneas}
  626. begin
  627. //reemplaza todos los saltos por #1
  628. txt := StringReplace(txt,#13#10,#1,[rfReplaceAll]);
  629. txt := StringReplace(txt,#13,#1,[rfReplaceAll]);
  630. txt := StringReplace(txt,#10,#1,[rfReplaceAll]);
  631. //incluye el salto final
  632. txt += #1;
  633. //Aplica el salto configurado
  634. case FLineDelimSend of
  635. LDS_CRLF: txt := StringReplace(txt,#1,#13#10,[rfReplaceAll]); //envía CRLF
  636. LDS_CR : txt := StringReplace(txt,#1,#13,[rfReplaceAll]); //envía CR
  637. LDS_LF : txt := StringReplace(txt,#1,#10,[rfReplaceAll]); //envía LF
  638. end;
  639. Send(txt);
  640. end;
  641. procedure TConsoleProc.SendFile(name: string);
  642. //Envía el contendio completo de un archivo
  643. var lins: TstringList;
  644. lin: String;
  645. begin
  646. lins:= TstringList.Create;
  647. if not FileExists(name) then exit;
  648. lins.LoadFromFile(name);
  649. for lin in lins do
  650. SendLn(lin);
  651. lins.Free;
  652. end;
  653. procedure TConsoleProc.SendVT100Key(var Key: Word; Shift: TShiftState);
  654. //Envía una tecla de control (obtenida del evento KeyDown), realizando primero
  655. //la transformación a secuencias de escapa.
  656. begin
  657. case Key of
  658. VK_END : begin
  659. if Shift = [] then Send(#27'[K');
  660. end;
  661. VK_HOME : begin
  662. if Shift = [] then Send(#27'[H');
  663. end;
  664. VK_LEFT : begin
  665. if Shift = [] then Send(#27'[D');
  666. end;
  667. VK_RIGHT: begin
  668. if Shift = [] then Send(#27'[C');
  669. end;
  670. VK_UP : begin
  671. if Shift = [] then Send(#27'[A');
  672. end;
  673. VK_DOWN : begin
  674. if Shift = [] then Send(#27'[B');
  675. end;
  676. VK_F1 : begin
  677. if Shift = [] then Send(#27'OP');
  678. end;
  679. VK_F2 : begin
  680. if Shift = [] then Send(#27'OQ');
  681. end;
  682. VK_F3 : begin
  683. if Shift = [] then Send(#27'OR');
  684. end;
  685. VK_F4 : begin
  686. if Shift = [] then Send(#27'OS');
  687. end;
  688. VK_BACK : begin
  689. if Shift = [] then Send(#8); //no transforma
  690. end;
  691. VK_TAB : begin
  692. if Shift = [] then Send(#9); //no transforma
  693. end;
  694. VK_A..VK_Z: begin
  695. if Shift = [ssCtrl] then begin //Ctrl+A, Ctrl+B, ... Ctrl+Z
  696. Send(chr(Key-VK_A+1));
  697. end;
  698. end;
  699. end;
  700. end;
  701. procedure TConsoleProc.ProcLoop(const lin: string);
  702. {método interno de respuesta al evento OnLineCompleted(), para usarse con RunInLoop()}
  703. begin
  704. if LoopList<>nil then begin
  705. LoopList.Add(lin); //solo acumula
  706. end;
  707. end;
  708. procedure TConsoleProc.SetLineDelimRecv(AValue: TUtLineDelRecv);
  709. begin
  710. FLineDelimRecv:=AValue;
  711. case FLineDelimRecv of
  712. LDR_CRLF : begin term.bhvCR := tbcNormal ; term.bhvLF := tbcNormal ; end;
  713. LDR_CR : begin term.bhvCR := tbcNewLine; term.bhvLF := tbcNone ; end;
  714. LDR_LF : begin term.bhvCR := tbcNone ; term.bhvLF := tbcNewLine; end;
  715. LDR_CR_LF: begin term.bhvCR := tbcNewLine; term.bhvLF := tbcNewLine; end;
  716. end;
  717. end;
  718. function TConsoleProc.RunInLoop(TimeoutSegs: integer = -1): boolean;
  719. {Ejecuta el proceso en un lazo, hasta que la aplicación termine o hasta que se
  720. cumpla el número de segundos indciados en "TimeoutSegs". Si se detiene por desborde
  721. devuelve TRUE, y un mensaje de error en "msjError".
  722. Se usa cuando no se puede usar el temporizador, como en las aplicaciones de consola.}
  723. var
  724. tic_proc: Integer;
  725. max_tics: integer;
  726. begin
  727. if TimeoutSegs=-1 then begin
  728. //ejecuta lazo hasta que termine el proceso
  729. repeat
  730. RefreshConnection(nil);
  731. sleep(50);
  732. until State = ECO_STOPPED;
  733. exit(false);
  734. end else begin
  735. //ejecuta hasta que termine el proceso o haya desborde de tiempo
  736. tic_proc := 0;
  737. max_tics := TimeoutSegs * 20;
  738. repeat
  739. RefreshConnection(nil); //necesario porque no funciona el Timer del LCL
  740. sleep(50);
  741. inc(tic_proc);
  742. until (State = ECO_STOPPED) or (tic_proc > max_tics);
  743. if tic_proc > max_tics then begin
  744. msjError := MSG_ERR_TIMEOUT;
  745. exit(true);
  746. end;
  747. exit(false);
  748. end;
  749. end;
  750. function TConsoleProc.RunInLoop(progPath, progParam: string;
  751. TimeoutSegs: integer = -1): boolean;
  752. {Versión de RunInLoop(), que ejecuta el proceso y el lazo de espera, a la vez.
  753. }
  754. begin
  755. Open(progPath, progParam);
  756. if msjError<>'' then exit;
  757. Result := RunInLoop(TimeoutSegs);
  758. //puede generar error
  759. end;
  760. function TConsoleProc.RunInLoop(progPath, progParam: string;
  761. TimeoutSegs: integer; var progOut: TStringList): boolean;
  762. {Versión de RunInLoop(), que ejecuta captura la salida del proceso en un TString
  763. }
  764. begin
  765. OnLineCompleted:=@ProcLoop;
  766. LoopList := progOut; //aquí se acumulará la salida
  767. Result := RunInLoop(progPath, progParam, TimeoutSegs);
  768. OnLineCompleted:=nil;
  769. //puede generar error
  770. end;
  771. //respuesta a eventos de term
  772. procedure TConsoleProc.termAddLine;
  773. //Se pide agregar líneas a la salida
  774. begin
  775. if OnAddLine<>nil then OnAddLine(term.height);
  776. end;
  777. procedure TConsoleProc.termRefreshLines(fIni, fFin: integer);
  778. //Se pide refrescar un rango de líneas
  779. begin
  780. if OnRefreshAll<>nil then OnRefreshAll(term.buf); //evento
  781. if fIni=fFin then begin //una sola línea
  782. if OnRefreshLine<> nil then OnRefreshLine(term.buf, fIni, term.height);
  783. end else begin
  784. if OnRefreshLines<> nil then OnRefreshLines(term.buf, fIni, fFin, term.height);
  785. end;
  786. end;
  787. procedure TConsoleProc.termRecSysComm(info: string);
  788. //Se ha recibido comando con información del sistema.
  789. begin
  790. //se indica que se recibe información del sistema
  791. if OnRecSysComm<>nil then OnRecSysComm(info, term.CurXY);
  792. //Se puede asumir que llega el prompt pero no siempre funciona
  793. // HayPrompt := true; //marca bandera
  794. // ChangeState(ECO_READY); //cambia el State
  795. end;
  796. procedure TConsoleProc.termLineCompleted(const lineCompleted: string);
  797. begin
  798. if OnLineCompleted<>nil then OnLineCompleted(lineCompleted);
  799. end;
  800. procedure TConsoleProc.AutoConfigPrompt;
  801. //Configura el prompt actual como el prompt por defecto. Esta configuración no es
  802. //para nada, precisa pero ahorrará tiempo en configurar casos sencillos
  803. var
  804. ultlin: String;
  805. function SimbolosIniciales(cad: string): string;
  806. //Toma uno o dos símbolos iniciales de la cadena. Se usan símbolos porque
  807. //suelen ser fijos, mientras que los caracteres alfabéticos suelen cambiar
  808. //en el prompt.
  809. begin
  810. Result := cad[1]; //el primer caracter se tomará siempre
  811. if length(cad)>3 then begin
  812. //agrega si es un símbolo.
  813. if not (cad[2] in ['a'..'z','A'..'Z']) then
  814. Result += cad[2];
  815. end;
  816. end;
  817. function SimbolosFinales(cad: string): string;
  818. //Toma uno o dos o tres caracteres finales de la cadena. Se usan símbolos porque
  819. //suelen ser fijos, mientras que los caracteres alfabéticos suelen cambiar
  820. //en el prompt.
  821. var
  822. p: Integer;
  823. hayEsp: Boolean;
  824. begin
  825. p := length(cad); //apunta al final
  826. hayEsp := (cad[p] = ' ');
  827. cad := TrimRight(cad); //quita espacios
  828. if length(cad)<=2 then begin
  829. //hay muy pocos caracteres
  830. Result := cad[p-1]+cad[p]; //toma los últimos
  831. exit;
  832. end;
  833. //hay suficientes caracteres
  834. p := length(cad); //apunta al final (sin espacios)
  835. Result := cad[p];
  836. //agrega si es un símbolo.
  837. if not (cad[p-1] in ['a'..'z','A'..'Z']) then
  838. Result := cad[p-1] + Result;
  839. //completa con espacio si hubiera
  840. if hayEsp then Result += ' ';
  841. end;
  842. begin
  843. //utiliza la línea actual del terminal
  844. promptIni := '';
  845. promptFin := '';
  846. ultlin := LastLine;
  847. if ultlin = '' then begin
  848. ShowMessage(MSG_NO_PRMP_FOUND);
  849. exit;
  850. end;
  851. //casos particulares
  852. If ultlin = '>>> ' Then begin //caso especial
  853. DetecPrompt := true;
  854. promptIni := '>>';
  855. promptFin := ' ';
  856. SendLn(''); //para que detecte el prompt
  857. exit;
  858. end;
  859. If ultlin = 'SQL> ' Then begin //caso especial
  860. DetecPrompt := true;
  861. promptIni := 'SQL> ';
  862. promptFin := '';
  863. SendLn(''); //para que detecte el prompt
  864. exit;
  865. end;
  866. If length(ultlin)<=3 Then begin //caso especial
  867. DetecPrompt := true;
  868. promptIni := ultlin;
  869. promptFin := '';
  870. SendLn(''); //para que detecte el prompt
  871. exit;
  872. end;
  873. //caso general
  874. DetecPrompt := true;
  875. promptIni := SimbolosIniciales(ultlin);
  876. promptFin := SimbolosFinales(ultlin);
  877. SendLn(''); //para que detecte el prompt
  878. end;
  879. //constructor y destructor
  880. constructor TConsoleProc.Create(PanControl: TStatusPanel);
  881. //Constructor
  882. begin
  883. lstTmp := TStringList.Create; //crea lista temporal
  884. p := TProcess.Create(nil); //Crea proceso
  885. ChangeState(ECO_STOPPED); //State inicial. Genera el primer evento
  886. //configura temporizador
  887. clock := TTimer.Create(nil);
  888. clock.interval:=50; {100 es un buen valor, pero para mayor velocidad de recepción, se
  889. puede usar 50 milisegundos}
  890. clock.OnTimer := @RefreshConnection;
  891. panel := PanControl; //inicia referencia a panel
  892. if panel<> nil then
  893. panel.Style:=psOwnerDraw; //configura panel para dibujarse por evento
  894. detecPrompt := true; //activa detección de prompt por defecto
  895. promptMatch := prmExactly; //debe ser exacta
  896. ClearOnOpen := true; //por defecto se limpia la pantalla
  897. //Crea y configura terminal
  898. term := TTermVT100.Create; //terminal
  899. term.OnRefreshLines:=@termRefreshLines;
  900. term.OnScrollLines:=@termAddLine;
  901. term.OnLineCompleted:=@termLineCompleted;
  902. term.OnRecSysComm:=@termRecSysComm; {usaremos este evento para detectar la llegada
  903. del prompt}
  904. //Configura delimitadores de línea iniciales
  905. LineDelimSend := LDS_CRLF;
  906. LineDelimRecv := LDR_LF;
  907. end;
  908. destructor TConsoleProc.Destroy;
  909. //Destructor
  910. begin
  911. term.Free;
  912. clock.Free; //destruye temporizador
  913. //verifica el proceso
  914. if p.Running then p.Terminate(0);
  915. //libera objetos
  916. FreeAndNIL(p);
  917. lstTmp.Free; //limpia
  918. end;
  919. end.