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