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(EUpdaterState NewState)
102{
103 CLockScope ls(m_Lock);
104 m_State = NewState;
105}
106
107IUpdater::EUpdaterState 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 switch(GetCurrentState())
168 {
169 case IUpdater::GOT_MANIFEST:
170 PerformUpdate();
171 break;
172 case IUpdater::DOWNLOADING:
173 RunningUpdate();
174 break;
175 case IUpdater::MOVE_FILES:
176 CommitUpdate();
177 break;
178 default:
179 return;
180 }
181}
182
183void CUpdater::AddFileJob(const char *pFile, bool Job)
184{
185 m_FileJobs.emplace_front(args&: pFile, args&: Job);
186}
187
188bool CUpdater::ReplaceClient()
189{
190 dbg_msg(sys: "updater", fmt: "replacing " PLAT_CLIENT_EXEC);
191 bool Success = true;
192 char aPath[IO_MAX_PATH_LENGTH];
193
194 // Replace running executable by renaming twice...
195 m_pStorage->RemoveBinaryFile(CLIENT_EXEC ".old");
196 Success &= m_pStorage->RenameBinaryFile(PLAT_CLIENT_EXEC, CLIENT_EXEC ".old");
197 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aClientExecTmp);
198 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_CLIENT_EXEC);
199#if !defined(CONF_FAMILY_WINDOWS)
200 m_pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
201 char aBuf[512];
202 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
203 if(system(command: aBuf))
204 {
205 dbg_msg(sys: "updater", fmt: "ERROR: failed to set client executable bit");
206 Success = false;
207 }
208#endif
209 return Success;
210}
211
212bool CUpdater::ReplaceServer()
213{
214 dbg_msg(sys: "updater", fmt: "replacing " PLAT_SERVER_EXEC);
215 bool Success = true;
216 char aPath[IO_MAX_PATH_LENGTH];
217
218 //Replace running executable by renaming twice...
219 m_pStorage->RemoveBinaryFile(SERVER_EXEC ".old");
220 Success &= m_pStorage->RenameBinaryFile(PLAT_SERVER_EXEC, SERVER_EXEC ".old");
221 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aServerExecTmp);
222 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_SERVER_EXEC);
223#if !defined(CONF_FAMILY_WINDOWS)
224 m_pStorage->GetBinaryPath(PLAT_SERVER_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
225 char aBuf[512];
226 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
227 if(system(command: aBuf))
228 {
229 dbg_msg(sys: "updater", fmt: "ERROR: failed to set server executable bit");
230 Success = false;
231 }
232#endif
233 return Success;
234}
235
236void CUpdater::ParseUpdate()
237{
238 char aPath[IO_MAX_PATH_LENGTH];
239 void *pBuf;
240 unsigned Length;
241 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))
242 return;
243
244 json_value *pVersions = json_parse(json: (json_char *)pBuf, length: Length);
245 free(ptr: pBuf);
246
247 if(pVersions && pVersions->type == json_array)
248 {
249 for(int i = 0; i < json_array_length(array: pVersions); i++)
250 {
251 const json_value *pTemp;
252 const json_value *pCurrent = json_array_get(array: pVersions, index: i);
253 if(str_comp(a: json_string_get(string: json_object_get(object: pCurrent, index: "version")), GAME_RELEASE_VERSION))
254 {
255 if(json_boolean_get(boolean: json_object_get(object: pCurrent, index: "client")))
256 m_ClientUpdate = true;
257 if(json_boolean_get(boolean: json_object_get(object: pCurrent, index: "server")))
258 m_ServerUpdate = true;
259 if((pTemp = json_object_get(object: pCurrent, index: "download"))->type == json_array)
260 {
261 for(int j = 0; j < json_array_length(array: pTemp); j++)
262 AddFileJob(pFile: json_string_get(string: json_array_get(array: pTemp, index: j)), Job: true);
263 }
264 if((pTemp = json_object_get(object: pCurrent, index: "remove"))->type == json_array)
265 {
266 for(int j = 0; j < json_array_length(array: pTemp); j++)
267 AddFileJob(pFile: json_string_get(string: json_array_get(array: pTemp, index: j)), Job: false);
268 }
269 }
270 else
271 break;
272 }
273 }
274 json_value_free(pVersions);
275}
276
277void CUpdater::InitiateUpdate()
278{
279 SetCurrentState(IUpdater::GETTING_MANIFEST);
280 FetchFile(pFile: "update.json");
281}
282
283void CUpdater::PerformUpdate()
284{
285 SetCurrentState(IUpdater::PARSING_UPDATE);
286 dbg_msg(sys: "updater", fmt: "parsing update.json");
287 ParseUpdate();
288 m_CurrentJob = m_FileJobs.begin();
289 SetCurrentState(IUpdater::DOWNLOADING);
290}
291
292void CUpdater::RunningUpdate()
293{
294 if(m_pCurrentTask)
295 {
296 if(!m_pCurrentTask->Done())
297 {
298 return;
299 }
300 else if(m_pCurrentTask->State() == EHttpState::ERROR || m_pCurrentTask->State() == EHttpState::ABORTED)
301 {
302 SetCurrentState(IUpdater::FAIL);
303 }
304 }
305
306 if(m_CurrentJob != m_FileJobs.end())
307 {
308 auto &Job = *m_CurrentJob;
309 if(Job.second)
310 {
311 const char *pFile = Job.first.c_str();
312 size_t len = str_length(str: pFile);
313 if(!str_comp_nocase(a: pFile + len - 4, b: ".dll"))
314 {
315#if defined(CONF_FAMILY_WINDOWS)
316 char aBuf[512];
317 str_copy(aBuf, pFile, sizeof(aBuf)); // SDL
318 str_copy(aBuf + len - 4, "-" PLAT_NAME, sizeof(aBuf) - len + 4); // -win32
319 str_append(aBuf, pFile + len - 4); // .dll
320 FetchFile(aBuf, pFile);
321#endif
322 // Ignore DLL downloads on other platforms
323 }
324 else if(!str_comp_nocase(a: pFile + len - 3, b: ".so"))
325 {
326#if defined(CONF_PLATFORM_LINUX)
327 char aBuf[512];
328 str_copy(dst: aBuf, src: pFile, dst_size: sizeof(aBuf)); // libsteam_api
329 str_copy(dst: aBuf + len - 3, src: "-" PLAT_NAME, dst_size: sizeof(aBuf) - len + 3); // -linux-x86_64
330 str_append(dst&: aBuf, src: pFile + len - 3); // .so
331 FetchFile(pFile: aBuf, pDestPath: pFile);
332#endif
333 // Ignore DLL downloads on other platforms, on Linux we statically link anyway
334 }
335 else
336 {
337 FetchFile(pFile);
338 }
339 }
340 else
341 {
342 m_pStorage->RemoveBinaryFile(pFilename: Job.first.c_str());
343 }
344
345 m_CurrentJob++;
346 }
347 else
348 {
349 if(m_ServerUpdate && !m_ServerFetched)
350 {
351 FetchFile(PLAT_SERVER_DOWN, pDestPath: m_aServerExecTmp);
352 m_ServerFetched = true;
353 return;
354 }
355
356 if(m_ClientUpdate && !m_ClientFetched)
357 {
358 FetchFile(PLAT_CLIENT_DOWN, pDestPath: m_aClientExecTmp);
359 m_ClientFetched = true;
360 return;
361 }
362
363 SetCurrentState(IUpdater::MOVE_FILES);
364 }
365}
366
367void CUpdater::CommitUpdate()
368{
369 bool Success = true;
370
371 for(auto &FileJob : m_FileJobs)
372 if(FileJob.second)
373 Success &= MoveFile(pFile: FileJob.first.c_str());
374
375 if(m_ClientUpdate)
376 Success &= ReplaceClient();
377 if(m_ServerUpdate)
378 Success &= ReplaceServer();
379 if(!Success)
380 SetCurrentState(IUpdater::FAIL);
381 else if(m_pClient->State() == IClient::STATE_ONLINE || m_pClient->EditorHasUnsavedData())
382 SetCurrentState(IUpdater::NEED_RESTART);
383 else
384 {
385 m_pClient->Restart();
386 }
387}
388