ovccclient-server.vala 17.5 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 95 96
    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;
    private Cancellable?          listen_loop_cancel = null;
    private unowned Thread<bool>  send_loop_thread = null;
97
    private int                   send_loop_running = 0;
98 99
    private Cancellable?          send_loop_cancel = null;
    private AsyncQueue<Message>   send_loop_queue = new AsyncQueue<Message> ();
100

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

117 118 119 120 121 122 123 124
    /**
     * 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
     */
125
    public Server (string host,
126
                   uint16 port)
127 128
    {
      socket = new SocketClient ();
129 130
      this.host = host;
      this.port = port;
131
    }
132

133
    /* loop listening to incoming messages */
134
    private bool listen_loop ()
135
    {
136 137 138 139
      /* 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;
140
      var running = true;
141 142
      
      while (running && ! cancellable.is_cancelled ()) {
143 144 145
        Message? msg;
        
        try {
146
          msg = Message.receive (input, cancellable);
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
        } catch (IOError.CANCELLED e) {
          break;
        } catch (Error e) {
          warning ("error receiving data: %s", e.message);
          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) {
          running = false; break;
        }
      }
169
      return true;
170
    }
171

172
    /* loop sending messages to the server */
173
    private bool send_loop ()
174
    {
175 176
      var cancellable = send_loop_cancel;
      
177 178 179
      while (! cancellable.is_cancelled () &&
             (send_loop_queue.length () > 0 ||
              AtomicInt.get (ref send_loop_running) > 0)) {
180
        Message?      msg;
181
        ServerError?  msg_e = null;
182 183 184 185 186 187
        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);
188
        if ((msg = send_loop_queue.timed_pop (ref tv)) == null) {
189 190 191
          /* no message yet, we'll sleep again */
          continue;
        }
192 193
        
        try {
194
          msg.send (output, cancellable);
195 196 197 198 199 200 201 202 203 204 205 206 207 208
        } catch (IOError.CANCELLED e) {
          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;
        });
      }
209
      return true;
210 211
    }

212
    /**
213
     * Asynchronous method allowing to pause until a given {@link OVCC.Network.MessageType}
214 215
     * get received.
     * 
216
     * @param mtype The {@link OVCC.Network.MessageType} to wait for
217
     * @param cancellable a Cancellable object or null
218 219
     * @return The message received
     */
220 221 222
    public async Message receive_type (MessageType  mtype,
                                       Cancellable? cancellable = null)
      throws IOError
223 224
    {
      Message msg = null;
225 226
      ulong   id = 0;
      ulong   cancel_id = 0;
227
      
228 229 230 231 232 233 234 235 236
      if (cancellable != null) {
        cancel_id = cancellable.connect (() => {
          cancel_id = 0;
          if (id != 0) {
            this.disconnect (id);
          }
          receive_type.callback ();
        });
      }
237 238
      id = this.message_received[mtype.to_string ()].connect ((m) => {
        msg = m;
239
        cancellable.disconnect (cancel_id);
240 241 242
        this.disconnect (id);
        receive_type.callback ();
      });
243
      
244
      yield;
245 246 247 248
      
      if (cancellable != null) {
        cancellable.set_error_if_cancelled ();
      }
249 250 251
      return msg;
    }

252 253 254
    /**
     * Asynchronous method to send messages to the remote server.
     * 
255
     * @param msg The {@link OVCC.Network.Message} to send
256
     * @param cancellable a Cancellable object or null
257 258
     * @return Whether the operation succeeded.
     */
259 260 261
    public async bool send_message (Message       msg,
                                    Cancellable?  cancellable = null)
      throws ServerError, IOError
