/*
* Eoulsan development code
*
* This code may be freely distributed and modified under the
* terms of the GNU Lesser General Public License version 2.1 or
* later and CeCILL-C. This should be distributed with the code.
* If you do not have a copy, see:
*
* http://www.gnu.org/licenses/lgpl-2.1.txt
* http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt
*
* Copyright for this code is held jointly by the Genomic platform
* of the Institut de Biologie de l'École normale supérieure and
* the individual authors. These should be listed in @author doc
* comments.
*
* For more information on the Eoulsan project and its aims,
* or to join the Eoulsan Google group, visit the home page
* at:
*
* http://outils.genomique.biologie.ens.fr/eoulsan
*
*/
package fr.ens.biologie.genomique.eoulsan.ui;
import static com.google.common.base.Preconditions.checkNotNull;
import static fr.ens.biologie.genomique.eoulsan.core.Step.StepState.ABORTED;
import static fr.ens.biologie.genomique.eoulsan.core.Step.StepState.DONE;
import static fr.ens.biologie.genomique.eoulsan.core.Step.StepState.FAILED;
import static fr.ens.biologie.genomique.eoulsan.core.Step.StepState.PARTIALLY_DONE;
import static fr.ens.biologie.genomique.eoulsan.core.Step.StepState.WORKING;
import static java.lang.String.format;
import java.util.HashMap;
import java.util.Map;
import com.googlecode.lanterna.TerminalFacade;
import com.googlecode.lanterna.terminal.Terminal;
import com.googlecode.lanterna.terminal.Terminal.Color;
import com.googlecode.lanterna.terminal.Terminal.SGR;
import com.googlecode.lanterna.terminal.TerminalSize;
import com.googlecode.lanterna.terminal.text.UnixTerminal;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.core.Step;
import fr.ens.biologie.genomique.eoulsan.core.Workflow;
import fr.ens.biologie.genomique.eoulsan.core.Step.StepState;
/**
* This class define an UI using Lanterna library.
* @author Laurent Jourdren
* @since 2.0
*/
public class LanternaUI extends AbstractUI implements Terminal.ResizeListener {
private Workflow workflow;
private final Map<Step, Double> steps = new HashMap<>();
private final Map<Step, Integer> submittedTasks = new HashMap<>();
private final Map<Step, Integer> runningTasks = new HashMap<>();
private final Map<Step, Integer> doneTasks = new HashMap<>();
private final Map<Step, Double> stepProgress = new HashMap<>();
private double globalProgress;
private UnixTerminal terminal;
private TerminalSize terminalSize;
private final Map<Step, Integer> stepLines = new HashMap<>();
private int lineCount;
private boolean jobDone;
//
// UI methods
//
@Override
public String getName() {
return "lanterna";
}
@Override
public void init(final Workflow workflow) {
checkNotNull(workflow, "workflow is null");
this.workflow = workflow;
// Search step to follow
searchSteps();
// Test if is interactive mode
if (!isInteractiveMode()) {
return;
}
// Set terminal object
this.terminal = TerminalFacade.createUnixTerminal();
// Get terminal size
this.terminal.enterPrivateMode();
this.terminalSize = this.terminal.getTerminalSize();
this.terminal.exitPrivateMode();
// Add resize listener
this.terminal.addResizeListener(this);
// Show Welcome message
System.out.println(Globals.WELCOME_MSG);
}
@Override
public void notifyStepState(final Step step) {
if (step == null || step.getWorkflow() != this.workflow) {
return;
}
switch (step.getState()) {
case READY:
notifyStepState(step, 0, 0, 0.0);
break;
case DONE:
case FAILED:
case ABORTED:
notifyStepState(step, 0, 0, 1.0);
break;
default:
break;
}
}
@Override
public void notifyStepState(final Step step, final int contextId,
final String contextName, final double progress) {
// Do nothing
}
@Override
public void notifyStepState(final Step step, final int terminatedTasks,
final int submittedTasks, final double progress) {
synchronized (this) {
if (!checkStep(step)) {
return;
}
if (step == null
|| step.getWorkflow() != this.workflow
|| !this.steps.containsKey(step)) {
return;
}
final StepState state = step.getState();
if (!(state == WORKING
|| state == PARTIALLY_DONE || state == DONE || state == FAILED
|| state == ABORTED)) {
return;
}
this.stepProgress.put(step, progress);
this.globalProgress = computeGlobalProgress(step, progress);
print(step, false, false, null);
}
}
@Override
public void notifyStepState(final Step step, final String note) {
// Do nothing
}
@Override
public void notifyWorkflowSuccess(final boolean success,
final String message) {
synchronized (this) {
// Do nothing if there is no terminal or if the job is completed
if (this.terminal == null || this.jobDone) {
return;
}
print(null, true, success, message);
}
}
@Override
public void notifyTaskSubmitted(final Step step, final int contextId) {
synchronized (this) {
if (!checkStep(step)) {
return;
}
notifyTask(step, contextId, this.submittedTasks, 1);
print(step, false, false, null);
}
}
@Override
public void notifyTaskRunning(final Step step, final int contextId) {
synchronized (this) {
if (!checkStep(step)) {
return;
}
notifyTask(step, contextId, this.runningTasks, 1);
print(step, false, false, null);
}
}
@Override
public void notifyTaskDone(final Step step, final int contextId) {
synchronized (this) {
if (!checkStep(step)) {
return;
}
notifyTask(step, contextId, this.runningTasks, -1);
notifyTask(step, contextId, this.doneTasks, 1);
print(step, false, false, null);
}
}
private void notifyTask(final Step step, final int contextId,
final Map<Step, Integer> map, final int diff) {
if (map.containsKey(step)) {
map.put(step, map.get(step) + diff);
} else {
map.put(step, 1);
}
}
//
// Update progress
//
private boolean checkStep(final Step step) {
// Do nothing if there is no terminal or if the job is completed
if (this.terminal == null || this.jobDone) {
return false;
}
if (step == null
|| step.getWorkflow() != this.workflow
|| !this.steps.containsKey(step)) {
return false;
}
final StepState state = step.getState();
if (!(state == WORKING
|| state == PARTIALLY_DONE || state == DONE || state == FAILED
|| state == ABORTED)) {
return false;
}
return true;
}
private synchronized void print(final Step step, final boolean endWorkflow,
final boolean success, final String successMessage) {
this.terminal.setCursorVisible(false);
if (endWorkflow) {
if (this.jobDone) {
return;
}
final int lastLineY = this.terminalSize.getRows() - 1;
// Update workflow progress
showWorkflowProgress(lastLineY, 1.0, success, successMessage);
this.terminal.moveCursor(0, lastLineY);
this.terminal.setCursorVisible(true);
this.jobDone = true;
return;
}
if (!this.stepLines.containsKey(step)) {
this.stepLines.put(step, this.lineCount);
this.lineCount++;
this.terminal.putCharacter('\n');
}
final int lastLineY = this.terminalSize.getRows() - 1;
final int stepLineY = lastLineY - this.lineCount + this.stepLines.get(step);
final Integer submittedTasks = this.submittedTasks.get(step);
final Integer runningTasks = this.runningTasks.get(step);
final Integer doneTasks = this.doneTasks.get(step);
final Double stepProgress = this.stepProgress.get(step);
// Update step progress
showStepProgress(stepLineY, step.getId(),
submittedTasks == null ? 0 : submittedTasks,
runningTasks == null ? 0 : runningTasks,
doneTasks == null ? 0 : doneTasks,
stepProgress == null ? 0.0 : stepProgress, step.getState());
// Update workflow progress
showWorkflowProgress(lastLineY, globalProgress, null, null);
this.terminal.moveCursor(0, lastLineY);
this.terminal.setCursorVisible(true);
}
/**
* Show the progress of a step.
* @param y y position of the line of the step
* @param stepId id of the step
* @param terminatedTasks the terminated tasks count
* @param submittedTasks the submitted tasks count
* @param progress progress of the step
*/
private void showStepProgress(final int y, final String stepId,
final int submittedTasks, final int runningTasks,
final int terminatedTasks, final double progress, final StepState state) {
int x = 0;
x = putString(x, y, " * Step ");
x = putStringSGR(x, y, String.format("%-40s", stepId), SGR.ENTER_BOLD);
x = putString(x, y, " ");
switch (state) {
case WORKING:
case PARTIALLY_DONE:
final String plural1 = submittedTasks > 1 ? "s" : "";
final String plural2 = runningTasks > 1 ? "s" : "";
x = putString(x, y,
format("%3.0f%% (%d/%d task%s done, %d task%s running)",
progress * 100, terminatedTasks, submittedTasks, plural1,
runningTasks, plural2));
break;
case DONE:
x = putStringColor(x, y, state.name(), Color.GREEN);
break;
case ABORTED:
case FAILED:
x = putStringColor(x, y, state.name(), Color.RED);
break;
default:
break;
}
clearEndOfLine(x, y);
}
/**
* Show the progress of the workflow.
* @param y y position of the line of the workflow
* @param progress progress of the workflow
* @param success success of the workflow
* @param message successMessage
*/
private void showWorkflowProgress(final int y, final double progress,
final Boolean success, final String message) {
int x = 0;
if (success != null) {
x = putString(x, y, " * Workflow ");
if (success) {
x = putStringColor(x, y, "DONE ", Color.GREEN);
} else {
x = putStringColor(x, y, "FAILED", Color.RED);
}
x = putString(x, y, " " + message);
this.terminal.putCharacter('\n');
} else {
x = putString(x, y,
String.format("%.0f%% workflow done", progress * 100.0));
}
clearEndOfLine(x, y);
}
//
// Terminal methods
//
/**
* Put a string on the terminal.
* @param x x position of the string
* @param y y position of the string
* @param s the string to print
* @return x position after printing the line
*/
private int putString(final int x, final int y, final String s) {
// Do nothing if the string is null
if (s == null) {
return x;
}
int currentX = x;
final int max = Math.min(s.length(), this.terminalSize.getColumns() - x);
for (int i = 0; i < max; i++) {
this.terminal.moveCursor(currentX, y);
this.terminal.putCharacter(s.charAt(i));
currentX++;
}
return currentX;
}
/**
* Put a string on the terminal with a defined color.
* @param x x position of the string
* @param y y position of the string
* @param s the string to print
* @param color the foreground color of the string
* @return x position after printing the line
*/
private int putStringColor(final int x, final int y, final String s,
final Terminal.Color color) {
if (color != null) {
this.terminal.applyForegroundColor(color);
}
final int result = putString(x, y, s);
if (color != null) {
this.terminal.applyForegroundColor(Color.DEFAULT);
}
return result;
}
/**
* Put a string on the terminal with a defined color.
* @param x x position of the string
* @param y y position of the string
* @param s the string to print
* @param sgr the formatting attribute to use
* @return x position after printing the line
*/
private int putStringSGR(final int x, final int y, final String s,
final Terminal.SGR sgr) {
if (sgr != null) {
this.terminal.applySGR(sgr);
}
final int result = putString(x, y, s);
if (sgr != null) {
final Terminal.SGR exitSGR;
switch (sgr) {
case ENTER_BLINK:
exitSGR = Terminal.SGR.EXIT_BLINK;
break;
case ENTER_BOLD:
exitSGR = Terminal.SGR.EXIT_BOLD;
break;
case ENTER_REVERSE:
exitSGR = Terminal.SGR.EXIT_REVERSE;
break;
case ENTER_UNDERLINE:
exitSGR = Terminal.SGR.EXIT_UNDERLINE;
break;
default:
exitSGR = Terminal.SGR.RESET_ALL;
break;
}
this.terminal.applySGR(exitSGR);
}
return result;
}
/**
* Clear the end of a terminal line.
* @param x x position where to start cleaning
* @param y y position of the line
*/
private void clearEndOfLine(final int x, final int y) {
final int max = this.terminalSize.getColumns();
for (int i = x; i < max; i++) {
this.terminal.moveCursor(i, y);
this.terminal.putCharacter(' ');
}
}
//
// Other methods
//
/**
* Search steps to follow.
*/
private void searchSteps() {
for (Step step : this.workflow.getSteps()) {
if (step == null) {
continue;
}
switch (step.getType()) {
case CHECKER_STEP:
case GENERATOR_STEP:
case STANDARD_STEP:
if (!step.isSkip()) {
this.steps.put(step, 0.0);
}
break;
default:
break;
}
}
}
/**
* Compute global progress.
* @param step step to update progress
* @param progress progress value of the step
* @return global progress as percent
*/
private double computeGlobalProgress(final Step step, final double progress) {
if (!this.steps.containsKey(step)) {
return -1;
}
// Update progress
this.steps.put(step, progress);
double sum = 0;
for (double p : this.steps.values()) {
sum += p;
}
return sum / this.steps.size();
}
//
// Listener
//
@Override
public void onResized(final TerminalSize terminalSize) {
synchronized (this) {
this.terminalSize = terminalSize;
}
}
}