diff --git a/src/rc_client.c b/src/rc_client.c index 98d8c1fe..9302b8c8 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -339,6 +339,28 @@ void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_ } } +static int rc_client_async_handle_valid(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int valid = 0; + size_t i; + + /* there is a small window of opportunity where the client could have been destroyed before calling + * this function, but this function assumes the possibility that the handle has been destroyed, so + * we can't check it for RC_CLIENT_ASYNC_DESTROYED before attempting to scan the client data */ + rc_mutex_lock(&client->state.mutex); + + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i] == async_handle) { + valid = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return valid; +} + static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) { if (!response->succeeded) { @@ -461,6 +483,10 @@ static int rc_client_should_retry(const rc_api_server_response_t* server_respons /* connection to server from cloudfare was dropped before request was completed */ return 1; + case 525: /* 525 SSL Handshake Failed */ + /* web server worker connection pool is exhausted */ + return 1; + case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR: /* client provided non-HTTP error (explicitly retryable) */ return 1; @@ -614,7 +640,13 @@ static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, rc_api_destroy_request(&request); - return &callback_data->async_handle; + /* if the user state has changed, the async operation completed synchronously */ + rc_mutex_lock(&client->state.mutex); + if (client->state.user != RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + callback_data = NULL; + rc_mutex_unlock(&client->state.mutex); + + return callback_data ? &callback_data->async_handle : NULL; } rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, @@ -2475,6 +2507,7 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons else { /* call the server to make sure the hash is valid for the loaded game */ rc_client_load_state_t* callback_data; + rc_client_async_handle_t* async_handle; rc_api_resolve_hash_request_t resolve_hash_request; rc_api_request_t request; int result; @@ -2500,12 +2533,14 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons callback_data->hash = game_hash; callback_data->game = game; - rc_client_begin_async(client, &callback_data->async_handle); + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); rc_api_destroy_request(&request); - return &callback_data->async_handle; + /* if handle is no longer valid, the async operation completed synchronously */ + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; } } @@ -3894,6 +3929,7 @@ static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_clien rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) { rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; rc_api_request_t request; int result; const char* error_message; @@ -3917,11 +3953,12 @@ static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_clien callback_data->callback_userdata = callback_userdata; callback_data->leaderboard_id = lbinfo_request->leaderboard_id; - rc_client_begin_async(client, &callback_data->async_handle); + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); rc_api_destroy_request(&request); - return &callback_data->async_handle; + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; } rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, diff --git a/test/test_rc_client.c b/test/test_rc_client.c index 7337432a..0aeafaed 100644 --- a/test/test_rc_client.c +++ b/test/test_rc_client.c @@ -545,6 +545,22 @@ static void rc_client_callback_expect_success(int result, const char* error_mess ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); } +static void rc_client_callback_expect_timeout(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Request has timed out."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_no_internet(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Internet not available."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) { ASSERT_FAIL("Callback should not have been called."); @@ -885,6 +901,26 @@ static void test_login_with_password_async_destroyed(void) "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); } +static void test_login_with_password_client_error(void) +{ + const rc_client_user_t* user; + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in(); + + mock_api_error("r=login2&u=User&p=Pa%24%24word", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_no_internet, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + ASSERT_PTR_NULL(handle); + + rc_client_destroy(g_client); +} + static void rc_client_callback_expect_login_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) { ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); @@ -1467,14 +1503,6 @@ static void test_load_game_startsession_failure(void) rc_client_destroy(g_client); } -static void rc_client_callback_expect_timeout(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); - ASSERT_STR_EQUALS(error_message, "Request has timed out."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - static void test_load_game_startsession_timeout(void) { g_client = mock_client_logged_in(); @@ -2676,7 +2704,7 @@ static void test_change_media_while_loading_later(void) free(image); } -static void test_change_media_aborted(void) +static void test_change_media_async_aborted(void) { rc_client_async_handle_t* handle; const size_t image_size = 32768; @@ -2722,6 +2750,39 @@ static void test_change_media_aborted(void) free(image); } +static void test_change_media_client_error(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + mock_api_error("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_no_internet, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + ASSERT_PTR_NULL(handle); + + rc_client_destroy(g_client); + free(image); +} + /* ----- get game image ----- */ static void test_game_get_image_url(void) @@ -4718,7 +4779,7 @@ static void rc_client_callback_expect_leaderboard_uncalled(int result, const cha ASSERT_FAIL("Callback should not have been called.") } -static void test_fetch_leaderboard_entries_aborted(void) +static void test_fetch_leaderboard_entries_async_aborted(void) { rc_client_async_handle_t* handle; @@ -4738,6 +4799,35 @@ static void test_fetch_leaderboard_entries_aborted(void) rc_client_destroy(g_client); } +static void rc_client_callback_expect_leaderboard_internet_not_available(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Internet not available."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + ASSERT_PTR_NULL(list); +} + +static void test_fetch_leaderboard_entries_client_error(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + g_leaderboard_entries = NULL; + + mock_api_error("r=lbinfo&i=4401&c=10", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_internet_not_available, g_callback_userdata); + + ASSERT_PTR_NULL(handle); + ASSERT_PTR_NULL(g_leaderboard_entries); + + rc_client_destroy(g_client); +} + /* ----- do frame ----- */ static void test_do_frame_bounds_check_system(void) @@ -7955,6 +8045,7 @@ void test_client(void) { TEST(test_login_with_password_async); TEST(test_login_with_password_async_aborted); TEST(test_login_with_password_async_destroyed); + TEST(test_login_with_password_client_error); /* logout */ TEST(test_logout); @@ -8024,7 +8115,8 @@ void test_client(void) { TEST(test_change_media_back_and_forth); TEST(test_change_media_while_loading); TEST(test_change_media_while_loading_later); - TEST(test_change_media_aborted); + TEST(test_change_media_async_aborted); + TEST(test_change_media_client_error); /* game */ TEST(test_game_get_image_url); @@ -8061,7 +8153,8 @@ void test_client(void) { TEST(test_fetch_leaderboard_entries_no_user); TEST(test_fetch_leaderboard_entries_around_user); TEST(test_fetch_leaderboard_entries_around_user_not_logged_in); - TEST(test_fetch_leaderboard_entries_aborted); + TEST(test_fetch_leaderboard_entries_async_aborted); + TEST(test_fetch_leaderboard_entries_client_error); /* do frame */ TEST(test_do_frame_bounds_check_system);