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 | |
15 | using std::string; |
16 | |
17 | class CUpdaterFetchTask : public CHttpRequest |
18 | { |
19 | char m_aBuf[256]; |
20 | char m_aBuf2[256]; |
21 | CUpdater *m_pUpdater; |
22 | |
23 | void OnProgress() override; |
24 | |
25 | protected: |
26 | void OnCompletion(EHttpState State) override; |
27 | |
28 | public: |
29 | CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath); |
30 | }; |
31 | |
32 | static 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 | |
38 | static 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 | |
48 | CUpdaterFetchTask::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 | |
55 | void CUpdaterFetchTask::OnProgress() |
56 | { |
57 | CLockScope ls(m_pUpdater->m_Lock); |
58 | m_pUpdater->m_Percent = Progress(); |
59 | } |
60 | |
61 | void 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 | |
77 | CUpdater::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 | |
93 | void 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 | |
101 | void CUpdater::SetCurrentState(EUpdaterState NewState) |
102 | { |
103 | CLockScope ls(m_Lock); |
104 | m_State = NewState; |
105 | } |
106 | |
107 | IUpdater::EUpdaterState CUpdater::GetCurrentState() |
108 | { |
109 | CLockScope ls(m_Lock); |
110 | return m_State; |
111 | } |
112 | |
113 | void 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 | |
119 | int CUpdater::GetCurrentPercent() |
120 | { |
121 | CLockScope ls(m_Lock); |
122 | return m_Percent; |
123 | } |
124 | |
125 | void 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 | |
133 | bool 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 | |
165 | void 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 | |
183 | void CUpdater::AddFileJob(const char *pFile, bool Job) |
184 | { |
185 | m_FileJobs.emplace_front(args&: pFile, args&: Job); |
186 | } |
187 | |
188 | bool 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 | |
212 | bool 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 | |
236 | void 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 | |
277 | void CUpdater::InitiateUpdate() |
278 | { |
279 | SetCurrentState(IUpdater::GETTING_MANIFEST); |
280 | FetchFile(pFile: "update.json" ); |
281 | } |
282 | |
283 | void 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 | |
292 | void 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 | |
367 | void 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 | |