262 263
      requires (connection != null)
    {
264 265 266 267
      if (AtomicInt.get (ref send_loop_running) == 0) {
        throw new ServerError.COMMUNICATION_FAILED ("Send loop is not running");
      } else {
        ulong         id = 0;
268
        ulong         cancel_id = 0;
269 270
        ServerError?  err = null;
        
271 272 273 274 275 276 277 278 279
        if (cancellable != null) {
          cancel_id = cancellable.connect (() => {
            cancel_id = 0;
            if (id != 0) {
              this.disconnect (id);
            }
            send_message.callback ();
          });
        }
280 281 282
        id = this.message_sent.connect ((m, e) => {
          if (msg == m) {
            err = e;
283
            cancellable.disconnect (cancel_id);
284 285 286 287 288
            this.disconnect (id);
            send_message.callback ();
          }
        });
        send_loop_queue.push (msg);
289
        
290
        yield;
291
        
292 293
        if (err != null) {
          throw err;
294 295
        } else if (cancellable != null) {
          cancellable.set_error_if_cancelled ();
296
        }
297 298
        
        return true;
299 300 301
      }
    }

302 303 304 305 306
    /**
     * Asynchronous method to connect to the remote server
     *
     * This is to be called once at the beginning of the network communications
     * 
307
     * @param cancellable a Cancellable object or null
308 309
     * @return Whether the connection succeeded
     */
310 311
    public async bool connect_to (Cancellable? cancellable = null)
      throws ServerError, IOError
312 313
      requires (connection == null)
    {
314
      try {
315
        SocketConnectable addr = NetworkAddress.parse (host, port);
316
        connection = yield socket.connect_async (addr, cancellable);
317 318
        input  = new DataInputStream  (connection.input_stream);
        output = new DataOutputStream (connection.output_stream);
319
        listen_loop_cancel = new Cancellable ();
320
        send_loop_cancel = new Cancellable ();
321
        listen_loop_thread = new Thread<bool>.try ("listen loop", listen_loop);
322
        send_loop_running = 1;
323
        send_loop_thread = new Thread<bool>.try ("send loop", send_loop);
324 325
      } catch (IOError.CANCELLED c) {
        throw c;
326 327
      } catch (Error e) {
        warning ("Trying to connect: %s", e.message);
328 329
        throw new ServerError.FAILED ("Failed to connect to server: %s",
                                      e.message);
330
      }
331
      
332
      Message msg = yield receive_type (MessageType.WELCOME, cancellable);
333 334 335 336 337
      debug ("Got welcome from server: %s", (msg as WelcomeMessage).message);

      return true;
    }

338 339 340 341 342 343 344
    /**
     * Asynchronous method to disconnect from the remote server
     *
     * This is to be called when network communications stop
     * 
     * @return Whether the disconnect succeeded
     */
345
    public bool disconnect_from ()
346
      throws ServerError
347
      requires (connection != null)
