ovccclient-server.vala 18.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/* 
 * 
 * Copyright (C) 2009-2011 Colomban Wendling <ban@herbesfolles.org>
 *                         Jonathan Michalon <studios.chalmion@no-log.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
/*
 * FIXME: there is a race condition when disconnecting if there are message
 * pending:
 * 
 * either the send thread might exit before the async result callback of
 * send_message() returned, thus leading to the connection to be closed at this
 * time, thus triggering and assertion failure;
 * (BTW: why is this a real problem? it should not prevent a message to be
 *  sent, in the worst case it should only make the code running after that to
 *  abort... not sure what's going on :/)
 * 
 * or we keep the thread alive until that callback returned, but since it runs
 * in the main thread, if we disconnect meanwhile from the main thread we'll
 * enter a deadlock since we'll wait for the thread to terminate, while the
 * thread waits from a main thread callback to terminate.
 */

38
using OVCC;
39
using OVCC.Network;
40

41
[CCode (cprefix = "OVCCClient", lower_case_cprefix = "ovccclient_")]
42 43
namespace OVCCClient
{
44 45 46
  /**
   * Error domain for the Server
   */
47 48
  public errordomain ServerError
  {
49 50 51
    /**
     * The communication with the server failed
     */
52
    COMMUNICATION_FAILED,
53 54 55
    /**
     * The nickname is not a valid one
     */
56
    INVALID_NICKNAME,
57 58 59
    /**
     * The nickname is already used
     */
60
    NICKNAME_IN_USE,
61 62 63
    /**
     * You didn't provide authentication but it's needed
     */
64
    MISSING_AUTHENTICATION,
65
    /**
66
     * No open game available
67
     */
68
    NO_OPEN_GAME,
69 70 71
    /**
     * Data could not be reached where it was searched for
     */
72
    DATA_UNREACHABLE,
73 74 75
    /**
     * Any other error
     */
76 77
    FAILED
  }
78 79 80 81 82 83 84

