diff --git a/.gitignore b/.gitignore index 90430b7ee7..6f0dbae399 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ deploy/build_64/* winbuild*.bat .cache/ .vscode/ +aqtinstall.log # Qt-es diff --git a/client/core/controllers/coreSignalHandlers.cpp b/client/core/controllers/coreSignalHandlers.cpp index ead4d733b9..df834a4c49 100644 --- a/client/core/controllers/coreSignalHandlers.cpp +++ b/client/core/controllers/coreSignalHandlers.cpp @@ -122,6 +122,12 @@ void CoreSignalHandlers::initSettingsSplitTunnelingHandler() void CoreSignalHandlers::initInstallControllerHandler() { + connect(m_coreController->m_installController, &InstallController::installationStepChanged, + m_coreController->m_installUiController, &InstallUiController::installationStepChanged, + Qt::QueuedConnection); + connect(m_coreController->m_installController, &InstallController::removalStepChanged, + m_coreController->m_installUiController, &InstallUiController::removalStepChanged, + Qt::QueuedConnection); connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy); connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation); connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged, diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index f87f6dbd2c..82df9c1bc8 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -106,55 +106,68 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials SshSession sshSession(this); ErrorCode e = ErrorCode::NoError; + emit installationStepChanged(tr("Checking server access…"), 0.03); e = isUserInSudo(credentials, sshSession); if (e) return e; + emit installationStepChanged(tr("Checking server readiness…"), 0.07); e = isServerDpkgBusy(credentials, sshSession); if (e) return e; + emit installationStepChanged(tr("Installing Docker on the server…"), 0.12); e = installDockerWorker(credentials, container, sshSession); if (e) return e; + emit installationStepChanged(tr("Docker ready"), 0.30); qDebug().noquote() << "InstallController::setupContainer installDockerWorker finished"; if (!isUpdate) { + emit installationStepChanged(tr("Checking port availability…"), 0.33); e = isServerPortBusy(credentials, container, config, sshSession); if (e) return e; } + emit installationStepChanged(tr("Preparing server environment…"), 0.38); e = prepareHostWorker(credentials, container, sshSession); if (e) return e; qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished"; + emit installationStepChanged(tr("Removing old container…"), 0.42); const amnezia::ScriptVars removeContainerVars = amnezia::genBaseVars(credentials, container, QString(), QString()); const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt); sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume)); qDebug().noquote() << "InstallController::setupContainer removeContainer finished"; + emit installationStepChanged(tr("Building the VPN container…"), 0.47); qDebug().noquote() << "buildContainerWorker start"; e = buildContainerWorker(credentials, container, config, sshSession); if (e) return e; + emit installationStepChanged(tr("Container image built"), 0.63); qDebug().noquote() << "InstallController::setupContainer buildContainerWorker finished"; + emit installationStepChanged(tr("Starting the container…"), 0.68); e = runContainerWorker(credentials, container, config, sshSession); if (e) return e; qDebug().noquote() << "InstallController::setupContainer runContainerWorker finished"; + emit installationStepChanged(tr("Configuring the protocol…"), 0.72); e = configureContainerWorker(credentials, container, config, sshSession); if (e) return e; qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished"; + emit installationStepChanged(tr("Setting up firewall rules…"), 0.77); setupServerFirewall(credentials, sshSession); qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished"; + emit installationStepChanged(tr("Running startup scripts…"), 0.90); return startupContainerWorker(credentials, container, config, sshSession); } @@ -413,6 +426,8 @@ ErrorCode InstallController::prepareContainerConfig(DockerContainer container, c return ErrorCode::NoError; } + emit installationStepChanged(tr("Generating client configuration…"), 0.96); + if (ContainerUtils::containerService(container) != ServiceType::Other) { Proto protocol = ContainerUtils::defaultProtocol(container); @@ -999,9 +1014,11 @@ ErrorCode InstallController::removeAllContainers(const QString &serverId) return ErrorCode::InternalError; } SshSession sshSession(this); + emit removalStepChanged(tr("Removing all containers…"), 0.15); ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers)); if (errorCode == ErrorCode::NoError) { + emit removalStepChanged(tr("Cleaning up configuration…"), 0.90); adminConfig->containers.clear(); adminConfig->defaultContainer = DockerContainer::None; m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin); @@ -1024,10 +1041,12 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont const amnezia::ScriptVars removeContainerVars = amnezia::genBaseVars(credentials, container, QString(), QString()); const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt); + emit removalStepChanged(tr("Removing container…"), 0.15); ErrorCode errorCode = sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume)); if (errorCode == ErrorCode::NoError) { + emit removalStepChanged(tr("Cleaning up configuration…"), 0.90); QMap containers = adminConfig->containers; containers.remove(container); diff --git a/client/core/controllers/selfhosted/installController.h b/client/core/controllers/selfhosted/installController.h index d68db6e37f..da991098f5 100644 --- a/client/core/controllers/selfhosted/installController.h +++ b/client/core/controllers/selfhosted/installController.h @@ -91,6 +91,8 @@ class InstallController : public QObject void configValidated(bool isValid); void validationErrorOccurred(ErrorCode errorCode); + void installationStepChanged(const QString &message, double progress); + void removalStepChanged(const QString &message, double progress); void serverIsBusy(const bool isBusy); void cancelInstallationRequested(); void clientRevocationRequested(const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container); diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index b1f95a688c..3f9dbad955 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -478,6 +478,91 @@ Already installed containers were found on the server. All installed containers Api config removed Конфигурация API удалена + + + Checking server access… + Проверка доступа к серверу… + + + + Checking server readiness… + Проверка готовности сервера… + + + + Installing Docker on the server… + Установка Docker на сервере… + + + + Docker ready + Docker готов + + + + Checking port availability… + Проверка доступности порта… + + + + Preparing server environment… + Подготовка окружения на сервере… + + + + Removing old container… + Удаление старого контейнера… + + + + Building the VPN container… + Сборка VPN-контейнера… + + + + Container image built + Образ контейнера собран + + + + Starting the container… + Запуск контейнера… + + + + Configuring the protocol… + Настройка протокола… + + + + Setting up firewall rules… + Настройка правил брандмауэра… + + + + Running startup scripts… + Выполнение стартовых скриптов… + + + + Generating client configuration… + Генерация клиентской конфигурации… + + + + Removing all containers… + Удаление всех контейнеров… + + + + Removing container… + Удаление контейнера… + + + + Cleaning up configuration… + Очистка конфигурации… + %1 cached profile cleared @@ -584,7 +669,17 @@ Already installed containers were found on the server. All installed containers Usually it takes no more than 5 minutes - Обычно это занимает не более 5 минут + Обычно это занимает не более 5 минут + + + + Removing… + Удаление… + + + + Removal failed + Ошибка удаления @@ -3608,6 +3703,21 @@ Thank you for staying with us! Usually it takes no more than 5 minutes Обычно это занимает не более 5 минут + + + Connecting to the server… + Подключение к серверу… + + + + Installation failed + Ошибка установки + + + + Server is busy with other updates, waiting… + Сервер занят другими обновлениями, ожидание… + PageSetupWizardProtocolSettings diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index 25c1fed4d5..cf196d295e 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -127,6 +127,8 @@ public slots: void passphraseRequestStarted(); void passphraseRequestFinished(); + void installationStepChanged(const QString &message, double progress); + void removalStepChanged(const QString &message, double progress); void serverIsBusy(const bool isBusy); void cancelInstallation(); diff --git a/client/ui/qml/Pages2/PageDeinstalling.qml b/client/ui/qml/Pages2/PageDeinstalling.qml index 18d1f52407..7561cb6f00 100644 --- a/client/ui/qml/Pages2/PageDeinstalling.qml +++ b/client/ui/qml/Pages2/PageDeinstalling.qml @@ -15,14 +15,81 @@ import "../Config" PageType { id: root - Component.onCompleted: PageController.disableTabBar(true) + Component.onCompleted: { + PageController.disableTabBar(true) + root.appendLog(qsTr("Removing…"), false) + } Component.onDestruction: PageController.disableTabBar(false) + property real targetProgress: 0.0 + property real displayProgress: 0.0 + + onTargetProgressChanged: { + if (root.displayProgress < root.targetProgress) { + catchUpAnim.to = root.targetProgress + catchUpAnim.restart() + } + } + + NumberAnimation { + id: catchUpAnim + target: root + property: "displayProgress" + duration: 800 + easing.type: Easing.OutCubic + } + + Timer { + id: driftTimer + interval: 300 + repeat: true + running: false + onTriggered: { + if (catchUpAnim.running) return + var cap = Math.min(root.targetProgress + 0.15, 0.99) + if (root.displayProgress < cap) + root.displayProgress = Math.min(root.displayProgress + 0.001, cap) + } + } + + property int dotPhase: 1 + + Timer { + id: dotTimer + interval: 500 + repeat: true + running: true + onTriggered: root.dotPhase = (root.dotPhase % 3) + 1 + } + + function appendLog(message, isError) { + if (logModel.count > 0) + logModel.setProperty(logModel.count - 1, "isLatest", false) + logModel.append({ "msg": message, "isError": isError, "isLatest": true }) + root.dotPhase = 1 + } + + Connections { + target: InstallController + + function onRemovalStepChanged(message, progress) { + root.targetProgress = progress + driftTimer.running = true + root.appendLog(message, false) + } + + function onInstallationErrorOccurred(errorCode) { + root.appendLog(qsTr("Removal failed"), true) + } + } + + ListModel { + id: logModel + } + SortFilterProxyModel { id: proxyServersModel - sourceModel: ServersModel - filters: [ ValueFilter { roleName: "serverId" @@ -36,8 +103,6 @@ PageType { anchors.fill: parent - spacing: 16 - model: proxyServersModel delegate: ColumnLayout { @@ -56,29 +121,79 @@ PageType { id: progressBar Layout.fillWidth: true + Layout.preferredHeight: 6 Layout.topMargin: 32 Layout.leftMargin: 16 Layout.rightMargin: 16 - Timer { - id: timer - - interval: 300 - repeat: true - running: true - onTriggered: { - progressBar.value += 0.003 - } - } + value: root.displayProgress } - ParagraphTextType { + Rectangle { Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: 28 Layout.leftMargin: 16 Layout.rightMargin: 16 + Layout.preferredHeight: 170 + + color: AmneziaStyle.color.onyxBlack + radius: 8 + layer.enabled: true + + HoverHandler { + cursorShape: Qt.ArrowCursor + } + + WheelHandler { + onWheel: function(event) { + var ticks = -event.angleDelta.y / 120 + var newY = logView.contentY + ticks * 36 + logView.contentY = Math.max(0, + Math.min(Math.max(0, logView.contentHeight - logView.height), newY)) + event.accepted = true + } + } - text: qsTr("Usually it takes no more than 5 minutes") + ListView { + id: logView + + anchors.fill: parent + anchors.margins: 12 + + model: logModel + clip: true + spacing: 4 + interactive: false + + ScrollBar.vertical: ScrollBarType {} + + onCountChanged: Qt.callLater(positionViewAtEnd) + + delegate: Text { + width: logView.width + + text: model.isLatest && model.msg.endsWith("…") + ? model.msg.slice(0, -1) + ".".repeat(root.dotPhase) + : model.msg + wrapMode: Text.WordWrap + + font.pixelSize: 13 + font.family: "PT Root UI VF" + font.bold: model.isLatest + + color: model.isError + ? AmneziaStyle.color.vibrantRed + : model.isLatest + ? AmneziaStyle.color.goldenApricot + : AmneziaStyle.color.mutedGray + + NumberAnimation on opacity { + from: 0 + to: 1 + duration: 150 + } + } + } } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 01ced7f03f..cf20df1f4f 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -18,60 +18,141 @@ PageType { Component.onCompleted: { root.installingContainerIndex = ServersUiController.processedContainerIndex PageController.disableTabBar(true) + root.appendLog(qsTr("Connecting to the server…"), false) } Component.onDestruction: PageController.disableTabBar(false) - property bool isTimerRunning: true - property string progressBarText: qsTr("Usually it takes no more than 5 minutes") property bool isCancelButtonVisible: false property int installingContainerIndex: -1 - Connections { - target: InstallController + property real targetProgress: 0.0 + property real displayProgress: 0.0 - function onInstallContainerFinished(finishedMessage, isServiceInstall) { - PageController.closePage() // close installing page - PageController.closePage() // close protocol settings page + onTargetProgressChanged: { + if (root.displayProgress < root.targetProgress) { + catchUpAnim.to = root.targetProgress + catchUpAnim.restart() + } + } - if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageHome)) { - PageController.restorePageHomeState(true) - } + NumberAnimation { + id: catchUpAnim + target: root + property: "displayProgress" + duration: 800 + easing.type: Easing.OutCubic + } + + Timer { + id: driftTimer + interval: 300 + repeat: true + running: false + onTriggered: { + if (catchUpAnim.running) return + var cap = Math.min(root.targetProgress + 0.15, 0.99) + if (root.displayProgress < cap) + root.displayProgress = Math.min(root.displayProgress + 0.001, cap) + } + } + + property int dotPhase: 1 + + Timer { + id: dotTimer + interval: 500 + repeat: true + running: true + onTriggered: root.dotPhase = (root.dotPhase % 3) + 1 + } + + property string pendingFinishMessage: "" + property bool pendingIsNewServer: false - if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSetupWizardProtocols)) { - PageController.goToPage(PageEnum.PageHome) + Timer { + id: successCloseTimer + interval: 1100 + repeat: false + onTriggered: { + if (root.pendingIsNewServer) { + PageController.goToPageHome() + } else { + PageController.closePage() + PageController.closePage() + + if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageHome)) { + PageController.restorePageHomeState(true) + } + + if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSetupWizardProtocols)) { + PageController.goToPage(PageEnum.PageHome) + } } + PageController.showNotificationMessage(root.pendingFinishMessage) + } + } - PageController.showNotificationMessage(finishedMessage) + function appendLog(message, isError) { + if (logModel.count > 0) + logModel.setProperty(logModel.count - 1, "isLatest", false) + logModel.append({ "msg": message, "isError": isError, "isLatest": true }) + root.dotPhase = 1 + } + + Connections { + target: InstallController + + function onInstallContainerFinished(finishedMessage, isServiceInstall) { + root.pendingFinishMessage = finishedMessage + root.pendingIsNewServer = false + root.targetProgress = 1.0 + catchUpAnim.to = 1.0 + catchUpAnim.restart() + dotTimer.running = false + successCloseTimer.start() } function onInstallServerFinished(finishedMessage) { - PageController.goToPageHome() - PageController.showNotificationMessage(finishedMessage) + root.pendingFinishMessage = finishedMessage + root.pendingIsNewServer = true + root.targetProgress = 1.0 + catchUpAnim.to = 1.0 + catchUpAnim.restart() + dotTimer.running = false + successCloseTimer.start() } function onServerAlreadyExists(serverIndex) { PageController.goToStartPage() ServersUiController.setProcessedServerId(ServersUiController.getServerId(serverIndex)) PageController.goToPage(PageEnum.PageSettingsServerInfo, false) - PageController.showErrorMessage(qsTr("The server has already been added to the application")) } + function onInstallationErrorOccurred(errorCode) { + root.appendLog(qsTr("Installation failed"), true) + } + + function onInstallationStepChanged(message, progress) { + root.targetProgress = progress + driftTimer.running = true + root.appendLog(message, false) + } + function onServerIsBusy(isBusy) { if (isBusy) { + root.appendLog(qsTr("Server is busy with other updates, waiting…"), false) root.isCancelButtonVisible = true - root.progressBarText = qsTr("Amnezia has detected that your server is currently ") + - qsTr("busy installing other software. Amnezia installation ") + - qsTr("will pause until the server finishes installing other software") - root.isTimerRunning = false } else { root.isCancelButtonVisible = false - root.progressBarText = qsTr("Usually it takes no more than 5 minutes") - root.isTimerRunning = true } } } + ListModel { + id: logModel + } + SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel @@ -89,7 +170,6 @@ PageType { anchors.fill: parent currentIndex: -1 - model: proxyContainersModel delegate: ColumnLayout { @@ -109,35 +189,84 @@ PageType { id: progressBar Layout.fillWidth: true + Layout.preferredHeight: 6 Layout.topMargin: 32 Layout.leftMargin: 16 Layout.rightMargin: 16 - Timer { - id: timer - - interval: 300 - repeat: true - running: root.isTimerRunning - onTriggered: { - progressBar.value += 0.003 - } - } + value: root.displayProgress } - ParagraphTextType { - id: progressText - + Rectangle { Layout.fillWidth: true - Layout.topMargin: 8 + Layout.topMargin: 28 Layout.leftMargin: 16 Layout.rightMargin: 16 + Layout.preferredHeight: 170 + + color: AmneziaStyle.color.onyxBlack + radius: 8 + layer.enabled: true + + HoverHandler { + cursorShape: Qt.ArrowCursor + } - text: root.progressBarText + WheelHandler { + onWheel: function(event) { + var ticks = -event.angleDelta.y / 120 + var newY = logView.contentY + ticks * 36 + logView.contentY = Math.max(0, + Math.min(Math.max(0, logView.contentHeight - logView.height), newY)) + event.accepted = true + } + } + + ListView { + id: logView + + anchors.fill: parent + anchors.margins: 12 + + model: logModel + clip: true + spacing: 4 + interactive: false + + ScrollBar.vertical: ScrollBarType {} + + onCountChanged: Qt.callLater(positionViewAtEnd) + + delegate: Text { + id: delegateText + width: logView.width + + text: model.isLatest && model.msg.endsWith("…") + ? model.msg.slice(0, -1) + ".".repeat(root.dotPhase) + : model.msg + wrapMode: Text.WordWrap + + font.pixelSize: 13 + font.family: "PT Root UI VF" + font.bold: model.isLatest + + color: model.isError + ? AmneziaStyle.color.vibrantRed + : model.isLatest + ? AmneziaStyle.color.goldenApricot + : AmneziaStyle.color.mutedGray + + NumberAnimation on opacity { + from: 0 + to: 1 + duration: 150 + } + } + } } BasicButtonType { - id: cancelIntallationButton + id: cancelInstallationButton Layout.fillWidth: true Layout.topMargin: 24