From 244001fd9900f31ae9371da4aeb3ad54862df366 Mon Sep 17 00:00:00 2001 From: Simon Ebner Date: Fri, 20 Dec 2013 08:54:55 +0100 Subject: [PATCH] First working (messy) version of client/server split FDA-1 --- ch.psi.fda/Readme.md | 2 +- .../java/ch/psi/fda/AcquisitionRMain.java | 426 ++++++++++++++++++ .../main/java/ch/psi/fda/StreamClient.java | 73 +++ .../src/main/java/ch/psi/fda/ViewerMain.java | 36 +- .../ch/psi/fda/rest/AcquisitionEngine.java | 10 + .../main/java/ch/psi/fda/rest/RestClient.java | 58 +++ .../main/java/ch/psi/fda/rest/RestServer.java | 8 +- .../ch/psi/fda/rest/services/ScanService.java | 8 +- 8 files changed, 583 insertions(+), 38 deletions(-) create mode 100644 ch.psi.fda/src/main/java/ch/psi/fda/AcquisitionRMain.java create mode 100644 ch.psi.fda/src/main/java/ch/psi/fda/StreamClient.java create mode 100644 ch.psi.fda/src/main/java/ch/psi/fda/rest/RestClient.java diff --git a/ch.psi.fda/Readme.md b/ch.psi.fda/Readme.md index 1a9aee4..1fa21b9 100644 --- a/ch.psi.fda/Readme.md +++ b/ch.psi.fda/Readme.md @@ -16,7 +16,7 @@ To use the FDA libary in an other project use: # Development When checking out the project from the repository there is the `target/generated-sources/xjc` folder missing. -After checking out the project execute `mvn compile` to create the folder and the required classes. +After checking out the project execute `mvn compile` to create the folder and the required classes. To build project use `mvn clean install`. diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/AcquisitionRMain.java b/ch.psi.fda/src/main/java/ch/psi/fda/AcquisitionRMain.java new file mode 100644 index 0000000..37f8888 --- /dev/null +++ b/ch.psi.fda/src/main/java/ch/psi/fda/AcquisitionRMain.java @@ -0,0 +1,426 @@ +/** + * + * Copyright 2010 Paul Scherrer Institute. All rights reserved. + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This code is distributed in the hope that it will be useful, + * but without any warranty; without even the implied warranty of + * merchantability or fitness for a particular purpose. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + * + */ + +package ch.psi.fda; + +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.ScrollPaneLayout; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; + +import sun.misc.Signal; +import sun.misc.SignalHandler; +import ch.psi.fda.aq.VisualizationMapper; +import ch.psi.fda.gui.ProgressPanel; +import ch.psi.fda.gui.ScrollableFlowPanel; +import ch.psi.fda.install.ApplicationConfigurator; +import ch.psi.fda.model.ModelManager; +import ch.psi.fda.model.v1.Configuration; +import ch.psi.fda.model.v1.Data; +import ch.psi.fda.rest.RestClient; +import ch.psi.fda.visualizer.Visualizer; + +/** + * Entry class for command line based data acquisition + */ +@SuppressWarnings("restriction") +public class AcquisitionRMain { + + private static Logger logger = Logger.getLogger(AcquisitionRMain.class.getName()); + + private static boolean abortedViaSignal = false; + + /** + * Main Program + * Process exit code: -1 if wrong number of arguments are passed + * Process exit code: 3 if aborted via Ctrl+C + * + * @param args Arguments of the program + */ + public static void main(String[] args) { + + String scriptname = "fda_scan"; + + Integer iterations = null; + boolean autoclose = false; + boolean nogui = false; + String files[] = null; + + HashMap varTable = new HashMap(); + + // Iterations option + OptionBuilder.hasArg(); + OptionBuilder.withArgName("iterations"); + OptionBuilder.withDescription("Number of iterations"); + OptionBuilder.withType(new Integer(1)); + Option o_iterations = OptionBuilder.create( "iterations"); + + // Variables option + OptionBuilder.hasArg(); + OptionBuilder.withArgName("variables"); + OptionBuilder.withDescription("Scan variables - variables are specified in the form var=value,var2=value2"); + OptionBuilder.withType(new Integer(1)); + Option o_variables = OptionBuilder.create( "variables"); + + Option o_autoclose = new Option( "autoclose", "Close down application after scan" ); + Option o_init = new Option( "initialize", "Initialize application directories and configuration files" ); + Option o_nogui = new Option( "nogui", "Do not show scan GUI" ); + + Options options = new Options(); + options.addOption(o_variables); + options.addOption(o_iterations); + options.addOption(o_autoclose); + options.addOption(o_init); + options.addOption(o_nogui); + + CommandLineParser parser = new GnuParser(); + // Parse the command line arguments + try { + CommandLine line = parser.parse( options, args ); + + // Initialize application + if( line.hasOption(o_init.getOpt()) ){ + // Initialize application + ApplicationConfigurator ac = new ApplicationConfigurator(); + ac.initializeApplication(); + System.exit(0); + } + + if(line.getArgs().length<1){ + throw new ParseException("One argument is required"); + } + + files=line.getArgs(); + + // Iterations option + if( line.hasOption(o_iterations.getOpt()) ){ + iterations = Integer.parseInt(line.getOptionValue(o_iterations.getOpt())); + } + + // Variables + if( line.hasOption(o_variables.getOpt()) ){ + String variables = line.getOptionValue(o_variables.getOpt() ); + String[] vars = variables.split(","); + for(String varp:vars){ + String[] pair = varp.split("="); + if(pair.length!=2){ + throw new ParseException("Variables are not specified the correct way. -variables var1=val1,var2=val2"); + } + varTable.put(pair[0], pair[1]); + } + } + + // Autoclose option + if( line.hasOption( o_autoclose.getOpt() ) ) { + autoclose = true; + } + + // No GUI option + if( line.hasOption( o_nogui.getOpt() ) ) { + nogui = true; + } + + } catch (ParseException e) { + System.err.println(e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printUsage(new PrintWriter(System.out, true), HelpFormatter.DEFAULT_WIDTH, scriptname, options); + System.exit(-1); + } + + // Run application + try{ + for(String file: files){ + run(new File(file), iterations, autoclose, nogui); + } + + // Close application automatically if autoclose option is set (and visualizations are specified) + if(nogui || autoclose ){ + System.exit(0); + } + + } + catch(Exception ee){ + System.out.println("Acquisition failed due to: "+ee.getMessage()); + logger.log(Level.SEVERE, "Acquisition failed due to: ", ee); // Do not print stack trace + System.exit(-1); + } + + } + + /** + * Run scan + * @param file Scan file + * @param iterations Number of iterations + * @param autoclose Flag whether to close the application automatically after the scan + * @param nogui Flag whether to run the scan with a GUI + */ + public static void run(File file, Integer iterations, boolean autoclose, boolean nogui){ + +// // Initialize application +// ApplicationConfigurator ac = new ApplicationConfigurator(); +// ac.initializeApplication(); + + if(!file.exists()){ + throw new RuntimeException("File "+file.getAbsolutePath()+" does not exist"); + } + + Configuration c; + try { + c = ModelManager.unmarshall(file); + } catch (Exception e) { + throw new RuntimeException("Unable to deserialize configuration: "+e.getMessage(), e); + } + + // Set data file name + // Determine name used for the data file + String name = file.getName(); + name = name.replaceAll("\\.xml$", ""); + + if(c.getData()!=null){ + Data data = c.getData(); + // Only update filename if no name is specified in xml file + if(data.getFileName()==null){ + data.setFileName(name); + } + } + else{ + Data data = new Data(); + data.setFileName(name); + c.setData(data); + } + + + // Override number of executions + if(iterations != null){ + c.setNumberOfExecution(iterations); + } + // Fix configuration if iterations is specified with 0 and no iterations option is specified + if(c.getNumberOfExecution()==0){ + c.setNumberOfExecution(1); + } + + // Create/get acquisition engine +// final Acquisition acquisition = new Acquisition(new DefaultChannelService(), new AcquisitionConfiguration()); + + boolean vis = false; + // Only register data visualization task/processor if there are visualizations + if(c.getVisualization().size()>0 && !nogui){ + vis=true; + } + + EventBus b = new AsyncEventBus(Executors.newSingleThreadExecutor()); + + final RestClient client = new RestClient(); + + +// acquisition.initalize(b, c); + + Visualizer visualizer = null; + // Only register data visualization task/processor if there are visualizations + if(vis){ + final StreamClient sclient = new StreamClient(b); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + sclient.listen(); + } + }); + + visualizer = new Visualizer(VisualizationMapper.mapVisualizations(c.getVisualization())); + b.register(visualizer); + + // TODO eventually set update on delimiter/dim boundary here + + // If there is a continous dimension only update plot at the end of a line + if(c.getScan() != null && c.getScan().getCdimension()!=null){ + visualizer.setUpdateAtStreamElement(false); + visualizer.setUpdateAtStreamDelimiter(true); + visualizer.setUpdateAtEndOfStream(true); + } + } + + // GUI GUI GUI GUI GUI GUI GUI + ProgressPanel progressPanel = null; + if(visualizer != null){ // Only bring up GUI if there are some plots ... + // Visualizations + JPanel opanel = new ScrollableFlowPanel(); + opanel.setLayout(new FlowLayout()); + + JScrollPane spane = new JScrollPane(opanel, ScrollPaneLayout.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneLayout.HORIZONTAL_SCROLLBAR_NEVER); + JTabbedPane tpane = new JTabbedPane(); + tpane.addTab("Overview", spane); + + for(JPanel p: visualizer.getPlotPanels()){ + opanel.add(p); + } + + final JFrame frame = new JFrame("FDA: "+file); + frame.setSize(1200,800); + + // Create progress panel + progressPanel = new ProgressPanel(); + progressPanel.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + try { + client.stop(); + } catch (Exception e1) { + logger.log(Level.SEVERE, "Exception occured while aborting scan", e1); + } + + } + }); + + + JSplitPane splitPane = new JSplitPane(); + splitPane.setLeftComponent(progressPanel); + splitPane.setRightComponent(tpane); + + frame.add(splitPane); + frame.addWindowListener(new WindowAdapter(){ + @Override + public void windowClosing(WindowEvent we){ + if(client.isActive()){ + // Abort acquisition + client.stop(); + } + + // Wait until acquisition is aborted. Maximum wait 10*100milliseconds before forcefully + // terminate application + int count=0; + while(client.isActive()){ + if(count == 10){ + break; + } + + // Sleep 100 milliseconds + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + count++; + } + + // Terminate program + System.exit(0); + } + }); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);//.DO_NOTHING_ON_CLOSE); +// frame.setVisible(true); + + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + frame.setVisible(true); + } + }); + } + // GUI GUI GUI GUI GUI GUI GUI + + // CLI CLI CLI CLI + // Register the scan engine as Signal Handler + Signal.handle(new Signal("INT"), new SignalHandler() { + /** + * Thread save signal counter + */ + private AtomicInteger signalCount= new AtomicInteger(0); + + /** + * Testing signal handler (in Eclipse) use this after starting scan: + * + * SL5: A=`ps -ef | tail -10 | grep jav[a] | awk '{printf $2}'`;kill -2 $A + * MacOS X: A=`ps -ef | grep AcquisitionMai[n] | awk '{printf $2}'`;kill -2 $A + * + * on the command line use CTRL-C + */ + @Override + public void handle(Signal signal) { + + logger.finest("Received signal: "+signal); + + int count = signalCount.incrementAndGet(); + + // If signal is received more than 1 time forcefully abort application + if(count>1){ + logger.info("Terminate application"); + System.exit(2); + } + + abortedViaSignal = true; + // Abort acquisition engine + if(client.isActive()){ + // Abort acquisition + client.stop(); + } + } + }); + // CLI CLI CLI CLI + + + if(visualizer != null){ + visualizer.configure(); + } + client.acquire(c); + + + // GUI GUI GUI GUI GUI GUI GUI + // Set progress panel to done +// if(progressPanel != null){ +// // Can this be done via a Listener? +// progressPanel.done(); +// } + // GUI GUI GUI GUI GUI GUI GUI + + if(abortedViaSignal){ + System.exit(3); + } + } + + + +} diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/StreamClient.java b/ch.psi.fda/src/main/java/ch/psi/fda/StreamClient.java new file mode 100644 index 0000000..c867296 --- /dev/null +++ b/ch.psi.fda/src/main/java/ch/psi/fda/StreamClient.java @@ -0,0 +1,73 @@ +/** + * + * Copyright 2013 Paul Scherrer Institute. All rights reserved. + * + * This code is free software: you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This code is distributed in the hope that it will be useful, but without any + * warranty; without even the implied warranty of merchantability or fitness for + * a particular purpose. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + * + */ +package ch.psi.fda; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jeromq.ZMQ; + +import com.google.common.eventbus.EventBus; + +/** + * + */ +public class StreamClient { + + private static final Logger logger = Logger.getLogger(StreamClient.class.getName()); + + private final EventBus bus; + + public StreamClient(EventBus bus){ + this.bus = bus; + } + + public void listen() { + ZMQ.Context context = ZMQ.context(); + zmq.ZError.clear(); // Clear error code + ZMQ.Socket socket = context.socket(ZMQ.SUB); + socket.connect("tcp://emac:10000"); + socket.subscribe(""); // SUBSCRIBE ! + + while (true) { + byte[] content = null; + // String header = socket.recvStr(); // header + socket.recvStr(); // header + while (socket.hasReceiveMore()) { + content = socket.recv(); + } + if (content == null) { // we lost something discard read messages + logger.warning("Lost some message - discard and continue"); + continue; + } + + try (ByteArrayInputStream bis = new ByteArrayInputStream(content); ObjectInput in = new ObjectInputStream(bis);) { + Object o = in.readObject(); + bus.post(o); + } catch (IOException | ClassNotFoundException e) { + logger.log(Level.WARNING, "Unable to deserialize message", e); + } + + } + } +} diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/ViewerMain.java b/ch.psi.fda/src/main/java/ch/psi/fda/ViewerMain.java index 79a51e8..16436ba 100644 --- a/ch.psi.fda/src/main/java/ch/psi/fda/ViewerMain.java +++ b/ch.psi.fda/src/main/java/ch/psi/fda/ViewerMain.java @@ -22,10 +22,6 @@ package ch.psi.fda; import java.awt.FlowLayout; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -37,8 +33,6 @@ import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.ScrollPaneLayout; -import org.jeromq.ZMQ; - import com.google.common.eventbus.EventBus; import ch.psi.fda.gui.ScrollableFlowPanel; @@ -107,35 +101,7 @@ public class ViewerMain { bus.register(visualizer); visualizer.configure(); - ZMQ.Context context = ZMQ.context(); - zmq.ZError.clear(); // Clear error code - ZMQ.Socket socket = context.socket(ZMQ.SUB); - socket.connect("tcp://emac:10000"); - socket.subscribe(""); // SUBSCRIBE ! - - while(true){ - byte[] content = null; -// String header = socket.recvStr(); // header - socket.recvStr(); // header - while(socket.hasReceiveMore()){ - content = socket.recv(); - } - if(content==null){ // we lost something discard read messages - logger.warning("Lost some message - discard and continue"); - continue; - } - - try ( - ByteArrayInputStream bis = new ByteArrayInputStream(content); - ObjectInput in = new ObjectInputStream(bis); - ) { - Object o = in.readObject(); - bus.post(o); - } catch (IOException | ClassNotFoundException e) { - logger.log(Level.WARNING,"Unable to deserialize message",e); - } - - } + new StreamClient(bus).listen(); } /** diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/rest/AcquisitionEngine.java b/ch.psi.fda/src/main/java/ch/psi/fda/rest/AcquisitionEngine.java index 027e9a4..e489076 100644 --- a/ch.psi.fda/src/main/java/ch/psi/fda/rest/AcquisitionEngine.java +++ b/ch.psi.fda/src/main/java/ch/psi/fda/rest/AcquisitionEngine.java @@ -167,4 +167,14 @@ public class AcquisitionEngine { } } } + + /** + * @return + */ + public boolean isActive() { + if(acquisition==null){ + return false; + } + return acquisition.isActive(); + } } diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestClient.java b/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestClient.java new file mode 100644 index 0000000..c03b4f3 --- /dev/null +++ b/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestClient.java @@ -0,0 +1,58 @@ +/** + * + * Copyright 2013 Paul Scherrer Institute. All rights reserved. + * + * This code is free software: you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This code is distributed in the hope that it will be useful, but without any + * warranty; without even the implied warranty of merchantability or fitness for + * a particular purpose. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + * + */ +package ch.psi.fda.rest; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.xml.bind.JAXBElement; +import javax.xml.namespace.QName; + +import org.glassfish.jersey.jackson.JacksonFeature; + +import ch.psi.fda.model.v1.Configuration; + +/** + * + */ +public class RestClient { + + + + private Client client = ClientBuilder.newClient().register(JacksonFeature.class); + private WebTarget target = client.target("http://emac:8080").path("fda"); + + + + public String acquire(Configuration c){ + // Wrap configuration in JAXBElement as there is no @XmlRootElement available within the generated Configuration class + JAXBElement jaxbElement = new JAXBElement<>(new QName("ROOT"), Configuration.class, c); + return target.request().post(Entity.entity(jaxbElement, MediaType.APPLICATION_XML), String.class); + } + + public void stop(){ + target.request().delete(); + } + + public boolean isActive(){ + return target.request().get(Boolean.class); + } +} diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestServer.java b/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestServer.java index eb75759..8a5879f 100644 --- a/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestServer.java +++ b/ch.psi.fda/src/main/java/ch/psi/fda/rest/RestServer.java @@ -55,11 +55,17 @@ public class RestServer { ResourceBinder binder = new ResourceBinder(); - ResourceConfig resourceConfig = new ResourceConfig(JacksonFeature.class); + ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.packages(RestServer.class.getPackage().getName()+".services"); // Services are located in services package resourceConfig.register(binder); + + resourceConfig.register(JacksonFeature.class); + + HttpServer server = GrizzlyHttpServerFactory.createHttpServer(baseUri, resourceConfig); + + // Static content // String home = System.getenv("FDA_BASE"); // if (home == null) { diff --git a/ch.psi.fda/src/main/java/ch/psi/fda/rest/services/ScanService.java b/ch.psi.fda/src/main/java/ch/psi/fda/rest/services/ScanService.java index 98b04d4..7e5449e 100644 --- a/ch.psi.fda/src/main/java/ch/psi/fda/rest/services/ScanService.java +++ b/ch.psi.fda/src/main/java/ch/psi/fda/rest/services/ScanService.java @@ -22,6 +22,7 @@ package ch.psi.fda.rest.services; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; @@ -37,7 +38,6 @@ public class ScanService { @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) -// @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public String execute(Configuration configuration) throws InterruptedException{ return aengine.submit(configuration); } @@ -46,4 +46,10 @@ public class ScanService { public void stop(){ aengine.terminate(); } + + @GET + public boolean isActive(){ + return aengine.isActive(); + } + }