C++ СИСТЕМА ОБНОВЛЕНИЯ ПРИЛОЖЕНИЯ

screen2

Зайду издалека. У меня дома 4 компа, еще один на даче, и везде установлена моя любимая операционная система Linux, а так же приложения, которые я пишу сам для себя. Но вот, прогресс идет, а ручное обновление приложений на каждой машине настолько муторное дело, что неизбежно родилась светлая мысль об автоматизации этого процесса. Думаю, решение должно быть универсальным и легко встраиваемым в любое свое самодельное ПО. Прежде чем приступить к задаче, пришлось потратить некоторое время на обдумывание архитектуры... Что, вообще нужно, чтобы реализовать этот проект? Ну, во-первых, нужен сервер. В моем случае, это будет система на базе Odroid-XU4. Там какая-то беда с Qt - Creator вроде ставится, но там по умолчанию не настроены комплекты и, если честно, мне пока очень-то хочется с ними разбираться, к тому же железо слабенькое, оперативки мало и засирание операционки лишними библиотеками не очень хорошо согласуется с принципами Фен-Шуй. Поэтому, скорее всего, серверная часть будет написана на чистом Си++, без всякой лишней ерунды. Какую она будет выполнять задачу? Очень простую. Она должна обрабатывать запрос клиента, проверять доступную версию его приложения исходя из данных, записаных в своем конфигурационном файле, и высылать ответ. Клиентская часть, соответственно, при каждом включении программы, обращается к серверу, получает ответ, сравнивает со своей версией, и если видит, что версия, предложенная сервером, новее, то инициирует закачку обновления. Вроде бы, ничего сложного.

Изначально я почему-то планировал использовать UDP-сокет, потратил всю ночь на изучение темы, но так ничего толком и не добился. В большинстве примеров по всему Интернету они работают либо на передачу, либо на прием, а мне нужен дуплекс - чтобы и туда, и сюда. В итоге под утро нашел вариант TCP сокета на чистом С++ для сервера и Qt-шный для клиента.

Далее практически непричечесаный код. Кое-что, конечно, выкинул, кое-что добавил, но основательно разобраться пока не успел. Реализации чтения конфига пока нет, но, наверно, появится в будущем, потому что каждый раз компилировать программу заново при обновлении подопечного ПО слишком сложно и лениво, проще заморочиться один раз, чтобы менять этот индекс максимально простым редактированием текстового файла. Тем не менее, программа уже работает. Подключаясь, клиент сообщает серверу свое имя, в данном случае "madCalc", и получает ответ - текущая версия: 2.5.

madserver.cpp

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>

