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 }