  /**
   * A class representing the server side of the network clients
   *
   * This class contains lots of methods making easy to communicate with
   * a remote OVCCServer.
   */
85 86
  public class Server : Object
  {
87 88 89 90 91 92 93 94
    private string                host;
    private uint16                port;
    private SocketClient          socket;
    private SocketConnection      connection = null;
    private DataInputStream       input;
    private DataOutputStream      output;
    /* why the hell can't I use Thread<void> here but in new()?? */
    private unowned Thread<bool>  listen_loop_thread = null;
95
    private int                   listen_loop_running = 0;
96 97
    private Cancellable?          listen_loop_cancel = null;
    private unowned Thread<bool>  send_loop_thread = null;
98
    private int                   send_loop_running = 0;
99 100
    private Cancellable?          send_loop_cancel = null;
    private AsyncQueue<Message>   send_loop_queue = new AsyncQueue<Message> ();
101

102 103 104
    /**
     * A signal emitted when a message was received from server
     * 
105
     * @param msg A {@link OVCC.Network.Message} instance containing what the server sent us
106
     */
107 108
    [Signal (detailed = true)]
    public signal void message_received (Message msg);
109 110 111
    /**
     * A signal emitted when a message in queue was sent to the server
     * 
112
     * @param msg A {@link OVCC.Network.Message} instance which was just sent to the server
113 114
     * @param err An optional error raised when sending
     */
115 116
    public signal void message_sent     (Message      msg,
                                         ServerError? err);
117

118 119 120 121 122 123 124 125
    /**
     * Constructor
     * 
     * @param host A string where the host name is stored, either IP or DN
     * @param port The remote port where to knock
     * @return A new {@link Server} instance which will be able to talk to
     * the given host:port remote server
     */
126
    public Server (string host,
127
                   uint16 port)
128 129
    {
      socket = new SocketClient ();
130 131
      this.host = host;
      this.port = port;
132
    }
133

134
    /* loop listening to incoming messages */
135
    private bool listen_loop ()
136
    {
137 138 139 140 141
      /* Take our ref to the cancellable and don't care about the field in
       * the object anymore. This allows the field to be changed (e.g. to null)
       * without problem for us. */
      var cancellable = listen_loop_cancel;
      
142 143
      while (AtomicInt.get (ref listen_loop_running) > 0 &&
             ! cancellable.is_cancelled ()) {
144 145 146
        Message? msg;
        
        try {
147
          msg = Message.receive (input, cancellable);
148
        } catch (IOError.CANCELLED e) {
149
          debug ("Listen loop cancelled");
150 151 152
          break;
        } catch (Error e) {
          warning ("error receiving data: %s", e.message);
153
          critical ("Assuming broken channel, shutting down listen loop");
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
          break;
        }
        
        if (msg == null) {
          debug ("Invalid message received");
          continue;
        }
        
        debug ("Message type %s received", msg.get_type ().name ());
        /* make sure we emit the signal from the main loop */
        Idle.add (() => {
          message_received[msg.message_type.to_string ()] (msg);
          return false;
        });
        if (msg.message_type == MessageType.DISCONNECT) {
169
          break;
170 171
        }
      }
172 173 174

      listen_loop_running = 0;

175
      return true;
176
    }
177

178
    /* loop sending messages to the server */
179
    private bool send_loop ()
180
    {
181 182
      var cancellable = send_loop_cancel;
      
183 184 185
      while (! cancellable.is_cancelled () &&
             (send_loop_queue.length () > 0 ||
              AtomicInt.get (ref send_loop_running) > 0)) {
186
        Message?      msg;
187
        ServerError?  msg_e = null;
188 189 190 191 192 193
        var           tv = TimeVal ();
        
         /* block at most for 500ms for the thread to be cancellable even when
          * there is no incoming messages */
        tv.get_current_time ();
        tv.add (500000);
194
        if ((msg = send_loop_queue.timed_pop (ref tv)) == null) {
195 196 197
          /* no message yet, we'll sleep again */
          continue;
        }
198 199
        
        try {
200
          msg.send (output, cancellable);
201
        } catch (IOError.CANCELLED e) {
202
          debug ("Send loop cancelled");
203 204 205 206 207 208 209 210 211 212 213 214 215
          break;
        } catch (Error e) {
          warning ("Trying to send message %s: %s", msg.get_type().name(), e.message);
          msg_e = new ServerError.COMMUNICATION_FAILED ("Cannot send message");
        }
        
        debug ("Message type %s sent", msg.get_type ().name ());
        /* make sure we emit the signal from the main loop */
        Idle.add (() => {
          message_sent (msg, msg_e);
          return false;
        });
      }
216 217 218

      send_loop_running = 0;

219
      return true;
220 221
    }

222
    /**
223
     * Asynchronous method allowing to pause until a given {@link OVCC.Network.MessageType}
224 225
     * get received.
     * 
226
     * @param mtype The {@link OVCC.Network.MessageType} to wait for
227
     * @param cancellable a Cancellable object or null
228 229
     * @return The message received
     */
230 231
    public async Message receive_type (MessageType  mtype,
                                       Cancellable? cancellable = null)
232
      throws ServerError, IOError
233 234
    {
      Message msg = null;
235 236
      ulong   id = 0;
      ulong   cancel_id = 0;
237
      
238 239 240 241
      if (AtomicInt.get (ref listen_loop_running) == 0) {
        throw new ServerError.COMMUNICATION_FAILED ("Listen loop is not running");
      }

242 243 244 245 246 247 248 249 250
      if (cancellable != null) {
        cancel_id = cancellable.connect (() => {
          cancel_id = 0;
          if (id != 0) {
            this.disconnect (id);
          }
          receive_type.callback ();
        });
      }
251 252
      id = this.message_received[mtype.to_string ()].connect ((m) => {
        msg = m;
253
        cancellable.disconnect (cancel_id);
254 255 256
        this.disconnect (id);
        receive_type.callback ();
      });
257
      
258
      yield;
259 260 261 262
      
      if (cancellable != null) {
        cancellable.set_error_if_cancelled ();
      }
263 264 265
      return msg;
    }

266 267 268
    /**
     * Asynchronous method to send messages to the remote server.
     * 
269
     * @param msg The {@link OVCC.Network.Message} to send
270
     * @param cancellable a Cancellable object or null
271 272
     * @return Whether the operation succeeded.
     */
273 274 275
    public async bool send_message (Message       msg,
                                    Cancellable?  cancellable = null)
      throws ServerError, IOError
276 277
      requires (connection != null)
    {
278 279
      if (AtomicInt.get (ref send_loop_running) == 0) {
        throw new ServerError.COMMUNICATION_FAILED ("Send loop is not running");
280 281
      } else if (msg == null) { /* send_loop_queue.push needs non-null */
        throw new ServerError.COMMUNICATION_FAILED ("Message to send is null");
282 283
      } else {
        ulong         id = 0;
284
        ulong         cancel_id = 0;
285 286
        ServerError?  err = null;
        
287 288 289 290 291 292 293 294 295
        if (cancellable != null) {
          cancel_id = cancellable.connect (() => {
            cancel_id = 0;
            if (id != 0) {
              this.disconnect (id);
            }
            send_message.callback ();
          });
        }
296 297 298
        id = this.message_sent.connect ((m, e) => {
          if (msg == m) {
            err = e;
299
            cancellable.disconnect (cancel_id);
300 301 302 303 304
            this.disconnect (id);
            send_message.callback ();
          }
        });
        send_loop_queue.push (msg);
305
        
306
        yield;
307
        
308 309
        if (err != null) {
          throw err;
310 311
        } else if (cancellable != null) {
          cancellable.set_error_if_cancelled ();
312 313 314 315
          if (cancellable.is_cancelled()) {
            /* maybe not sufficient if already popped out */
            send_loop_queue.remove(msg);
          }
316
        }
317 318
        
        return true;
319 320 321
      }
    }

322 323 324 325 326
    /**
     * Asynchronous method to connect to the remote server
     *
     * This is to be called once at the beginning of the network communications
     * 
327
     * @param cancellable a Cancellable object or null
328 329
     * @return Whether the connection succeeded
     */
330 331
    public async bool connect_to (Cancellable? cancellable = null)
      throws ServerError, IOError
332 333
      requires (connection == null)
    {
334
      try {
335
        SocketConnectable addr = NetworkAddress.parse (host, port);
336
        connection = yield socket.connect_async (addr, cancellable);
337 338
        input  = new DataInputStream  (connection.input_stream);
        output = new DataOutputStream (connection.output_stream);
339
        listen_loop_cancel = new Cancellable ();
340
        send_loop_cancel = new Cancellable ();
341
        listen_loop_running = 1;
342
        listen_loop_thread = new Thread<bool>.try ("listen loop", listen_loop);
343
        send_loop_running = 1;
344
        send_loop_thread = new Thread<bool>.try ("send loop", send_loop);
345 346
      } catch (IOError.CANCELLED c) {
        throw c;
347 348
      } catch (Error e) {
        warning ("Trying to connect: %s", e.message);
349 350
        throw new ServerError.FAILED ("Failed to connect to server: %s",
                                      e.message);
351
      }
352
      
353
      Message msg = yield receive_type (MessageType.WELCOME, cancellable);
354 355 356 357 358
      debug ("Got welcome from server: %s", (msg as WelcomeMessage).message);

      return true;
    }

359 360 361 362 363 364 365
    /**
     * Asynchronous method to disconnect from the remote server
     *
     * This is to be called when network communications stop
     * 
     * @return Whether the disconnect succeeded
     */
366
    public bool disconnect_from ()
367
      throws ServerError
368
      requires (connection != null)
369
    {
370
      if (listen_loop_cancel != null && AtomicInt.get (ref listen_loop_running) > 0) {
371 372
        listen_loop_cancel.cancel ();
        listen_loop_cancel = null;
373
        listen_loop_thread.join ();
374
      }
375
      if (send_loop_cancel != null && AtomicInt.get (ref send_loop_running) > 0) {
376
        send_loop_cancel = null;
377
        AtomicInt.set (ref send_loop_running, 0);
378
        send_loop_thread.join ();
379
      }
380
      try {
381 382 383
        /* hack: we can't send using the thread since we just shut it down,
         * so send the disconnect message synchronously for now */
        new DisconnectMessage ().send (output);
384
        connection.socket.close();
385
      } catch (Error e) {
386 387
        throw new ServerError.COMMUNICATION_FAILED ("Disconnection failed: %s",
                                                    e.message);
388
      }
389 390 391
      connection  = null;
      input       = null;
      output      = null;
392 393

      return true;
394 395
    }

396 397 398 399 400
    /**
     * Login to the server with login & password
     * 
     * @param login The login
     * @param password The password
401
     * @param cancellable a Cancellable object or null
402 403
     * @return Whether login was successful
     */
404 405 406 407
    public async bool login_to (string        login,
                                string        password,
                                Cancellable?  cancellable = null)
      throws ServerError, IOError
408 409
      requires (connection != null)
    {
410 411
      yield send_message (new LoginMessage (login, password), cancellable);
      Message msg = yield receive_type (MessageType.LOGIN, cancellable);
412
      
413
      if ((msg as LoginMessage).status == LoginMessage.State.ALREADY_EXISTS) {
414
        throw new ServerError.NICKNAME_IN_USE ("Nickname already in use");
415 416
      }
      if ((msg as LoginMessage).status == LoginMessage.State.MISSING_AUTHENTICATION) {
417
        throw new ServerError.MISSING_AUTHENTICATION ("Authentication required");
418
      }
419
      if ((msg as LoginMessage).status != LoginMessage.State.OK) {
420 421 422
        throw new ServerError.COMMUNICATION_FAILED ("Invalid login status");
      }

423 424 425
      return true;
    }

426 427 428 429 430
    /**
     * Checks whether we are connected to the remote server
     * 
     * @return The connection state
     */
431 432 433 434 435
    public bool is_connected ()
    {
      return connection != null;
    }

436
    /**
437
     * Enumerate some games available on the server
438
     *
439
     * This method retrieves a list of available games currently on the server
440 441
     * matching the given filter
     * 
442 443
     * @param filter a mask of {@link OVCC.GameState}s to filter in
     * @return A list of {@link OVCC.GameDescription} on the remote server matching //filter//
444
     */
445 446
    public async GameDescription[]? enumerate_games (GameState?   filter      = null,
                                                     Cancellable? cancellable = null)
447
      throws ServerError, IOError
448
      requires (connection != null)
449
    {
450
      /* ask the server to send the list */
451
      yield send_message (new ListGamesMessage (), cancellable);
452 453

      /* wait for answer */
454 455
      Message msg = yield receive_type (MessageType.LIST_GAMES, cancellable);
      ListGamesMessage list = msg as ListGamesMessage;
456

457 458 459 460 461 462 463 464 465 466 467
      /* filter the list */
      GameDescription[] filtered = {};
      if (filter != null) {
        foreach (var d in list.descriptions) {
          if (d.state in filter) {
            filtered += d;
          }
        }
      } else {
        filtered = list.descriptions;
      }
468

469
      return filtered;
470
    }
471

472
    /**
473
     * Join a game, index -1 means any open game
474
     *
475
     * The index is typically within the list returned by enumerate_games()
476
     * 
477
     * @param index The game index
478
     * @param player The player to make join
479
     * @param cancellable a Cancellable object or null
480 481
     * @return The game instance corresponding to the one joint on the remote server
     */
482 483
    public async Game join_game (int           index,
                                 Player        player,
484
                                 string?       name = null,
485
                                 Cancellable?  cancellable = null)
486
      throws ServerError, IOError
487
      requires (connection != null)
488
    {
489 490
      TilesDef tiles = new TilesDef ();
      TileSet  tileset = new TileSet ();
491 492

      /* call the server */
493
      yield send_message (new JoinMessage (index, name), cancellable);
494

495 496 497 498 499
      /* wait for answer */
      Message msg = yield receive_type (MessageType.JOIN, cancellable);
      JoinMessage join = msg as JoinMessage;
      if (join.status != JoinMessage.State.OK) {
        if (index == -1) {
500
          throw new ServerError.NO_OPEN_GAME ("Server said that no open game is available");
501
        } else {
502
          throw new ServerError.NO_OPEN_GAME ("Server said that it is not an open game");
503 504 505
        }
      }

506
      /* wait for data */
507
      Message msg2 = yield receive_type (MessageType.GAMEDATA, cancellable);
508 509 510 511 512 513
      GamedataMessage data = msg2 as GamedataMessage;
      if (data.status != GamedataMessage.State.OK) {
        throw new ServerError.COMMUNICATION_FAILED ("Failed to get game data");
      }

      /* create our game */
514
      try {
515
        tiles.load_from_string (data.tiles_data);
516
      } catch (Error e1) {
517 518
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tiles data from server: %s",
                                                e1.message);
519 520
      }
      try {
521
        tileset.load_from_string (tiles, data.tileset_data);
522
      } catch (Error e2) {
523 524
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tileset data from server: %s",
                                                e2.message);
525
      }
526 527

