Skip to content

Add auto-update feature for pgAdmin 4 desktop (macOS only). #5766 #8825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ APP_REVISION := $(shell grep ^APP_REVISION web/version.py | awk -F"=" '{print $$
# Include only platform-independent builds in all
all: docs pip src

# Add BUILD_OPTS variable to pass arguments
appbundle:
./pkg/mac/build.sh
./pkg/mac/build.sh $(BUILD_OPTS)

install-node:
cd web && yarn install
Expand Down
153 changes: 153 additions & 0 deletions docs/en_US/desktop_deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,156 @@ The configuration settings are stored in *runtime_config.json* file, which
will be available on Unix systems (~/.local/share/pgadmin/),
on Mac OS X (~/Library/Preferences/pgadmin),
and on Windows (%APPDATA%/pgadmin).


Auto-Update of pgAdmin 4 Desktop Application
********************************************

pgAdmin 4's desktop application includes an automated update system built using
Electron's ``autoUpdater`` module. This feature enables users to receive and install
updates seamlessly, ensuring they always have access to the latest features and security fixes.

Supported Platforms
===================

- **macOS:** Fully supported with automatic updates enabled by default
- **Windows:** Not supported
- **Linux:** Not supported

Update Process Overview
=======================

1. **Check for Updates:**

- Automatic check on application startup
- Manual check available via pgAdmin 4 menu > Check for Updates
- Uses Electron's ``autoUpdater`` API to query update server

2. **Download Process:**

- Updates download automatically when detected
- Progress shown via notifications
- Background download prevents interruption of work

3. **Installation Flow:**

- User prompted to Install & Restart or Restart Later when update ready
- Update applied during application restart

The flow chart for the update process is as follows:

.. image:: images/auto_update_desktop_app.png
:alt: Runtime View Log
:align: center

Technical Architecture
======================

1. **Main Process**

Handles core update functionality:

File: runtime/src/js/autoUpdaterHandler.js

.. code-block:: javascript

autoUpdater.on('checking-for-update', () => {
misc.writeServerLog('[Auto-Updater]: Checking for update...');
});

autoUpdater.on('update-available', () => {
setConfigAndRefreshMenu('update-available');
misc.writeServerLog('[Auto-Updater]: Update downloading...');
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_downloading: true});
});

2. **Renderer Process**

Manages user interface updates:

File: web/pgadmin/static/js/BrowserComponent.jsx

.. code-block:: javascript

if (window.electronUI && typeof window.electronUI.notifyAppAutoUpdate === 'function') {
window.electronUI.notifyAppAutoUpdate((data)=>{
if (data?.check_version_update) {
pgAdmin.Browser.check_version_update(true);
} else if (data.update_downloading) {
appAutoUpdateNotifier('Update downloading...', 'info', null, 10000);
} else if (data.no_update_available) {
appAutoUpdateNotifier('No update available...', 'info', null, 10000);
} else if (data.update_downloaded) {
const UPDATE_DOWNLOADED_MESSAGE = gettext('An update is ready. Restart the app now to install it, or later to keep using the current version.');
appAutoUpdateNotifier(UPDATE_DOWNLOADED_MESSAGE, 'warning', installUpdate, null, 'Update downloaded', 'update_downloaded');
} else if (data.error) {
appAutoUpdateNotifier(`${data.errMsg}`, 'error');
} else if (data.update_installed) {
const UPDATE_INSTALLED_MESSAGE = gettext('Update installed successfully!');
appAutoUpdateNotifier(UPDATE_INSTALLED_MESSAGE, 'success');
}
});
}

3. **Update Server Communication**

- Configures update feed URL based on version information
- Handles server response validation
- Manages error conditions

User Interface Components
=========================

1. **Notification Types:**

- Update available
- Download progress
- Update ready to install
- Error notifications

2. **Menu Integration:**

- Check for Updates option in pgAdmin 4 menu
- Restart to Update option when update available

Error Handling
==============

The system includes comprehensive error handling:

1. **Network Errors:**

- Connection timeouts
- Download failures
- Server unavailability

2. **Installation Errors:**

- Corrupted downloads

3. **Recovery Mechanisms:**

- Fallback to manual update
- Error reporting to logs

Security Considerations
=======================

The update system implements below security measures:

1. **Secure Communication:**

- Protected update metadata

Platform-Specific Notes
=======================

1. **macOS:**

- Uses native update mechanisms
- Requires signed packages

References
==========

- `Electron autoUpdater API Documentation <https://www.electronjs.org/docs/latest/api/auto-updater>`_
Binary file added docs/en_US/images/auto_update_desktop_app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions pkg/mac/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ Either build the sources or get them from macports or similar:
*notarization.conf* and set the values accordingly. Note that notarization
will fail if the code isn't signed.

4. To build, go to pgAdmin4 source root directory and execute:
4. To build only DMG file, go to pgAdmin4 source root directory and execute:

