ovccclient-server.vala 16.8 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 67
    /**
     * No open table available
     */
68
    NO_OPEN_TABLE,
69 70 71
    /**
     * Data could not be reached where it was searched for
     */
72
    DATA_UNREACHABLE,
73 74 75
    /**
     * Any other error
     */
76 77 78
    FAILED
  }
  
79 80 81
  /**
   * Flags for filtering table list
   */
82 83 84
  [Flags]
  public enum TablesFilter
  {
85 86 87
    /**
     * All tables
     */
88
    ALL,
89 90 91
    /**
     * Only open tables
     */
92
    OPEN,
93 94 95
    /**
     * Only tables with people waiting
     */
96
    PEOPLE_WAITING,
97 98 99
    /**
     * Only tables where people ask for other players
     */
100
    ASKING_FOR_PEOPLE
101
  }
102

103
  
104 105 106 107 108 109
  /**
   * A class representing the server side of the network clients
   *
   * This class contains lots of methods making easy to communicate with
   * a remote OVCCServer.
   */
110 111
  public class Server : Object
  {
112 113 114 115 116 117 118 119 120 121
    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;
122
    private int                   send_loop_running = 0;
123 124
    private Cancellable?          send_loop_cancel = null;
    private AsyncQueue<Message>   send_loop_queue = new AsyncQueue<Message> ();
125

126 127 128 129 130
    /**
     * A signal emitted when a message was received from server
     * 
     * @param msg A {@link Message} instance containing what the server sent us
     */
131 132
    [Signal (detailed = true)]
    public signal void message_received (Message msg);
133 134 135 136 137 138
    /**
     * A signal emitted when a message in queue was sent to the server
     * 
     * @param msg A {@link Message} instance which was just sent to the server
     * @param err An optional error raised when sending
     */
139 140
    public signal void message_sent     (Message      msg,
                                         ServerError? err);
141

142 143 144 145 146 147 148 149
    /**
     * 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
     */
150
    public Server (string host,
151
                   uint16 port)
152 153
    {
      socket = new SocketClient ();
154 155
      this.host = host;
      this.port = port;
156
    }
157

158
    /* loop listening to incoming messages */
159
    private bool listen_loop ()
160
    {
161 162 163 164
      /* 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;
165
      var running = true;
166 167
      
      while (running && ! cancellable.is_cancelled ()) {
168 169 170
        Message? msg;
        
        try {
171
          msg = Message.receive (input, cancellable);
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
        } 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;
        }
      }
194
      return true;
195
    }
196

197
    /* loop sending messages to the server */
198
    private bool send_loop ()
199
    {
200 201
      var cancellable = send_loop_cancel;
      
202 203 204
      while (! cancellable.is_cancelled () &&
             (send_loop_queue.length () > 0 ||
              AtomicInt.get (ref send_loop_running) > 0)) {
205
        Message?      msg;
206
        ServerError?  msg_e = null;
207 208 209 210 211 212
        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);
213
        if ((msg = send_loop_queue.timed_pop (ref tv)) == null) {
214 215 216
          /* no message yet, we'll sleep again */
          continue;
        }
217 218
        
        try {
219
          msg.send (output, cancellable);
220 221 222 223 224 225 226 227 228 229 230 231 232 233
        } 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;
        });
      }
234
      return true;
235 236
    }

237 238 239 240 241
    /**
     * Asynchronous method allowing to pause until a given {@link MessageType}
     * get received.
     * 
     * @param mtype The {@link MessageType} to wait for
242
     * @param cancellable a Cancellable object or null
243 244
     * @return The message received
     */
245 246 247
    public async Message receive_type (MessageType  mtype,
                                       Cancellable? cancellable = null)
      throws IOError
248 249
    {
      Message msg = null;
250 251
      ulong   id = 0;
      ulong   cancel_id = 0;
252
      
253 254 255 256 257 258 259 260 261
      if (cancellable != null) {
        cancel_id = cancellable.connect (() => {
          cancel_id = 0;
          if (id != 0) {
            this.disconnect (id);
          }
          receive_type.callback ();
        });
      }
262 263
      id = this.message_received[mtype.to_string ()].connect ((m) => {
        msg = m;
264
        cancellable.disconnect (cancel_id);
265 266 267
        this.disconnect (id);
        receive_type.callback ();
      });
268
      
269
      yield;
270 271 272 273
      
      if (cancellable != null) {
        cancellable.set_error_if_cancelled ();
      }
274 275 276
      return msg;
    }

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

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

      return true;
    }

