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