Add stream-events command
The stream-events command outputs patchset added, comment added, change merged, and change abandoned events in JSON format over SSH. Streaming ends when the client disconnects or aborts the command. Change-Id: Iba3ff56c23ab91cf3b855d5dde6c40d9735b643f
This commit is contained in:
committed by
Shawn O. Pearce
parent
f458bf6337
commit
15ac1b83f5
228
gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
Executable file
228
gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
Executable file
@@ -0,0 +1,228 @@
|
||||
// Copyright (C) 2010 The Android Open Source Project
|
||||
//
|
||||
// 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.google.gerrit.sshd.commands;
|
||||
|
||||
import com.google.gerrit.common.ChangeHookRunner;
|
||||
import com.google.gerrit.common.ChangeListener;
|
||||
import com.google.gerrit.common.ChangeHookRunner.ChangeEvent;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.git.WorkQueue;
|
||||
import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
|
||||
import com.google.gerrit.sshd.BaseCommand;
|
||||
import com.google.gerrit.sshd.StreamCommandExecutor;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.apache.sshd.server.Environment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
final class StreamEvents extends BaseCommand {
|
||||
/** Maximum number of events that may be queued up for each connection. */
|
||||
private static final int MAX_EVENTS = 128;
|
||||
|
||||
/** Number of events to write before yielding off the thread. */
|
||||
private static final int BATCH_SIZE = 32;
|
||||
|
||||
@Inject
|
||||
private IdentifiedUser currentUser;
|
||||
|
||||
@Inject
|
||||
private ChangeHookRunner hooks;
|
||||
|
||||
@Inject
|
||||
@StreamCommandExecutor
|
||||
private WorkQueue.Executor pool;
|
||||
|
||||
/** Queue of events to stream to the connected user. */
|
||||
private final LinkedBlockingQueue<ChangeEvent> queue =
|
||||
new LinkedBlockingQueue<ChangeEvent>(MAX_EVENTS);
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/** Special event to notify clients they missed other events. */
|
||||
private final Object droppedOutputEvent = new Object() {
|
||||
@SuppressWarnings("unused")
|
||||
final String type = "dropped-output";
|
||||
};
|
||||
|
||||
private final ChangeListener listener = new ChangeListener() {
|
||||
@Override
|
||||
public void onChangeEvent(final ChangeEvent event) {
|
||||
offer(event);
|
||||
}
|
||||
};
|
||||
|
||||
private final CancelableRunnable writer = new CancelableRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
writeEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
onExit(0);
|
||||
}
|
||||
};
|
||||
|
||||
/** True if {@link #droppedOutputEvent} needs to be sent. */
|
||||
private volatile boolean dropped;
|
||||
|
||||
/** Lock to protect {@link #queue}, {@link #task}, {@link #done}. */
|
||||
private final Object taskLock = new Object();
|
||||
|
||||
/** True if no more messages should be sent to the output. */
|
||||
private boolean done;
|
||||
|
||||
/**
|
||||
* Currently scheduled task to spin out {@link #queue}.
|
||||
* <p>
|
||||
* This field is usually {@code null}, unless there is at least one object
|
||||
* present inside of {@link #queue} ready for delivery. Tasks are only started
|
||||
* when there are events to be sent.
|
||||
*/
|
||||
private Future<?> task;
|
||||
|
||||
private PrintWriter stdout;
|
||||
|
||||
@Override
|
||||
public void start(final Environment env) throws IOException {
|
||||
try {
|
||||
parseCommandLine();
|
||||
} catch (UnloggedFailure e) {
|
||||
String msg = e.getMessage();
|
||||
if (!msg.endsWith("\n")) {
|
||||
msg += "\n";
|
||||
}
|
||||
err.write(msg.getBytes("UTF-8"));
|
||||
err.flush();
|
||||
onExit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
stdout = toPrintWriter(out);
|
||||
hooks.addChangeListener(listener, currentUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExit(final int rc) {
|
||||
hooks.removeChangeListener(listener);
|
||||
|
||||
synchronized (taskLock) {
|
||||
done = true;
|
||||
}
|
||||
|
||||
super.onExit(rc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
hooks.removeChangeListener(listener);
|
||||
|
||||
final boolean exit;
|
||||
synchronized (taskLock) {
|
||||
if (task != null) {
|
||||
task.cancel(true);
|
||||
exit = false; // onExit will be invoked by the task cancellation.
|
||||
} else {
|
||||
exit = !done;
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
if (exit) {
|
||||
onExit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void offer(final ChangeEvent event) {
|
||||
synchronized (taskLock) {
|
||||
if (!queue.offer(event)) {
|
||||
dropped = true;
|
||||
}
|
||||
|
||||
if (task == null && !done) {
|
||||
task = pool.submit(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChangeEvent poll() {
|
||||
synchronized (taskLock) {
|
||||
ChangeEvent event = queue.poll();
|
||||
if (event == null) {
|
||||
task = null;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEvents() {
|
||||
int processed = 0;
|
||||
|
||||
while (processed < BATCH_SIZE) {
|
||||
if (Thread.interrupted() || stdout.checkError()) {
|
||||
// The other side either requested a shutdown by calling our
|
||||
// destroy() above, or it closed the stream and is no longer
|
||||
// accepting output. Either way terminate this instance.
|
||||
//
|
||||
hooks.removeChangeListener(listener);
|
||||
flush();
|
||||
onExit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropped) {
|
||||
write(droppedOutputEvent);
|
||||
dropped = false;
|
||||
}
|
||||
|
||||
final ChangeEvent event = poll();
|
||||
if (event == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
write(event);
|
||||
processed++;
|
||||
}
|
||||
|
||||
flush();
|
||||
|
||||
if (BATCH_SIZE <= processed) {
|
||||
// We processed the limit, but more might remain in the queue.
|
||||
// Schedule the write task again so we will come back here and
|
||||
// can process more events.
|
||||
//
|
||||
synchronized (taskLock) {
|
||||
task = pool.submit(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void write(final Object message) {
|
||||
final String msg = gson.toJson(message) + "\n";
|
||||
synchronized (stdout) {
|
||||
stdout.print(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
synchronized (stdout) {
|
||||
stdout.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user