![JAR search and dependency download from the Maven repository](/logo.png)
protoj.lang.DispatchFeature Maven / Gradle / Ivy
Show all versions of protoj-jdk6 Show documentation
/**
* Copyright 2009 Ashley Williams
*
* 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 protoj.lang;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Date;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import protoj.lang.command.JavaCommand;
import protoj.lang.internal.InformationException;
import protoj.lang.internal.ProjectReporter;
import protoj.util.ArgRunnable;
import protoj.util.JavaTask;
/**
* Responsible for dispatching each instruction from the
* {@link InstructionChain} to the matching command and executing that command.
* See {@link #dispatchCommands()}.
*
*
* @author Ashley Williams
*
*/
public final class DispatchFeature {
/**
* System property added when a new vm is being started via
* {@link #startVm(String, String[], String, ArgRunnable)} to indicate the
* vm is being started in-code.
*/
private static final String SCRIPT_VM_PROPERTY = "protoj.scriptVm";
/**
* See {@link #getJavaCommand()}.
*/
private JavaCommand javaCommand;
/**
* See {@link #getReporter()}.
*/
private ProjectReporter reporter;
/**
* See {@link #getMainClass()}.
*/
private String mainClass;
/**
* See {@link #getMainMemory()}.
*/
private String mainMemory;
/**
* The parent project.
*/
private final StandardProject parent;
/**
* See {@link #getCurrentMainClass()}.
*/
private String currentMainClass;
/**
* See {@link #isScriptVm()}.
*/
private boolean scriptVm;
/**
* {@link #isStatusWindow()}.
*/
private boolean statusWindow;
/**
* See {@link #getStartVmConfig()}.
*/
private ArgRunnable startVmConfig;
/**
* See {@link #getElapsedHours()}.
*/
private long elapsedHours;
/**
* See {@link #getElapsedMinutes()}.
*/
private long elapsedMinutes;
/**
* See {@link #getElapsedSeconds()}.
*/
private long elapsedSeconds;
/**
* See {@link #getWeaverJar()}.
*/
private File weaverJar;
/**
* Thrown by {@link #startVm(String, String[], String, ArgRunnable)} to
* indicate the started vm has returned with a non-zero exit code.
*/
private final static class StartVmException extends RuntimeException {
private static final long serialVersionUID = 1L;
public StartVmException(String message) {
super(message);
}
}
/**
* The created instance will be configured to bootstrap to the currently
* running jvm.
*
* @param parent
*/
public DispatchFeature(StandardProject parent) {
this.parent = parent;
this.javaCommand = new JavaCommand(this);
this.reporter = new ProjectReporter(parent);
this.currentMainClass = null;
this.currentMainClass = createCurrentMainClass();
this.scriptVm = createScriptVm();
this.statusWindow = false;
initBootstrap(getCurrentMainClass(), null);
}
/**
* Call in order to be able to start to the specified project main with the
* specified default amount of memory. Instructions that don't have commands
* available in the initial vm are delegated to the project vm.
*
* This memory may be overridden by the
* {@link StandardProperties#getMainMemory()} value as configured in the
* CoreConfig aspect.
*
* @param mainClass
* @param mainMemory
*/
public void initBootstrap(String mainClass, String mainMemory) {
this.mainClass = mainClass;
this.mainMemory = mainMemory;
}
/**
* ProtoJ commands are each executed in their own virtual machines. Use this
* method to ensure that aspectj load time weaving is globally used. This
* works by using the weaver jar as a java agent when the vm is started, so
* the name of the jar file must be specified.
*
* The lib directory will be searched for a jar file whose name contains the
* partial name parameter, which will be used as the javaagent jarpath.
* Specify null to use the default, which is "aspectjweaver.jar". If there
* is no match or more than one match is found then an exception is thrown
*
* @param partialName
* the partial name, specify null to use "aspectjweaver.jar"
*/
public void initLoadTimeWeaving(String partialName) {
final String searchName;
if (partialName == null) {
searchName = "aspectjweaver.jar";
} else {
searchName = partialName;
}
File libDir = parent.getLayout().getLibDir();
String[] matches = libDir.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.contains(searchName);
}
});
if (matches.length == 0) {
throw new InformationException(
"load time weaving bad config: couldn't find a jar file whose name contains "
+ searchName);
} else if (matches.length > 1) {
throw new InformationException(
"load time weaving bad config: found more than one jar file whose name contains "
+ searchName);
}
weaverJar = new File(libDir, matches[0]);
}
/**
* Creation method for the currentMainClass property. See
* {@link #getCurrentMainClass()}.
*
* @return
*/
private String createCurrentMainClass() {
try {
throw new RuntimeException();
} catch (Exception e) {
StackTraceElement[] trace = e.getStackTrace();
return trace[trace.length - 1].getClassName();
}
}
/**
* Creation method for the scriptVm property. See {@link #isScriptVm()}.
*
* @return
*/
private boolean createScriptVm() {
String scriptVm = System.getProperty(SCRIPT_VM_PROPERTY, Boolean.TRUE
.toString());
return Boolean.valueOf(scriptVm);
}
/**
* The helper used to report dispatch progress.
*
* @return
*/
public ProjectReporter getReporter() {
return reporter;
}
/**
* The elapsed time in hours.
*
* @return
*/
public long getElapsedHours() {
return elapsedHours;
}
/**
* The elapsed time in minutes.
*
* @return
*/
public long getElapsedMinutes() {
return elapsedMinutes;
}
/**
* The elapsed time in seconds.
*
* @return
*/
public long getElapsedSeconds() {
return elapsedSeconds;
}
/**
* Iterates over all the instructions from
* {@link StandardProject#getInstructionChain()} and executes each
* corresponding command from {@link StandardProject#getCommandStore()}. The
* instructions usually originate from the command line but may have been
* specified in-code.
*
* Where there is no corresponding command in the command store then we
* assume that this code is running in the hand-compiled bootstrapping vm
* that contains just the bare minimum of commands. Therefore we attempt to
* bootstrap to the main project vm resulting in the following behavior:
*
* - if we are already in the main project vm then there is nowhere else
* to try and so we log that the command cannot be found and terminate with
* an error code of 1
* - if we are not in the main project vm then we bootstrap to it with the
* remaining instructions and don't process any more here
*
*/
public void dispatchCommands() {
// the error handling is a little awkward due to the fact that a new vm
// could have been started and therefore we sometimes need to prevent
// status reporting if another vm has already done the reporting
try {
long startTimeMillis = new Date().getTime();
try {
parent.getInstructionChain().visitAndRemove(new Dispatcher());
} finally {
updateElapsedTime(startTimeMillis);
}
if (isScriptVm()) {
reporter.projectSucceeded();
}
exitApplication(0);
} catch (StartVmException e) {
if (e.getMessage() != null) {
// a failed started vm has already reported its own failure
// so just report additional information here
System.out.println(e.getMessage());
}
exitApplication(1);
} catch (InformationException e) {
reporter.projectFailed(e.getMessage());
exitApplication(1);
} catch (Throwable e) {
reporter.projectFailed(e);
exitApplication(1);
}
}
/**
* Exits the application with the specified error code. A status window will
* be shown in a non-headless environment when the exitWindow property is
* set to true.
*
* @param code
*/
private void exitApplication(int code) {
if (!isScriptVm()) {
System.exit(code);
} else {
if (isStatusWindow()) {
showStatusWindow(code);
} else {
System.exit(code);
}
}
}
/**
* Updates the amount of time the build took message. The results are
* available by calling getElapsedXXX methods.
*
* @param startTimeMillis
*/
private void updateElapsedTime(long startTimeMillis) {
long endTime = new Date().getTime();
long elapsedMillis = endTime - startTimeMillis;
long oneHourInMillis = 60 * 60 * 1000;
long oneMinuteInMillis = 60 * 1000;
long oneSecondInMillis = 1000;
elapsedHours = (elapsedMillis / oneHourInMillis);
float leftoverFromHours = (float) elapsedMillis
% (float) oneHourInMillis;
elapsedMinutes = (long) (leftoverFromHours / oneMinuteInMillis);
float leftoverFromMinutes = leftoverFromHours % oneMinuteInMillis;
elapsedSeconds = (long) (leftoverFromMinutes / oneSecondInMillis);
}
/**
* Shows the status window, but only from the script vm. Lots of vms get
* launched, so we don't want a window for all of them. TODO need to find a
* way to quit the swing frame with an error code of 1 not 0.
*
* @param code
*/
private void showStatusWindow(int code) {
JFrame frame = new JFrame();
frame.setSize(300, 100);
frame.setResizable(false);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
String elapsedTime = String.format("%02d:%02d:%02d", getElapsedHours(),
getElapsedMinutes(), getElapsedSeconds());
JLabel label = new JLabel(elapsedTime, SwingConstants.CENTER);
Font font = label.getFont().deriveFont(200f);
label.setFont(font);
panel.add(label);
panel.setBackground(code == 0 ? Color.green : Color.red);
frame.getContentPane().add(panel);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
frame.setSize(screenSize.width, screenSize.height);
frame.validate();
frame.setVisible(true);
}
/**
* Executes the command for the current instruction or delegates to the
* project vm if it can't be found. Requests early termination by calling
* {@link InstructionChain#breakVisit()} if after all this the command still
* can't be found.
*
* @author Ashley Williams
*
*/
private final class Dispatcher implements ArgRunnable {
public void run(Instruction instruction) {
CommandStore commandStore = parent.getCommandStore();
InstructionChain instructionChain = parent.getInstructionChain();
String name = instruction.getName();
Command command = commandStore.getCommand(name);
if (command != null) {
command.execute(instruction);
if (command.equals(javaCommand)) {
// remaining commands have been bootstrapped to new vm
instructionChain.breakVisit();
}
} else {
if (!isInProjectMain()) {
// the command isn't known here, let's try the project vm
startProjectMainVm();
} else {
// already at project vm so the command is confirmed unknown
StringBuilder reason = new StringBuilder();
reason
.append("the following instruction isn't recognized: \"");
reason.append(instruction.getText());
reason.append("\"");
throw new InformationException(reason.toString());
}
// we are done in this loop since we got to an unknown command
instructionChain.breakVisit();
}
}
}
/**
* Returns true if we are in the main class specified by
* {@link #getMainClass()}, false otherwise.
*
* @return
*/
private boolean isInProjectMain() {
return getCurrentMainClass().equals(getMainClass());
}
/**
* Starts a forked vm to the project main.
*
* This method was added to support {@link CoreProject#dispatchCommands()}
* since it needs to start the project vm for the purposes of accessing the
* set of new classes that may have been compiled.
*/
private void startProjectMainVm() {
String[] args = parent.getInstructionChain().createArgsAndRemove(false);
startVm(mainClass, args, mainMemory, getStartVmConfig());
}
/**
* Starts a forked vm with the specified parameters. Advice is also applied
* at a joinpoint for this method from the StandardConfig aspect in order to
* apply any debug settings.
*
* This method was added to support {@link Command#execute(Instruction)},
* since it needs to restart the same vm for the purpose of reconfiguring
* memory.
*
* @param mainClass
* the class that contains the main() method
* @param args
* the arguments to the main method
* @param memory
* the maximum amount of memory to be assigned to the new vm
* @param config
* the callback used to configure the vm before it is executed
*
* @return
*/
public JavaTask startVm(String mainClass, String[] args, String memory,
ArgRunnable config) {
// log details of the vm about to start
StringBuilder builder = new StringBuilder();
builder.append("starting vm " + mainClass + ":" + memory);
for (String arg : args) {
builder.append(" ");
builder.append(arg);
}
parent.getLogger().info(builder);
ProjectLayout layout = parent.getLayout();
JavaTask vm = new JavaTask();
vm.setMainClass(mainClass);
vm.initFork(memory, false);
vm.initArgs(args);
vm.initClasspath(layout.getClasspathConfig());
if (weaverJar != null) {
vm.initWeaving(weaverJar);
}
vm.initJvmargs("-D" + SCRIPT_VM_PROPERTY + "=false");
if (config != null) {
config.run(vm);
}
vm.execute();
vm.writeOutput();
if (!vm.isSuccess()) {
String message;
String stderr = vm.getStderr();
if (stderr != null
&& stderr.contains("java.lang.NoClassDefFoundError")) {
message = "did you remember to execute the compile command first of all?";
} else {
message = "";
}
throw new StartVmException(message);
}
return vm;
}
/**
* Calculates the name of the main() method containing class that was used
* to start the currently executing vm.
*
* @return
*/
public String getCurrentMainClass() {
return currentMainClass;
}
/**
* The main() method containing entry point class for the new vm.
*
* @return
*/
public String getMainClass() {
return mainClass;
}
/**
* The maximum amount of memory that should be allocated by the new vm.
*
* @return
*/
public String getMainMemory() {
return mainMemory;
}
/**
* Returns whether or not the current vm was started directly from the shell
* script or in code by using the
* {@link #startVm(String, String[], String, ArgRunnable)} method. Each time
* startVm() is called, the {@link #SCRIPT_VM_PROPERTY} is added as a jvm
* arg so that we can tell the difference.
*
* @return
*/
public boolean isScriptVm() {
return scriptVm;
}
/**
* See {@link #isStatusWindow()}.
*
* @param statusWindow
*/
public void setStatusWindow(boolean statusWindow) {
if (statusWindow && GraphicsEnvironment.isHeadless()) {
throw new RuntimeException(
"can't configure a status window in a headless environment");
}
this.statusWindow = statusWindow;
}
/**
* Returns whether or not the {@link #dispatchCommands()} will exit with a
* gui. If true then a window will be displayed with a green background
* indicating no errors or a red background indicating errors. The window
* will fill the screen and the application will exit when it is closed. It
* is an error to set this property to true in a headless environment
* resulting in an exception.
*
* @return
*/
public boolean isStatusWindow() {
return statusWindow;
}
/**
* See {@link #setStartVmConfig(ArgRunnable)}.
*
* @param startVmConfig
*/
public void setStartVmConfig(ArgRunnable startVmConfig) {
this.startVmConfig = startVmConfig;
}
/**
* The instance used to provide additional configuration before the vm is
* launched inside the
* {@link #startVm(String, String[], String, ArgRunnable)} method.
*
* @return
*/
public ArgRunnable getStartVmConfig() {
return startVmConfig;
}
public StandardProject getParent() {
return parent;
}
/**
* The jar file used for load time weaving, or null if not configured.
*
* @return
*/
public File getWeaverJar() {
return weaverJar;
}
/**
* The command to execute a java process.
*
* @return
*/
public JavaCommand getJavaCommand() {
return javaCommand;
}
}