Add 'src/be/nikiroo/utils/' from commit '46add0670fdee4bd936a13fe2448c5e20a7ffd0a'
[fanfix.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
8 import be.nikiroo.utils.TraceHandler;
9
10 /**
11 * This class implements a simple server that can listen for connections and
12 * send/receive objects.
13 * <p>
14 * Note: this {@link Server} has to be discarded after use (cannot be started
15 * twice).
16 *
17 * @author niki
18 */
19 abstract class Server implements Runnable {
20 protected final String key;
21 protected long id = 0;
22
23 private final String name;
24 private final Object lock = new Object();
25 private final Object counterLock = new Object();
26
27 private ServerSocket ss;
28 private int port;
29
30 private boolean started;
31 private boolean exiting = false;
32 private int counter;
33
34 private long bytesReceived;
35 private long bytesSent;
36
37 private TraceHandler tracer = new TraceHandler();
38
39 /**
40 * Create a new {@link ConnectActionServer} to handle a request.
41 *
42 * @param s
43 * the socket to service
44 *
45 * @return the action
46 */
47 abstract ConnectActionServer createConnectActionServer(Socket s);
48
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 key
58 * an optional key to encrypt all the communications (if NULL,
59 * everything will be sent in clear text)
60 *
61 * @throws IOException
62 * in case of I/O error
63 * @throws UnknownHostException
64 * if the IP address of the host could not be determined
65 * @throws IllegalArgumentException
66 * if the port parameter is outside the specified range of valid
67 * port values, which is between 0 and 65535, inclusive
68 */
69 public Server(int port, String key) throws IOException {
70 this((String) null, port, key);
71 }
72
73 /**
74 * Create a new server that will start listening on the network when
75 * {@link Server#start()} is called.
76 * <p>
77 * All the communications will happen in plain text.
78 *
79 * @param name
80 * the server name (only used for debug info and traces)
81 * @param port
82 * the port to listen on
83 *
84 * @throws IOException
85 * in case of I/O error
86 * @throws UnknownHostException
87 * if the IP address of the host could not be determined
88 * @throws IllegalArgumentException
89 * if the port parameter is outside the specified range of valid
90 * port values, which is between 0 and 65535, inclusive
91 */
92 public Server(String name, int port) throws IOException {
93 this(name, port, null);
94 }
95
96 /**
97 * Create a new server that will start listening on the network when
98 * {@link Server#start()} is called.
99 *
100 * @param name
101 * the server name (only used for debug info and traces)
102 * @param port
103 * the port to listen on
104 * @param key
105 * an optional key to encrypt all the communications (if NULL,
106 * everything will be sent in clear text)
107 *
108 * @throws IOException
109 * in case of I/O error
110 * @throws UnknownHostException
111 * if the IP address of the host could not be determined
112 * @throws IllegalArgumentException
113 * if the port parameter is outside the specified range of valid
114 * port values, which is between 0 and 65535, inclusive
115 */
116 public Server(String name, int port, String key) throws IOException {
117 this.name = name;
118 this.port = port;
119 this.key = key;
120 this.ss = new ServerSocket(port);
121
122 if (this.port == 0) {
123 this.port = this.ss.getLocalPort();
124 }
125 }
126
127 /**
128 * The traces handler for this {@link Server}.
129 *
130 * @return the traces handler
131 */
132 public TraceHandler getTraceHandler() {
133 return tracer;
134 }
135
136 /**
137 * The traces handler for this {@link Server}.
138 *
139 * @param tracer
140 * the new traces handler
141 */
142 public void setTraceHandler(TraceHandler tracer) {
143 if (tracer == null) {
144 tracer = new TraceHandler(false, false, false);
145 }
146
147 this.tracer = tracer;
148 }
149
150 /**
151 * The name of this {@link Server} if any.
152 * <p>
153 * Used for traces and debug purposes only.
154 *
155 * @return the name or NULL
156 */
157 public String getName() {
158 return name;
159 }
160
161 /**
162 * Return the assigned port.
163 *
164 * @return the assigned port
165 */
166 public int getPort() {
167 return port;
168 }
169
170 /**
171 * The total amount of bytes received.
172 *
173 * @return the amount of bytes received
174 */
175 public long getBytesReceived() {
176 return bytesReceived;
177 }
178
179 /**
180 * The total amount of bytes sent.
181 *
182 * @return the amount of bytes sent
183 */
184 public long getBytesSent() {
185 return bytesSent;
186 }
187
188 /**
189 * Start the server (listen on the network for new connections).
190 * <p>
191 * Can only be called once.
192 * <p>
193 * This call is asynchronous, and will just start a new {@link Thread} on
194 * itself (see {@link Server#run()}).
195 */
196 public void start() {
197 new Thread(this).start();
198 }
199
200 /**
201 * Start the server (listen on the network for new connections).
202 * <p>
203 * Can only be called once.
204 * <p>
205 * You may call it via {@link Server#start()} for an asynchronous call, too.
206 */
207 @Override
208 public void run() {
209 ServerSocket ss = null;
210 boolean alreadyStarted = false;
211 synchronized (lock) {
212 ss = this.ss;
213 if (!started && ss != null) {
214 started = true;
215 } else {
216 alreadyStarted = started;
217 }
218 }
219
220 if (alreadyStarted) {
221 tracer.error(name + ": cannot start server on port " + port
222 + ", it is already started");
223 return;
224 }
225
226 if (ss == null) {
227 tracer.error(name + ": cannot start server on port " + port
228 + ", it has already been used");
229 return;
230 }
231
232 try {
233 tracer.trace(name + ": server starting on port " + port + " ("
234 + (key != null ? "encrypted" : "plain text") + ")");
235
236 while (started && !exiting) {
237 count(1);
238 final Socket s = ss.accept();
239 new Thread(new Runnable() {
240 @Override
241 public void run() {
242 ConnectActionServer action = null;
243 try {
244 action = createConnectActionServer(s);
245 action.connect();
246 } finally {
247 count(-1);
248 if (action != null) {
249 bytesReceived += action.getBytesReceived();
250 bytesSent += action.getBytesSent();
251 }
252 }
253 }
254 }).start();
255 }
256
257 // Will be covered by @link{Server#stop(long)} for timeouts
258 while (counter > 0) {
259 Thread.sleep(10);
260 }
261 } catch (Exception e) {
262 if (counter > 0) {
263 onError(e);
264 }
265 } finally {
266 try {
267 ss.close();
268 } catch (Exception e) {
269 onError(e);
270 }
271
272 this.ss = null;
273
274 started = false;
275 exiting = false;
276 counter = 0;
277
278 tracer.trace(name + ": client terminated on port " + port);
279 }
280 }
281
282 /**
283 * Will stop the server, synchronously and without a timeout.
284 */
285 public void stop() {
286 tracer.trace(name + ": stopping server");
287 stop(0, true);
288 }
289
290 /**
291 * Stop the server.
292 *
293 * @param timeout
294 * the maximum timeout to wait for existing actions to complete,
295 * or 0 for "no timeout"
296 * @param wait
297 * wait for the server to be stopped before returning
298 * (synchronous) or not (asynchronous)
299 */
300 public void stop(final long timeout, final boolean wait) {
301 if (wait) {
302 stop(timeout);
303 } else {
304 new Thread(new Runnable() {
305 @Override
306 public void run() {
307 stop(timeout);
308 }
309 }).start();
310 }
311 }
312
313 /**
314 * Stop the server (synchronous).
315 *
316 * @param timeout
317 * the maximum timeout to wait for existing actions to complete,
318 * or 0 for "no timeout"
319 */
320 private void stop(long timeout) {
321 tracer.trace(name + ": server stopping on port " + port);
322 synchronized (lock) {
323 if (started && !exiting) {
324 exiting = true;
325
326 try {
327 getConnectionToMe().connect();
328 long time = 0;
329 while (ss != null && timeout > 0 && timeout > time) {
330 Thread.sleep(10);
331 time += 10;
332 }
333 } catch (Exception e) {
334 if (ss != null) {
335 counter = 0; // will stop the main thread
336 onError(e);
337 }
338 }
339 }
340 }
341
342 // return only when stopped
343 while (started || exiting) {
344 try {
345 Thread.sleep(10);
346 } catch (InterruptedException e) {
347 }
348 }
349 }
350
351 /**
352 * Return a connection to this server (used by the Exit code to send an exit
353 * message).
354 *
355 * @return the connection
356 *
357 * @throws UnknownHostException
358 * the host should always be NULL (localhost)
359 * @throws IOException
360 * in case of I/O error
361 */
362 abstract protected ConnectActionClient getConnectionToMe()
363 throws UnknownHostException, IOException;
364
365 /**
366 * Change the number of currently serviced actions.
367 *
368 * @param change
369 * the number to increase or decrease
370 *
371 * @return the current number after this operation
372 */
373 private int count(int change) {
374 synchronized (counterLock) {
375 counter += change;
376 return counter;
377 }
378 }
379
380 /**
381 * This method will be called on errors.
382 * <p>
383 * By default, it will only call the trace handler (so you may want to call
384 * super {@link Server#onError} if you override it).
385 *
386 * @param e
387 * the error
388 */
389 protected void onError(Exception e) {
390 tracer.error(e);
391 }
392
393 /**
394 * Return the next ID to use.
395 *
396 * @return the next ID
397 */
398 protected synchronized long getNextId() {
399 return id++;
400 }
401
402 /**
403 * Method called when
404 * {@link ServerObject#onRequest(ConnectActionServerObject, Object, long)}
405 * has successfully finished.
406 * <p>
407 * Can be used to know how much data was transmitted.
408 *
409 * @param id
410 * the ID used to identify the request
411 * @param bytesReceived
412 * the bytes received during the request
413 * @param bytesSent
414 * the bytes sent during the request
415 */
416 @SuppressWarnings("unused")
417 protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
418 }
419 }