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