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