1 /+ 2 Author: 3 Colin Grogan 4 github.com/grogancolin 5 6 Description: 7 Implementation of the expect tool (http://expect.sourceforge.net/) in D. 8 9 License: 10 Boost Software License - Version 1.0 - August 17th, 2003 11 12 Permission is hereby granted, free of charge, to any person or organization 13 obtaining a copy of the software and accompanying documentation covered by 14 this license (the "Software") to use, reproduce, display, distribute, 15 execute, and transmit the Software, and to prepare derivative works of the 16 Software, and to permit third-parties to whom the Software is furnished to 17 do so, all subject to the following: 18 19 The copyright notices in the Software and this entire statement, including 20 the above license grant, this restriction and the following disclaimer, 21 must be included in all copies of the Software, in whole or in part, and 22 all derivative works of the Software, unless such copies or derivative 23 works are solely in the form of machine-executable object code generated by 24 a source language processor. 25 26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 29 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 30 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 31 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 32 DEALINGS IN THE SOFTWARE. 33 +/ 34 module dexpect; 35 36 import std.conv : to; 37 import std..string; 38 import core.thread : Thread, Duration, msecs; 39 import std.datetime : Clock; 40 import std.algorithm : canFind; 41 import std.path : isAbsolute; 42 import std.stdio : File, stdout; 43 import std.range : isOutputRange; 44 45 //alias Expect = ExpectImpl!ExpectSink; 46 47 /// ExpectImpl spawns a process in a Spawn object. 48 /// You then call expect("desired output"); sendLine("desired Input"); 49 /// to interact with said process 50 public class ExpectImpl(OutputRange) if(isOutputRange!(OutputRange, string)){ 51 /// Amount of time to wait before ending expect call. 52 private Duration _timeout=5000.msecs; 53 54 /// The index in allData where the last succesful expect was found 55 private size_t indexLastExpect; 56 57 /// The spawn object. Platform dependant. See $Spawn 58 private Spawn spawn; 59 60 /// String containing the last string expect function was called on 61 private string lastExpect; 62 63 OutputRange _sink; 64 /// Constructs an Expect that runs cmd with no args 65 this(string cmd, OutputRange sink){ 66 this(cmd, [], sink); 67 } 68 /// Constructs an Expect that runs cmd with args 69 /// On linux, this passes the args with cmd on front if required 70 /// On windows, it passes the args as a single string seperated by spaces 71 this(string cmd, string[] args, OutputRange sink){ 72 this.sink = sink; 73 this._sink.put("Spawn : %s %(%s %)", cmd, args); 74 this.spawn.spawn(cmd, args); 75 } 76 77 /// Calls the spawns cleanup routine. 78 ~this(){ 79 this.spawn.cleanup(); 80 } 81 82 /// Expects toExpect in output of spawn within Spawns timeout 83 public int expect(string toExpect){ 84 return expect(toExpect, this.timeout); 85 } 86 87 /// Expects toExpect in output of spawn within custom timeout 88 public int expect(string toExpect, Duration timeout){ 89 return expect([toExpect], timeout); 90 91 /+this._sink.put("Expect : %s", toExpect); 92 Thread.sleep(50.msecs); 93 auto startTime = Clock.currTime; 94 auto timeLastPrintedMessage = Clock.currTime; 95 while(Clock.currTime < startTime + timeout){ 96 this.spawn.readNextChunk; 97 // gives a status update to the user. Takes longer than the default timeout, 98 // so usually you wont see it 99 if(Clock.currTime >= timeLastPrintedMessage + 5100.msecs){ 100 string update = this.data.length > 50 ? this.data[$-50..$] : this.data; 101 this._sink.put("Last %s chars of data: %s", update.length, update.strip); 102 timeLastPrintedMessage = Clock.currTime; 103 } 104 // check if we finally have what we want in the output, if so, return 105 if(this.spawn.allData[indexLastExpect..$].canFind(toExpect)){ 106 indexLastExpect = this.spawn.allData.lastIndexOf(toExpect); 107 this.lastExpect = toExpect; 108 return 1; 109 } 110 } 111 throw new ExpectException(format("Duration: %s. Timed out expecting %s in {\n%s\n}",timeout, toExpect, this.data)); 112 +/ 113 } 114 115 public int expect(string[] arr, Duration timeout){ 116 this._sink.put("Expect : %s", arr); 117 Thread.sleep(50.msecs); 118 auto startTime = Clock.currTime; 119 auto timeLastPrintedMessage = Clock.currTime; 120 while(Clock.currTime < startTime + timeout){ 121 // gives a status update to the user. Takes longer than the default timeout, 122 // so usually you wont see it 123 if(Clock.currTime >= timeLastPrintedMessage + 5100.msecs){ 124 string update = this.data.length > 50 ? this.data[$-50..$] : this.data; 125 this._sink.put("Last %s chars of data: %s", update.length, update.strip); 126 timeLastPrintedMessage = Clock.currTime; 127 } 128 foreach(int idx, string toExpect; arr){ 129 this.spawn.readNextChunk; 130 // check if we finally have what we want in the output, if so, return 131 if(this.spawn.allData[indexLastExpect..$].canFind(toExpect)){ 132 indexLastExpect = this.spawn.allData.lastIndexOf(toExpect); 133 this.lastExpect = toExpect; 134 return idx; 135 } 136 } 137 } 138 throw new ExpectException(format("Duration: %s. Timed out expecting %s in {\n%s\n}", timeout, arr, this.data)); 139 } 140 /// Sends a line to the pty. Ensures it ends with newline 141 public void sendLine(string command){ 142 if(command.length == 0 || command[$-1] != '\n') 143 this.send(command ~ '\n'); 144 else 145 this.send(command); 146 } 147 /// Sends command to the pty 148 public void send(string command){ 149 this._sink.put("Sending: %s", command.replace("\n", "\\n")); 150 this.spawn.sendData(command); 151 } 152 /// Reads data from the spawn 153 /// This function will sleep for timeToWait' Duration if nothing was read first time 154 /// timeToWait is 50.msecs by default 155 public void read(Duration timeToWait=150.msecs){ 156 auto len = this.data.length; 157 this.spawn.readNextChunk; 158 if(len == this.data.length){ 159 Thread.sleep(timeToWait); 160 this.spawn.readNextChunk; 161 } 162 } 163 /// Reads all available data. Ends when subsequent reads dont increase length of allData 164 public void readAllAvailable(){ 165 auto len = this.spawn.allData.length; 166 while(true){ 167 this.read; 168 if(len == this.spawn.allData.length) break; 169 len = this.spawn.allData.length; 170 } 171 } 172 173 public: 174 /// Sets the default timeout 175 @property timeout(Duration t) { this._timeout = t; } 176 /// Sets the timeout to t milliseconds 177 @property timeout(long t) { this._timeout = t.msecs; } 178 /// Returns the timeout 179 @property auto timeout(){ return this._timeout; } 180 /// Returns all data before the last succesfull expect 181 @property string before(){ return this.spawn.allData[0..indexLastExpect]; } 182 /// Reads and then returns all data after the last succesfull expect. WARNING: May block if spawn is constantly writing data 183 @property string after(){ readAllAvailable; return this.spawn.allData[indexLastExpect..$]; } 184 @property string data(){ return this.spawn.allData; } 185 186 @property auto sink(){ return this._sink; } 187 @property void sink(OutputRange f){ this._sink = f; } 188 189 void putAllData(){ 190 this._sink.put("\n>>>>>\n%s\n<<<<<", this.data); 191 } 192 } 193 194 public class Expect : ExpectImpl!ExpectSink{ 195 196 this(string cmd, File[] oFiles ...){ 197 ExpectSink newSink = ExpectSink(oFiles); 198 this(cmd, newSink); 199 } 200 this(string cmd, string[] args, File[] oFiles ...){ 201 ExpectSink newSink = ExpectSink(oFiles); 202 this(cmd, args, newSink); 203 } 204 this(string cmd, ExpectSink sink){ 205 this(cmd, [], sink); 206 } 207 this(string cmd, string[] args, ExpectSink sink){ 208 super(cmd, args, sink); 209 } 210 211 } 212 213 public struct ExpectSink{ 214 File[] files; 215 @property void addFile(File f){ files ~= f; } 216 @property File[] file(){ return files; } 217 218 void put(Args...)(string fmt, Args args){ 219 foreach(file; files){ 220 file.lockingTextWriter.put(format(fmt, args) ~ "\n"); 221 version(Windows){ 222 file.flush; // ensure it's printed. Only needed on windows for some reason... 223 } 224 } 225 } 226 } 227 228 /// Holds information on how to spawn off subprocesses 229 /// On Linux systems, it uses forkpty 230 /// On Windows systems, it uses OVERLAPPED io on named pipes 231 struct Spawn{ 232 string allData; 233 version(Posix){ 234 private Pty pty; 235 } 236 version(Windows){ 237 HANDLE inWritePipe; 238 HANDLE outReadPipe; 239 OVERLAPPED overlapped; 240 ubyte[4096] overlappedBuffer; 241 } 242 243 void spawn(string cmd){ 244 this.spawn(cmd, []); 245 } 246 void spawn(string cmd, string[] args){ 247 version(Posix){ 248 import std.path; 249 string firstArg = constructPathToExe(cmd); 250 if(args.length == 0 || args[0] != firstArg) 251 args = [firstArg] ~ args; 252 this.pty = spawnProcessInPty(cmd, args); 253 } 254 version(Windows){ // FIXME the constructing path to exe here is broken 255 // what happens when you send a relative path to this function? it breaks. 256 string fqp = cmd; 257 if(!cmd.isAbsolute) 258 fqp = cmd.constructPathToExe; 259 auto pipes = startChild(fqp, ([fqp] ~ args).join(" ")); 260 this.inWritePipe = pipes.inwritepipe; 261 this.outReadPipe = pipes.outreadpipe; 262 overlapped.hEvent = overlappedBuffer.ptr; 263 Thread.sleep(100.msecs); // need to give the pipes a moment to connect 264 } 265 } 266 /// On windows, calls CloseHandle on the io handles Spawn uses 267 /// Does nothing on linux as linux automatically closes resources when parent dies 268 void cleanup(){ 269 version(Windows){ 270 CloseHandle(this.inWritePipe); 271 CloseHandle(this.outReadPipe); 272 } 273 } 274 /// Sends command to the pty 275 public void sendData(string command){ 276 version(Posix){ 277 this.pty.sendToPty(command); 278 } 279 version(Windows){ 280 this.inWritePipe.writeData(command); 281 } 282 } 283 /// Returns the next toRead of data as a string 284 public void readNextChunk(){ 285 version(Posix){ 286 auto data = this.pty.readFromPty(); 287 import std.stdio; 288 if(data.length > 0) 289 allData ~= data.idup; 290 } 291 version(Windows){ 292 OVERLAPPED ov; 293 ov.Offset = allData.length; 294 if(ReadFileEx(this.outReadPipe, overlappedBuffer.ptr, overlappedBuffer.length, &ov, cast(void*)&readData) == 0){ 295 if(GetLastError == 997) 296 throw new ExpectException("readNextChunk - pending io"); 297 else { 298 // may need to handle other errors here 299 // TODO: Investigate 300 } 301 } 302 allData ~= (cast(char*)overlappedBuffer).fromStringz; 303 overlappedBuffer.destroy; 304 Thread.sleep(100.msecs); 305 } 306 } 307 } 308 309 version(Posix){ 310 extern(C) static int forkpty(int* master, char* name, void* termp, void* winp); 311 extern(C) static char* ttyname(int fd); 312 313 const toRead = 4096; 314 /** 315 * A data structure to hold information on a Pty session 316 * Holds its fd and a utility property to get its name 317 */ 318 public struct Pty{ 319 int fd; 320 @property string name(){ return ttyname(fd).fromStringz.idup; }; 321 } 322 323 /** 324 * Sets the Pty session to non-blocking mode 325 */ 326 void setNonBlocking(Pty pty){ 327 import core.sys.posix.unistd; 328 import core.sys.posix.fcntl; 329 int currFlags = fcntl(pty.fd, F_GETFL, 0) | O_NONBLOCK; 330 fcntl(pty.fd, F_SETFL, currFlags); 331 } 332 333 /** 334 * Spawns a process in a pty session 335 * By convention the first arg in args should be == program 336 */ 337 public Pty spawnProcessInPty(string program, string[] args) 338 { 339 import core.sys.posix.unistd; 340 import core.sys.posix.fcntl; 341 import core.thread; 342 Pty master; 343 int pid = forkpty(&(master).fd, null, null, null); 344 assert(pid != -1, "Error forking pty"); 345 if(pid==0){ //child 346 execl(program.toStringz, 347 args.length > 0 ? args.join(" ").toStringz : null , null); 348 } 349 else{ // master 350 int currFlags = fcntl(master.fd, F_GETFL, 0); 351 currFlags |= O_NONBLOCK; 352 fcntl(master.fd, F_SETFL, currFlags); 353 Thread.sleep(100.msecs); // slow down the main thread to give the child a chance to write something 354 return master; 355 } 356 return Pty(-1); 357 } 358 359 /** 360 * Sends a string to a pty. 361 */ 362 void sendToPty(Pty pty, string data){ 363 import core.sys.posix.unistd; 364 const(void)[] rawData = cast(const(void)[]) data; 365 while(rawData.length){ 366 long sent = write(pty.fd, rawData.ptr, rawData.length); 367 if(sent < 0) 368 throw new Exception(format("Error writing to %s", pty.name)); 369 rawData = rawData[sent..$]; 370 } 371 } 372 373 /** 374 * Reads from a pty session 375 * Returns the string that was read 376 */ 377 string readFromPty(Pty pty){ 378 import core.sys.posix.unistd; 379 import std.conv : to; 380 ubyte[toRead] buf; 381 immutable long len = read(pty.fd, buf.ptr, toRead); 382 if(len >= 0){ 383 return cast(string)(buf[0..len]); 384 } 385 return ""; 386 } 387 388 } 389 390 version(Windows){ 391 392 import core.sys.windows.windows; 393 394 /+ The below was stolen (and slightly modified) from Adam Ruppe's terminal emulator. 395 https://github.com/adamdruppe/terminal-emulator/blob/master/terminalemulator.d 396 Thanks Adam! 397 +/ 398 extern(Windows){ 399 /// Reads from an IO device (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365468%28v=vs.85%29.aspx) 400 BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*); 401 402 BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM); 403 404 BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, 405 PVOID Context, ULONG dwMilliseconds, ULONG dwFlags); 406 407 BOOL SetHandleInformation(HANDLE, DWORD, DWORD); 408 409 HANDLE CreateNamedPipeA( LPCTSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, 410 DWORD nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes); 411 412 BOOL UnregisterWait(HANDLE); 413 void SetLastError(DWORD); 414 private void readData(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped){ 415 auto data = (cast(ubyte*) overlapped.hEvent)[0 .. numberOfBytes]; 416 } 417 private void writeData(HANDLE h, string data){ 418 uint written; 419 // convert data into a c string 420 auto cstr = cast(void*)data.toStringz; 421 if(WriteFile(h, cstr, data.length, &written, null) == 0) 422 throw new ExpectException("WriteFile " ~ to!string(GetLastError())); 423 } 424 } 425 426 __gshared HANDLE waitHandle; 427 __gshared bool childDead; 428 429 void childCallback(void* tidp, bool) { 430 auto tid = cast(DWORD) tidp; 431 UnregisterWait(waitHandle); 432 433 PostThreadMessageA(tid, WM_QUIT, 0, 0); 434 childDead = true; 435 } 436 437 /// this is good. best to call it with plink.exe so it can talk to unix 438 /// note that plink asks for the password out of band, so it won't actually work like that. 439 /// thus specify the password on the command line or better yet, use a private key file 440 /// e.g. 441 /// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\""); 442 auto startChild(string program, string commandLine, bool pipeStderrToStdout=true) { 443 // thanks for a random person on stack overflow for this function 444 static BOOL MyCreatePipeEx(PHANDLE lpReadPipe, PHANDLE lpWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, 445 DWORD nSize, DWORD dwReadMode, DWORD dwWriteMode) 446 { 447 HANDLE ReadPipeHandle, WritePipeHandle; 448 DWORD dwError; 449 CHAR[MAX_PATH] PipeNameBuffer; 450 451 if (nSize == 0) { 452 nSize = 4096; 453 } 454 455 static int PipeSerialNumber = 0; 456 457 import core.stdc..string; 458 import core.stdc.stdio; 459 460 // could use format here, but C function will add \0 like windows wants 461 // so may as well use it 462 sprintf(PipeNameBuffer.ptr, 463 "\\\\.\\Pipe\\DExpectPipe.%08x.%08x".ptr, 464 GetCurrentProcessId(), 465 PipeSerialNumber++ 466 ); 467 468 ReadPipeHandle = CreateNamedPipeA( 469 PipeNameBuffer.ptr, 470 1/*PIPE_ACCESS_INBOUND*/ | dwReadMode, 471 0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/, 472 1, // Number of pipes 473 nSize, // Out buffer size 474 nSize, // In buffer size 475 120 * 1000, // Timeout in ms 476 lpPipeAttributes 477 ); 478 479 if (! ReadPipeHandle) { 480 return FALSE; 481 } 482 483 WritePipeHandle = CreateFileA( 484 PipeNameBuffer.ptr, 485 GENERIC_WRITE, 486 0, // No sharing 487 lpPipeAttributes, 488 OPEN_EXISTING, 489 FILE_ATTRIBUTE_NORMAL | dwWriteMode, 490 null // Template file 491 ); 492 493 if (INVALID_HANDLE_VALUE == WritePipeHandle) { 494 dwError = GetLastError(); 495 CloseHandle( ReadPipeHandle ); 496 SetLastError(dwError); 497 return FALSE; 498 } 499 500 *lpReadPipe = ReadPipeHandle; 501 *lpWritePipe = WritePipeHandle; 502 503 return( TRUE ); 504 } 505 506 SECURITY_ATTRIBUTES saAttr; 507 saAttr.nLength = SECURITY_ATTRIBUTES.sizeof; 508 saAttr.bInheritHandle = true; 509 saAttr.lpSecurityDescriptor = null; 510 511 HANDLE inreadPipe; 512 HANDLE inwritePipe; 513 if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0) 514 throw new Exception("CreatePipe"); 515 if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 516 throw new Exception("SetHandleInformation"); 517 HANDLE outreadPipe; 518 HANDLE outwritePipe; 519 if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0) 520 throw new Exception("CreatePipe"); 521 if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 522 throw new Exception("SetHandleInformation"); 523 524 STARTUPINFO startupInfo; 525 startupInfo.cb = startupInfo.sizeof; 526 527 startupInfo.dwFlags = STARTF_USESTDHANDLES; 528 startupInfo.hStdInput = inreadPipe; 529 startupInfo.hStdOutput = outwritePipe; 530 if(pipeStderrToStdout) 531 startupInfo.hStdError = outwritePipe; 532 else 533 startupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe; 534 535 536 PROCESS_INFORMATION pi; 537 538 if(commandLine.length > 255) 539 throw new Exception("command line too long"); 540 char[256] cmdLine; 541 cmdLine[0 .. commandLine.length] = commandLine[]; 542 cmdLine[commandLine.length] = 0; 543 544 if(CreateProcessA(program is null ? null : toStringz(program), cmdLine.ptr, null, null, true, 0/*0x08000000 /* CREATE_NO_WINDOW */, null /* environment */, null, &startupInfo, &pi) == 0) 545 throw new Exception("CreateProcess " ~ to!string(GetLastError())); 546 547 if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0) 548 throw new Exception("RegisterWaitForSingleObject"); 549 550 struct Pipes { HANDLE inwritepipe, outreadpipe; } 551 return Pipes(inwritePipe, outreadPipe); 552 553 } 554 } 555 556 /+ --------------- Utils --------------- +/ 557 /** 558 * Exceptions thrown during expecting data. 559 */ 560 class ExpectException : Exception { 561 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 562 super(message, file, line, next); 563 } 564 } 565 566 /** 567 * Searches all dirs on path for exe if required, 568 * or simply calls it if it's a relative or absolute path 569 */ 570 string constructPathToExe(string exe){ 571 import std.path; 572 import std.algorithm; 573 import std.file : exists; 574 import std.process : environment; 575 576 // if it already has a / or . at the start, assume the exe is correct 577 if(exe[0..1]==dirSeparator || exe[0..1]==".") return exe; 578 auto matches = environment["PATH"].split(pathSeparator) 579 .map!(path => path~dirSeparator~exe) 580 .filter!(path => path.exists); 581 return matches.empty ? exe : matches.front; 582 } 583 version(Posix){ 584 unittest{ 585 assert("sh".constructPathToExe == "/bin/sh"); 586 assert("./myexe".constructPathToExe == "./myexe"); 587 assert("/myexe".constructPathToExe == "/myexe"); 588 } 589 }