Improve TraceHandler, fix server system + tests:
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / Server.java
CommitLineData
ce0974c4
NR
1package be.nikiroo.utils.serial;
2
3import java.io.IOException;
4import java.net.ServerSocket;
5import java.net.Socket;
6import java.util.ArrayList;
7import java.util.List;
8
9import javax.net.ssl.SSLServerSocket;
10import javax.net.ssl.SSLServerSocketFactory;
11import javax.net.ssl.SSLSocket;
12import javax.net.ssl.SSLSocketFactory;
13
08a58812 14import be.nikiroo.utils.TraceHandler;
ce0974c4
NR
15import be.nikiroo.utils.Version;
16
08a58812
NR
17/**
18 * This class implements a simple server that can listen for connections and
19 * send/receive objects.
20 * <p>
21 * Note: this {@link Server} has to be discarded after use (cannot be started
22 * twice).
23 *
24 * @author niki
25 */
ce0974c4
NR
26abstract public class Server implements Runnable {
27 static private final String[] ANON_CIPHERS = getAnonCiphers();
28
08a58812
NR
29 private final String name;
30 private final boolean ssl;
31 private final Object lock = new Object();
32 private final Object counterLock = new Object();
33
ce0974c4 34 private ServerSocket ss;
08a58812
NR
35 private int port;
36
ce0974c4
NR
37 private boolean started;
38 private boolean exiting = false;
39 private int counter;
08a58812
NR
40
41 private TraceHandler tracer = new TraceHandler();
ce0974c4 42
cd0c27d2
NR
43 @Deprecated
44 public Server(@SuppressWarnings("unused") Version notUsed, int port,
45 boolean ssl) throws IOException {
46 this(port, ssl);
47 }
48
08a58812
NR
49 /**
50 * Create a new server that will start listening on the network when
51 * {@link Server#start()} is called.
52 *
53 * @param port
54 * the port to listen on, or 0 to assign any unallocated port
55 * found (which can later on be queried via
56 * {@link Server#getPort()}
57 * @param ssl
58 * use a SSL connection (or not)
59 *
60 * @throws IOException
61 * in case of I/O error
62 */
cd0c27d2 63 public Server(int port, boolean ssl) throws IOException {
08a58812
NR
64 this((String) null, port, ssl);
65 }
66
67 /**
68 * Create a new server that will start listening on the network when
69 * {@link Server#start()} is called.
70 *
71 * @param name
72 * the server name (only used for debug info and traces)
73 * @param port
74 * the port to listen on
75 * @param ssl
76 * use a SSL connection (or not)
77 *
78 * @throws IOException
79 * in case of I/O error
80 */
81 public Server(String name, int port, boolean ssl) throws IOException {
82 this.name = name;
ce0974c4
NR
83 this.port = port;
84 this.ssl = ssl;
85 this.ss = createSocketServer(port, ssl);
08a58812
NR
86
87 if (this.port == 0) {
88 this.port = this.ss.getLocalPort();
89 }
90 }
91
92 /**
93 * The traces handler for this {@link Server}.
94 *
95 * @return the traces handler
96 */
97 public TraceHandler getTraceHandler() {
98 return tracer;
ce0974c4
NR
99 }
100
08a58812
NR
101 /**
102 * The traces handler for this {@link Server}.
103 *
104 * @param tracer
105 * the new traces handler
106 */
107 public void setTraceHandler(TraceHandler tracer) {
108 if (tracer == null) {
109 tracer = new TraceHandler(false, false, false);
110 }
111
112 this.tracer = tracer;
113 }
114
115 /**
116 * Return the assigned port.
117 */
118 public int getPort() {
119 return port;
120 }
121
122 /**
123 * Start the server (listen on the network for new connections).
124 * <p>
125 * Can only be called once.
126 */
ce0974c4 127 public void start() {
08a58812 128 boolean ok = false;
ce0974c4 129 synchronized (lock) {
08a58812 130 if (!started && ss != null) {
ce0974c4
NR
131 started = true;
132 new Thread(this).start();
08a58812 133 ok = true;
ce0974c4
NR
134 }
135 }
08a58812
NR
136
137 if (ok) {
138 tracer.trace(name + ": server started on port " + port);
139 } else if (ss == null) {
140 tracer.error(name + ": cannot start server on port " + port
141 + ", it has already been used");
142 } else {
143 tracer.error(name + ": cannot start server on port " + port
144 + ", it is already started");
145 }
ce0974c4
NR
146 }
147
08a58812
NR
148 /**
149 * Will stop the server, synchronously and without a timeout.
150 */
ce0974c4 151 public void stop() {
08a58812 152 tracer.trace(name + ": stopping server");
ce0974c4
NR
153 stop(0, true);
154 }
155
08a58812
NR
156 /**
157 * Stop the server.
158 *
159 * @param timeout
160 * the maximum timeout to wait for existing actions to complete,
161 * or 0 for "no timeout"
162 * @param wait
163 * wait for the server to be stopped before returning
164 * (synchronous) or not (asynchronous)
165 */
ce0974c4
NR
166 public void stop(final long timeout, final boolean wait) {
167 if (wait) {
168 stop(timeout);
169 } else {
170 new Thread(new Runnable() {
cd0c27d2 171 @Override
ce0974c4
NR
172 public void run() {
173 stop(timeout);
174 }
175 }).start();
176 }
177 }
178
08a58812
NR
179 /**
180 * Stop the server (synchronous).
181 *
182 * @param timeout
183 * the maximum timeout to wait for existing actions to complete,
184 * or 0 for "no timeout"
185 */
ce0974c4
NR
186 private void stop(long timeout) {
187 synchronized (lock) {
188 if (started && !exiting) {
189 exiting = true;
190
191 try {
192 new ConnectActionClient(createSocket(null, port, ssl)) {
193 @Override
194 public void action(Version serverVersion)
195 throws Exception {
196 }
197 }.connect();
198
199 long time = 0;
200 while (ss != null && timeout > 0 && timeout > time) {
201 Thread.sleep(10);
202 time += 10;
203 }
204 } catch (Exception e) {
205 if (ss != null) {
206 counter = 0; // will stop the main thread
207 onError(e);
208 }
209 }
210 }
211
212 // only return when stopped
213 while (started || exiting) {
214 try {
215 Thread.sleep(10);
216 } catch (InterruptedException e) {
217 }
218 }
219 }
220 }
221
cd0c27d2 222 @Override
ce0974c4
NR
223 public void run() {
224 try {
08a58812 225 tracer.trace(name + ": server starting on port " + port);
ce0974c4
NR
226 while (started && !exiting) {
227 count(1);
228 Socket s = ss.accept();
229 new ConnectActionServer(s) {
230 private Version clientVersion = new Version();
231
232 @Override
233 public void action(Version dummy) throws Exception {
234 try {
235 for (Object data = flush(); true; data = flush()) {
236 Object rep = null;
237 try {
238 rep = onRequest(this, clientVersion, data);
239 } catch (Exception e) {
240 onError(e);
241 }
242 send(rep);
243 }
244 } catch (NullPointerException e) {
245 // Client has no data any more, we quit
246 }
247 }
248
249 @Override
250 public void connect() {
251 try {
252 super.connect();
253 } finally {
254 count(-1);
255 }
cd0c27d2 256 }
ce0974c4
NR
257
258 @Override
259 protected void onClientVersionReceived(Version clientVersion) {
260 this.clientVersion = clientVersion;
cd0c27d2 261 }
ce0974c4
NR
262 }.connectAsync();
263 }
264
265 // Will be covered by @link{Server#stop(long)} for timeouts
266 while (counter > 0) {
267 Thread.sleep(10);
268 }
269 } catch (Exception e) {
270 if (counter > 0) {
271 onError(e);
272 }
273 } finally {
274 try {
275 ss.close();
276 } catch (Exception e) {
277 onError(e);
278 }
279
280 ss = null;
281
282 started = false;
283 exiting = false;
284 counter = 0;
285 }
286 }
287
08a58812
NR
288 /**
289 * This is the method that is called on each client request.
290 * <p>
291 * You are expected to react to it and return an answer (which can be NULL).
292 *
293 * @param action
294 * the client action
295 * @param clientVersion
296 * the client version
297 * @param data
298 * the data sent by the client (which can be NULL)
299 *
300 * @return the answer to return to the client (which can be NULL)
301 *
302 * @throws Exception
303 * in case of an exception, the error will only be logged
304 */
ce0974c4
NR
305 abstract protected Object onRequest(ConnectActionServer action,
306 Version clientVersion, Object data) throws Exception;
307
08a58812
NR
308 /**
309 * This method will be called on errors.
310 * <p>
311 * By default, it will only call the trace handler (so you may want to call
312 * super {@link Server#onError} if you override it).
313 *
314 * @param e
315 * the error
316 */
ce0974c4 317 protected void onError(Exception e) {
08a58812 318 tracer.error(e);
ce0974c4
NR
319 }
320
08a58812
NR
321 /**
322 * Change the number of currently serviced actions.
323 *
324 * @param change
325 * the number to increase or decrease
326 *
327 * @return the current number after this operation
328 */
ce0974c4
NR
329 private int count(int change) {
330 synchronized (counterLock) {
331 counter += change;
332 return counter;
333 }
334 }
335
08a58812
NR
336 /**
337 * Create a {@link Socket}.
338 *
339 * @param host
340 * the host to connect to
341 * @param port
342 * the port to connect to
343 * @param ssl
344 * TRUE for SSL mode (or FALSE for plain text mode)
345 *
346 * @return the {@link Socket}
347 *
348 * @throws IOException
349 * in case of I/O error
350 */
ce0974c4
NR
351 static Socket createSocket(String host, int port, boolean ssl)
352 throws IOException {
353 Socket s;
354 if (ssl) {
355 s = SSLSocketFactory.getDefault().createSocket(host, port);
356 ((SSLSocket) s).setEnabledCipherSuites(ANON_CIPHERS);
357 } else {
358 s = new Socket(host, port);
359 }
360
361 return s;
362 }
363
08a58812
NR
364 /**
365 * Create a {@link ServerSocket}.
366 *
367 * @param port
368 * the port to accept connections on
369 * @param ssl
370 * TRUE for SSL mode (or FALSE for plain text mode)
371 *
372 * @return the {@link ServerSocket}
373 *
374 * @throws IOException
375 * in case of I/O error
376 */
ce0974c4
NR
377 static ServerSocket createSocketServer(int port, boolean ssl)
378 throws IOException {
379 ServerSocket ss;
380 if (ssl) {
381 ss = SSLServerSocketFactory.getDefault().createServerSocket(port);
382 ((SSLServerSocket) ss).setEnabledCipherSuites(ANON_CIPHERS);
383 } else {
384 ss = new ServerSocket(port);
385 }
386
387 return ss;
388 }
389
08a58812
NR
390 /**
391 * Return all the supported ciphers that do not use authentication.
392 *
393 * @return the list of such supported ciphers
394 */
ce0974c4
NR
395 private static String[] getAnonCiphers() {
396 List<String> anonCiphers = new ArrayList<String>();
397 for (String cipher : ((SSLSocketFactory) SSLSocketFactory.getDefault())
398 .getSupportedCipherSuites()) {
399 if (cipher.contains("_anon_")) {
400 anonCiphers.add(cipher);
401 }
402 }
403
404 return anonCiphers.toArray(new String[] {});
405 }
406}