From 08a58812f12617289463b00161c98d7c59490bf2 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Mon, 27 Nov 2017 20:41:30 +0100 Subject: [PATCH] Improve TraceHandler, fix server system + tests: - TraceHandler is easier to work with - a Server can now send a second object (bugfix) - more tests for the Server class (jDoc + renames incoming) --- src/be/nikiroo/utils/TraceHandler.java | 58 +++-- .../nikiroo/utils/serial/ConnectAction.java | 7 +- src/be/nikiroo/utils/serial/Server.java | 207 ++++++++++++++++-- src/be/nikiroo/utils/test/SerialTest.java | 204 +++++++++++++++++ 4 files changed, 429 insertions(+), 47 deletions(-) diff --git a/src/be/nikiroo/utils/TraceHandler.java b/src/be/nikiroo/utils/TraceHandler.java index 5fa0843..d7f6046 100644 --- a/src/be/nikiroo/utils/TraceHandler.java +++ b/src/be/nikiroo/utils/TraceHandler.java @@ -7,44 +7,32 @@ package be.nikiroo.utils; * @author niki */ public class TraceHandler { - private boolean showErrorDetails; + private boolean showErrors; private boolean showTraces; + private boolean showErrorDetails; /** - * Show more details (usually equivalent to the value of DEBUG). - * - * @return TRUE or FALSE + * Create a default {@link TraceHandler} that will print errors on stderr + * (without details) and no traces. */ - public boolean isShowErrorDetails() { - return showErrorDetails; + public TraceHandler() { + this(true, false, false); } /** - * Show more details (usually equivalent to the value of DEBUG). + * Create a default {@link TraceHandler}. * + * @param showErrors + * show errors on stderr * @param showErrorDetails - * TRUE or FALSE - */ - public void setShowErrorDetails(boolean showErrorDetails) { - this.showErrorDetails = showErrorDetails; - } - - /** - * Show DEBUG traces. - * - * @return TRUE or FALSE - */ - public boolean isShowTraces() { - return showTraces; - } - - /** - * Show DEBUG traces. - * + * show more details when printing errors * @param showTraces - * TRUE or FALSE + * show traces on stdout */ - public void setShowTraces(boolean showTraces) { + public TraceHandler(boolean showErrors, boolean showErrorDetails, + boolean showTraces) { + this.showErrors = showErrors; + this.showErrorDetails = showErrorDetails; this.showTraces = showTraces; } @@ -55,10 +43,12 @@ public class TraceHandler { * the exception */ public void error(Exception e) { - if (isShowErrorDetails()) { - e.printStackTrace(); - } else { - error(e.getMessage()); + if (showErrors) { + if (showErrorDetails) { + e.printStackTrace(); + } else { + error(e.getMessage()); + } } } @@ -69,7 +59,9 @@ public class TraceHandler { * the error message */ public void error(String message) { - System.err.println(message); + if (showErrors) { + System.err.println(message); + } } /** @@ -81,7 +73,7 @@ public class TraceHandler { * the trace message */ public void trace(String message) { - if (isShowTraces()) { + if (showTraces) { System.out.println(message); } } diff --git a/src/be/nikiroo/utils/serial/ConnectAction.java b/src/be/nikiroo/utils/serial/ConnectAction.java index 1f98336..2a25c5b 100644 --- a/src/be/nikiroo/utils/serial/ConnectAction.java +++ b/src/be/nikiroo/utils/serial/ConnectAction.java @@ -42,6 +42,7 @@ abstract class ConnectAction { }).start(); } + // connect, do the action (sync) public void connect() { try { in = new BufferedReader(new InputStreamReader(s.getInputStream(), @@ -81,7 +82,11 @@ abstract class ConnectAction { NoSuchMethodException, ClassNotFoundException { synchronized (lock) { String rep = sendString(new Exporter().append(data).toString(true)); - return new Importer().read(rep).getValue(); + if (rep != null) { + return new Importer().read(rep).getValue(); + } + + return null; } } diff --git a/src/be/nikiroo/utils/serial/Server.java b/src/be/nikiroo/utils/serial/Server.java index bc4e637..e15680e 100644 --- a/src/be/nikiroo/utils/serial/Server.java +++ b/src/be/nikiroo/utils/serial/Server.java @@ -11,19 +11,34 @@ import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import be.nikiroo.utils.TraceHandler; import be.nikiroo.utils.Version; +/** + * This class implements a simple server that can listen for connections and + * send/receive objects. + *

+ * Note: this {@link Server} has to be discarded after use (cannot be started + * twice). + * + * @author niki + */ abstract public class Server implements Runnable { static private final String[] ANON_CIPHERS = getAnonCiphers(); - private int port; - private boolean ssl; + private final String name; + private final boolean ssl; + private final Object lock = new Object(); + private final Object counterLock = new Object(); + private ServerSocket ss; + private int port; + private boolean started; private boolean exiting = false; private int counter; - private Object lock = new Object(); - private Object counterLock = new Object(); + + private TraceHandler tracer = new TraceHandler(); @Deprecated public Server(@SuppressWarnings("unused") Version notUsed, int port, @@ -31,27 +46,123 @@ abstract public class Server implements Runnable { this(port, ssl); } + /** + * Create a new server that will start listening on the network when + * {@link Server#start()} is called. + * + * @param port + * the port to listen on, or 0 to assign any unallocated port + * found (which can later on be queried via + * {@link Server#getPort()} + * @param ssl + * use a SSL connection (or not) + * + * @throws IOException + * in case of I/O error + */ public Server(int port, boolean ssl) throws IOException { + this((String) null, port, ssl); + } + + /** + * Create a new server that will start listening on the network when + * {@link Server#start()} is called. + * + * @param name + * the server name (only used for debug info and traces) + * @param port + * the port to listen on + * @param ssl + * use a SSL connection (or not) + * + * @throws IOException + * in case of I/O error + */ + public Server(String name, int port, boolean ssl) throws IOException { + this.name = name; this.port = port; this.ssl = ssl; this.ss = createSocketServer(port, ssl); + + if (this.port == 0) { + this.port = this.ss.getLocalPort(); + } + } + + /** + * The traces handler for this {@link Server}. + * + * @return the traces handler + */ + public TraceHandler getTraceHandler() { + return tracer; } + /** + * The traces handler for this {@link Server}. + * + * @param tracer + * the new traces handler + */ + public void setTraceHandler(TraceHandler tracer) { + if (tracer == null) { + tracer = new TraceHandler(false, false, false); + } + + this.tracer = tracer; + } + + /** + * Return the assigned port. + */ + public int getPort() { + return port; + } + + /** + * Start the server (listen on the network for new connections). + *

+ * Can only be called once. + */ public void start() { + boolean ok = false; synchronized (lock) { - if (!started) { + if (!started && ss != null) { started = true; new Thread(this).start(); + ok = true; } } + + if (ok) { + tracer.trace(name + ": server started on port " + port); + } else if (ss == null) { + tracer.error(name + ": cannot start server on port " + port + + ", it has already been used"); + } else { + tracer.error(name + ": cannot start server on port " + port + + ", it is already started"); + } } + /** + * Will stop the server, synchronously and without a timeout. + */ public void stop() { + tracer.trace(name + ": stopping server"); stop(0, true); } - // wait = wait before returning (sync VS async) timeout in ms, 0 or -1 for - // never + /** + * Stop the server. + * + * @param timeout + * the maximum timeout to wait for existing actions to complete, + * or 0 for "no timeout" + * @param wait + * wait for the server to be stopped before returning + * (synchronous) or not (asynchronous) + */ public void stop(final long timeout, final boolean wait) { if (wait) { stop(timeout); @@ -65,7 +176,13 @@ abstract public class Server implements Runnable { } } - // timeout in ms, 0 or -1 or never + /** + * Stop the server (synchronous). + * + * @param timeout + * the maximum timeout to wait for existing actions to complete, + * or 0 for "no timeout" + */ private void stop(long timeout) { synchronized (lock) { if (started && !exiting) { @@ -105,6 +222,7 @@ abstract public class Server implements Runnable { @Override public void run() { try { + tracer.trace(name + ": server starting on port " + port); while (started && !exiting) { count(1); Socket s = ss.accept(); @@ -167,17 +285,47 @@ abstract public class Server implements Runnable { } } + /** + * This is the method that is called on each client request. + *

+ * You are expected to react to it and return an answer (which can be NULL). + * + * @param action + * the client action + * @param clientVersion + * the client version + * @param data + * the data sent by the client (which can be NULL) + * + * @return the answer to return to the client (which can be NULL) + * + * @throws Exception + * in case of an exception, the error will only be logged + */ abstract protected Object onRequest(ConnectActionServer action, Version clientVersion, Object data) throws Exception; + /** + * This method will be called on errors. + *

+ * By default, it will only call the trace handler (so you may want to call + * super {@link Server#onError} if you override it). + * + * @param e + * the error + */ protected void onError(Exception e) { - if (e == null) { - e = new Exception("Unknown error"); - } - - e.printStackTrace(); + tracer.error(e); } + /** + * Change the number of currently serviced actions. + * + * @param change + * the number to increase or decrease + * + * @return the current number after this operation + */ private int count(int change) { synchronized (counterLock) { counter += change; @@ -185,6 +333,21 @@ abstract public class Server implements Runnable { } } + /** + * Create a {@link Socket}. + * + * @param host + * the host to connect to + * @param port + * the port to connect to + * @param ssl + * TRUE for SSL mode (or FALSE for plain text mode) + * + * @return the {@link Socket} + * + * @throws IOException + * in case of I/O error + */ static Socket createSocket(String host, int port, boolean ssl) throws IOException { Socket s; @@ -198,6 +361,19 @@ abstract public class Server implements Runnable { return s; } + /** + * Create a {@link ServerSocket}. + * + * @param port + * the port to accept connections on + * @param ssl + * TRUE for SSL mode (or FALSE for plain text mode) + * + * @return the {@link ServerSocket} + * + * @throws IOException + * in case of I/O error + */ static ServerSocket createSocketServer(int port, boolean ssl) throws IOException { ServerSocket ss; @@ -211,6 +387,11 @@ abstract public class Server implements Runnable { return ss; } + /** + * Return all the supported ciphers that do not use authentication. + * + * @return the list of such supported ciphers + */ private static String[] getAnonCiphers() { List anonCiphers = new ArrayList(); for (String cipher : ((SSLSocketFactory) SSLSocketFactory.getDefault()) diff --git a/src/be/nikiroo/utils/test/SerialTest.java b/src/be/nikiroo/utils/test/SerialTest.java index 22f04c6..4233a7b 100644 --- a/src/be/nikiroo/utils/test/SerialTest.java +++ b/src/be/nikiroo/utils/test/SerialTest.java @@ -1,6 +1,7 @@ package be.nikiroo.utils.test; import be.nikiroo.utils.Version; +import be.nikiroo.utils.serial.ConnectActionClient; import be.nikiroo.utils.serial.ConnectActionServer; import be.nikiroo.utils.serial.Exporter; import be.nikiroo.utils.serial.Importer; @@ -26,9 +27,212 @@ class SerialTest extends TestLauncher { super("Serial test", null); } + private TestCase[] createServerTestCases(final boolean ssl) { + final String ssls = (ssl ? "(ssl)" : "(plain text)"); + return new TestCase[] { + new TestCase("Client/Server simple connection " + ssls) { + @Override + public void test() throws Exception { + final Object[] rec = new Object[1]; + + Server server = new Server("testy", 0, ssl) { + @Override + protected Object onRequest( + ConnectActionServer action, + Version clientVersion, Object data) + throws Exception { + return null; + } + }; + + assertEquals("A port should have been assigned", true, + server.getPort() > 0); + + server.start(); + + try { + new ConnectActionClient(null, server.getPort(), ssl) { + @Override + public void action(Version serverVersion) + throws Exception { + rec[0] = true; + } + }.connect(); + } finally { + server.stop(); + } + + assertNotNull("The client action was not run", rec[0]); + assertEquals(true, (boolean) ((Boolean) rec[0])); + } + }, // + new TestCase("Client/Server simple exchange " + ssls) { + final Object[] rec = new Object[3]; + + @Override + public void test() throws Exception { + Server server = new Server("testy", 0, ssl) { + @Override + protected Object onRequest( + ConnectActionServer action, + Version clientVersion, Object data) + throws Exception { + rec[0] = data; + return "pong"; + } + + @Override + protected void onError(Exception e) { + super.onError(e); + rec[2] = e; + } + }; + + assertEquals("A port should have been assigned", true, + server.getPort() > 0); + + server.start(); + + try { + new ConnectActionClient(null, server.getPort(), ssl) { + @Override + public void action(Version serverVersion) + throws Exception { + rec[1] = send("ping"); + } + }.connect(); + } finally { + server.stop(); + } + + if (rec[2] != null) { + fail("An exception was thrown: " + + ((Exception) rec[2]).getMessage()); + } + + assertEquals("ping", rec[0]); + assertEquals("pong", rec[1]); + } + }, // + new TestCase("Client/Server multiple exchanges " + ssls) { + final Object[] sent = new Object[3]; + final Object[] recd = new Object[3]; + final Exception[] err = new Exception[1]; + + @Override + public void test() throws Exception { + Server server = new Server("testy", 0, ssl) { + @Override + protected Object onRequest( + ConnectActionServer action, + Version clientVersion, Object data) + throws Exception { + sent[0] = data; + action.send("pong"); + sent[1] = action.flush(); + return "pong2"; + } + + @Override + protected void onError(Exception e) { + super.onError(e); + err[0] = e; + } + }; + + server.start(); + + try { + new ConnectActionClient(null, server.getPort(), ssl) { + @Override + public void action(Version serverVersion) + throws Exception { + recd[0] = send("ping"); + recd[1] = send("ping2"); + } + }.connect(); + } finally { + server.stop(); + } + + if (err[0] != null) { + fail("An exception was thrown: " + + err[0].getMessage()); + } + + assertEquals("ping", sent[0]); + assertEquals("pong", recd[0]); + assertEquals("ping2", sent[1]); + assertEquals("pong2", recd[1]); + } + }, // + new TestCase("Client/Server multiple call from client " + ssls) { + final Object[] sent = new Object[3]; + final Object[] recd = new Object[3]; + final Exception[] err = new Exception[1]; + + @Override + public void test() throws Exception { + Server server = new Server("testy", 0, ssl) { + @Override + protected Object onRequest( + ConnectActionServer action, + Version clientVersion, Object data) + throws Exception { + sent[(Integer) data] = data; + return ((Integer) data) * 2; + } + + @Override + protected void onError(Exception e) { + super.onError(e); + err[0] = e; + } + }; + + server.start(); + + try { + new ConnectActionClient(null, server.getPort(), ssl) { + @Override + public void action(Version serverVersion) + throws Exception { + for (int i = 0; i < 3; i++) { + recd[i] = send(i); + } + } + }.connect(); + } finally { + server.stop(); + } + + if (err[0] != null) { + fail("An exception was thrown: " + + err[0].getMessage()); + } + + assertEquals(0, sent[0]); + assertEquals(0, recd[0]); + assertEquals(1, sent[1]); + assertEquals(2, recd[1]); + assertEquals(2, sent[2]); + assertEquals(4, recd[2]); + } + }, // + }; + } + public SerialTest(String[] args) { super("Serial test", args); + for (TestCase test : createServerTestCases(false)) { + addTest(test); + } + + for (TestCase test : createServerTestCases(true)) { + addTest(test); + } + addTest(new TestCase("Simple class Import/Export") { @Override public void test() throws Exception { -- 2.27.0