Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions synodic_client/application/update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def _on_download_finished(self, success: bool, version: str) -> None:
f'v{version} installing\u2026',
UPDATE_STATUS_AVAILABLE_STYLE,
)
self._apply_update()
self._apply_update(silent=True)
return

# Manual mode — show ready banner and let user choose when to restart
Expand All @@ -303,13 +303,18 @@ def _on_download_error(self, error: str) -> None:
# Apply
# ------------------------------------------------------------------

def _apply_update(self) -> None:
"""Apply the downloaded update and restart."""
def _apply_update(self, *, silent: bool = False) -> None:
"""Apply the downloaded update and restart.

Args:
silent: When ``True``, suppress the Velopack splash window
by using ``wait_exit_then_apply_updates``.
"""
if self._client.updater is None:
return

try:
self._client.apply_update_on_exit(restart=True)
self._client.apply_update_on_exit(restart=True, silent=silent)
logger.info('Update scheduled — restarting application')
self._app.quit()
except Exception as e:
Expand Down
7 changes: 4 additions & 3 deletions synodic_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ def download_update(self, progress_callback: Callable[[int], None] | None = None

return self._updater.download_update(progress_callback)

def apply_update_on_exit(self, restart: bool = True) -> None:
def apply_update_on_exit(self, restart: bool = True, *, silent: bool = False) -> None:
"""Schedule the update to apply when the application exits.

Args:
restart: Whether to restart after applying
restart: Whether to restart after applying.
silent: When ``True``, suppress the Velopack splash window.
"""
if self._updater is None:
logger.warning('Updater not initialized')
return

self._updater.apply_update_on_exit(restart=restart)
self._updater.apply_update_on_exit(restart=restart, silent=silent)
45 changes: 22 additions & 23 deletions synodic_client/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,16 +252,24 @@ def download_update(self, progress_callback: Callable[[int], None] | None = None
self._update_info.error = str(e)
return False

def apply_update_on_exit(self, restart: bool = True, restart_args: list[str] | None = None) -> None:
"""Apply the downloaded update, optionally restarting the application.
def apply_update_on_exit(
self,
restart: bool = True,
silent: bool = False,
restart_args: list[str] | None = None,
) -> None:
"""Stage the downloaded update to apply when the process exits.

When *restart* is ``True`` the Velopack runtime applies the update
**and** relaunches the new version (the call does not return).
When ``False`` the update is staged and applied after the process
exits without relaunching.
Uses ``wait_exit_then_apply_updates`` which returns immediately.
The Velopack Update.exe runs after the current process exits,
applies the update, and optionally relaunches the application.

The caller is responsible for shutting down the process (e.g.
``QApplication.quit()``) after this method returns.

Args:
restart: Whether to restart the application after applying.
silent: When ``True``, suppress the Velopack splash window.
restart_args: Optional arguments to pass to the restarted application.
"""
if not self.is_installed:
Expand All @@ -278,23 +286,14 @@ def apply_update_on_exit(self, restart: bool = True, restart_args: list[str] | N
if manager is None:
raise RuntimeError('Velopack manager not available')

logger.info('Applying update (restart=%s)', restart)

if restart:
self._state = UpdateState.APPLYING
if restart_args:
manager.apply_updates_and_restart_with_args(
self._update_info._velopack_info,
restart_args,
)
else:
manager.apply_updates_and_restart(self._update_info._velopack_info)
# apply_updates_and_restart terminates the process;
# fall through only as a safety net.
sys.exit(0)
else:
manager.apply_updates_and_exit(self._update_info._velopack_info)
self._state = UpdateState.APPLIED
logger.info('Applying update (restart=%s, silent=%s)', restart, silent)
self._state = UpdateState.APPLYING
manager.wait_exit_then_apply_updates(
self._update_info._velopack_info,
silent=silent,
restart=restart,
restart_args=restart_args or [],
)

except Exception as e:
logger.exception('Failed to apply update')
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/qt/test_update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,13 @@ class TestDownloadFinished:

@staticmethod
def test_auto_apply_calls_apply_update() -> None:
"""When auto_apply=True, a successful download should call _apply_update."""
"""When auto_apply=True, a successful download should call _apply_update(silent=True)."""
ctrl, app, client, banner, settings = _make_controller(auto_apply=True)

with patch.object(ctrl, '_apply_update') as mock_apply:
ctrl._on_download_finished(True, '2.0.0')

mock_apply.assert_called_once()
mock_apply.assert_called_once_with(silent=True)

@staticmethod
def test_auto_apply_does_not_show_ready_banner() -> None:
Expand Down Expand Up @@ -213,7 +213,7 @@ def test_apply_update_calls_client_and_quits() -> None:
ctrl, app, client, banner, settings = _make_controller()
ctrl._apply_update()

client.apply_update_on_exit.assert_called_once_with(restart=True)
client.apply_update_on_exit.assert_called_once_with(restart=True, silent=False)
app.quit.assert_called_once()

@staticmethod
Expand Down
10 changes: 9 additions & 1 deletion tests/unit/test_client_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,12 @@ def test_apply_update_on_exit_with_init(client_with_updater: Client) -> None:
with patch.object(client_with_updater._updater, 'apply_update_on_exit') as mock_apply:
client_with_updater.apply_update_on_exit(restart=False)

mock_apply.assert_called_once_with(restart=False)
mock_apply.assert_called_once_with(restart=False, silent=False)

@staticmethod
def test_apply_update_on_exit_silent(client_with_updater: Client) -> None:
"""Verify apply_update_on_exit forwards silent flag to updater."""
with patch.object(client_with_updater._updater, 'apply_update_on_exit') as mock_apply:
client_with_updater.apply_update_on_exit(restart=True, silent=True)

mock_apply.assert_called_once_with(restart=True, silent=True)
114 changes: 104 additions & 10 deletions tests/unit/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def test_apply_on_exit_no_downloaded_update(updater: Updater) -> None:

@staticmethod
def test_apply_on_exit_with_restart(updater: Updater) -> None:
"""Verify apply_update_on_exit(restart=True) uses apply_updates_and_restart."""
"""Verify apply_update_on_exit(restart=True) stages update via wait_exit_then_apply_updates."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
Expand All @@ -352,16 +352,20 @@ def test_apply_on_exit_with_restart(updater: Updater) -> None:
with (
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
pytest.raises(SystemExit, match='0'),
):
updater.apply_update_on_exit(restart=True)

assert updater.state == UpdateState.APPLYING
mock_manager.apply_updates_and_restart.assert_called_once_with(mock_velopack_info)
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
silent=False,
restart=True,
restart_args=[],
)

@staticmethod
def test_apply_on_exit_with_restart_args(updater: Updater) -> None:
"""Verify restart_args are forwarded to apply_updates_and_restart_with_args."""
"""Verify restart_args are forwarded to wait_exit_then_apply_updates."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
Expand All @@ -376,19 +380,20 @@ def test_apply_on_exit_with_restart_args(updater: Updater) -> None:
with (
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
pytest.raises(SystemExit, match='0'),
):
updater.apply_update_on_exit(restart=True, restart_args=['--minimized'])

assert updater.state == UpdateState.APPLYING
mock_manager.apply_updates_and_restart_with_args.assert_called_once_with(
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
['--minimized'],
silent=False,
restart=True,
restart_args=['--minimized'],
)

@staticmethod
def test_apply_on_exit_no_restart(updater: Updater) -> None:
"""Verify apply_update_on_exit(restart=False) uses apply_updates_and_exit."""
"""Verify apply_update_on_exit(restart=False) stages update without restart."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
Expand All @@ -406,8 +411,97 @@ def test_apply_on_exit_no_restart(updater: Updater) -> None:
):
updater.apply_update_on_exit(restart=False)