348
    {
349 350 351
      if (listen_loop_cancel != null) {
        listen_loop_cancel.cancel ();
        listen_loop_cancel = null;
352
        listen_loop_thread.join ();
353
      }
354 355
      if (send_loop_cancel != null) {
        send_loop_cancel = null;
356
        AtomicInt.set (ref send_loop_running, 0);
357
        send_loop_thread.join ();
358
      }
359
      try {
360 361 362
        /* 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);
363
        connection.socket.close();
364
      } catch (Error e) {
365 366
        throw new ServerError.COMMUNICATION_FAILED ("Disconnection failed: %s",
                                                    e.message);
367
      }
368 369 370
      connection  = null;
      input       = null;
      output      = null;
371 372

      return true;
373 374
    }

375 376 377 378 379
    /**
     * Login to the server with login & password
     * 
     * @param login The login
     * @param password The password
380
     * @param cancellable a Cancellable object or null
381 382
     * @return Whether login was successful
     */
383 384 385 386
    public async bool login_to (string        login,
                                string        password,
                                Cancellable?  cancellable = null)
      throws ServerError, IOError
387 388
      requires (connection != null)
    {
389 390
      yield send_message (new LoginMessage (login, password), cancellable);
      Message msg = yield receive_type (MessageType.LOGIN, cancellable);
391
      
392
      if ((msg as LoginMessage).status == LoginMessage.State.ALREADY_EXISTS) {
393
        throw new ServerError.NICKNAME_IN_USE ("Nickname already in use");
394 395
      }
      if ((msg as LoginMessage).status == LoginMessage.State.MISSING_AUTHENTICATION) {
396
        throw new ServerError.MISSING_AUTHENTICATION ("Authentication required");
397
      }
398
      if ((msg as LoginMessage).status != LoginMessage.State.OK) {
399 400 401
        throw new ServerError.COMMUNICATION_FAILED ("Invalid login status");
      }

402 403 404
      return true;
    }

405 406 407 408 409
    /**
     * Checks whether we are connected to the remote server
     * 
     * @return The connection state
     */
410 411 412 413 414
    public bool is_connected ()
    {
      return connection != null;
    }

415
    /**
416
     * Enumerate some games available on the server
417
     *
418
     * This method retrieves a list of available games currently on the server
419 420
     * matching the given filter
     * 
421 422
     * @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//
423
     */
424 425
    public async GameDescription[]? enumerate_games (GameState?   filter      = null,
                                                     Cancellable? cancellable = null)
426
      throws ServerError, IOError
427
      requires (connection != null)
428
    {
429
      /* ask the server to send the list */
430
      yield send_message (new ListGamesMessage (), cancellable);
431 432

      /* wait for answer */
433 434
      Message msg = yield receive_type (MessageType.LIST_GAMES, cancellable);
      ListGamesMessage list = msg as ListGamesMessage;
435

436 437 438 439 440 441 442 443 444 445 446
      /* 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;
      }
447

448
      return filtered;
449
    }
450

451
    /**
452
     * Join a game, index -1 means any open game
453
     *
454
     * The index is typically within the list returned by enumerate_games()
455
     * 
456
     * @param index The game index
457
     * @param player The player to make join
458
     * @param cancellable a Cancellable object or null
459 460
     * @return The game instance corresponding to the one joint on the remote server
     */
461 462 463
    public async Game join_game (int           index,
                                 Player        player,
                                 Cancellable?  cancellable = null)
464
      throws ServerError, IOError
465
      requires (connection != null)
466
    {
467 468
      TilesDef tiles = new TilesDef ();
      TileSet  tileset = new TileSet ();
469 470

      /* call the server */
471
      yield send_message (new JoinMessage (index), cancellable);
472

473 474 475 476 477
      /* 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) {
478
          throw new ServerError.NO_OPEN_GAME ("Server said that no open game is available");
479
        } else {
480
          throw new ServerError.NO_OPEN_GAME ("Server said that it is not an open game");
481 482 483
        }
      }

484
      /* wait for data */
485
      Message msg2 = yield receive_type (MessageType.GAMEDATA, cancellable);
486 487 488 489 490 491
      GamedataMessage data = msg2 as GamedataMessage;
      if (data.status != GamedataMessage.State.OK) {
        throw new ServerError.COMMUNICATION_FAILED ("Failed to get game data");
      }

      /* create our game */
492
      try {
493
        tiles.load_from_string (data.tiles_data);
494
      } catch (Error e1) {
495 496
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tiles data from server: %s",
                                                e1.message);
497 498
      }
      try {
499
        tileset.load_from_string (tiles, data.tileset_data);
500
      } catch (Error e2) {
501 502
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tileset data from server: %s",
                                                e2.message);
503
      }
504 505

      var stack = new Stack.from_tile_ids (data.stack_ids, tiles);
506
      
507 508 509
      Game game = new Game (tileset, stack);

      foreach (var nick in data.player_nicks) {
510 511 512
        try {
          game.add_player (new Player (nick));
        } catch (GameError e3) {
513 514
          throw new ServerError.COMMUNICATION_FAILED ("Failed to add a player (%s) as requested, you may be out of sync",
                                                      e3.message);
515
        }
516
      }
Jonathan Michalon's avatar
Jonathan Michalon committed
517

518
      /* start listening to game signals */
519 520 521 522
      var signal_handle = new SignalHandle (game, player);
      signal_handle.send_signal.connect ((m) => {
        send_message.begin (m as Message);
      });
523 524 525 526 527 528 529 530
      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
531

532 533 534 535 536
      try {
        game.add_player (player);
      } catch (GameError e4) {
        throw new ServerError.INVALID_NICKNAME (e4.message);
      }
537
      
Jonathan Michalon's avatar
Jonathan Michalon committed
538
      return game;
539
    }
540 541 542
  }
}