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(int NewState) |
102 | { |
103 | CLockScope ls(m_Lock); |
104 | m_State = NewState; |
105 | } |
106 | |
107 | int 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 | 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 | |
184 | void CUpdater::AddFileJob(const char *pFile, bool Job) |
185 | { |
186 | m_FileJobs.emplace_front(args: std::make_pair(x&: pFile, y&: Job)); |
187 | } |
188 | |
189 | bool 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 | |
213 | bool 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 | |
237 | void 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 | |
278 | void CUpdater::InitiateUpdate() |
279 | { |
280 | SetCurrentState(IUpdater::GETTING_MANIFEST); |
281 | FetchFile(pFile: "update.json" ); |
282 | } |
283 | |
284 | void 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 | |
293 | void 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 | |
368 | void 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 | |