      var stack = new Stack.from_tile_ids (data.stack_ids, tiles);
528
      
529 530 531
      Game game = new Game (tileset, stack);

      foreach (var nick in data.player_nicks) {
532 533 534
        try {
          game.add_player (new Player (nick));
        } catch (GameError e3) {
535 536
          throw new ServerError.COMMUNICATION_FAILED ("Failed to add a player (%s) as requested, you may be out of sync",
                                                      e3.message);
537
        }
538
      }
Jonathan Michalon's avatar
Jonathan Michalon committed
539

540
      /* start listening to game signals */
541 542 543 544
      var signal_handle = new SignalHandle (game, player);
      signal_handle.send_signal.connect ((m) => {
        send_message.begin (m as Message);
      });
545 546 547 548 549 550 551 552
      var sh_sigq = new SigQueue ();
      sh_sigq.add (this, message_received[MessageType.SIGNAL.to_string ()].connect ((m) => {
        signal_handle.emit_received (m as SignalMessage);
      }));
      sh_sigq.add (this, message_received[MessageType.DISCONNECT.to_string ()].connect ((m) => {
        sh_sigq.remove_all ();
        signal_handle = null;
      }));
Jonathan Michalon's avatar
Jonathan Michalon committed
553

554 555 556 557 558
      try {
        game.add_player (player);
      } catch (GameError e4) {
        throw new ServerError.INVALID_NICKNAME (e4.message);
      }
559
      
Jonathan Michalon's avatar
Jonathan Michalon committed
560
      return game;
561
    }
562 563 564
  }
}