Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: a07a5059cd726fdb68689edbf3839fe7eee1dfec (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*******************************************************************************
 * Copyright (c) 2019 Paul Pazderski and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Paul Pazderski - initial API and implementation
 *******************************************************************************/
package org.eclipse.debug.tests.console;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A mockup process which can either simulate generation of output or wait for
 * input to read.
 */
public class MockProcess extends Process {
	/**
	 * Use as run time parameter if mockup process should not terminate until
	 * {@link #destroy()} is used.
	 */
	public static final int RUN_FOREVER = -1;

	/** Mockup processe's standard streams. */
	private final ByteArrayOutputStream stdin = new ByteArrayOutputStream();
	private final InputStream stdout;
	private final InputStream stderr;

	/** Lock used in {@link #waitFor()}. */
	private final Object waitForTerminationLock = new Object();
	/**
	 * Store number of bytes received which are not buffered anymore (i.e. those
	 * input was already passed through {@link #getReceivedInput()}).
	 */
	private AtomicInteger receivedInput = new AtomicInteger(0);
	/**
	 * The time (in epoch milliseconds) when the mockup process terminates.
	 * <p>
	 * If this value is in the future it is the processes timeout. If it is in
	 * the past it is the processes termination time. If it is <code>-1</code>
	 * the process does not terminate and must be stopped using
	 * {@link #destroy()}.
	 * </p>
	 */
	private long endTime;

	/**
	 * Create new silent mockup process which runs for a given amount of time.
	 * Does not read input or produce any output.
	 *
	 * @param runTimeMs runtime of the mockup process in milliseconds. If
	 *            <code>0</code> the process terminates immediately. A
	 *            <i>negative</i> value means the mockup process never
	 *            terminates and must stopped with {@link #destroy()}.
	 */
	public MockProcess(long runTimeMs) {
		this(null, null, runTimeMs);
	}

	/**
	 * Create new mockup process and feed standard output streams with given
	 * content.
	 *
	 * @param stdout mockup process standard output stream. May be
	 *            <code>null</code>.
	 * @param stderr mockup process standard error stream. May be
	 *            <code>null</code>.
	 * @param runTimeMs runtime of the mockup process in milliseconds. If
	 *            <code>0</code> the process terminates immediately. A
	 *            <i>negative</i> value means the mockup process never
	 *            terminates and must stopped with {@link #destroy()}.
	 */
	public MockProcess(InputStream stdout, InputStream stderr, long runTimeMs) {
		super();
		this.stdout = (stdout != null ? stdout : new ByteArrayInputStream(new byte[0]));
		this.stderr = (stderr != null ? stderr : new ByteArrayInputStream(new byte[0]));
		this.endTime = runTimeMs < 0 ? RUN_FOREVER : System.currentTimeMillis() + runTimeMs;
	}

	/**
	 * Create new mockup process and wait for input on standard input stream.
	 * The mockup process terminates after receiving the given amount of data or
	 * after it's timeout.
	 *
	 * @param expectedInputSize number of bytes to receive before termination
	 * @param timeoutMs mockup process will be stopped after given amount of
	 *            milliseconds. If <i>negative</i> timeout is disabled.
	 */
	public MockProcess(final int expectedInputSize, long timeoutMs) {
		super();
		this.stdout = new ByteArrayInputStream(new byte[0]);
		this.stderr = new ByteArrayInputStream(new byte[0]);
		this.endTime = (timeoutMs > 0 ? System.currentTimeMillis() + timeoutMs : RUN_FOREVER);

		final Thread inputMonitor = new Thread(() -> {
			while (!MockProcess.this.isTerminated()) {
				synchronized (waitForTerminationLock) {
					if (receivedInput.get() + stdin.size() >= expectedInputSize) {
						endTime = System.currentTimeMillis();
						waitForTerminationLock.notifyAll();
						break;
					}
				}
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					break;
				}
			}
		}, "Mockup Process Input Monitor");
		inputMonitor.setDaemon(true);
		inputMonitor.start();
	}

	/**
	 * Get bytes received through stdin since last invocation of this method.
	 * <p>
	 * Not thread safe. It may miss some input if new content is written while
	 * this method is executed.
	 * </p>
	 *
	 * @return standard input since last invocation
	 */
	public synchronized byte[] getReceivedInput() {
		final byte[] content = stdin.toByteArray();
		stdin.reset();
		receivedInput.addAndGet(content.length);
		return content;
	}

	@Override
	public OutputStream getOutputStream() {
		return stdin;
	}

	@Override
	public InputStream getInputStream() {
		return stdout;
	}

	@Override
	public InputStream getErrorStream() {
		return stderr;
	}

	@Override
	public int waitFor() throws InterruptedException {
		synchronized (waitForTerminationLock) {
			while (!isTerminated()) {
				if (endTime == RUN_FOREVER) {
					waitForTerminationLock.wait();
				} else {
					final long waitTime = endTime - System.currentTimeMillis();
					if (waitTime > 0) {
						waitForTerminationLock.wait(waitTime);
					}
				}
			}
		}
		return 0;
	}

	@Override
	public int exitValue() {
		if (!isTerminated()) {
			final String end = (endTime == RUN_FOREVER ? "never." : "in " + (endTime - System.currentTimeMillis()) + " ms.");
			throw new IllegalThreadStateException("Mockup process terminates " + end);
		}
		return 0;
	}

	@Override
	public void destroy() {
		synchronized (waitForTerminationLock) {
			endTime = System.currentTimeMillis();
			waitForTerminationLock.notifyAll();
		}
	}

	/**
	 * Check if this process is already terminated.
	 *
	 * @return <code>true</code> if process is terminated
	 */
	private boolean isTerminated() {
		return endTime != RUN_FOREVER && System.currentTimeMillis() > endTime;
	}
}

Back to the top