/*
* Copyright 2015-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.util;
import com.facebook.buck.timing.SettableFakeClock;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.Multimap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/** Fake implementation of {@link ListeningProcessExecutor} for tests. */
public class FakeListeningProcessExecutor extends ListeningProcessExecutor {
private final Function<ProcessExecutorParams, Collection<FakeListeningProcessState>>
processStatesFunction;
private final SettableFakeClock clock;
public FakeListeningProcessExecutor(
Multimap<ProcessExecutorParams, FakeListeningProcessState> processStates) {
this(Functions.forMap(processStates.asMap()), new SettableFakeClock(0, 0));
}
public FakeListeningProcessExecutor(
Multimap<ProcessExecutorParams, FakeListeningProcessState> processStates,
SettableFakeClock clock) {
this(Functions.forMap(processStates.asMap()), clock);
}
public FakeListeningProcessExecutor(
Function<ProcessExecutorParams, Collection<FakeListeningProcessState>> processStatesFunction,
SettableFakeClock clock) {
this.processStatesFunction = processStatesFunction;
this.clock = clock;
}
private static class FakeLaunchedProcessImpl implements LaunchedProcess {
public final ProcessListener listener;
public final Iterator<FakeListeningProcessState> states;
public final SettableFakeClock clock;
public final long processExecTimeNanos;
public final ByteBuffer stdinBuffer;
public boolean processingStates;
public FakeListeningProcessState currentState;
public ByteArrayOutputStream stdinBytes;
public WritableByteChannel stdinBytesChannel;
public boolean stdinClosed;
public boolean wantsWrite;
public int exitCode;
public long startTimeNanos;
public long processTimeNanos;
public FakeLaunchedProcessImpl(
ProcessListener listener,
Iterator<FakeListeningProcessState> states,
SettableFakeClock clock,
long processExecTimeNanos) {
this.listener = listener;
this.states = states;
this.clock = clock;
this.processExecTimeNanos = processExecTimeNanos;
this.stdinBuffer = ByteBuffer.allocate(BUFFER_CAPACITY);
this.stdinBytes = new ByteArrayOutputStream();
this.stdinBytesChannel = Channels.newChannel(stdinBytes);
this.exitCode = -1;
this.startTimeNanos = this.clock.nanoTime();
}
public void processAllStates() {
if (processingStates) {
// Don't recurse.
return;
}
if (currentState != null) {
if (!processState(currentState)) {
return;
} else {
currentState = null;
}
}
while (states.hasNext()) {
currentState = states.next();
if (!processState(currentState)) {
return;
} else {
currentState = null;
}
}
}
private boolean processState(FakeListeningProcessState state) {
processingStates = true;
boolean result = true;
switch (state.getType()) {
case EXPECT_STDIN:
if (stdinClosed) {
throw new RuntimeException("stdin is closed");
}
if (!wantsWrite) {
result = false;
break;
}
while (wantsWrite) {
wantsWrite = listener.onStdinReady(stdinBuffer);
try {
stdinBytesChannel.write(stdinBuffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
stdinBuffer.clear();
}
if (!ByteBuffer.wrap(stdinBytes.toByteArray()).equals(state.getExpectedStdin().get())) {
throw new RuntimeException("Did not reach expected stdin state");
}
stdinBytes = new ByteArrayOutputStream();
stdinBytesChannel = Channels.newChannel(stdinBytes);
break;
case EXPECT_STDIN_CLOSED:
if (!stdinClosed) {
result = false;
break;
}
break;
case STDOUT:
while (state.getStdout().get().hasRemaining()) {
listener.onStdout(state.getStdout().get(), false);
}
break;
case STDERR:
while (state.getStderr().get().hasRemaining()) {
listener.onStderr(state.getStderr().get(), false);
}
break;
case WAIT:
long stateWaitTime = state.getWaitNanos().get();
if (clock.nanoTime() < startTimeNanos + processTimeNanos + stateWaitTime) {
result = false;
break;
}
processTimeNanos += stateWaitTime;
break;
case EXIT:
exitCode = state.getExitCode().get();
ByteBuffer empty = ByteBuffer.allocate(0);
listener.onStdout(empty, true);
listener.onStderr(empty, true);
listener.onExit(exitCode);
break;
}
processingStates = false;
return result;
}
@Override
public void wantWrite() {
this.wantsWrite = true;
processAllStates();
}
@Override
public void writeStdin(ByteBuffer buffer) {
try {
stdinBytesChannel.write(buffer);
processAllStates();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void closeStdin(boolean force) {
stdinClosed = true;
processAllStates();
}
@Override
public boolean hasPendingWrites() {
return stdinBytes.size() > 0;
}
@Override
public boolean isRunning() {
return states.hasNext();
}
}
@Override
public LaunchedProcess launchProcess(
ProcessExecutorParams params, final ProcessListener listener) {
Collection<FakeListeningProcessState> fakeProcessStates = processStatesFunction.apply(params);
long processExecTimeNanos = 0;
for (FakeListeningProcessState state : fakeProcessStates) {
if (state.getType() == FakeListeningProcessState.Type.WAIT) {
processExecTimeNanos += state.getWaitNanos().get();
}
}
FakeLaunchedProcessImpl process =
new FakeLaunchedProcessImpl(
listener, processStatesFunction.apply(params).iterator(), clock, processExecTimeNanos);
listener.onStart(process);
return process;
}
@Override
public int waitForProcess(LaunchedProcess process, long timeout, TimeUnit timeUnit)
throws InterruptedException {
FakeLaunchedProcessImpl processImpl = (FakeLaunchedProcessImpl) process;
clock.advanceTimeNanos(Math.min(processImpl.processExecTimeNanos, timeUnit.toNanos(timeout)));
processImpl.processAllStates();
if (processImpl.isRunning()) {
return Integer.MIN_VALUE;
} else {
return processImpl.exitCode;
}
}
@Override
public void destroyProcess(LaunchedProcess process, boolean force) {
FakeLaunchedProcessImpl processImpl = (FakeLaunchedProcessImpl) process;
while (processImpl.states.hasNext()) {
processImpl.states.next();
}
processImpl.currentState = null;
}
}