diff --git a/files/securedrop-client b/files/securedrop-client index 1da697b0a..4951775ce 100755 --- a/files/securedrop-client +++ b/files/securedrop-client @@ -10,5 +10,13 @@ cd /opt/venvs/securedrop-client # Check if qubes-db exists (and we are running in qubes) if [ ! -f "/usr/bin/qubesdb-read" ]; then echo "Not running in Qubes, client not starting." && exit; fi +# EXPERIMENTAL(#1547): Check for the SDEXTENDEDTIMEOUT_N service flag and export it as +# SDEXTENDEDTIMEOUT=N. +timeout_flag_value=$(qubesdb-list /qubes-service/SDEXTENDEDTIMEOUT_) +if [ -n "$timeout_flag_value" ]; then + echo "SDEXTENDEDTIMEOUT=$timeout_flag_value" + export SDEXTENDEDTIMEOUT="$timeout_flag_value" +fi + # Now execute the actual client, only if running in an sd-app if [ "$(qubesdb-read /name)" = "sd-app" ]; then ./bin/sd-client; else echo "Not running in sd-app, client not starting."; fi diff --git a/securedrop_client/api_jobs/sync.py b/securedrop_client/api_jobs/sync.py index 3973a2c97..ee57140b1 100644 --- a/securedrop_client/api_jobs/sync.py +++ b/securedrop_client/api_jobs/sync.py @@ -1,4 +1,5 @@ import logging +import os from typing import Any, List, Optional from sdclientapi import API @@ -18,6 +19,7 @@ class MetadataSyncJob(ApiJob): Update source metadata such that new download jobs can be added to the queue. """ + DEFAULT_REQUEST_TIMEOUT = 60 # sec NUMBER_OF_TIMES_TO_RETRY_AN_API_CALL = 2 def __init__(self, data_dir: str, app_state: Optional[state.State] = None) -> None: @@ -40,7 +42,15 @@ def call_api(self, api_client: API, session: Session) -> Any: # # This timeout is used for 3 different requests: `get_sources`, `get_all_submissions`, and # `get_all_replies` - api_client.default_request_timeout = 60 + api_client.default_request_timeout = int( + os.environ.get("SDEXTENDEDTIMEOUT", self.DEFAULT_REQUEST_TIMEOUT) + ) + if api_client.default_request_timeout != self.DEFAULT_REQUEST_TIMEOUT: + logger.warn( + f"{self.__class__.__name__} will use " + f"default_request_timeout={api_client.default_request_timeout}" + ) + users = api_client.get_users() MetadataSyncJob._update_users(session, users) sources, submissions, replies = get_remote_data(api_client) diff --git a/tests/api_jobs/test_sync.py b/tests/api_jobs/test_sync.py index e8ec2342a..68f8ee6a1 100644 --- a/tests/api_jobs/test_sync.py +++ b/tests/api_jobs/test_sync.py @@ -14,6 +14,8 @@ Submission = namedtuple("Submission", ["uuid", "source_uuid", "is_file", "is_downloaded"]) File = namedtuple("File", ["is_downloaded"]) +TIMEOUT_OVERRIDE = 600 # sec + class TestUpdateState(unittest.TestCase): def setUp(self): @@ -37,6 +39,31 @@ def test_handles_missing_files_gracefully(self): assert self._state.file("9") +def test_MetadataSyncJob_has_default_timeout(mocker, homedir, session, session_maker): + api_client = mocker.patch("securedrop_client.logic.sdclientapi.API") + remote_user = factory.RemoteUser() + api_client.get_users = mocker.MagicMock(return_value=[remote_user]) + + job = MetadataSyncJob(homedir) + job.call_api(api_client, session) + assert api_client.default_request_timeout == job.DEFAULT_REQUEST_TIMEOUT + + +def test_MetadataSyncJob_takes_overriden_timeout(mocker, homedir, session, session_maker): + api_client = mocker.patch("securedrop_client.logic.sdclientapi.API") + remote_user = factory.RemoteUser() + api_client.get_users = mocker.MagicMock(return_value=[remote_user]) + + os.environ["SDEXTENDEDTIMEOUT"] = str(TIMEOUT_OVERRIDE) # environment value must be string + + job = MetadataSyncJob(homedir) + job.call_api(api_client, session) + assert api_client.default_request_timeout == TIMEOUT_OVERRIDE + + # Don't pollute the environment for subsequent/out-of-order tests. + del os.environ["SDEXTENDEDTIMEOUT"] + + def test_MetadataSyncJob_creates_new_user(mocker, homedir, session, session_maker): api_client = mocker.patch("securedrop_client.logic.sdclientapi.API") remote_user = factory.RemoteUser()