363 364 365 366 367 368 369
    /**
     * Asynchronous method to disconnect from the remote server
     *
     * This is to be called when network communications stop
     * 
     * @return Whether the disconnect succeeded
     */
370
    public bool disconnect_from ()
371
      throws ServerError
372
      requires (connection != null)
373
    {
374 375 376
      if (listen_loop_cancel != null) {
        listen_loop_cancel.cancel ();
        listen_loop_cancel = null;
377
        listen_loop_thread.join ();
378
      }
379 380
      if (send_loop_cancel != null) {
        send_loop_cancel = null;
381
        AtomicInt.set (ref send_loop_running, 0);
382
        send_loop_thread.join ();
383
      }
384
      try {
385 386 387
        /* 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);
388
        connection.socket.close();
389
      } catch (Error e) {
390 391
        throw new ServerError.COMMUNICATION_FAILED ("Disconnection failed: %s",
                                                    e.message);
392
      }
393 394 395
      connection  = null;
      input       = null;
      output      = null;
396 397

      return true;
398 399
    }

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

427 428 429
      return true;
    }

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

440 441 442 443 444 445 446 447 448
    /**
     * Enumerate some tables available on the server
     *
     * This method retrieves a list of available tables currently on the server
     * matching the given filter
     * 
     * @param filter the {@link TablesFilter} to use
     * @return A list of table indexes on the remote server matching //filter//
     */
449
    public List<int>? enumerate_tables (TablesFilter filter)
450
      requires (connection != null)
451
    {
452
      return null;
453
    }
454

455 456 457 458 459 460 461
    /**
     * Join a table, index -1 means any open table
     *
     * The index is typically within the list returned by enumerate_tables()
     * 
     * @param index The table index
     * @param player The player to make join
462
     * @param cancellable a Cancellable object or null
463 464
     * @return The game instance corresponding to the one joint on the remote server
     */
465 466 467 468
    public async Game join_table (int           index,
                                  Player        player,
                                  Cancellable?  cancellable = null)
      throws ServerError, IOError
469
      requires (connection != null)
470
    {
471 472 473
      /* FIXME load right tileset and do something with stack... */
      TilesDef tiles = new TilesDef ();
      TileSet  tileset = new TileSet ();
474 475

      /* call the server */
476
      yield send_message (new JoinMessage (index), cancellable);
477 478

      /* wait for data */
479
      Message msg2 = yield receive_type (MessageType.GAMEDATA, cancellable);
480 481 482 483 484 485
      GamedataMessage data = msg2 as GamedataMessage;
      if (data.status != GamedataMessage.State.OK) {
        throw new ServerError.COMMUNICATION_FAILED ("Failed to get game data");
      }

      /* create our game */
486 487 488
      try {
        tiles.load (File.new_for_path (data.tiles_filename));
      } catch (Error e1) {
489 490
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tiles data from \"%s\": %s",
                                                data.tiles_filename, e1.message);
491 492 493 494
      }
      try {
        tileset.load (tiles, File.new_for_path (data.tileset_filename));
      } catch (Error e2) {
495 496
        throw new ServerError.DATA_UNREACHABLE ("Failed to load tileset data from \"%s\": %s",
                                                data.tileset_filename, e2.message);
497
      }
498 499

      var stack = new Stack.from_tile_ids (data.stack_ids, tiles);
500
      
501 502 503
      Game game = new Game (tileset, stack);

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

512
      /* start listening to game signals */
513 514 515 516
      var signal_handle = new SignalHandle (game, player);
      signal_handle.send_signal.connect ((m) => {
        send_message.begin (m as Message);
      });
517 518 519 520 521 522 523 524
      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
525

526 527 528 529 530
      try {
        game.add_player (player);
      } catch (GameError e4) {
        throw new ServerError.INVALID_NICKNAME (e4.message);
      }
531
      
Jonathan Michalon's avatar
Jonathan Michalon committed
532
      return game;
533
    }
534 535 536
  }
}