Compare commits

...
Sign in to create a new pull request.

47 commits

Author SHA1 Message Date
aiamnezia
273eeb78c6 Merge branch 'dev' into feature/app-update 2025-06-17 01:10:37 +04:00
aiamnezia
6e06b86cb2 update check refactoring 2025-06-17 00:50:00 +04:00
aiamnezia
943e76043a Add release date downloading from endpoint 2025-05-24 16:54:58 +04:00
aiamnezia
7023b27029 add Release date file creation to s3 deploy script 2025-05-24 13:38:00 +04:00
aiamnezia
68708114d5 Change updater downloading method to retrieving link from the gateway 2025-05-22 16:50:16 +04:00
aiamnezia
22bf2fbc1d Merge branch 'dev' into feature/app-update 2025-05-22 14:27:37 +04:00
aiamnezia
cb6a2c9195 add .vscode to .gitignore 2025-05-22 14:05:44 +04:00
vladimir.kuznetsov
449a8070c1 chore: added changelog text processing depend on OS 2025-03-05 13:14:07 +07:00
vladimir.kuznetsov
c627b5a3cc Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2025-03-05 11:08:42 +07:00
aiamnezia
49990f0122 decrease version for testing 2025-01-10 09:42:35 +04:00
vladimir.kuznetsov
574773fa7c chore: add missing ifdef 2025-01-09 15:06:25 +07:00
vladimir.kuznetsov
98a5219137 Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2025-01-09 13:56:08 +07:00
vladimir.kuznetsov
694b7896e5 chore: post merge fixes 2025-01-02 14:05:25 +07:00
vladimir.kuznetsov
cda9b5d496 chore: remove duplicate lines 2025-01-02 13:56:11 +07:00
vladimir.kuznetsov
af0fe754fc Merge branch 'dev' of github.com:amnezia-vpn/amnezia-client into HEAD 2025-01-02 13:43:31 +07:00
vladimir.kuznetsov
44082462b7 chore: fixed macos update script 2025-01-02 13:40:55 +07:00
aiamnezia
eb6c40f92a Disable updates checking for Android and iOS 2024-12-25 18:17:00 +04:00
aiamnezia
4588aa3713 Merge branch 'dev' into feature/app-update 2024-12-24 19:34:00 +04:00
aiamnezia
5d334e365c Add draft for MacOS installation 2024-12-24 19:33:26 +04:00
aiamnezia
89df1df886 Add logs for UpdateController 2024-12-24 18:53:35 +04:00
aiamnezia
44376847e2 Add linux_install script to resources 2024-12-24 17:07:55 +04:00
aiamnezia
fe9be23536 Clean service code 2024-12-19 19:20:31 +04:00
aiamnezia
11f9c7bc7c Move installer launch logic to client side for Windows 2024-12-19 19:10:40 +04:00
aiamnezia
caa5132355 Merge branch 'dev' into feature/app-update 2024-12-19 18:15:22 +04:00
aiamnezia
8de7ad6b41 Move installer running to client side for Ubuntu 2024-12-19 18:10:46 +04:00
aiamnezia
2029c108e5 Optimized code 2024-12-12 06:54:10 +04:00
aiamnezia
bac71ed3e7 Add logs from installattion shell on Windows 2024-12-11 18:34:16 +04:00
aiamnezia
a73234ec2a Add some logs 2024-12-11 18:11:46 +04:00
aiamnezia
9df9314e73 Merge branch 'dev' into feature/app-update 2024-12-11 17:04:59 +04:00
aiamnezia
3b300a203f Fix installation for Windows and MacOS 2024-12-11 20:24:59 +04:00
aiamnezia
6a21994736 Fix formatting 2024-12-10 19:04:11 +04:00
Cyril Anisimov
9e7cf7fa1f feature/xray user management (#972)
* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>
2024-12-10 18:25:06 +04:00
Nethius
321f0727d2 feature: added subscription expiration date for premium v2 (#1261)
* feature: added subscription expiration date for premium v2

* feature: added a check for the presence of the “services” field in the response body of the getServicesList() function

* feature: added prohibition to change location when connection is active

* bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend
2024-12-10 18:25:06 +04:00
KsZnak
8d2fe39ea3 Update README.md 2024-12-10 18:25:05 +04:00
KsZnak
1858bb9f85 Update README_RU.md 2024-12-10 18:25:05 +04:00
Nethius
e20f8bead2 chore: added clang-format config files (#1293) 2024-12-10 18:25:05 +04:00
KsZnak
061c63d5bd Add files via upload 2024-12-10 18:25:05 +04:00
KsZnak
086d6c4ae3 Update README_RU.md 2024-12-10 18:25:05 +04:00
Pokamest Nikak
9aef463b60 ru readme 2024-12-10 18:25:05 +04:00
aiamnezia
e748ac35c9 Add service side of installation logic for Windows 2024-12-10 18:25:05 +04:00
aiamnezia
506f96c5d0 Add client side of installation logic for Windows and MacOS 2024-12-10 18:25:05 +04:00
aiamnezia
42e4768483 Add debug logs about installer in service 2024-12-04 15:38:55 +04:00
aiamnezia
12587b44cf Merge branch 'dev' into feature/app-update 2024-12-04 14:59:10 +04:00
aiamnezia
99f610edf9 implement Linux updating 2024-11-29 19:21:23 +04:00
aiamnezia
efdd47a63d Created a scaffold for Linux installation 2024-11-28 11:43:16 +04:00
aiamnezia
a71ca29b2f Merge branch 'dev' into feature/app-update 2024-10-30 16:55:07 +04:00
vladimir.kuznetsov
871037f887 added changelog drawer 2024-05-25 10:04:41 +02:00
15 changed files with 748 additions and 34 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ deploy/build_32/*
deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
# Qt-es

View file

@ -0,0 +1,44 @@
#!/bin/bash
EXTRACT_DIR="$1"
INSTALLER_PATH="$2"
# Create and clean extract directory
rm -rf "$EXTRACT_DIR"
mkdir -p "$EXTRACT_DIR"
# Extract ZIP archive
unzip "$INSTALLER_PATH" -d "$EXTRACT_DIR"
if [ $? -ne 0 ]; then
echo 'Failed to extract ZIP archive'
exit 1
fi
# Find and extract TAR archive
TAR_FILE=$(find "$EXTRACT_DIR" -name '*.tar' -type f)
if [ -z "$TAR_FILE" ]; then
echo 'TAR file not found'
exit 1
fi
tar -xf "$TAR_FILE" -C "$EXTRACT_DIR"
if [ $? -ne 0 ]; then
echo 'Failed to extract TAR archive'
exit 1
fi
rm -f "$TAR_FILE"
# Find and run installer
INSTALLER=$(find "$EXTRACT_DIR" -type f -executable)
if [ -z "$INSTALLER" ]; then
echo 'Installer not found'
exit 1
fi
"$INSTALLER"
EXIT_CODE=$?
# Cleanup
rm -rf "$EXTRACT_DIR"
exit $EXIT_CODE

View file

@ -0,0 +1,56 @@
#!/bin/bash
EXTRACT_DIR="$1"
INSTALLER_PATH="$2"
# Create and clean extract directory
rm -rf "$EXTRACT_DIR"
mkdir -p "$EXTRACT_DIR"
# Mount the DMG
MOUNT_POINT="$EXTRACT_DIR/mounted_dmg"
hdiutil attach "$INSTALLER_PATH" -mountpoint "$MOUNT_POINT"
if [ $? -ne 0 ]; then
echo "Failed to mount DMG"
exit 1
fi
# Check if the application exists in the mounted DMG
if [ ! -d "$MOUNT_POINT/AmneziaVPN.app" ]; then
echo "Error: AmneziaVPN.app not found in the mounted DMG."
hdiutil detach "$MOUNT_POINT" #-quiet
exit 1
fi
# Run the application
echo "Running AmneziaVPN.app from the mounted DMG..."
open "$MOUNT_POINT/AmneziaVPN.app"
# Get the PID of the app launched from the DMG
APP_PATH="$MOUNT_POINT/AmneziaVPN.app"
PID=$(pgrep -f "$APP_PATH")
if [ -z "$PID" ]; then
echo "Failed to retrieve PID for AmneziaVPN.app"
hdiutil detach "$MOUNT_POINT"
exit 1
fi
# Wait for the specific PID to exit
echo "Waiting for AmneziaVPN.app to exit..."
while kill -0 "$PID" 2>/dev/null; do
sleep 1
done
# Unmount the DMG
hdiutil detach "$EXTRACT_DIR/mounted_dmg"
if [ $? -ne 0 ]; then
echo "Failed to unmount DMG"
exit 1
fi
# Optional: Remove the DMG file
rm "$INSTALLER_PATH"
echo "Installation completed successfully"
exit 0

View file

@ -151,6 +151,9 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_updateController.reset(new UpdateController(m_settings));
m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get());
}
void CoreController::initAndroidController()
@ -226,6 +229,7 @@ void CoreController::initSignalHandlers()
initImportPremiumV2VpnKeyHandler();
initShowMigrationDrawerHandler();
initStrictKillSwitchHandler();
initUpdateFoundHandler();
}
void CoreController::initNotificationHandler()
@ -393,6 +397,16 @@ void CoreController::initStrictKillSwitchHandler()
&VpnConnection::onKillSwitchModeChanged);
}
void CoreController::initUpdateFoundHandler()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_updateController.get(), &UpdateController::updateFound, this,
[this]() { QTimer::singleShot(1000, this, [this]() { m_pageController->showChangelogDrawer(); }); });
m_updateController->checkForUpdates();
#endif
}
QSharedPointer<PageController> CoreController::pageController() const
{
return m_pageController;

View file

@ -19,6 +19,7 @@
#include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h"
#include "ui/controllers/systemController.h"
#include "ui/controllers/updateController.h"
#include "ui/models/allowed_dns_model.h"
#include "ui/models/containers_model.h"
@ -86,6 +87,7 @@ private:
void initImportPremiumV2VpnKeyHandler();
void initShowMigrationDrawerHandler();
void initStrictKillSwitchHandler();
void initUpdateFoundHandler();
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
std::shared_ptr<Settings> m_settings;
@ -109,6 +111,7 @@ private:
QScopedPointer<SystemController> m_systemController;
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
QScopedPointer<AllowedDnsController> m_allowedDnsController;
QScopedPointer<UpdateController> m_updateController;
QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController;

View file

@ -54,6 +54,15 @@ QString amnezia::scriptName(ProtocolScriptType type)
}
}
QString amnezia::scriptName(ClientScriptType type)
{
switch (type) {
case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh");
case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh");
default: return QString();
}
}
QString amnezia::scriptData(amnezia::SharedScriptType type)
{
QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type));
@ -81,3 +90,19 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co
data.replace("\r", "");
return data;
}
QString amnezia::scriptData(ClientScriptType type)
{
QString fileName = QString(":/client_scripts/%1").arg(amnezia::scriptName(type));
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "Warning: script missing" << fileName;
return "";
}
QByteArray data = file.readAll();
if (data.isEmpty()) {
qDebug() << "Warning: script is empty" << fileName;
}
data.replace("\r", "");
return data;
}

View file

@ -1,11 +1,12 @@
#ifndef SCRIPTS_REGISTRY_H
#define SCRIPTS_REGISTRY_H
#include <QLatin1String>
#include "core/defs.h"
#include "containers/containers_defs.h"
#include "core/defs.h"
#include <QLatin1String>
namespace amnezia {
namespace amnezia
{
enum SharedScriptType {
// General scripts
@ -19,6 +20,7 @@ enum SharedScriptType {
check_server_is_busy,
check_user_in_sudo
};
enum ProtocolScriptType {
// Protocol scripts
dockerfile,
@ -31,14 +33,21 @@ enum ProtocolScriptType {
xray_template
};
enum ClientScriptType {
// Client-side scripts
linux_installer,
mac_installer
};
QString scriptFolder(DockerContainer container);
QString scriptName(SharedScriptType type);
QString scriptName(ProtocolScriptType type);
QString scriptName(ClientScriptType type);
QString scriptData(SharedScriptType type);
QString scriptData(ProtocolScriptType type, DockerContainer container);
QString scriptData(ClientScriptType type);
}
#endif // SCRIPTS_REGISTRY_H

View file

@ -59,6 +59,9 @@
<file>images/tray/active.png</file>
<file>images/tray/default.png</file>
<file>images/tray/error.png</file>
<file>client_scripts/linux_installer.sh</file>
<file>client_scripts/mac_installer.sh</file>
<file>server_scripts/openvpn_cloak/Dockerfile</file>
<file>server_scripts/awg/configure_container.sh</file>
<file>server_scripts/awg/Dockerfile</file>
<file>server_scripts/awg/run_container.sh</file>
@ -175,10 +178,11 @@
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
<file>ui/qml/Controls2/WarningType.qml</file>
<file>ui/qml/Components/ChangelogDrawer.qml</file>
<file>ui/qml/Modules/Style/qmldir</file>
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
<file>ui/qml/main2.qml</file>
<file>ui/qml/Modules/Style/AmneziaStyle.qml</file>
<file>ui/qml/Modules/Style/qmldir</file>
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
<file>ui/qml/Pages2/PageDevMenu.qml</file>
<file>ui/qml/Pages2/PageHome.qml</file>

View file

@ -145,6 +145,7 @@ signals:
void escapePressed();
void closeTopDrawer();
void showChangelogDrawer();
private:
QSharedPointer<ServersModel> m_serversModel;

View file

@ -9,7 +9,7 @@ class SystemController : public QObject
{
Q_OBJECT
public:
explicit SystemController(const std::shared_ptr<Settings> &setting, QObject *parent = nullptr);
explicit SystemController(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
static void saveFile(const QString &fileName, const QString &data);
static bool readFile(const QString &fileName, QByteArray &data);

View file

@ -0,0 +1,385 @@
#include "updateController.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QVersionNumber>
#include <QtConcurrent>
#include <QUrl>
#include <QEventLoop>
#include <QJsonDocument>
#include <QJsonObject>
#include "amnezia_application.h"
#include "core/errorstrings.h"
#include "core/scripts_registry.h"
#include "logger.h"
#include "version.h"
#include "core/controllers/gatewayController.h"
namespace
{
Logger logger("UpdateController");
#ifdef Q_OS_MACOS
const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg";
#elif defined Q_OS_WINDOWS
const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar.zip";
#endif
}
UpdateController::UpdateController(const std::shared_ptr<Settings> &settings, QObject *parent) : QObject(parent), m_settings(settings)
{
}
QString UpdateController::getHeaderText()
{
return tr("New version released: %1 (%2)").arg(m_version, m_releaseDate);
}
QString UpdateController::getChangelogText()
{
QStringList lines = m_changelogText.split("\n");
QStringList filteredChangeLogText;
bool add = false;
QString osSection;
#ifdef Q_OS_WINDOWS
osSection = "### Windows";
#elif defined(Q_OS_MACOS)
osSection = "### macOS";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
osSection = "### Linux";
#endif
for (const QString &line : lines) {
if (line.startsWith("### General")) {
add = true;
} else if (line.startsWith("### ") && line != osSection) {
add = false;
} else if (line == osSection) {
add = true;
}
if (add) {
filteredChangeLogText.append(line);
}
}
return filteredChangeLogText.join("\n");
}
void UpdateController::checkForUpdates()
{
qDebug() << "checkForUpdates";
if (!fetchGatewayUrl()) return;
if (!fetchVersionInfo()) return;
if (!isNewVersionAvailable()) return;
if (!fetchChangelog()) return;
if (!fetchReleaseDate()) return;
m_downloadUrl = composeDownloadUrl();
emit updateFound();
}
bool UpdateController::fetchGatewayUrl()
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
7000,
m_settings->isStrictKillSwitchEnabled());
QByteArray gatewayResponse;
auto err = gatewayController.get(QStringLiteral("%1v1/updater_endpoint"), gatewayResponse);
if (err != ErrorCode::NoError) {
logger.error() << errorString(err);
return false;
}
QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object();
qDebug() << "gatewayData:" << gatewayData;
QString baseUrl = gatewayData.value("url").toString();
if (baseUrl.endsWith('/')) {
baseUrl.chop(1);
}
m_baseUrl = baseUrl;
return true;
}
bool UpdateController::fetchVersionInfo()
{
QByteArray data;
if (!doSyncGet("/VERSION", data)) {
return false;
}
m_version = QString::fromUtf8(data).trimmed();
return true;
}
bool UpdateController::isNewVersionAvailable()
{
auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION));
auto newVersion = QVersionNumber::fromString(m_version);
return newVersion > currentVersion;
}
bool UpdateController::fetchChangelog()
{
QByteArray data;
if (!doSyncGet("/CHANGELOG", data)) {
m_changelogText = tr("Failed to load changelog text");
} else {
m_changelogText = QString::fromUtf8(data);
}
return true;
}
bool UpdateController::fetchReleaseDate()
{
QByteArray data;
if (!doSyncGet("/RELEASE_DATE", data)) {
m_releaseDate = QStringLiteral("Failed to load release date");
} else {
m_releaseDate = QString::fromUtf8(data).trimmed();
}
return true;
}
void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation)
{
QObject::connect(reply, &QNetworkReply::errorOccurred, [this, reply, operation](QNetworkReply::NetworkError error) {
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
.arg(operation, reply->errorString(), QString::number(error));
});
QObject::connect(reply, &QNetworkReply::sslErrors, [this, reply, operation](const QList<QSslError> &errors) {
QStringList errorStrings;
for (const QSslError &err : errors) {
errorStrings << err.errorString();
}
logger.error() << QString("SSL errors while fetching %1: %2").arg(operation, errorStrings.join("; "));
});
}
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
}
QString UpdateController::composeDownloadUrl()
{
QString fileName;
#if defined(Q_OS_WINDOWS)
fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version);
#elif defined(Q_OS_MACOS)
fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version);
#endif
return m_baseUrl + "/" + fileName;
}
void UpdateController::runInstaller()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty";
return;
}
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setUrl(m_downloadUrl);
QNetworkReply *reply = amnApp->networkManager()->get(request);
QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QFile file(installerPath);
if (!file.open(QIODevice::WriteOnly)) {
logger.error() << "Failed to open installer file for writing:" << installerPath << "Error:" << file.errorString();
reply->deleteLater();
return;
}
if (file.write(reply->readAll()) == -1) {
logger.error() << "Failed to write installer data to file:" << installerPath << "Error:" << file.errorString();
file.close();
reply->deleteLater();
return;
}
file.close();
#if defined(Q_OS_WINDOWS)
runWindowsInstaller(installerPath);
#elif defined(Q_OS_MACOS)
runMacInstaller(installerPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
runLinuxInstaller(installerPath);
#endif
} else {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
}
reply->deleteLater();
});
#endif
}
#if defined(Q_OS_WINDOWS)
int UpdateController::runWindowsInstaller(const QString &installerPath)
{
qint64 pid;
bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid);
if (success) {
logger.info() << "Installation process started with PID:" << pid;
} else {
logger.error() << "Failed to start installation process";
return -1;
}
return 0;
}
#endif
#if defined(Q_OS_MACOS)
int UpdateController::runMacInstaller(const QString &installerPath)
{
// Create temporary directory for extraction
QTemporaryDir extractDir;
extractDir.setAutoRemove(false);
if (!extractDir.isValid()) {
logger.error() << "Failed to create temporary directory";
return -1;
}
logger.info() << "Temporary directory created:" << extractDir.path();
// Create script file in the temporary directory
QString scriptPath = extractDir.path() + "/mac_installer.sh";
QFile scriptFile(scriptPath);
if (!scriptFile.open(QIODevice::WriteOnly)) {
logger.error() << "Failed to create script file";
return -1;
}
// Get script content from registry
QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::mac_installer);
if (scriptContent.isEmpty()) {
logger.error() << "macOS installer script content is empty";
scriptFile.close();
return -1;
}
scriptFile.write(scriptContent.toUtf8());
scriptFile.close();
logger.info() << "Script file created:" << scriptPath;
// Make script executable
QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
// Start detached process
qint64 pid;
bool success =
QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
if (success) {
logger.info() << "Installation process started with PID:" << pid;
} else {
logger.error() << "Failed to start installation process";
return -1;
}
return 0;
}
#endif
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
int UpdateController::runLinuxInstaller(const QString &installerPath)
{
// Create temporary directory for extraction
QTemporaryDir extractDir;
extractDir.setAutoRemove(false);
if (!extractDir.isValid()) {
logger.error() << "Failed to create temporary directory";
return -1;
}
logger.info() << "Temporary directory created:" << extractDir.path();
// Create script file in the temporary directory
QString scriptPath = extractDir.path() + "/installer.sh";
QFile scriptFile(scriptPath);
if (!scriptFile.open(QIODevice::WriteOnly)) {
logger.error() << "Failed to create script file";
return -1;
}
// Get script content from registry
QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer);
scriptFile.write(scriptContent.toUtf8());
scriptFile.close();
logger.info() << "Script file created:" << scriptPath;
// Make script executable
QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser);
// Start detached process
qint64 pid;
bool success =
QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid);
if (success) {
logger.info() << "Installation process started with PID:" << pid;
} else {
logger.error() << "Failed to start installation process";
return -1;
}
return 0;
}
#endif
bool UpdateController::doSyncGet(const QString& endpoint, QByteArray& outData)
{
QNetworkRequest req;
req.setTransferTimeout(7000);
req.setUrl(QUrl(m_baseUrl + endpoint));
QNetworkReply* reply = amnApp->networkManager()->get(req);
setupNetworkErrorHandling(reply, endpoint);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
bool ok = (reply->error() == QNetworkReply::NoError);
if (ok) {
outData = reply->readAll();
} else {
handleNetworkError(reply, endpoint);
}
reply->deleteLater();
return ok;
}

View file

@ -0,0 +1,54 @@
#ifndef UPDATECONTROLLER_H
#define UPDATECONTROLLER_H
#include <QObject>
#include <QNetworkReply>
#include "settings.h"
class UpdateController : public QObject
{
Q_OBJECT
public:
explicit UpdateController(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
Q_PROPERTY(QString changelogText READ getChangelogText NOTIFY updateFound)
Q_PROPERTY(QString headerText READ getHeaderText NOTIFY updateFound)
public slots:
QString getHeaderText();
QString getChangelogText();
void checkForUpdates();
void runInstaller();
signals:
void updateFound();
private:
bool fetchGatewayUrl();
bool fetchVersionInfo();
bool fetchChangelog();
bool fetchReleaseDate();
bool isNewVersionAvailable();
bool doSyncGet(const QString& endpoint, QByteArray& outData);
void setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation);
void handleNetworkError(QNetworkReply* reply, const QString& operation);
QString composeDownloadUrl();
std::shared_ptr<Settings> m_settings;
QString m_baseUrl;
QString m_changelogText;
QString m_version;
QString m_releaseDate;
QString m_downloadUrl;
#if defined(Q_OS_WINDOWS)
int runWindowsInstaller(const QString &installerPath);
#elif defined(Q_OS_MACOS)
int runMacInstaller(const QString &installerPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
int runLinuxInstaller(const QString &installerPath);
#endif
};
#endif // UPDATECONTROLLER_H

View file

@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
DrawerType2 {
id: root
anchors.fill: parent
expandedHeight: parent.height * 0.9
expandedStateContent: Item {
implicitHeight: root.expandedHeight
Header2TextType {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 16
text: UpdateController.headerText
}
FlickableType {
anchors.top: header.bottom
anchors.bottom: updateButton.top
contentHeight: changelog.height + 32
ParagraphTextType {
id: changelog
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 16
HoverHandler {
enabled: parent.hoveredLink
cursorShape: Qt.PointingHandCursor
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
text: UpdateController.changelogText
textFormat: Text.MarkdownText
}
}
BasicButtonType {
id: updateButton
anchors.bottom: skipButton.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 8
anchors.rightMargin: 16
anchors.leftMargin: 16
text: qsTr("Update")
clickedFunc: function() {
PageController.showBusyIndicator(true)
UpdateController.runInstaller()
PageController.showBusyIndicator(false)
root.closeTriggered()
}
}
BasicButtonType {
id: skipButton
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 16
anchors.rightMargin: 16
anchors.leftMargin: 16
defaultColor: "transparent"
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
pressedColor: Qt.rgba(1, 1, 1, 0.12)
disabledColor: "#878B91"
textColor: "#D7D8DB"
borderWidth: 1
text: qsTr("Skip")
clickedFunc: function() {
root.closeTriggered()
}
}
}
}

View file

@ -96,6 +96,10 @@ Window {
busyIndicator.visible = visible
PageController.disableControls(visible)
}
function onShowChangelogDrawer() {
changelogDrawer.openTriggered()
}
}
Connections {
@ -285,4 +289,14 @@ Window {
onAccepted: SystemController.fileDialogClosed(true)
onRejected: SystemController.fileDialogClosed(false)
}
Item {
anchors.fill: parent
ChangelogDrawer {
id: changelogDrawer
anchors.fill: parent
}
}
}

View file

@ -12,7 +12,8 @@ mkdir -p dist
cd dist
echo $VERSION >> VERSION
echo $VERSION > VERSION
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE
curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG
if [[ $(cat CHANGELOG) = null ]]; then