Как сделать обновления для программы

.

Сегодня я бы хотел рассказать про то, как сделать лаунчер (updater). Зачем это нужно спросите вы? В ответ я расскажу вам небольшую историю.

Немного истории...
За окном шел 2012 года, а я начинал свой проект в страховой компании. Поначалу это был простой клиент для выполнения нескольких действий: ведение учета изготовленных полисов и еще несколько не сложных действий. Но проект рос как на дрожжах – постепенно он стал полностью заменять «древнее» ПО.
По началу программа стояла только в офисе на 5 машинах. Если нужно было обновить ее, то ничего сложного: скинул файл «.exe» в файлопомойку и пошел, залил на все машины. Меня это не сильно утруждало. Дальше интереснее - я поставил свое ПО на компьютеры в офисе за 300 км (еще 7 машин). Вот тут понеслась жара. Обновление программы: подключиться к каждому по TeamViewer–у и скинуть новый файл «.exe». Не надо говорить, что через пару обновлений меня такое положение дел изрядно достало.
Именно в тот момент меня озарило – если вы пишите даже несложный проект, вы будете делать патчи или обновления. Скидывать вручную эти обновления – это маразм, необходима автоматизация. Спасибо, за помощь и идею данного решения моему товарищу Олегу Чередниченко (Format_C_eft).

Схема решения Update…
Схема проста, но вмести с тем - гениальна. Все представлено на картинке ниже, рассмотрим ее:

Схема работы обновления лаунчера updatee

После запуска Update.exe (давайте так назовем наш лаунчер) получает версию программы, которая установлена на данный момент на машине (1) и получает версию последнего релиза программы с «Сервера обновления» (2).
Сравниваем полученные версии, при реализации этой задачи я принял допущение, что на сервере всегда правильная версия. Например: локальная версия (1.0.4.65), а на сервере (1.0.4.63). Сравнивая эти две версии, с учетом описанного допущения программа запустит обновление.
Плюсы такого подхода:
а) возможность без проблемно откатить обновление. Представим, вы выпустили обновление, но оно не удачное. Пользователи вопят, что не могут работать, и им не объяснить, что необходимо время на исправление проблемы. Самый лучший вариант, вернуть предыдущую версию и заняться исправлением багов обновления. В этом случае просто изменяем версию на сервере на предыдущую версию.
б) простота реализации. Как давно уже известно, отказоустойчивость системы обратно пропорциональна ее сложности.

Сравнение версий…
Перейдем непосредственно к коду. Сравнения двух версий.

function compare_ver(ver_old,ver_basa:String):Boolean; //Возвращает истину если версии не var //равны, ложь - версии равны. v_o1,v_o2,v_o3,v_o4:String; //Версия принимается в виде X.X.Х.Х v_b1,v_b2,v_b3,v_b4:String; i1,i2:Integer; begin как сделать обновления для программы v_o1:=copy(ver_old,1,(Pos('.',(ver_old))-1)); delete(ver_old,1,Pos('.',(ver_old))); v_o2:=copy(ver_old,1,(Pos('.',(ver_old))-1)); delete(ver_old,1,Pos('.',(ver_old))); v_o3:=copy(ver_old,1,(Pos('.',(ver_old))-1)); delete(ver_old,1,Pos('.',(ver_old))); v_o4:=ver_old; v_b1:=copy(ver_basa,1,(Pos('.',(ver_basa))-1)); delete(ver_basa,1,Pos('.',(ver_basa))); v_b2:=copy(ver_basa,1,(Pos('.',(ver_basa))-1)); delete(ver_basa,1,Pos('.',(ver_basa))); v_b3:=copy(ver_basa,1,(Pos('.',(ver_basa))-1)); delete(ver_basa,1,Pos('.',(ver_basa))); v_b4:=ver_basa; i1:=StrToInt(v_o4); i2:=StrToInt(v_b4); if i1<>i2 then begin Result:=True; end else begin i1:=StrToInt(v_o3); i2:=StrToInt(v_b3); if i1<>i2 then begin Result:=True; end else begin i1:=StrToInt(v_o2); i2:=StrToInt(v_b2); if i1<>i2 then begin Result:=True; end else begin i1:=StrToInt(v_o1); i2:=StrToInt(v_b1); if i1<>i2 then begin Result:=True; end else begin Result:=False; end; end; end; end; end;

