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 190 public class Expect : ExpectImpl!ExpectSink{ 191 192 this(string cmd, File[] oFiles ...){ 193 ExpectSink newSink = ExpectSink(oFiles); 194 this(cmd, newSink); 195 } 196 this(string cmd, string[] args, File[] oFiles ...){ 197 ExpectSink newSink = ExpectSink(oFiles); 198 this(cmd, args, newSink); 199 } 200 this(string cmd, ExpectSink sink){ 201 this(cmd, [], sink); 202 } 203 this(string cmd, string[] args, ExpectSink sink){ 204 super(cmd, args, sink); 205 } 206 207 } 208 209 public struct ExpectSink{ 210 File[] files; 211 @property void addFile(File f){ files ~= f; } 212 @property File[] file(){ return files; } 213 214 void put(Args...)(string fmt, Args args){ 215 foreach(file; files){ 216 file.lockingTextWriter.put(format(fmt, args) ~ "\n"); 217 } 218 } 219 } 220 221 /// Holds information on how to spawn off subprocesses 222 /// On Linux systems, it uses forkpty 223 /// On Windows systems, it uses OVERLAPPED io on named pipes 224 struct Spawn{ 225 string allData; 226 version(Posix){ 227 private Pty pty; 228 } 229 version(Windows){ 230 HANDLE inWritePipe; 231 HANDLE outReadPipe; 232 OVERLAPPED overlapped; 233 ubyte[4096] overlappedBuffer; 234 } 235 236 void spawn(string cmd){ 237 this.spawn(cmd, []); 238 } 239 void spawn(string cmd, string[] args){ 240 version(Posix){ 241 import std.path; 242 string firstArg = constructPathToExe(cmd); 243 if(args.length == 0 || args[0] != firstArg) 244 args = [firstArg] ~ args; 245 this.pty = spawnProcessInPty(cmd, args); 246 } 247 version(Windows){ // FIXME the constructing path to exe here is broken 248 // what happens when you send a relative path to this function? it breaks. 249 string fqp = cmd; 250 if(!cmd.isAbsolute) 251 fqp = cmd.constructPathToExe; 252 auto pipes = startChild(fqp, ([fqp] ~ args).join(" ")); 253 this.inWritePipe = pipes.inwritepipe; 254 this.outReadPipe = pipes.outreadpipe; 255 overlapped.hEvent = overlappedBuffer.ptr; 256 Thread.sleep(100.msecs); // need to give the pipes a moment to connect 257 } 258 } 259 /// On windows, calls CloseHandle on the io handles Spawn uses 260 /// Does nothing on linux as linux automatically closes resources when parent dies 261 void cleanup(){ 262 version(Windows){ 263 CloseHandle(this.inWritePipe); 264 CloseHandle(this.outReadPipe); 265 } 266 } 267 /// Sends command to the pty 268 public void sendData(string command){ 269 version(Posix){ 270 this.pty.sendToPty(command); 271 } 272 version(Windows){ 273 this.inWritePipe.writeData(command); 274 } 275 } 276 /// Returns the next toRead of data as a string 277 public void readNextChunk(){ 278 version(Posix){ 279 auto data = this.pty.readFromPty(); 280 import std.stdio; 281 if(data.length > 0) 282 allData ~= data.idup; 283 } 284 version(Windows){ 285 OVERLAPPED ov; 286 ov.Offset = allData.length; 287 if(ReadFileEx(this.outReadPipe, overlappedBuffer.ptr, overlappedBuffer.length, &ov, cast(void*)&readData) == 0){ 288 if(GetLastError == 997) 289 throw new ExpectException("readNextChunk - pending io"); 290 else { 291 // may need to handle other errors here 292 // TODO: Investigate 293 } 294 } 295 allData ~= (cast(char*)overlappedBuffer).fromStringz; 296 overlappedBuffer.destroy; 297 Thread.sleep(100.msecs); 298 } 299 } 300 } 301 302 version(Posix){ 303 extern(C) static int forkpty(int* master, char* name, void* termp, void* winp); 304 extern(C) static char* ttyname(int fd); 305 306 const toRead = 4096; 307 /** 308 * A data structure to hold information on a Pty session 309 * Holds its fd and a utility property to get its name 310 */ 311 public struct Pty{ 312 int fd; 313 @property string name(){ return ttyname(fd).fromStringz.idup; }; 314 } 315 316 /** 317 * Sets the Pty session to non-blocking mode 318 */ 319 void setNonBlocking(Pty pty){ 320 import core.sys.posix.unistd; 321 import core.sys.posix.fcntl; 322 int currFlags = fcntl(pty.fd, F_GETFL, 0) | O_NONBLOCK; 323 fcntl(pty.fd, F_SETFL, currFlags); 324 } 325 326 /** 327 * Spawns a process in a pty session 328 * By convention the first arg in args should be == program 329 */ 330 public Pty spawnProcessInPty(string program, string[] args) 331 { 332 import core.sys.posix.unistd; 333 import core.sys.posix.fcntl; 334 import core.thread; 335 Pty master; 336 int pid = forkpty(&(master).fd, null, null, null); 337 assert(pid != -1, "Error forking pty"); 338 if(pid==0){ //child 339 execl(program.toStringz, 340 args.length > 0 ? args.join(" ").toStringz : null , null); 341 } 342 else{ // master 343 int currFlags = fcntl(master.fd, F_GETFL, 0); 344 currFlags |= O_NONBLOCK; 345 fcntl(master.fd, F_SETFL, currFlags); 346 Thread.sleep(100.msecs); // slow down the main thread to give the child a chance to write something 347 return master; 348 } 349 return Pty(-1); 350 } 351 352 /** 353 * Sends a string to a pty. 354 */ 355 void sendToPty(Pty pty, string data){ 356 import core.sys.posix.unistd; 357 const(void)[] rawData = cast(const(void)[]) data; 358 while(rawData.length){ 359 long sent = write(pty.fd, rawData.ptr, rawData.length); 360 if(sent < 0) 361 throw new Exception(format("Error writing to %s", pty.name)); 362 rawData = rawData[sent..$]; 363 } 364 } 365 366 /** 367 * Reads from a pty session 368 * Returns the string that was read 369 */ 370 string readFromPty(Pty pty){ 371 import core.sys.posix.unistd; 372 import std.conv : to; 373 ubyte[toRead] buf; 374 immutable long len = read(pty.fd, buf.ptr, toRead); 375 if(len >= 0){ 376 return cast(string)(buf[0..len]); 377 } 378 return ""; 379 } 380 381 } 382 383 version(Windows){ 384 385 import core.sys.windows.windows; 386 387 /+ The below was stolen (and slightly modified) from Adam Ruppe's terminal emulator. 388 https://github.com/adamdruppe/terminal-emulator/blob/master/terminalemulator.d 389 Thanks Adam! 390 +/ 391 extern(Windows){ 392 /// Reads from an IO device (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365468%28v=vs.85%29.aspx) 393 BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*); 394 395 BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM); 396 397 BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, 398 PVOID Context, ULONG dwMilliseconds, ULONG dwFlags); 399 400 BOOL SetHandleInformation(HANDLE, DWORD, DWORD); 401 402 HANDLE CreateNamedPipeA( LPCTSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, 403 DWORD nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes); 404 405 BOOL UnregisterWait(HANDLE); 406 void SetLastError(DWORD); 407 private void readData(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped){ 408 auto data = (cast(ubyte*) overlapped.hEvent)[0 .. numberOfBytes]; 409 } 410 private void writeData(HANDLE h, string data){ 411 uint written; 412 // convert data into a c string 413 auto cstr = cast(void*)data.toStringz; 414 if(WriteFile(h, cstr, data.length, &written, null) == 0) 415 throw new ExpectException("WriteFile " ~ to!string(GetLastError())); 416 } 417 } 418 419 __gshared HANDLE waitHandle; 420 __gshared bool childDead; 421 422 void childCallback(void* tidp, bool) { 423 auto tid = cast(DWORD) tidp; 424 UnregisterWait(waitHandle); 425 426 PostThreadMessageA(tid, WM_QUIT, 0, 0); 427 childDead = true; 428 } 429 430 /// this is good. best to call it with plink.exe so it can talk to unix 431 /// note that plink asks for the password out of band, so it won't actually work like that. 432 /// thus specify the password on the command line or better yet, use a private key file 433 /// e.g. 434 /// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\""); 435 auto startChild(string program, string commandLine) { 436 // thanks for a random person on stack overflow for this function 437 static BOOL MyCreatePipeEx(PHANDLE lpReadPipe, PHANDLE lpWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, 438 DWORD nSize, DWORD dwReadMode, DWORD dwWriteMode) 439 { 440 HANDLE ReadPipeHandle, WritePipeHandle; 441 DWORD dwError; 442 CHAR[MAX_PATH] PipeNameBuffer; 443 444 if (nSize == 0) { 445 nSize = 4096; 446 } 447 448 static int PipeSerialNumber = 0; 449 450 import core.stdc..string; 451 import core.stdc.stdio; 452 453 // could use format here, but C function will add \0 like windows wants 454 // so may as well use it 455 sprintf(PipeNameBuffer.ptr, 456 "\\\\.\\Pipe\\DExpectPipe.%08x.%08x".ptr, 457 GetCurrentProcessId(), 458 PipeSerialNumber++ 459 ); 460 461 ReadPipeHandle = CreateNamedPipeA( 462 PipeNameBuffer.ptr, 463 1/*PIPE_ACCESS_INBOUND*/ | dwReadMode, 464 0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/, 465 1, // Number of pipes 466 nSize, // Out buffer size 467 nSize, // In buffer size 468 120 * 1000, // Timeout in ms 469 lpPipeAttributes 470 ); 471 472 if (! ReadPipeHandle) { 473 return FALSE; 474 } 475 476 WritePipeHandle = CreateFileA( 477 PipeNameBuffer.ptr, 478 GENERIC_WRITE, 479 0, // No sharing 480 lpPipeAttributes, 481 OPEN_EXISTING, 482 FILE_ATTRIBUTE_NORMAL | dwWriteMode, 483 null // Template file 484 ); 485 486 if (INVALID_HANDLE_VALUE == WritePipeHandle) { 487 dwError = GetLastError(); 488 CloseHandle( ReadPipeHandle ); 489 SetLastError(dwError); 490 return FALSE; 491 } 492 493 *lpReadPipe = ReadPipeHandle; 494 *lpWritePipe = WritePipeHandle; 495 496 return( TRUE ); 497 } 498 499 SECURITY_ATTRIBUTES saAttr; 500 saAttr.nLength = SECURITY_ATTRIBUTES.sizeof; 501 saAttr.bInheritHandle = true; 502 saAttr.lpSecurityDescriptor = null; 503 504 HANDLE inreadPipe; 505 HANDLE inwritePipe; 506 if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0) 507 throw new Exception("CreatePipe"); 508 if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 509 throw new Exception("SetHandleInformation"); 510 HANDLE outreadPipe; 511 HANDLE outwritePipe; 512 if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0) 513 throw new Exception("CreatePipe"); 514 if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 515 throw new Exception("SetHandleInformation"); 516 517 STARTUPINFO startupInfo; 518 startupInfo.cb = startupInfo.sizeof; 519 520 startupInfo.dwFlags = STARTF_USESTDHANDLES; 521 startupInfo.hStdInput = inreadPipe; 522 startupInfo.hStdOutput = outwritePipe; 523 startupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe; 524 525 PROCESS_INFORMATION pi; 526 527 if(commandLine.length > 255) 528 throw new Exception("command line too long"); 529 char[256] cmdLine; 530 cmdLine[0 .. commandLine.length] = commandLine[]; 531 cmdLine[commandLine.length] = 0; 532 533 if(CreateProcessA(program is null ? null : toStringz(program), cmdLine.ptr, null, null, true, 0/*0x08000000 /* CREATE_NO_WINDOW */, null /* environment */, null, &startupInfo, &pi) == 0) 534 throw new Exception("CreateProcess " ~ to!string(GetLastError())); 535 536 if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0) 537 throw new Exception("RegisterWaitForSingleObject"); 538 539 struct Pipes { HANDLE inwritepipe, outreadpipe; } 540 return Pipes(inwritePipe, outreadPipe); 541 542 } 543 } 544 545 /+ --------------- Utils --------------- +/ 546 /** 547 * Exceptions thrown during expecting data. 548 */ 549 class ExpectException : Exception { 550 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 551 super(message, file, line, next); 552 } 553 } 554 555 /** 556 * Searches all dirs on path for exe if required, 557 * or simply calls it if it's a relative or absolute path 558 */ 559 string constructPathToExe(string exe){ 560 import std.path; 561 import std.algorithm; 562 import std.file : exists; 563 import std.process : environment; 564 565 // if it already has a / or . at the start, assume the exe is correct 566 if(exe[0..1]==dirSeparator || exe[0..1]==".") return exe; 567 auto matches = environment["PATH"].split(pathSeparator) 568 .map!(path => path~dirSeparator~exe) 569 .filter!(path => path.exists); 570 return matches.empty ? exe : matches.front; 571 } 572 version(Posix){ 573 unittest{ 574 assert("sh".constructPathToExe == "/bin/sh"); 575 assert("./myexe".constructPathToExe == "./myexe"); 576 assert("/myexe".constructPathToExe == "/myexe"); 577 } 578 }