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
