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