Как же получить версию локального файла? Признаюсь, не помню, откуда я взял этом модуль. Здесь просто приведу пример. В Windows есть version.dll. Используя эту библиотеку можно получить версию файла прописанную в атрибутах следующих типов «.exe» и «.dll». Важный момент: в uses прописать модуль VerInfo. Я приложу его в сорцах в конце статьи.
Вот собственно и пример.

Uses VerInfo; function FileVersion(AFileName: string): string; var szName: array[0..255] of Char; P: Pointer; Value: Pointer; Len: UINT; GetTranslationString: string; FFileName: PChar; FValid: boolean; FSize: DWORD; FHandle: DWORD; FBuffer: PChar; begin try FFileName := StrPCopy(StrAlloc(Length(AFileName) + 1), AFileName); FValid := False; FSize := GetFileVersionInfoSize(FFileName, FHandle); if FSize > 0 then try GetMem(FBuffer, FSize); FValid := GetFileVersionInfo(FFileName, FHandle, FSize, FBuffer); except FValid := False; raise; end; Result := ''; if FValid then VerQueryValue(FBuffer, '\VarFileInfo\Translation', p, Len) else p := nil; if P <> nil then GetTranslationString := IntToHex(MakeLong(HiWord(Longint(P^)), LoWord(Longint(P^))), 8); if FValid then begin StrPCopy(szName, '\StringFileInfo\' + GetTranslationString + '\FileVersion'); if VerQueryValue(FBuffer, szName, Value, Len) then Result := StrPas(PChar(Value)); end; finally try if FBuffer <> nil then FreeMem(FBuffer, FSize); except end; try StrDispose(FFileName); except end; end; end;

Бэкапим..Качаем…Проверяем….

Далее, наверное самый интересный момент это реализации. Просто смотрите код, я все поясню в комментариях.

procedure Update_client; var p,path_old,path_new,path_basa,md5_zag,md5_basa,sql:string; begin path_old:= ExtractFilePath(ParamStr(0)); path_old:=path_old+'client.exe'; //Путь к файлу программы if not FileExists(path_old) then //Это проверка существует файл или нет begin //Встречался такой момент когда из-за плохого соединения //программа скачивания зависает и в итоге файл не доканчивается, а старый в олдах //При следующем запуске она не может найти client.exe и возникает ошибка. Error_Diconnect; end; version_old:=FileVersion(path_old); //Текущая версия if Length(version_old)=0 then //На случай если не удалось получить версию файла begin form1.Label1.Caption:='Ошибка получения версии файла.'+#13+' Сообщите в IT отдел.'; Application.ProcessMessages; exit; end; if form1.ZConnection1.Connected=False then //Проверка состояние конекта Zeosdbo begin form1.Label1.Caption:='Ошибка соединения.'+#13+' Проверьте подключение.'; Application.ProcessMessages; exit; end; //Программа не умела автоматически реконектиться version_basa:=form1.tb.FieldByName('ver').AsString; //Версия хранимая в базе form1.Label1.Caption:='Идет проверка обновления...'; Application.ProcessMessages; if compare_ver(version_old,version_basa) then begin update_prog; //Запуск обновления end else begin path_old:= ExtractFilePath(ParamStr(0)); path_old:=path_old+'client.exe'; WinExec(Pchar(path_old), SW_SHOW); //Запуск программы и закрытие лаучера (update) form1.qr_up.Close; form1.tb.Close; form1.ZConnection1.Disconnect; Application.ProcessMessages; Application.Terminate; end; end;

Ну а вот самое сердце всей программы обновления проекта.

procedure update_prog; var p,path_old,path_new,path_basa,md5_zag,md5_basa,sql:string; VerInfoR : TVerInfoRes; begin form1.Label1.Caption:='Требуется обновление...'; Application.ProcessMessages; path_old:= ExtractFilePath(ParamStr(0)); path_old:=path_old+'client.exe'; //Путь к файлу программы path_new := ChangeFileExt(path_old, '.old'); //Создание олда if RenameFile(path_old, path_new) then begin //Успешное создание олда form1.Label1.Caption:='Создание копии старой версии...'; Application.ProcessMessages; path_basa:=form1.tb.FieldByName('path').AsString; //Путь к обновлению path_new:= ExtractFilePath(ParamStr(0)); path_new:=path_new+'client.exe'; //Путь к тому, что обновляем form1.Label1.Caption:='Обновление программы...'; Application.ProcessMessages; if GetInetFile(path_basa, path_new)=True then begin md5_basa:=form1.tb.FieldByName('md5').AsString; //Хеш в базе path_old:= ExtractFilePath(ParamStr(0)); path_old:=path_old+'client.exe'; md5_zag:=MD5DigestToStr(MD5File(path_old)); //Хеш загруженного файла form1.Label1.Caption:='Проверка целостности обновления...'; Application.ProcessMessages; if md5_zag=md5_basa then begin path_new:= ExtractFilePath(ParamStr(0)); path_new:=path_new+'clients.old'; form1.Label1.Caption:='Удаление старой версии...'; Application.ProcessMessages; if DeleteFile(Pchar(path_new)) then //Удаление олда begin form1.qr_up.SQL.Clear; //Этот кусок повторяется для того чтобы в текущем соединении с //сервером MySql в Zeosdbo получать кирилицу в нормальной кодировке form1.qr_up.SQL.Add('SET Names cp1251'); form1.qr_up.ExecSQL; sql:='insert into log_update (data_up,comp_name,Ip_adress,Mac_adress,status) values ('+Chr(39)+Data_Deis+Chr(39)+', '+Chr(39)+GetComputerNetName+Chr(39)+', ' +Chr(39)+GetLocalIP+Chr(39)+', '+Chr(39)+GetMACAdress+Chr(39)+', '+Chr(39)+ 'Обновление успешно завершено! До версии:'+version_basa+Chr(39)+')'; form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add(sql); form1.qr_up.ExecSQL; path_old:= ExtractFilePath(ParamStr(0)); path_old:=path_old+'clients.exe'; WinExec(Pchar(path_old), SW_SHOW); form1.qr_up.Close; form1.tb.Close; form1.ZConnection1.Disconnect; Application.ProcessMessages; Application.Terminate; end else begin form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add('SET Names cp1251'); form1.qr_up.ExecSQL; sql:='insert into log_update (data_up,comp_name,Ip_adress,Mac_adress,status) values ('+ Chr(39)+Data_Deis+Chr(39)+', '+Chr(39)+GetComputerNetName+Chr(39)+', '+Chr(39)+GetLocalIP+Chr(39)+ ', '+Chr(39)+GetMACAdress+Chr(39)+', '+Chr(39)+'Ошибка удаления old : '+IntToStr(GetLastError)+ ' Версия установленая:'+version_old+' Актуальная:'+version_basa+Chr(39)+')'; form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add(sql); form1.qr_up.ExecSQL; Last_Old; end; end else begin form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add('SET Names cp1251'); form1.qr_up.ExecSQL; sql:='insert into log_update (data_up,comp_name,Ip_adress,Mac_adress,status) values ('+ Chr(39)+Data_Deis+Chr(39)+', '+Chr(39)+GetComputerNetName+Chr(39)+', '+Chr(39)+GetLocalIP+Chr(39)+ ', '+Chr(39)+GetMACAdress+Chr(39)+', '+Chr(39)+'Откат обновления. Не совпал md5 хэш!'+ ' Версия установленная:'+version_old+' Актуальная:'+version_basa+Chr(39)+')'; form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add(sql); form1.qr_up.ExecSQL; Last_Old; end; end else begin form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add('SET Names cp1251'); form1.qr_up.ExecSQL; sql:='insert into log_update (data_up,comp_name,Ip_adress,Mac_adress,status) values ('+ Chr(39)+Data_Deis+Chr(39)+', '+Chr(39)+GetComputerNetName+Chr(39)+', '+Chr(39)+GetLocalIP+Chr(39)+ ', '+Chr(39)+GetMACAdress+Chr(39)+', '+Chr(39)+ 'Ошибка при загрузке файла. Версия установленная:'+version_old+' Актуальная:'+version_basa+Chr(39)+')'; form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add(sql); form1.qr_up.ExecSQL; Last_Old; end; end else begin form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add('SET Names cp1251'); form1.qr_up.ExecSQL; sql:='insert into log_update (data_up,comp_name,Ip_adress,Mac_adress,status) values ('+ Chr(39)+Data_Deis+Chr(39)+', '+Chr(39)+GetComputerNetName+Chr(39)+', '+Chr(39)+GetLocalIP+Chr(39)+ ', '+Chr(39)+GetMACAdress+Chr(39)+', '+Chr(39)+'При создании old-файла произошла ошибка : '+ IntToStr(GetLastError)+' Версия установленая:'+version_old+' Актуальная:'+version_basa+Chr(39)+')'; form1.qr_up.SQL.Clear; form1.qr_up.SQL.Add(sql); form1.qr_up.ExecSQL; Last_Old; end; end;

Обратите внимание, что если возникают проблемы при создании, переименовании, удалении файла и так далее, выполняется процедура Last_Old – аварийной откат обновления.
Обновление качается с помощью компонента IdHTTP1. Эту функцию я где-то нашел (только, честно говоря не помню где), запилил в свою реализацию, и получился простенький лаунчер (updater).

function GetInetFile(fileURL, FileName: String):Boolean; var LoadStream: TMemoryStream; begin try LoadStream := TMemoryStream.Create; // выделение памяти под переменную form1.idHTTP1.Get(fileURL, LoadStream); // загрузка в поток данных из сети LoadStream.SaveToFile(FileName); // сохраняем данные из потока на жестком диске LoadStream.Free; // освобождаем память except Result:=False; exit; end; Result:=True; end;

Она хорошая и простая, единственный минус – не имеет возможность докачки файла. Если процесс скачивания прерывается, при восстановлении связи скачивание начнется с начала. Процесс закачки файла можно отображать в виде прогресс бара. В это плане мне нравятся компонент Gauge, он умеет отображать процесс в процентном отношении. Чтоб процесс скачивание имел связь с компонентом Gauge, нужно описать три события компонента IdHTTP1.

События для скачивания файла компонент IdHTTP

procedure TForm1.IdHTTP1Work(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer); begin Gauge1.Progress:=AWorkCount; end; procedure TForm1.IdHTTP1WorkBegin(Sender: TObject; AWorkMode: TWorkMode; const AWorkCountMax: Integer); begin Gauge1.Progress:=0; Gauge1.MaxValue:=AWorkCountMax; end; procedure TForm1.IdHTTP1WorkEnd(Sender: TObject; AWorkMode: TWorkMode); begin Gauge1.Progress:=0; end;

Немного о сервере обновления…
Сервер обновления можно условно разделить на две части.
Первая: хранилище релизов – это может быть просто ftp-сервер, ваш сайт - не важно просто место, где лежит файл и к нему можно получить доступ по url. В моем случае это ftp-сервер. На нем лежит куча папок. Имя папки – это версия программы. А в самой папке уже лежит файл. Такая система позволяет вести историю релизов. И при острой необходимости откатиться на предыдущую версию.
Вторая – хранилище информации. В базе, на сервере MySql, создаются две таблички. Первая update – таблица с данными для релиза. Вторая log_updatet – системный лог, в который программа пишет, обновилась или нет. Если не обновилась - то по какой причине.
Таблица update

CREATE TABLE `update` ( id int(10) NOT NULL AUTO_INCREMENT, name varchar(255) DEFAULT NULL COMMENT 'Имя файла', opisanie varchar(255) DEFAULT NULL COMMENT 'Описание файла', ver char(15) NOT NULL DEFAULT '0' COMMENT 'Версия', path varchar(1000) NOT NULL DEFAULT '0' COMMENT 'Путь с серваку', md5 varchar(1000) NOT NULL DEFAULT '0' COMMENT 'Хэш', PRIMARY KEY (id) ) ENGINE = INNODB AUTO_INCREMENT = 25 AVG_ROW_LENGTH = 682 CHARACTER SET cp1251 COLLATE cp1251_general_ci COMMENT = 'Таблица обновления ';

Таблица log_update

CREATE TABLE log_update ( id int(10) NOT NULL AUTO_INCREMENT, data_up datetime DEFAULT NULL, comp_name char(255) DEFAULT '0', status char(255) DEFAULT NULL, ip_adress char(100) DEFAULT '0.0.0.0', MAC_adress char(100) DEFAULT '-', PRIMARY KEY (id) ) ENGINE = INNODB AUTO_INCREMENT = 8716 AVG_ROW_LENGTH = 985 CHARACTER SET cp1251 COLLATE cp1251_general_ci COMMENT = 'Лог обновления программы';

Ну вот, в принципе я описал все тонкие моменты реализации лаунчера (updater). В следующей статье я хотел бы рассказать о том, как реализовать лаунчер с докачкой файла. Причем реализацию скачивания списка файлов, а не одного. Как обычно исходники здесь. С уважением, ваш Shinobi.


Закрыть ... [X]

Автоматическое обновление программы - Общие вопросы Delphi - Форум Пожелания друзьям в день семьи

Как сделать обновления для программы Как сделать обновления для программы Как сделать обновления для программы Как сделать обновления для программы Как сделать обновления для программы Как сделать обновления для программы