Update Server, breaks API + remove deprecated
[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 Socket s = ss.accept();
188 createConnectActionServer(s).connectAsync();
189 }
190
191 // Will be covered by @link{Server#stop(long)} for timeouts
192 while (counter > 0) {
193 Thread.sleep(10);
194 }
195 } catch (Exception e) {
196 if (counter > 0) {
197 onError(e);
198 }
199 } finally {
200 try {
201 ss.close();
202 } catch (Exception e) {
203 onError(e);
204 }
205
206 this.ss = null;
207
208 started = false;
209 exiting = false;
210 counter = 0;
211
212 tracer.trace(name + ": client terminated on port " + port);
213 }
214 }
215
216 /**
217 * Will stop the server, synchronously and without a timeout.
218 */
219 public void stop() {
220 tracer.trace(name + ": stopping server");
221 stop(0, true);
222 }
223
224 /**
225 * Stop the server.
226 *
227 * @param timeout
228 * the maximum timeout to wait for existing actions to complete,
229 * or 0 for "no timeout"
230 * @param wait
231 * wait for the server to be stopped before returning
232 * (synchronous) or not (asynchronous)
233 */
234 public void stop(final long timeout, final boolean wait) {
235 if (wait) {
236 stop(timeout);
237 } else {
238 new Thread(new Runnable() {
239 @Override
240 public void run() {
241 stop(timeout);
242 }
243 }).start();
244 }
245 }
246
247 /**
248 * Stop the server (synchronous).
249 *
250 * @param timeout
251 * the maximum timeout to wait for existing actions to complete,
252 * or 0 for "no timeout"
253 */
254 private void stop(long timeout) {
255 tracer.trace(name + ": server stopping on port " + port);
256 synchronized (lock) {
257 if (started && !exiting) {
258 exiting = true;
259
260 try {
261 new ConnectActionClientObject(createSocket(null, port, ssl))
262 .connect();
263 long time = 0;
264 while (ss != null && timeout > 0 && timeout > time) {
265 Thread.sleep(10);
266 time += 10;
267 }
268 } catch (Exception e) {
269 if (ss != null) {
270 counter = 0; // will stop the main thread
271 onError(e);
272 }
273 }
274 }
275
276 // only return when stopped
277 while (started || exiting) {
278 try {
279 Thread.sleep(10);
280 } catch (InterruptedException e) {
281 }
282 }
283 }
284 }
285
286 /**
287 * This method will be called on errors.
288 * <p>
289 * By default, it will only call the trace handler (so you may want to call
290 * super {@link Server#onError} if you override it).
291 *
292 * @param e
293 * the error
294 */
295 protected void onError(Exception e) {
296 tracer.error(e);
297 }
298
299 /**
300 * Change the number of currently serviced actions.
301 *
302 * @param change
303 * the number to increase or decrease
304 *
305 * @return the current number after this operation
306 */
307 int count(int change) {
308 synchronized (counterLock) {
309 counter += change;
310 return counter;
311 }
312 }
313
314 /**
315 * Create a {@link Socket}.
316 *
317 * @param host
318 * the host to connect to
319 * @param port
320 * the port to connect to
321 * @param ssl
322 * TRUE for SSL mode (or FALSE for plain text mode)
323 *
324 * @return the {@link Socket}
325 *
326 * @throws IOException
327 * in case of I/O error
328 */
329 static Socket createSocket(String host, int port, boolean ssl)
330 throws IOException {
331 Socket s;
332 if (ssl) {
333 s = SSLSocketFactory.getDefault().createSocket(host, port);
334 ((SSLSocket) s).setEnabledCipherSuites(ANON_CIPHERS);
335 } else {
336 s = new Socket(host, port);
337 }
338
339 return s;
340 }
341
342 /**
343 * Create a {@link ServerSocket}.
344 *
345 * @param port
346 * the port to accept connections on
347 * @param ssl
348 * TRUE for SSL mode (or FALSE for plain text mode)
349 *
350 * @return the {@link ServerSocket}
351 *
352 * @throws IOException
353 * in case of I/O error
354 */
355 static ServerSocket createSocketServer(int port, boolean ssl)
356 throws IOException {
357 ServerSocket ss;
358 if (ssl) {
359 ss = SSLServerSocketFactory.getDefault().createServerSocket(port);
360 ((SSLServerSocket) ss).setEnabledCipherSuites(ANON_CIPHERS);
361 } else {
362 ss = new ServerSocket(port);
363 }
364
365 return ss;
366 }
367
368 /**
369 * Return all the supported ciphers that do not use authentication.
370 *
371 * @return the list of such supported ciphers
372 */
373 private static String[] getAnonCiphers() {
374 List<String> anonCiphers = new ArrayList<String>();
375 for (String cipher : ((SSLSocketFactory) SSLSocketFactory.getDefault())
376 .getSupportedCipherSuites()) {
377 if (cipher.contains("_anon_")) {
378 anonCiphers.add(cipher);
379 }
380 }
381
382 return anonCiphers.toArray(new String[] {});
383 }
384 }