1#include <base/system.h>
2
3#include "updater.h"
4#include <engine/client.h>
5#include <engine/engine.h>
6#include <engine/external/json-parser/json.h>
7#include <engine/shared/http.h>
8#include <engine/shared/json.h>
9#include <engine/storage.h>
10
11#include <game/version.h>
12
13#include <cstdlib> // system
14
15using std::string;
16
17class CUpdaterFetchTask : public CHttpRequest
18{
19 char m_aBuf[256];
20 char m_aBuf2[256];
21 CUpdater *m_pUpdater;
22
23 void OnProgress() override;
24
25protected:
26 void OnCompletion(EHttpState State) override;
27
28public:
29 CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath);
30};
31
32static const char *GetUpdaterUrl(char *pBuf, int BufSize, const char *pFile)
33{
34 str_format(buffer: pBuf, buffer_size: BufSize, format: "https://update.ddnet.org/%s", pFile);
35 return pBuf;
36}
37
38static const char *GetUpdaterDestPath(char *pBuf, int BufSize, const char *pFile, const char *pDestPath)
39{
40 if(!pDestPath)
41 {
42 pDestPath = pFile;
43 }
44 str_format(buffer: pBuf, buffer_size: BufSize, format: "update/%s", pDestPath);
45 return pBuf;
46}
47
48CUpdaterFetchTask::CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath) :
49 CHttpRequest(GetUpdaterUrl(pBuf: m_aBuf, BufSize: sizeof(m_aBuf), pFile)),
50 m_pUpdater(pUpdater)
51{
52 WriteToFile(pStorage: pUpdater->m_pStorage, pDest: GetUpdaterDestPath(pBuf: m_aBuf2, BufSize: sizeof(m_aBuf2), pFile, pDestPath), StorageType: -2);
53}
54
55void CUpdaterFetchTask::OnProgress()
56{
57 CLockScope ls(m_pUpdater->m_Lock);
58 m_pUpdater->m_Percent = Progress();
59}
60
61void CUpdaterFetchTask::OnCompletion(EHttpState State)
62{
63 const char *pFileName = 0;
64 for(const char *pPath = Dest(); *pPath; pPath++)
65 if(*pPath == '/')
66 pFileName = pPath + 1;
67 pFileName = pFileName ? pFileName : Dest();
68 if(!str_comp(a: pFileName, b: "update.json"))
69 {
70 if(State == EHttpState::DONE)
71 m_pUpdater->SetCurrentState(IUpdater::GOT_MANIFEST);
72 else if(State == EHttpState::ERROR)
73 m_pUpdater->SetCurrentState(IUpdater::FAIL);
74 }
75}
76
77CUpdater::CUpdater()
78{
79 m_pClient = nullptr;
80 m_pStorage = nullptr;
81 m_pEngine = nullptr;
82 m_pHttp = nullptr;
83 m_State = CLEAN;
84 m_Percent = 0;
85 m_pCurrentTask = nullptr;
86
87 m_ClientUpdate = m_ServerUpdate = m_ClientFetched = m_ServerFetched = false;
88
89 IStorage::FormatTmpPath(aBuf: m_aClientExecTmp, BufSize: sizeof(m_aClientExecTmp), CLIENT_EXEC);
90 IStorage::FormatTmpPath(aBuf: m_aServerExecTmp, BufSize: sizeof(m_aServerExecTmp), SERVER_EXEC);
91}
92
93void CUpdater::Init(CHttp *pHttp)
94{
95 m_pClient = Kernel()->RequestInterface<IClient>();
96 m_pStorage = Kernel()->RequestInterface<IStorage>();
97 m_pEngine = Kernel()->RequestInterface<IEngine>();
98 m_pHttp = pHttp;
99}
100
101void CUpdater::SetCurrentState(int NewState)
102{
103 CLockScope ls(m_Lock);
104 m_State = NewState;
105}
106
107int CUpdater::GetCurrentState()
108{
109 CLockScope ls(m_Lock);
110 return m_State;
111}
112
113void CUpdater::GetCurrentFile(char *pBuf, int BufSize)
114{
115 CLockScope ls(m_Lock);
116 str_copy(dst: pBuf, src: m_aStatus, dst_size: BufSize);
117}
118
119int CUpdater::GetCurrentPercent()
120{
121 CLockScope ls(m_Lock);
122 return m_Percent;
123}
124
125void CUpdater::FetchFile(const char *pFile, const char *pDestPath)
126{
127 CLockScope ls(m_Lock);
128 m_pCurrentTask = std::make_shared<CUpdaterFetchTask>(args: this, args&: pFile, args&: pDestPath);
129 str_copy(dst&: m_aStatus, src: m_pCurrentTask->Dest());
130 m_pHttp->Run(pRequest: m_pCurrentTask);
131}
132
133bool CUpdater::MoveFile(const char *pFile)
134{
135 char aBuf[256];
136 size_t len = str_length(str: pFile);
137 bool Success = true;
138
139#if !defined(CONF_FAMILY_WINDOWS)
140 if(!str_comp_nocase(a: pFile + len - 4, b: ".dll"))
141 return Success;
142#endif
143
144#if !defined(CONF_PLATFORM_LINUX)
145 if(!str_comp_nocase(pFile + len - 3, ".so"))
146 return Success;
147#endif
148
149 if(!str_comp_nocase(a: pFile + len - 4, b: ".dll") || !str_comp_nocase(a: pFile + len - 4, b: ".ttf") || !str_comp_nocase(a: pFile + len - 3, b: ".so"))
150 {
151 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.old", pFile);
152 m_pStorage->RenameBinaryFile(pOldFilename: pFile, pNewFilename: aBuf);
153 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "update/%s", pFile);
154 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aBuf, pNewFilename: pFile);
155 }
156 else
157 {
158 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "update/%s", pFile);
159 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aBuf, pNewFilename: pFile);
160 }
161
162 return Success;
163}
164
165void CUpdater::Update()
166{
167 auto State = GetCurrentState();
168 switch(State)
169 {
170 case IUpdater::GOT_MANIFEST:
171 PerformUpdate();
172 break;
173 case IUpdater::DOWNLOADING:
174 RunningUpdate();
175 break;
176 case IUpdater::MOVE_FILES:
177 CommitUpdate();
178 break;
179 default:
180 return;
181 }
182}
183
184void CUpdater::AddFileJob(const char *pFile, bool Job)
185{
186 m_FileJobs.emplace_front(args: std::make_pair(x&: pFile, y&: Job));
187}
188
189bool CUpdater::ReplaceClient()
190{
191 dbg_msg(sys: "updater", fmt: "replacing " PLAT_CLIENT_EXEC);
192 bool Success = true;
193 char aPath[IO_MAX_PATH_LENGTH];
194
195 // Replace running executable by renaming twice...
196 m_pStorage->RemoveBinaryFile(CLIENT_EXEC ".old");
197 Success &= m_pStorage->RenameBinaryFile(PLAT_CLIENT_EXEC, CLIENT_EXEC ".old");
198 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aClientExecTmp);
199 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_CLIENT_EXEC);
200#if !defined(CONF_FAMILY_WINDOWS)
201 m_pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
202 char aBuf[512];
203 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
204 if(system(command: aBuf))
205 {
206 dbg_msg(sys: "updater", fmt: "ERROR: failed to set client executable bit");
207 Success = false;
208 }
209#endif
210 return Success;
211}
212
213bool CUpdater::ReplaceServer()
214{
215 dbg_msg(sys: "updater", fmt: "replacing " PLAT_SERVER_EXEC);
216 bool Success = true;
217 char aPath[IO_MAX_PATH_LENGTH];
218
219 //Replace running executable by renaming twice...
220 m_pStorage->RemoveBinaryFile(SERVER_EXEC ".old");
221 Success &= m_pStorage->RenameBinaryFile(PLAT_SERVER_EXEC, SERVER_EXEC ".old");
222 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aServerExecTmp);
223 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_SERVER_EXEC);
224#if !defined(CONF_FAMILY_WINDOWS)
225 m_pStorage->GetBinaryPath(PLAT_SERVER_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
226 char aBuf[512];
227 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
228 if(system(command: aBuf))
229 {
230 dbg_msg(sys: "updater", fmt: "ERROR: failed to set server executable bit");
231 Success = false;
232 }
233#endif
234 return Success;
235}
236
237void CUpdater::ParseUpdate()
238{
239 char aPath[IO_MAX_PATH_LENGTH];
240 void *pBuf;
241 unsigned Length;
242 if(!m_pStorage->ReadFile(pFilename: m_pStorage->GetBinaryPath(pFilename: "update/update.json", pBuffer: aPath, BufferSize: sizeof(aPath)), Type: IStorage::TYPE_ABSOLUTE, ppResult: &pBuf, pResultLen: &Length))
243 return;
244
245 json_value *pVersions = json_parse(json: (json_char *)pBuf, length: Length);
246 free(ptr: pBuf);
247
248 if(pVersions && pVersions->type == json_array)
249 {
250 for(int i = 0; i < json_array_length(array: pVersions); i++)
251 {
252 const json_value *pTemp;
253 const json_value *pCurrent = json_array_get(array: pVersions, index: i);
254 if(str_comp(a: json_string_get(string: json_object_get(object: pCurrent, index: "version")), GAME_RELEASE_VERSION))
255 {
256 if(json_boolean_get(boolean: json_object_get(object: pCurrent, index: "client")))
257 m_ClientUpdate = true;
258 if(json_boolean_get(boolean: json_object_get(object: pCurrent, index: "server")))
259 m_ServerUpdate = true;
260 if((pTemp = json_object_get(object: pCurrent, index: "download"))->type == json_array)
261 {
262 for(int j = 0; j < json_array_length(array: pTemp); j++)
263 AddFileJob(pFile: json_string_get(string: json_array_get(array: pTemp, index: j)), Job: true);
264 }
265 if((pTemp = json_object_get(object: pCurrent, index: "remove"))->type == json_array)
266 {
267 for(int j = 0; j < json_array_length(array: pTemp); j++)
268 AddFileJob(pFile: json_string_get(string: json_array_get(array: pTemp, index: j)), Job: false);
269 }
270 }
271 else
272 break;
273 }
274 }
275 json_value_free(pVersions);
276}
277
278void CUpdater::InitiateUpdate()
279{
280 SetCurrentState(IUpdater::GETTING_MANIFEST);
281 FetchFile(pFile: "update.json");
282}
283
284void CUpdater::PerformUpdate()
285{
286 SetCurrentState(IUpdater::PARSING_UPDATE);
287 dbg_msg(sys: "updater", fmt: "parsing update.json");
288 ParseUpdate();
289 m_CurrentJob = m_FileJobs.begin();
290 SetCurrentState(IUpdater::DOWNLOADING);
291}
292
293void CUpdater::RunningUpdate()
294{
295 if(m_pCurrentTask)
296 {
297 if(!m_pCurrentTask->Done())
298 {
299 return;
300 }
301 else if(m_pCurrentTask->State() == EHttpState::ERROR || m_pCurrentTask->State() == EHttpState::ABORTED)
302 {
303 SetCurrentState(IUpdater::FAIL);
304 }
305 }
306
307 if(m_CurrentJob != m_FileJobs.end())
308 {
309 auto &Job = *m_CurrentJob;
310 if(Job.second)
311 {
312 const char *pFile = Job.first.c_str();
313 size_t len = str_length(str: pFile);
314 if(!str_comp_nocase(a: pFile + len - 4, b: ".dll"))
315 {
316#if defined(CONF_FAMILY_WINDOWS)
317 char aBuf[512];
318 str_copy(aBuf, pFile, sizeof(aBuf)); // SDL
319 str_copy(aBuf + len - 4, "-" PLAT_NAME, sizeof(aBuf) - len + 4); // -win32
320 str_append(aBuf, pFile + len - 4); // .dll
321 FetchFile(aBuf, pFile);
322#endif
323 // Ignore DLL downloads on other platforms
324 }
325 else if(!str_comp_nocase(a: pFile + len - 3, b: ".so"))
326 {
327#if defined(CONF_PLATFORM_LINUX)
328 char aBuf[512];
329 str_copy(dst: aBuf, src: pFile, dst_size: sizeof(aBuf)); // libsteam_api
330 str_copy(dst: aBuf + len - 3, src: "-" PLAT_NAME, dst_size: sizeof(aBuf) - len + 3); // -linux-x86_64
331 str_append(dst&: aBuf, src: pFile + len - 3); // .so
332 FetchFile(pFile: aBuf, pDestPath: pFile);
333#endif
334 // Ignore DLL downloads on other platforms, on Linux we statically link anyway
335 }
336 else
337 {
338 FetchFile(pFile);
339 }
340 }
341 else
342 {
343 m_pStorage->RemoveBinaryFile(pFilename: Job.first.c_str());
344 }
345
346 m_CurrentJob++;
347 }
348 else
349 {
350 if(m_ServerUpdate && !m_ServerFetched)
351 {
352 FetchFile(PLAT_SERVER_DOWN, pDestPath: m_aServerExecTmp);
353 m_ServerFetched = true;
354 return;
355 }
356
357 if(m_ClientUpdate && !m_ClientFetched)
358 {
359 FetchFile(PLAT_CLIENT_DOWN, pDestPath: m_aClientExecTmp);
360 m_ClientFetched = true;
361 return;
362 }
363
364 SetCurrentState(IUpdater::MOVE_FILES);
365 }
366}
367
368void CUpdater::CommitUpdate()
369{
370 bool Success = true;
371
372 for(auto &FileJob : m_FileJobs)
373 if(FileJob.second)
374 Success &= MoveFile(pFile: FileJob.first.c_str());
375
376 if(m_ClientUpdate)
377 Success &= ReplaceClient();
378 if(m_ServerUpdate)
379 Success &= ReplaceServer();
380 if(!Success)
381 SetCurrentState(IUpdater::FAIL);
382 else if(m_pClient->State() == IClient::STATE_ONLINE || m_pClient->EditorHasUnsavedData())
383 SetCurrentState(IUpdater::NEED_RESTART);
384 else
385 {
386 m_pClient->Restart();
387 }
388}
389