Merge from sourcemod-1.1 branch.
3 * =============================================================================
5 * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved.
6 * =============================================================================
8 * This program is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, version 3.0, as published by the
10 * Free Software Foundation.
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <http://www.gnu.org/licenses/>.
20 * As a special exception, AlliedModders LLC gives you permission to link the
21 * code of this program (as well as its derivative works) to "Half-Life 2," the
22 * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
23 * by the Valve Corporation. You must obey the GNU General Public License in
24 * all respects for all other code used. Additionally, AlliedModders LLC grants
25 * this exception to all derivative works. AlliedModders LLC defines further
26 * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
27 * or <http://www.sourcemod.net/license.php>.
32 #include "GameDataFetcher.h"
35 #ifdef PLATFORM_WINDOWS
39 #include <sys/types.h>
40 #include <sys/socket.h>
41 #include <netinet/in.h>
43 #include <arpa/inet.h>
45 #define INVALID_SOCKET -1
46 #define closesocket close
47 #define WSAGetLastError() errno
50 #include "sh_vector.h"
51 #include "sh_string.h"
52 #include "sm_version.h"
54 #if SOURCE_ENGINE == SE_LEFT4DEAD
55 #include "convar_sm_l4d.h"
56 #elif SOURCE_ENGINE == SE_ORANGEBOX
57 #include "convar_sm_ob.h"
59 #include "convar_sm.h"
62 #include "sourcemm_api.h"
65 #include "compat_wrappers.h"
66 #include "sm_stringutil.h"
68 #include "frame_hooks.h"
70 #define QUERY_MAX_LENGTH 1024
72 static BuildMD5ableBuffer g_MD5Builder;
73 static FetcherThread g_FetchThread;
75 static FILE *logfile = NULL;
77 bool g_disableGameDataUpdate = false;
80 * Note on this. If we issue a reload and changelevel, my srcds.exe will emit
81 * Assertion Failed: !m_bServiceStarted
82 * on quit. This seems like a non-issue, because before we just terminated the
83 * server anyway. If anyone notices and files a bug, we can look into it further.
85 bool g_restartAfterUpdate = false;
87 static bool was_level_started = false;
88 static int g_serverPort = 6500;
89 static char g_serverAddress[100] = "smupdate.alliedmods.net";
91 static void _ForceRestart(void *data)
94 g_Logger.LogMessage("Automatically restarting SourceMod after a successful gamedata update.");
95 UTIL_Format(cmd, sizeof(cmd), "meta unload %d\n", g_PLID);
96 engine->ServerCommand(cmd);
97 UTIL_Format(cmd, sizeof(cmd), "changelevel \"%s\"\n", STRING(gpGlobals->mapname));
98 engine->ServerCommand(cmd);
99 UTIL_Format(cmd, sizeof(cmd), "echo SourceMod restarted after gamedata update.\n");
100 engine->ServerCommand(cmd);
103 static void ForceRestart()
107 action.action = _ForceRestart;
109 AddFrameAction(action);
112 void FetcherThread::RunThread(IThreadHandle *pHandle)
114 char lock_path[PLATFORM_MAX_PATH];
115 g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp");
116 g_LibSys.CreateFolder(lock_path);
118 g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp/gamedata.lock");
120 char log_path[PLATFORM_MAX_PATH];
121 g_SourceMod.BuildPath(Path_SM, log_path, sizeof(log_path), "logs/gamedata");
123 g_LibSys.CreateFolder(log_path);
127 tm *curtime = localtime(&t);
129 g_SourceMod.BuildPath(Path_SM, log_path, sizeof(log_path), "logs/gamedata/L%02d%02d.log", curtime->tm_mon + 1, curtime->tm_mday);
131 logfile = fopen(log_path, "a");
139 //Create a blank lock file
140 FILE *fp = fopen(lock_path, "w");
146 char query[QUERY_MAX_LENGTH];
148 /* Check for updated gamedata files */
149 int len = BuildGameDataQuery(query, QUERY_MAX_LENGTH);
153 g_Logger.LogToFileOnly(logfile, "Could not build gamedata query!");
159 /* We check this late so we have the MD5 sums available. This may change in the future. */
160 if (g_disableGameDataUpdate)
162 g_Logger.LogToFileOnly(logfile, "Skipping gamedata fetcher (DisableAutoUpdate set)");
168 /* Create a new socket for this connection */
169 int socketDescriptor = ConnectSocket();
171 if (socketDescriptor == INVALID_SOCKET)
178 int sent = SendData(socketDescriptor, query, len);
181 g_Logger.LogToFileOnly(logfile, "Sent gamedata query");
187 g_Logger.LogToFileOnly(logfile, "Failed to send gamedata query data to remote host");
190 closesocket(socketDescriptor);
196 ProcessGameDataQuery(socketDescriptor);
198 /* And we're done! */
199 closesocket(socketDescriptor);
204 void FetcherThread::OnTerminate(IThreadHandle *pHandle, bool cancel)
206 g_blockGameDataLoad = false;
215 HandleUpdateStatus(updateStatus, build);
219 if (g_restartAfterUpdate)
221 if (was_level_started)
228 g_Logger.LogMessage("Your gamedata files have been updated, please restart your server.");
232 else if (!g_disableGameDataUpdate)
234 g_Logger.LogError("An error occurred in the gamedata fetcher, see your gamedata log files for more information.");
238 int FetcherThread::BuildGameDataQuery(char *buffer, int maxlen)
240 char gamedata_path[PLATFORM_MAX_PATH];
241 g_SourceMod.BuildPath(Path_SM, gamedata_path, sizeof(gamedata_path), "gamedata");
243 IDirectory *dir = g_LibSys.OpenDirectory(gamedata_path);
250 bf_write Writer = bf_write("GameDataQuery", buffer, maxlen);
252 Writer.WriteByte('A'); //Generic Header char
253 Writer.WriteByte('G'); //G for gamedata query, or green, like my hat.
255 short build[4] = { SVN_FILE_VERSION };
257 Writer.WriteBytes(&build[0], 8);
259 Writer.WriteByte(0); // Initialize the file counter - Index 10
261 while (dir->MoreFiles())
263 if (dir->IsEntryFile())
265 const char *name = dir->GetEntryName();
266 size_t len = strlen(name);
267 if (len >= 4 && strcmp(&name[len-4], ".txt") == 0)
272 unsigned char raw[16];
273 char file[PLATFORM_MAX_PATH];
275 g_LibSys.PathFormat(file, sizeof(file), "%s/%s", gamedata_path, name);
277 g_MD5Builder.checksum = &md5;
278 if ((err = g_TextParser.ParseFile_SMC(file, &g_MD5Builder, &states)) == SMCError_Okay)
281 (uint8_t)buffer[10]++; //Increment the file counter
282 Writer.WriteBytes(raw, 16);
284 FileData *data = new FileData();
285 data->filename = new SourceHook::String(file);
286 md5.hex_digest(data->checksum);
287 filenames.push_back(data);
290 g_Logger.LogToFileOnly(logfile, "Parsed file: %s as %s", file, data->checksum);
297 const char *error = g_TextParser.GetSMCErrorString(err);
298 g_Logger.LogToFileOnly(logfile, "Parsing of file %s failed: %s", file, error);
306 return Writer.GetNumBytesWritten();
309 int FetcherThread::ConnectSocket()
311 #if defined PLATFORM_WINDOWS
313 WSAStartup(0x0101, &wsaData);
316 struct protoent *ptrp;
318 if ((ptrp = getprotobyname("tcp")) == NULL)
320 g_Logger.LogToFileOnly(logfile, "Error: Failed to find TCP protocol");
321 return INVALID_SOCKET;
324 int socketDescriptor = socket(AF_INET, SOCK_STREAM, ptrp->p_proto);
326 if (socketDescriptor == INVALID_SOCKET)
329 g_LibSys.GetPlatformErrorEx(WSAGetLastError(), error, sizeof(error));
330 g_Logger.LogToFileOnly(logfile, "Error: Failed to create socket: %s", error);
331 closesocket(socketDescriptor);
332 return INVALID_SOCKET;
336 struct sockaddr_in local_addr;
338 local_addr.sin_family = AF_INET;
339 local_addr.sin_port = htons((u_short)g_serverPort);
341 he = gethostbyname(g_serverAddress);
345 if ((local_addr.sin_addr.s_addr = inet_addr(g_serverAddress)) == INADDR_NONE)
347 g_Logger.LogToFileOnly(logfile, "Couldn't locate address: %s", g_serverAddress);
348 closesocket(socketDescriptor);
349 return INVALID_SOCKET;
354 memcpy(&local_addr.sin_addr, (struct in_addr *)he->h_addr, he->h_length);
357 if (connect(socketDescriptor, (struct sockaddr *) &local_addr, sizeof(local_addr)) < 0)
360 g_LibSys.GetPlatformErrorEx(WSAGetLastError(), error, sizeof(error));
361 g_Logger.LogToFileOnly(logfile, "Couldn't connect to %s: %s", g_serverAddress, error);
362 closesocket(socketDescriptor);
363 return INVALID_SOCKET;
366 return socketDescriptor;
369 void FetcherThread::ProcessGameDataQuery(int socketDescriptor)
374 g_Logger.LogToFileOnly(logfile, "Waiting for reply!");
377 //Read in the header bytes
378 int returnLen = RecvData(socketDescriptor, buffer, 12);
384 g_LibSys.GetPlatformErrorEx(WSAGetLastError(), error, sizeof(error));
385 g_Logger.LogToFileOnly(logfile, "Did not receive reply: %s", error);
390 g_Logger.LogToFileOnly(logfile, "Received Header!");
393 bf_read Reader = bf_read("GameDataQuery", buffer, 12);
395 if (Reader.ReadByte() != 'A' || Reader.ReadByte() != 'G')
398 g_Logger.LogToFileOnly(logfile, "Unknown Query Response");
403 updateStatus = (UpdateStatus)Reader.ReadByte();
405 build[0] = Reader.ReadShort();
406 build[1] = Reader.ReadShort();
407 build[2] = Reader.ReadShort();
408 build[3] = Reader.ReadShort();
411 g_Logger.LogToFileOnly(logfile,
412 "Update Status: %i - Latest %i.%i.%i.%i",
420 int changedFiles = Reader.ReadByte();
423 g_Logger.LogToFileOnly(logfile, "Files to download: %i", changedFiles);
426 for (int i=0; i<changedFiles; i++)
428 //Read in the file index and byte count
429 returnLen = RecvData(socketDescriptor, buffer, 5);
433 /* Timeout or fail? */
437 Reader.StartReading(buffer, 5);
439 int index = Reader.ReadByte();
440 int tempLen = Reader.ReadUBitLong(32);
443 g_Logger.LogToFileOnly(logfile, "File index %i and length %i", index, tempLen);
447 memtable->CreateMem(tempLen+1, &memPtr);
449 //Read the contents of our file into the memtable
450 returnLen = RecvData(socketDescriptor, (char *)memPtr, tempLen);
453 g_Logger.LogToFileOnly(logfile, "Received %i bytes", returnLen);
458 /* Timeout or fail? */
462 ((unsigned char *)memPtr)[tempLen] = '\0';
464 FileData *data = filenames.at(index);
465 const char *filename;
468 filename = data->filename->c_str();
470 FILE *fp = fopen(filename, "w");
474 fprintf(fp, "%s", (const char *)memPtr);
479 g_Logger.LogToFileOnly(logfile, "Failed to open file \"%s\" for writing", filename);
489 g_Logger.LogToFileOnly(logfile, "Updated file: %s", filename);
493 g_Logger.LogToFileOnly(logfile, "File Downloads Completed!");
496 needsRestart = false;
498 if (changedFiles > 0)
503 //Read changed file count
504 returnLen = RecvData(socketDescriptor, buffer, 1);
509 g_LibSys.GetPlatformErrorEx(WSAGetLastError(), error, sizeof(error));
510 g_Logger.LogToFileOnly(logfile, "Did not receive count reply: %s", error);
514 Reader.StartReading(buffer, 1);
516 changedFiles = Reader.ReadByte();
518 if (changedFiles < 1)
521 g_Logger.LogToFileOnly(logfile, "No unknown files. We're all done");
526 char *changedFileIndexes = new char[changedFiles];
529 g_Logger.LogToFileOnly(logfile, "%i files were unknown", changedFiles);
532 returnLen = RecvData(socketDescriptor, changedFileIndexes, changedFiles);
537 g_LibSys.GetPlatformErrorEx(WSAGetLastError(), error, sizeof(error));
538 g_Logger.LogToFileOnly(logfile, "Did not receive list reply: %s", error);
542 Reader.StartReading(changedFileIndexes, changedFiles);
544 for (int i=0; i<changedFiles; i++)
546 int index = Reader.ReadByte();
549 FileData *data = filenames.at(index);
550 const char* pathname;
553 pathname = data->filename->c_str();
560 g_LibSys.GetFileFromPath(fileName, sizeof(fileName), pathname);
562 g_Logger.LogToFileOnly(logfile, "Unknown File %i : %s", index, fileName);
566 delete [] changedFileIndexes;
571 int FetcherThread::RecvData(int socketDescriptor, char *buffer, int len)
576 /* Create a 10 Second Timeout */
580 int bytesReceivedTotal = 0;
582 while (bytesReceivedTotal < len)
584 /* Add our socket to a socket set */
586 FD_SET(socketDescriptor, &fds);
588 /* Wait max of 10 seconds for recv to become available */
589 select(socketDescriptor+1, &fds, NULL, NULL, &tv);
591 int bytesReceived = 0;
593 /* Is there a limit on how much we can receive? Some site said 1024 bytes, which will be well short of a file */
594 if (FD_ISSET(socketDescriptor, &fds))
596 bytesReceived = recv(socketDescriptor, buffer+bytesReceivedTotal, len-bytesReceivedTotal, 0);
599 if (bytesReceived == 0 || bytesReceived == -1)
604 bytesReceivedTotal += bytesReceived;
607 return bytesReceivedTotal;
610 int FetcherThread::SendData(int socketDescriptor, char *buffer, int len)
618 int sentBytesTotal = 0;
620 while (sentBytesTotal < len)
623 FD_SET(socketDescriptor, &fds);
625 select(socketDescriptor+1, NULL, &fds, NULL, &tv);
629 if (FD_ISSET(socketDescriptor, &fds))
631 sentBytes = send(socketDescriptor, buffer+sentBytesTotal, len-sentBytesTotal, 0);
634 if (sentBytes == 0 || sentBytes == -1)
639 sentBytesTotal += sentBytes;
642 return sentBytesTotal;
645 void FetcherThread::HandleUpdateStatus(UpdateStatus status, short version[4])
655 case Update_NewBuild:
660 case Update_MinorAvailable:
662 g_Logger.LogMessage("SourceMod Update: A new release of SourceMod is now available from sourcemod.net");
663 g_Logger.LogMessage("Current Version: %i.%i.%i Available: %i.%i.%i", version[0], version[1], version[2], version[0], version[1], version[2]);
667 case Update_MajorAvailable:
669 g_Logger.LogMessage("SourceMod Update: An major release of SourceMod is now available from sourcemod.net");
670 g_Logger.LogMessage("Current Version: %i.%i.%i Available: %i.%i.%i", version[0], version[1], version[2], version[0], version[1], version[2]);
674 case Update_CriticalAvailable:
676 g_Logger.LogError("SourceMod Update: A critical SourceMod release is available from sourcemod.net. It is strongly recommended that you update!");
677 g_Logger.LogMessage("Current Version: %i.%i.%i Available: %i.%i.%i", version[0], version[1], version[2], version[0], version[1], version[2]);
683 bool g_blockGameDataLoad = false;
684 static IThreadHandle *fetch_thread_hndl;
686 class InitFetch : public SMGlobalClass
689 void OnSourceModAllInitialized_Post()
691 char lock_path[PLATFORM_MAX_PATH];
692 g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp/gamedata.lock");
694 if (g_LibSys.IsPathFile(lock_path) && g_LibSys.PathExists(lock_path))
696 g_Logger.LogError("sourcemod/data/temp/gamedata.lock file detected. This is most likely due to a crash during GameData updating - Blocking GameData loading");
697 g_Logger.LogError("If this error persists delete the file manually");
698 g_blockGameDataLoad = true;
701 ThreadParams fetchThreadParams = ThreadParams();
702 fetchThreadParams.prio = ThreadPrio_Low;
703 fetch_thread_hndl = g_pThreader->MakeThread(&g_FetchThread, &fetchThreadParams);
706 void OnSourceModShutdown()
708 fetch_thread_hndl->WaitForThread();
709 fetch_thread_hndl->DestroyThis();
712 void OnSourceModLevelActivated()
714 was_level_started = true;
716 if (g_restartAfterUpdate &&
717 g_FetchThread.wasSuccess &&
718 g_FetchThread.needsRestart)
724 void OnSourceModLevelEnd()
726 was_level_started = false;
729 ConfigResult OnSourceModConfigChanged(const char *key,
735 if (strcasecmp(key, "DisableAutoUpdate") == 0)
737 if (strcasecmp(value, "yes") == 0)
739 g_disableGameDataUpdate = true;
740 return ConfigResult_Accept;
742 else if (strcasecmp(value, "no") == 0)
744 g_disableGameDataUpdate = false;
745 return ConfigResult_Accept;
748 return ConfigResult_Reject;
751 if (strcasecmp(key, "ForceRestartAfterUpdate") == 0)
753 if (strcasecmp(value, "yes") == 0)
755 g_restartAfterUpdate = true;
756 return ConfigResult_Accept;
758 else if (strcasecmp(value, "no") == 0)
760 g_restartAfterUpdate = false;
761 return ConfigResult_Accept;
764 return ConfigResult_Reject;
767 if (strcasecmp(key, "AutoUpdateServer") == 0)
769 UTIL_Format(g_serverAddress, sizeof(g_serverAddress), "%s", value);
771 return ConfigResult_Accept;
774 if (strcasecmp(key, "AutoUpdatePort") == 0)
776 int port = atoi(value);
780 return ConfigResult_Reject;
785 return ConfigResult_Accept;
788 return ConfigResult_Ignore;
792 BuildMD5ableBuffer::BuildMD5ableBuffer()
794 stringTable = new BaseStringTable(2048);
797 BuildMD5ableBuffer::~BuildMD5ableBuffer()
802 void BuildMD5ableBuffer::ReadSMC_ParseStart()
804 stringTable->Reset();
807 SMCResult BuildMD5ableBuffer::ReadSMC_KeyValue(const SMCStates *states,
811 stringTable->AddString(key);
812 stringTable->AddString(value);
814 return SMCResult_Continue;
817 SMCResult BuildMD5ableBuffer::ReadSMC_NewSection(const SMCStates *states, const char *name)
819 stringTable->AddString(name);
821 return SMCResult_Continue;
824 void BuildMD5ableBuffer::ReadSMC_ParseEnd(bool halted, bool failed)
826 if (halted || failed)
831 void *data = stringTable->GetMemTable()->GetAddress(0);
835 checksum->update((unsigned char *)data, stringTable->GetMemTable()->GetActualMemUsed());
838 checksum->finalize();
841 CON_COMMAND(sm_gamedata_md5, "Checks the MD5 sum for a given gamedata file")
843 #if SOURCE_ENGINE == SE_EPISODEONE
849 g_SMAPI->ConPrint("Usage: sm_gamedata_md5 <file>\n");
853 const char *file = args.Arg(1);
854 if (!file || file[0] == '\0')
856 g_SMAPI->ConPrint("Usage: sm_gamedata_md5 <file>\n");
860 SourceHook::CVector<FileData *>::iterator iter = g_FetchThread.filenames.begin();
864 while (iter != g_FetchThread.filenames.end())
870 g_LibSys.GetFileFromPath(fileName, sizeof(fileName), curData->filename->c_str());
872 if (strcmpi(fileName, file) == 0)
874 g_SMAPI->ConPrintf("MD5 Sum: %s\n", curData->checksum);
881 g_SMAPI->ConPrint("File not found!\n");
884 FetcherThread::~FetcherThread()
886 SourceHook::CVector<FileData *>::iterator iter = filenames.begin();
890 while (iter != filenames.end())
893 delete curData->filename;
895 iter = filenames.erase(iter);
899 FetcherThread::FetcherThread()
901 memtable = new BaseMemTable(4096);
903 needsRestart = false;