make appbundle

To build both DMG and ZIP files, go to pgAdmin4 source root directory and execute:

make appbundle BUILD_OPTS="--zip"

This will create the python virtual environment and install all the required
python modules mentioned in the requirements file using pip, build the
runtime code and finally create the app bundle and the DMG in *./dist*
runtime code and finally create the app bundle and the DMG and/or ZIP in *./dist*
directory.
70 changes: 55 additions & 15 deletions pkg/mac/build-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,24 @@ _codesign_bundle() {
-i org.pgadmin.pgadmin4 \
--sign "${DEVELOPER_ID}" \
"${BUNDLE_DIR}"

echo "Verifying the signature from bundle dir..."
codesign --verify --deep --verbose=4 "${BUNDLE_DIR}"
}

_create_zip() {
ZIP_NAME="${DMG_NAME%.dmg}.zip"
echo "ZIP_NAME: ${ZIP_NAME}"

echo "Compressing pgAdmin 4.app in bundle dir into ${ZIP_NAME}..."
ditto -c -k --sequesterRsrc --keepParent "${BUNDLE_DIR}" "${ZIP_NAME}"

if [ $? -ne 0 ]; then
echo "Failed to create the ZIP file. Exiting."
exit 1
fi

echo "Successfully created ZIP file: ${ZIP_NAME}"
}

_create_dmg() {
Expand Down Expand Up @@ -426,46 +444,68 @@ _codesign_dmg() {
"${DMG_NAME}"
}


_notarize_pkg() {
local FILE_NAME="$1"
local STAPLE_TARGET="$2"
local FILE_LABEL="$3"

if [ "${CODESIGN}" -eq 0 ]; then
return
fi

echo "Uploading DMG for Notarization ..."
STATUS=$(xcrun notarytool submit "${DMG_NAME}" \
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}" 2>&1)
echo "Uploading ${FILE_LABEL} for Notarization ..."
STATUS=$(xcrun notarytool submit "${FILE_NAME}" \
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}" 2>&1)

echo "${STATUS}"

# Get the submission ID
SUBMISSION_ID=$(echo "${STATUS}" | awk -F ': ' '/id:/ { print $2; exit; }')
echo "Notarization submission ID: ${SUBMISSION_ID}"

echo "Waiting for Notarization to be completed ..."
xcrun notarytool wait "${SUBMISSION_ID}" \
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}"
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}"

# Print status information
REQUEST_STATUS=$(xcrun notarytool info "${SUBMISSION_ID}" \
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}" 2>&1 | \
awk -F ': ' '/status:/ { print $2; }')
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}" 2>&1 | \
awk -F ': ' '/status:/ { print $2; }')

if [[ "${REQUEST_STATUS}" != "Accepted" ]]; then
echo "Notarization failed."
exit 1
fi

# Staple the notarization
echo "Stapling the notarization to the pgAdmin DMG..."
if ! xcrun stapler staple "${DMG_NAME}"; then
echo "Stapling the notarization to the ${FILE_LABEL}..."
if ! xcrun stapler staple "${STAPLE_TARGET}"; then
echo "Stapling failed."
exit 1
fi

# For ZIP, recreate the zip after stapling
if [[ "${FILE_LABEL}" == "ZIP" ]]; then
ditto -c -k --keepParent "${BUNDLE_DIR}" "${ZIP_NAME}"
if [ $? != 0 ]; then
echo "ERROR: could not staple ${ZIP_NAME}"
exit 1
fi
fi

echo "Notarization completed successfully."
}

_notarize_zip() {
_notarize_pkg "${ZIP_NAME}" "${BUNDLE_DIR}" "ZIP"
}

_notarize_dmg() {
_notarize_pkg "${DMG_NAME}" "${DMG_NAME}" "DMG"
}
42 changes: 39 additions & 3 deletions pkg/mac/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,32 @@ if [ "${PGADMIN_PYTHON_VERSION}" == "" ]; then
export PGADMIN_PYTHON_VERSION=3.13.1
fi

# Initialize variables
CREATE_ZIP=0
CREATE_DMG=1

# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--zip)
CREATE_ZIP=1
shift
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --zip Create both ZIP and DMG files"
echo " --help Display this help message"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done

# shellcheck disable=SC1091
source "${SCRIPT_DIR}/build-functions.sh"

Expand All @@ -69,6 +95,16 @@ _complete_bundle
_generate_sbom
_codesign_binaries
_codesign_bundle
_create_dmg
_codesign_dmg
_notarize_pkg

# Handle ZIP creation if requested
if [ "${CREATE_ZIP}" -eq 1 ]; then
_create_zip
_notarize_zip
fi

# Handle DMG creation if not disabled
if [ "${CREATE_DMG}" -eq 1 ]; then
_create_dmg
_codesign_dmg
_notarize_dmg
fi
Loading
Loading