assert updater.state == UpdateState.APPLIED
mock_manager.apply_updates_and_exit.assert_called_once_with(mock_velopack_info)
assert updater.state == UpdateState.APPLYING
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
silent=False,
restart=False,
restart_args=[],
)

@staticmethod
def test_apply_on_exit_silent_restart(updater: Updater) -> None:
"""Verify silent=True suppresses the splash window."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
current_version=Version('1.0.0'),
latest_version=Version('2.0.0'),
_velopack_info=mock_velopack_info,
)
updater._state = UpdateState.DOWNLOADED

mock_manager = MagicMock(spec=velopack.UpdateManager)

with (
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
):
updater.apply_update_on_exit(restart=True, silent=True)

assert updater.state == UpdateState.APPLYING
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
silent=True,
restart=True,
restart_args=[],
)

@staticmethod
def test_apply_on_exit_silent_no_restart(updater: Updater) -> None:
"""Verify silent=True, restart=False stages update silently."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
current_version=Version('1.0.0'),
latest_version=Version('2.0.0'),
_velopack_info=mock_velopack_info,
)
updater._state = UpdateState.DOWNLOADED

mock_manager = MagicMock(spec=velopack.UpdateManager)

with (
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
):
updater.apply_update_on_exit(restart=False, silent=True)

assert updater.state == UpdateState.APPLYING
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
silent=True,
restart=False,
restart_args=[],
)

@staticmethod
def test_apply_on_exit_silent_with_restart_args(updater: Updater) -> None:
"""Verify silent mode forwards restart_args."""
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
updater._update_info = UpdateInfo(
available=True,
current_version=Version('1.0.0'),
latest_version=Version('2.0.0'),
_velopack_info=mock_velopack_info,
)
updater._state = UpdateState.DOWNLOADED

mock_manager = MagicMock(spec=velopack.UpdateManager)

with (
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
):
updater.apply_update_on_exit(restart=True, silent=True, restart_args=['--minimized'])

assert updater.state == UpdateState.APPLYING
mock_manager.wait_exit_then_apply_updates.assert_called_once_with(
mock_velopack_info,
silent=True,
restart=True,
restart_args=['--minimized'],
)


class TestInitializeVelopack:
Expand Down