short SocketCreate(void)
{
    short hSocket;
    printf("Create the socket\n");
    hSocket = socket(AF_INET, SOCK_STREAM, 0);
    return hSocket;
}
int BindCreatedSocket(int hSocket)
{
    int iRetval=-1;
    int ClientPort = 5555;
    struct sockaddr_in  remote= {0};
    /* Internet address family */
    remote.sin_family = AF_INET;
    /* Any incoming interface */
    remote.sin_addr.s_addr = htonl(INADDR_ANY);
    remote.sin_port = htons(ClientPort); /* Local port */
    iRetval = bind(hSocket,(struct sockaddr *)&remote,sizeof(remote));
    return iRetval;
}
int main(int argc, char *argv[])
{
    using namespace std;
    int socket_desc, sock, clientLen;
    struct sockaddr_in client;
    char client_message[200]= {0};
    char message[100] = {0};
    const char *pMessage = "Hello from madmentat.ru!";
    //Create socket
    socket_desc = SocketCreate();
    if (socket_desc == -1)
    {
        printf("Could not create socket");
        return 1;
    }
    printf("Socket created\n");
    //Bind
    if( BindCreatedSocket(socket_desc) < 0)
    {
        //print the error message
        perror("bind failed.");
        return 1;
    }
    printf("bind done\n");
    //Listen
    listen(socket_desc, 3);
    //Accept and incoming connection
    while(1)
    {
        printf("Waiting for incoming connections...\n");
        clientLen = sizeof(struct sockaddr_in);
        //accept connection from an incoming client
        sock = accept(socket_desc,(struct sockaddr *)&client,(socklen_t*)&clientLen);
        if (sock < 0)
        {
            perror("accept failed");
            return 1;
        }
        printf("Connection accepted\n");
        memset(client_message, '\0', sizeof client_message);
        memset(message, '\0', sizeof message);
        //Receive a reply from the client
        if( recv(sock, client_message, 200, 0) < 0)
        {
            printf("recv failed");
            break;
        }
        printf("Client reply : %s\n",client_message);
        
        
        if(strcmp(pMessage,client_message)==0)
        {
            strcpy(message,"Hi there !");
        }
        else
        {
	    
            string CM(client_message);
            if (CM == "madCalc"){
            strcpy(message,"2.5");  
            }
            else {
            strcpy(message,"Программа не опознана!"); 
            }
        }
        // Send some data
        if( send(sock, message, strlen(message), 0) < 0)
        {
            printf("Send failed");
            return 1;
        }
        //close(sock); //Сокет будет закрываться со стороны клиента
        sleep(1);
    }
    return 0;

Компилируем:

g++ -Wall -o madserver madserver.cpp

Перенесем готовую программу в специальную папку:

sudo cp madserver /usr/local/bin/madserver

Добавим наш сервер в автозагрузку:

sudo nano /etc/rc.local

madserver
exit 0

Прикол, только через некоторое время заметил что rc.local не работает в десктопной Ubuntu 20.04, хотя на моем серваке установлена более свежая версия 22.04 и такой способ прокатил без проблем. Видимо, arm-версия чем-то отличается в нюансах. Чтобы исправить это недоразумение, можно воспользоваться этой статьей.

Далее, заметил, что madserver иногда падает. Причина пока не ясна, но чтобы избежать подобных сбоев, решил добавить в автозагрузку простенький скриптик, который будет в бесконечном цикле проверять, запущен madserver или нет, и если нет, то  запуcтит его заново.

madcheck.sh

#!/bin/bash
while : #Запускаем бесконечный цикл
do
        ret=$(ps aux | grep [m]adserver | wc -l)
        if [ "$ret" -eq 0 ]
then {
        echo "madserver is not running" #Сообщение на случай дебага, это пойдет в терминал
        sleep 15s  #delay
        madserver$disown  #Запускаем серер и отвязываем процесс от данного скрипта
}
else {
        echo "madserver already running!"
        sleep 15s
}
fi;
done

 Сделаем файл скрипта исполняемым:

sudo +x madcheck.sh

И тут, если уж речь все равно уже пошла о systemd, то, пожалуй, есть мысл создать юнит, который будет запускать этот скрипт и перезапускать его, если он по какой-то причине вырубится.

sudo nano /etc/systemd/system/mad.service

[Unit]
Description=madserverFuckupDaemon

[Service]
ExecStart=/data/madcheck.sh

[Install]
WantedBy=multi-user.target

Далее мы должны включить наш сервис:

sudo systemctl enable mad.service

Заработает он только после перезагрузки системы.

sudo reboot now

sudo systemctl status mad.service

madUnit

Ну вот, теперь если наш мэдсервер упадет, то скрипт поднимет его обратно.

Переходим к клиентской части. Отдельную программу выложил здесь, а в этой статье будут представлены как следует допиленные фрагменты кода, готовые к интеграции в любое  Qt-приложение.

Итак, Qt-код для соединения с сервером.

cpp

madCalc::madCalc(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::madCalc)
{
    ui->setupUi(this);
    updateCheck();
}

void madCalc::updateCheck()
{
QString myname = "madCalc"; QString IP = "madmentat.ru"; int port = 5555; socket->abort(); //Connecting servers socket->connectToHost(IP, port); //Waiting for the connection to succeed if(!socket->waitForConnected(30000)) { qDebug() << "Connection failed!"; return; } qDebug() << "Connect successfully!"; qDebug() << "Send request to https://update.madmentat.ru: " << myname; //Берем имя программы из myname и отсылаем на сервер as ASCII code socket->write(myname.toLatin1()); socket->flush(); socket_Read_Data(); } void madCalc::socket_Read_Data() { QByteArray buffer; buffer = socket->readAll(); //Read Buffer Data if(!buffer.isEmpty()) { QString str = buffer; qDebug() << "Current madCalc version received: " << buffer; socket->disconnectFromHost(); //Disconnect } if (buffer > version){ //Здесь version - это глобальная переменная, которая хранит текущей номер версии программы mUpdate->show(); //И если он меньше той, что пришла с сервера, тогда открываем окошко с вопросом об обновлении emit signalVersion(buffer); //Отправляем туда номер новой версии } } void madCalc::socket_Disconnected() { //Здесь практически нефига не делается, только добавим в дебаг сообщение qDebug() << "Disconnected!"; }

Следующая проверка, приняв из другой формы переменную, определит, решил пользователь обновиться или нет.

void madCalc::updateState(QString uState) //Здесь мы принимаем решение пользователя об обновлении
{
    if (uState == "Yes"){ //Если пользователь согласился, принимаем Yes
    upState = uState;
    qDebug() << "User has decided to update!";
    update();             //И затем инициируем функцию update()
    }
}

И если он все-таки решился, тогда запускается следующий метод, который закрывает основное приложение и вызывает вспомогательную  программу, которая, в свою очередь, запустится независимо от родителя, скачает и установит обновление, удалит скачанный архив и запустит обновленную программу.

void madCalc::update() //Здесь мы вызовем вспомогательную программу-обновлялку и закроем madCalc
{

#ifdef Q_OS_LINUX //задаем условия компиляции для Linux
        QString PATH = QCoreApplication::applicationDirPath();   //Получим полный путь к madCalc и запишем в переменную PATH
        qDebug() << "Linux-based operation system determinated"; //Сообщим в дебаг что у нас Линукс
        qDebug() << "Working path is: " + PATH;                  //Выведем туда же рабочую папку нашей программы
        QProcess process;                                        //Запустим процесс.
        process.setProgram("update");                            //Укажем имя под-программы обновления
        process.setWorkingDirectory(PATH);                       //Укажем путь к под-программе обновления
        process.setStandardOutputFile(QProcess::nullDevice());   //Для стандартных сообщений под-программы
        process.setStandardErrorFile(QProcess::nullDevice());    //Для сообщений об ошибках нашей под-программы
        qint64 pid;                                              //Непонятная магия. Так подозреваю, что назначается PID
        process.startDetached(&pid);                             //Запускается дочерний процесс, независимый от родительского 
        QApplication::quit();                                    //Закрываем родительский процесс
#endif
}

Здесь важно уяснить одну вещь, а именно причину, по которой мы используем p.startDetached(). Если использовать просто "p -> start()", тогда дочерний процесс умрет сразу же после смерти родителя, что затрудняет перезапуск всей программы в целом. Ну и, соответственно, рассмотрим саму программку на С++, которая скачивает с сервера архив, распаковывает его и запускает новую версию нашего ПО (В данном случае madCalc),

Тут, пожалуй, покажу старую версию. На самом деле, она прекрасно работает... но только в Linux! Для этого нам потребуется установить парочку пакетов.

Для компиляции потребуется установить пакеты curl.

sudo apt-get update && apt-get install curl

sudo apt install libcurl-openssl1.0-dev

И явно указать на curl при компиляции.

g++ update.cpp -lcurl -o update

Как заставить эту программу корректно работать в Windows, я так и не понял. Неделю ломал голову, так страдал - чуть ли не до слез, открывал какие-то треды на ru.stackoverflow.com, спать по ночам не мог, а потом меня внезапно осенило, что curl по умолчанию вшит в ОС и доступен из коробки в большинстве случаев, так что мучиться с ним просто не надо, а надо просто тупо вызвать его через system!

update.cpp:

//Compile command 
//g++ update.cpp -lcurl -o update
#include <stdio.h>
#include <curl/curl.h>
#include <string>

size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) {
    size_t written = fwrite(ptr, size, nmemb, stream);
    return written;
}

int main(void) {
    CURL *curl;
    FILE *fp;
    CURLcode res;
    char *url = "https://update.madmentat.ru/madcalc.linux.tar.xz";
    char outfilename[FILENAME_MAX] = "madcalc.linux.tar.xz";
    curl = curl_easy_init();
    if (curl) {
        fp = fopen(outfilename,"wb");
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
        res = curl_easy_perform(curl);
        /* always cleanup */
        curl_easy_cleanup(curl);
        fclose(fp);
        system("tar -zxvf madcalc.linux.tar.xz");
	    system("rm madcalc.linux.tar.xz");
        system("killall madCalc");
	    system("./madCalc");
    }
    return 0;
}

Новая версия выглядит гораздо более изящно:

#include <string>
#include <stdio.h> 
int main(void) {
system("echo Starting to download madcalc.windows.zip");
system("curl https://update.madmentat.ru/madcalc.windows.zip --output madcalc.windows.zip");
system("tar -xf madcalc.windows.zip");
system("del madcalc.windows.zip");
system("madCalc.exe");
}

Пока все. В будущем статья будет дополнена и немного подредактирована. Подробнее тему автообновлерия можно изучить на  примере:

Сырцы: Linux-madCalc2.3

А вот здесь можно скачать уже скомпилированную версию Linux-madCalc2.3. Скачать, распаковать, дать права на запуск, запустить. Скрипт потребует ввести пароль суперпользователя для того чтобы установить пакет libxcb-xinerama0 и создать симлинк madCalc в /usr/local/bin. Можете отказаться и сделать все это вручную, но как бы то ни было - иначе лыжи не поедут.

wget https://update.madmentat.ru/madCalc-install.sh

chmod +x madCalc-install.sh

sh madCalc-install.sh

win.madcalc.install.exe