1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2007, matthes
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// dialogs for update, and the actual update application code
19
20#include "C4Include.h"
21#include "C4UpdateDlg.h"
22#include "C4DownloadDlg.h"
23
24#include <C4Log.h>
25
26#include <format>
27
28#ifdef _WIN32
29#include <shellapi.h>
30#else
31#include <unistd.h>
32#include <fcntl.h>
33#include <errno.h>
34#include <sys/wait.h>
35#include <netinet/in.h>
36#include <arpa/inet.h>
37#endif
38
39int C4UpdateDlg::pid;
40int C4UpdateDlg::c4group_output[2];
41bool C4UpdateDlg::succeeded;
42
43// C4UpdateDlg
44
45C4UpdateDlg::C4UpdateDlg() : C4GUI::InfoDialog(LoadResStr(id: C4ResStrTableKey::IDS_TYPE_UPDATE), 10)
46{
47 // initial text update
48 UpdateText();
49 // assume update running
50 UpdateRunning = true;
51}
52
53void C4UpdateDlg::UpdateText()
54{
55 if (!UpdateRunning)
56 return;
57#ifdef _WIN32
58 AddLine("Win32 is currently not using the C4UpdateDlg!");
59 UpdateRunning = false;
60#else
61 char c4group_output_buf[513];
62 ssize_t amount_read;
63 // transfer output to log window
64 amount_read = read(fd: c4group_output[0], buf: c4group_output_buf, nbytes: 512);
65 // error
66 if (amount_read == -1)
67 {
68 if (errno == EAGAIN)
69 return;
70 const std::string errorMessage{std::format(fmt: "read error from c4group: {}", args: strerror(errno))};
71 LogNTr(level: spdlog::level::err, message: errorMessage);
72 AddLine(szText: errorMessage.c_str());
73 UpdateRunning = false;
74 succeeded = false;
75 }
76 // EOF: Update done.
77 else if (amount_read == 0)
78 {
79 // Close c4group output
80 close(fd: c4group_output[0]);
81 // If c4group did not exit but something else caused EOF, then that's bad. But don't hang.
82 int child_status = 0;
83 if (waitpid(pid: pid, stat_loc: &child_status, WNOHANG) == -1)
84 {
85 LogNTr(level: spdlog::level::err, fmt: "error in waitpid: {}", args: strerror(errno));
86 AddLine(szText: std::format(fmt: "error in waitpid: {}", args: strerror(errno)).c_str());
87 succeeded = false;
88 }
89 // check if c4group failed.
90 else if (WIFEXITED(child_status) && WEXITSTATUS(child_status))
91 {
92 LogNTr(level: spdlog::level::err, fmt: "c4group returned status {}", WEXITSTATUS(child_status));
93 AddLine(szText: std::format(fmt: "c4group returned status {}", WEXITSTATUS(child_status)).c_str());
94 succeeded = false;
95 }
96 else if (WIFSIGNALED(child_status))
97 {
98 LogNTr(level: spdlog::level::err, fmt: "c4group killed with signal {}", WTERMSIG(child_status));
99 AddLine(szText: std::format(fmt: "c4group killed with signal {}", WTERMSIG(child_status)).c_str());
100 succeeded = false;
101 }
102 else
103 {
104 LogNTr(message: "Done.");
105 AddLine(szText: "Done.");
106 }
107 UpdateRunning = false;
108 }
109 else
110 {
111 c4group_output_buf[amount_read] = 0;
112 // Fixme: This adds spurious newlines in the middle of the output.
113 LogNTr(message: c4group_output_buf);
114 AddLine(szText: c4group_output_buf);
115 }
116#endif
117
118 // Scroll to bottom
119 if (pTextWin)
120 {
121 pTextWin->UpdateHeight();
122 pTextWin->ScrollToBottom();
123 }
124}
125
126// static update application function
127
128bool C4UpdateDlg::DoUpdate(const C4GameVersion &rUpdateVersion, C4GUI::Screen *pScreen)
129{
130 std::string updateFile;
131 std::string updateURL;
132 // Double check for valid update
133 if (!IsValidUpdate(rNewVer: rUpdateVersion)) return false;
134 // Objects major update: we will update to the first minor of the next major version - we can not skip major versions or jump directly to a higher minor of the next major version.
135 if (rUpdateVersion.iVer[2] > C4XVER3)
136 updateFile = std::format(C4CFG_UpdateMajor, args: rUpdateVersion.iVer[0], args: rUpdateVersion.iVer[1], C4XVER3 + 1, args: 0, C4_OS);
137 // Objects version match: engine update only
138 else if ((rUpdateVersion.iVer[2] == C4XVER3) && (rUpdateVersion.iVer[3] == C4XVER4))
139 updateFile = std::format(C4CFG_UpdateEngine, args: rUpdateVersion.iBuild, C4_OS);
140 // Objects version mismatch: full objects update
141 else
142 updateFile = std::format(C4CFG_UpdateObjects, args: rUpdateVersion.iVer[0], args: rUpdateVersion.iVer[1], args: rUpdateVersion.iVer[2], args: rUpdateVersion.iVer[3], args: rUpdateVersion.iBuild, C4_OS);
143 // Compose full update URL by using update server address and replacing last path element name with the update file
144 int iLastElement = SCharLastPos(cTarget: '/', szInStr: Config.Network.UpdateServerAddress);
145 if (iLastElement > -1)
146 {
147 updateURL = Config.Network.UpdateServerAddress;
148 updateURL.resize(n: iLastElement + 1);
149 updateURL += updateFile;
150 }
151 else
152 {
153 // No last slash in update server address?
154 // Append update file as new segment instead - maybe somebody wants
155 // to set up their update server this way
156 updateURL = Config.Network.UpdateServerAddress;
157 updateURL += '/';
158 updateURL += updateFile;
159 }
160 // Determine local filename for update group
161 StdStrBuf strLocalFilename; strLocalFilename.Copy(pnData: GetFilename(path: updateFile.c_str()));
162 // Download update group to temp path
163 strLocalFilename.Copy(pnData: Config.AtTempPath(szFilename: strLocalFilename.getData()));
164 // Download update group
165 if (!C4DownloadDlg::DownloadFile(szDLType: LoadResStr(id: C4ResStrTableKey::IDS_TYPE_UPDATE), pScreen, szURL: updateURL.c_str(), szSaveAsFilename: strLocalFilename.getData(), szNotFoundMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_UPDATENOTAVAILABLE)))
166 // Download failed (return success, because error message has already been shown)
167 return true;
168 // Apply downloaded update
169 return ApplyUpdate(strUpdateFile: strLocalFilename.getData(), fDeleteUpdate: true, pScreen);
170}
171
172bool C4UpdateDlg::ApplyUpdate(const char *strUpdateFile, bool fDeleteUpdate, C4GUI::Screen *pScreen)
173{
174 // Determine name of update program
175 StdStrBuf strUpdateProg; strUpdateProg.Copy(C4CFN_UpdateProgram);
176 // Windows: manually append extension because ExtractEntry() cannot properly glob and Extract() doesn't return failure values
177#ifdef _WIN32
178 strUpdateProg += ".exe";
179#endif
180 // Extract update program (the update should be applied using the new version)
181 C4Group UpdateGroup, SubGroup;
182 char strSubGroup[1024 + 1];
183 if (!UpdateGroup.Open(szGroupName: strUpdateFile)) return false;
184 // Look for update program at top level
185 if (!UpdateGroup.ExtractEntry(szFilename: strUpdateProg.getData(), szExtractTo: strUpdateProg.getData()))
186 // Not found: look for an engine update pack one level down
187 if (UpdateGroup.FindEntry(szWildCard: std::format(fmt: "lc_*_{}.c4u", C4_OS).c_str(), sFileName: strSubGroup))
188 // Extract update program from sub group
189 if (SubGroup.OpenAsChild(pMother: &UpdateGroup, szEntryName: strSubGroup))
190 {
191 SubGroup.ExtractEntry(szFilename: strUpdateProg.getData(), szExtractTo: strUpdateProg.getData());
192 SubGroup.Close();
193 }
194 UpdateGroup.Close();
195 // Execute update program
196 Log(id: C4ResStrTableKey::IDS_PRC_LAUNCHINGUPDATE);
197 succeeded = true;
198#ifdef _WIN32
199 // Close editor if open
200 HWND hwnd = FindWindowA(nullptr, C4EDITORCAPTION);
201 if (hwnd) PostMessage(hwnd, WM_CLOSE, 0, 0);
202 const std::string updateArgs{std::format("\"{}\" /p -w \"" C4ENGINECAPTION "\" -w \"" C4EDITORCAPTION "\" -w 2000 {}", strUpdateFile, fDeleteUpdate ? "-yd" : "-y")};
203 const auto iError = ShellExecuteA(nullptr, "runas", strUpdateProg.getData(), updateArgs.c_str(), Config.General.ExePath, SW_SHOW);
204 if (reinterpret_cast<intptr_t>(iError) <= 32) return false;
205 // must quit ourselves for update program to work
206 if (succeeded) Application.Quit();
207#else
208 if (pipe(pipedes: c4group_output) == -1)
209 {
210 LogNTr(level: spdlog::level::err, message: "Error creating pipe");
211 return false;
212 }
213 switch (pid = fork())
214 {
215 // Error
216 case -1:
217 LogNTr(level: spdlog::level::err, message: "Error creating update child process.");
218 return false;
219 // Child process
220 case 0:
221 // Close unused read end
222 close(fd: c4group_output[0]);
223 // redirect stdout and stderr to the parent
224 dup2(fd: c4group_output[1], STDOUT_FILENO);
225 dup2(fd: c4group_output[1], STDERR_FILENO);
226 if (c4group_output[1] != STDOUT_FILENO && c4group_output[1] != STDERR_FILENO)
227 close(fd: c4group_output[1]);
228 execl(C4CFN_UpdateProgram, C4CFN_UpdateProgram, "-v", strUpdateFile, (fDeleteUpdate ? "-yd" : "-y"), static_cast<char *>(0));
229 printf(format: "execl failed: %s\n", strerror(errno));
230 exit(status: 1);
231 // Parent process
232 default:
233 // Close unused write end
234 close(fd: c4group_output[1]);
235 // disable blocking
236 fcntl(fd: c4group_output[0], F_SETFL, O_NONBLOCK);
237 // Open the update log dialog (this will update itself automatically from c4group_output)
238 pScreen->ShowRemoveDlg(pDlg: new C4UpdateDlg());
239 break;
240 }
241#endif
242 // done
243 return succeeded;
244}
245
246bool C4UpdateDlg::IsValidUpdate(const C4GameVersion &rNewVer)
247{
248 // Engine or game version mismatch
249 if ((rNewVer.iVer[0] != C4XVER1) || (rNewVer.iVer[1] != C4XVER2)) return false;
250 // Objects major is higher...
251 if ((rNewVer.iVer[2] > C4XVER3)
252 // ...or objects major is the same and objects minor is higher...
253 || ((rNewVer.iVer[2] == C4XVER3) && (rNewVer.iVer[3] > C4XVER4))
254 // ...or build number is higher
255 || (rNewVer.iBuild > C4XVERBUILD))
256 // Update okay
257 return true;
258 // Otherwise
259 return false;
260}
261
262bool C4UpdateDlg::CheckForUpdates(C4GUI::Screen *pScreen, bool fAutomatic)
263{
264 // Automatic update only once a day
265 if (fAutomatic)
266 if (time(timer: nullptr) - Config.Network.LastUpdateTime < 60 * 60 * 24)
267 return false;
268 // Store the time of this update check (whether it's automatic or not or successful or not)
269 Config.Network.LastUpdateTime = static_cast<int32_t>(time(timer: nullptr));
270 // Get current update version from server
271 C4GameVersion UpdateVersion;
272 C4GUI::Dialog *pWaitDlg = nullptr;
273 if (pScreen && C4GUI::IsGUIValid())
274 {
275 pWaitDlg = new C4GUI::MessageDialog(LoadResStr(id: C4ResStrTableKey::IDS_MSG_LOOKINGFORUPDATES), Config.Network.UpdateServerAddress, C4GUI::MessageDialog::btnAbort, C4GUI::Ico_Ex_Update, C4GUI::MessageDialog::dsRegular);
276 pWaitDlg->SetDelOnClose(false);
277 pScreen->ShowDialog(pDlg: pWaitDlg, fFade: false);
278 }
279 C4Network2VersionInfoClient VerChecker;
280 bool fSuccess = false, fAborted = false;
281 StdStrBuf strUpdateRedirect;
282 const std::string query{std::format(fmt: "{}?action=version", args: +Config.Network.UpdateServerAddress)};
283 if (VerChecker.Init() && VerChecker.SetServer(serverAddress: query) && VerChecker.QueryVersion())
284 {
285 Application.InteractiveThread.AddProc(pProc: &VerChecker);
286 // wait for version check to terminate
287 while (VerChecker.isBusy())
288 {
289 C4AppHandleResult hr;
290 while ((hr = Application.HandleMessage()) == HR_Message) {}
291 // check for program abort
292 if (hr == HR_Failure) { fAborted = true; break; }
293 // check for dialog close
294 if (pScreen && pWaitDlg) if (!C4GUI::IsGUIValid() || !pWaitDlg->IsShown()) { fAborted = true; break; }
295 }
296 if (!fAborted)
297 {
298 fSuccess = VerChecker.GetVersion(pSaveToVer: &UpdateVersion);
299 VerChecker.GetRedirect(rRedirect&: strUpdateRedirect);
300 }
301 Application.InteractiveThread.RemoveProc(pProc: &VerChecker);
302 }
303 if (pScreen && C4GUI::IsGUIValid()) delete pWaitDlg;
304 // User abort
305 if (fAborted)
306 {
307 return false;
308 }
309 // Error during update check
310 if (!fSuccess)
311 {
312 if (pScreen)
313 {
314 StdStrBuf sError; sError.Copy(pnData: LoadResStr(id: C4ResStrTableKey::IDS_MSG_UPDATEFAILED));
315 const char *szErrMsg = VerChecker.GetError();
316 if (szErrMsg)
317 {
318 sError.Append(pnData: ": ");
319 sError.Append(pnData: szErrMsg);
320 }
321 pScreen->ShowMessage(szMessage: sError.getData(), szCaption: Config.Network.UpdateServerAddress, icoIcon: C4GUI::Ico_Ex_Update);
322 }
323 return false;
324 }
325
326 // UpdateServer Redirection
327 if ((strUpdateRedirect.getLength() > 0) && !SEqual(szStr1: strUpdateRedirect.getData(), szStr2: Config.Network.UpdateServerAddress))
328 {
329 // this is a new redirect. Inform the user and auto-change servers if desired
330 const char *newServer = strUpdateRedirect.getData();
331 if (pScreen)
332 {
333 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECTMSG, args&: newServer)};
334 if (!pScreen->ShowMessageModal(szMessage: message.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECT), dwButtons: C4GUI::MessageDialog::btnYesNo, icoIcon: C4GUI::Ico_OfficialServer))
335 {
336 // apply new server setting
337 SCopy(szSource: newServer, sTarget: Config.Network.UpdateServerAddress, iMaxL: CFG_MaxString);
338 Config.Save();
339 pScreen->ShowMessageModal(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECTDONE), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_NET_SERVERREDIRECT), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_OfficialServer);
340 // abort the update check - user should try again
341 return false;
342 }
343 }
344 else
345 {
346 SCopy(szSource: newServer, sTarget: Config.Network.UpdateServerAddress, iMaxL: CFG_MaxString);
347 Config.Save();
348 return false;
349 }
350 }
351
352 if (!pScreen)
353 {
354 return C4UpdateDlg::IsValidUpdate(rNewVer: UpdateVersion);
355 }
356
357 // Applicable update available
358 if (C4UpdateDlg::IsValidUpdate(rNewVer: UpdateVersion))
359 {
360 // Prompt user, then apply update
361 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_ANUPDATETOVERSIONISAVAILA, args: UpdateVersion.GetString())};
362 if (pScreen->ShowMessageModal(szMessage: message.c_str(), szCaption: Config.Network.UpdateServerAddress, dwButtons: C4GUI::MessageDialog::btnYesNo, icoIcon: C4GUI::Ico_Ex_Update))
363 if (!DoUpdate(rUpdateVersion: UpdateVersion, pScreen))
364 pScreen->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_UPDATEFAILED), szCaption: Config.Network.UpdateServerAddress, icoIcon: C4GUI::Ico_Ex_Update);
365 else
366 return true;
367 }
368 // No applicable update available
369 else
370 {
371 // Message (if not automatic)
372 if (!fAutomatic)
373 pScreen->ShowMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NOUPDATEAVAILABLEFORTHISV), szCaption: Config.Network.UpdateServerAddress, icoIcon: C4GUI::Ico_Ex_Update);
374 }
375 // Done (and no update has been done)
376 return false;
377}
378
379// *** C4Network2VersionInfoClient
380
381bool C4Network2VersionInfoClient::QueryVersion()
382{
383 // Perform an Query query
384 return Query(Data: StdBuf{}, binary: false);
385}
386
387bool C4Network2VersionInfoClient::GetVersion(C4GameVersion *piVerOut)
388{
389 // Sanity check
390 if (isBusy() || !isSuccess()) return false;
391 // Parse response
392 piVerOut->Set(szEngine: "", iVer1: 0, iVer2: 0, iVer3: 0, iVer4: 0, iVerBuild: 0);
393 try
394 {
395 CompileFromBuf<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(
396 rValue: mkNamingAdapt(
397 rValue: mkParAdapt(rObj&: *piVerOut, rPar: false),
398 szName: "Version"),
399 C4ENGINENAME), SrcBuf: getResultString());
400 }
401 catch (const StdCompiler::Exception &e)
402 {
403 SetError(e.what());
404 return false;
405 }
406 // validate version
407 if (!piVerOut->iVer[0])
408 {
409 SetError(LoadResStr(id: C4ResStrTableKey::IDS_ERR_INVALIDREPLYFROMSERVER));
410 return false;
411 }
412 // done; version OK!
413 return true;
414}
415
416bool C4Network2VersionInfoClient::GetRedirect(StdStrBuf &rRedirect)
417{
418 // Sanity check
419 if (isBusy() || !isSuccess()) return false;
420 StdStrBuf strUpdateRedirect;
421 try
422 {
423 CompileFromBuf<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(
424 rValue: mkNamingAdapt(rValue: mkParAdapt(rObj&: strUpdateRedirect, rPar: StdCompiler::RCT_All), szName: "UpdateServerRedirect", rDefault: ""),
425 C4ENGINENAME), SrcBuf: getResultString());
426 }
427 catch (const StdCompiler::Exception &e)
428 {
429 SetError(e.what());
430 return false;
431 }
432 // did we get something?
433 if (strUpdateRedirect.getLength() > 0)
434 {
435 rRedirect.Copy(Buf2: strUpdateRedirect);
436 return true;
437 }
438 // no, we didn't
439 rRedirect.Clear();
440 return false;
441}
442