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 }