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

Зайду издалека. У меня дома 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 иногда падает. Пока не ясно, из-за чего, но чтобы избежать подобных сбоев, решил добавить юнит systemd, который перезапустит программу в случае если она по каким-то причинам наебнется.
sudo nano /etc/systemd/system/mad.service
[Unit] Description=madserverFuckupDaemon [Service] ExecStart=/usr/local/bin/madserver [Install] WantedBy=multi-user.target
Далее мы должны включить наш сервис:
sudo systemctl enable mad.service
Заработает он только после перезагрузки системы.
sudo reboot now
sudo systemctl status mad.service

Ну вот, теперь если наш мэдсервер упадет, то скрипт поднимет его обратно.
Переходим к клиентской части. Отдельную программу выложил здесь, а в этой статье будут представлены как следует допиленные фрагменты кода, готовые к интеграции в любое 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
