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