AutopatcherClientTest.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. /*
  2. * Copyright (c) 2014, Oculus VR, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. *
  9. */
  10. // serverIP should be ip address of patcher
  11. // pathToGame should be something like "C:\Games\mygame", whatever the installation path was
  12. // gameName should be patcherHostSubdomainURL found in AutopatcherServer_SelfScaling
  13. // patchImmediately should be 1
  14. // portToStartOn should be 0
  15. // serverPort should be 60000, see LISTEN_PORT_TCP_PATCHER in AutopatcherServer_SelfScaling
  16. // fullScan should be '1' to fully scan all files. 0 to use the last patch.
  17. // Common includes
  18. #include <stdio.h>
  19. #include <stdlib.h>
  20. #include "Kbhit.h"
  21. #include "GetTime.h"
  22. #include "RakPeerInterface.h"
  23. #include "MessageIdentifiers.h"
  24. #include "BitStream.h"
  25. #include "StringCompressor.h"
  26. #include "PacketizedTCP.h"
  27. #include "RakNetSocket2.h"
  28. // Client only includes
  29. #include "FileListTransferCBInterface.h"
  30. #include "FileListTransfer.h"
  31. #include "AutopatcherClient.h"
  32. #include "AutopatcherPatchContext.h"
  33. #include "Gets.h"
  34. #include "RakSleep.h"
  35. #include "CloudClient.h"
  36. void GetServers(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid);
  37. void GetClientSubscription(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid);
  38. void UploadInstanceToCloud(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid);
  39. #define CLOUD_CLIENT_PRIMARY_KEY "SelfScaling_Patcher_PK"
  40. class TestCB : public RakNet::AutopatcherClientCBInterface
  41. {
  42. public:
  43. virtual bool OnFile(OnFileStruct *onFileStruct)
  44. {
  45. if (onFileStruct->context.op==PC_HASH_1_WITH_PATCH || onFileStruct->context.op==PC_HASH_2_WITH_PATCH)
  46. printf("Patched: ");
  47. else if (onFileStruct->context.op==PC_WRITE_FILE)
  48. printf("Written: ");
  49. else if (onFileStruct->context.op==PC_ERROR_FILE_WRITE_FAILURE)
  50. printf("Write Failure: ");
  51. else if (onFileStruct->context.op==PC_ERROR_PATCH_TARGET_MISSING)
  52. printf("Patch target missing: ");
  53. else if (onFileStruct->context.op==PC_ERROR_PATCH_APPLICATION_FAILURE)
  54. printf("Patch process failure: ");
  55. else if (onFileStruct->context.op==PC_ERROR_PATCH_RESULT_CHECKSUM_FAILURE)
  56. printf("Patch checksum failure: ");
  57. else if (onFileStruct->context.op==PC_NOTICE_WILL_COPY_ON_RESTART)
  58. printf("Copy pending restart: ");
  59. else if (onFileStruct->context.op==PC_NOTICE_FILE_DOWNLOADED)
  60. printf("Downloaded: ");
  61. else if (onFileStruct->context.op==PC_NOTICE_FILE_DOWNLOADED_PATCH)
  62. printf("Downloaded Patch: ");
  63. else
  64. RakAssert(0);
  65. printf("%i. (100%%) %i/%i %s %ib / %ib\n", onFileStruct->setID, onFileStruct->fileIndex+1, onFileStruct->numberOfFilesInThisSet,
  66. onFileStruct->fileName, onFileStruct->byteLengthOfThisFile,
  67. onFileStruct->byteLengthOfThisSet);
  68. // Return false for the file data to be deallocated automatically
  69. return false;
  70. }
  71. virtual void OnFileProgress(FileProgressStruct *fps)
  72. {
  73. printf("Downloading: %i. (%i%%) %i/%i %s %ib/%ib %ib/%ib total\n", fps->onFileStruct->setID,
  74. (int) (100.0*(double)fps->onFileStruct->bytesDownloadedForThisFile/(double)fps->onFileStruct->byteLengthOfThisFile),
  75. fps->onFileStruct->fileIndex+1, fps->onFileStruct->numberOfFilesInThisSet, fps->onFileStruct->fileName,
  76. fps->onFileStruct->bytesDownloadedForThisFile,
  77. fps->onFileStruct->byteLengthOfThisFile,
  78. fps->onFileStruct->bytesDownloadedForThisSet,
  79. fps->onFileStruct->byteLengthOfThisSet
  80. );
  81. }
  82. virtual PatchContext ApplyPatchBase(const char *oldFilePath, char **newFileContents, unsigned int *newFileSize, char *patchContents, unsigned int patchSize, uint32_t patchAlgorithm)
  83. {
  84. if (patchAlgorithm==0)
  85. {
  86. return ApplyPatchBSDiff(oldFilePath, newFileContents, newFileSize, patchContents, patchSize);
  87. }
  88. else
  89. {
  90. char WORKING_DIRECTORY[MAX_PATH];
  91. GetTempPath(MAX_PATH, WORKING_DIRECTORY);
  92. if (WORKING_DIRECTORY[strlen(WORKING_DIRECTORY)-1]=='\\' || WORKING_DIRECTORY[strlen(WORKING_DIRECTORY)-1]=='/')
  93. WORKING_DIRECTORY[strlen(WORKING_DIRECTORY)-1]=0;
  94. char buff[128];
  95. RakNet::TimeUS time = RakNet::GetTimeUS();
  96. #if defined(_WIN32)
  97. sprintf(buff, "%I64u", time);
  98. #else
  99. sprintf(buff, "%llu", (long long unsigned int) time);
  100. #endif
  101. char pathToPatch1[MAX_PATH], pathToPatch2[MAX_PATH];
  102. sprintf(pathToPatch1, "%s/patchClient_%s.tmp", WORKING_DIRECTORY, buff);
  103. FILE *fpPatch = fopen(pathToPatch1, "wb");
  104. if (fpPatch==0)
  105. return PC_ERROR_PATCH_TARGET_MISSING;
  106. fwrite(patchContents, 1, patchSize, fpPatch);
  107. fclose(fpPatch);
  108. // Invoke xdelta
  109. // See https://code.google.com/p/xdelta/wiki/CommandLineSyntax
  110. char commandLine[512];
  111. _snprintf(commandLine, sizeof(commandLine)-1, "-d -f -s %s %s/patchClient_%s.tmp %s/newFile_%s.tmp", oldFilePath, WORKING_DIRECTORY, buff, WORKING_DIRECTORY, buff);
  112. commandLine[511]=0;
  113. SHELLEXECUTEINFO shellExecuteInfo;
  114. shellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFO);
  115. shellExecuteInfo.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE;
  116. shellExecuteInfo.hwnd = NULL;
  117. shellExecuteInfo.lpVerb = "open";
  118. shellExecuteInfo.lpFile = "xdelta3-3.0.6-win32.exe";
  119. shellExecuteInfo.lpParameters = commandLine;
  120. shellExecuteInfo.lpDirectory = NULL;
  121. shellExecuteInfo.nShow = SW_SHOWNORMAL;
  122. shellExecuteInfo.hInstApp = NULL;
  123. ShellExecuteEx(&shellExecuteInfo);
  124. // // ShellExecute is blocking, but if it writes a file to disk that file is not always immediately accessible after it returns. And this only happens in release, and only when not running in the debugger
  125. // ShellExecute(NULL, "open", "xdelta3-3.0.6-win32.exe", commandLine, NULL, SW_SHOWNORMAL);
  126. sprintf(pathToPatch2, "%s/newFile_%s.tmp", WORKING_DIRECTORY, buff);
  127. fpPatch = fopen(pathToPatch2, "r+b");
  128. RakNet::TimeUS stopWaiting = time + 60000000;
  129. while (fpPatch==0 && RakNet::GetTimeUS() < stopWaiting)
  130. {
  131. RakSleep(1000);
  132. fpPatch = fopen(pathToPatch2, "r+b");
  133. }
  134. if (fpPatch==0)
  135. {
  136. printf("\nERROR: Could not open %s.\nerr=%i (%s)\narguments=%s\n", pathToPatch2, errno, strerror(errno), commandLine);
  137. return PC_ERROR_PATCH_TARGET_MISSING;
  138. }
  139. fseek(fpPatch, 0, SEEK_END);
  140. *newFileSize = ftell(fpPatch);
  141. fseek(fpPatch, 0, SEEK_SET);
  142. *newFileContents = (char*) rakMalloc_Ex(*newFileSize, _FILE_AND_LINE_);
  143. fread(*newFileContents, 1, *newFileSize, fpPatch);
  144. fclose(fpPatch);
  145. int unlinkRes1 = _unlink(pathToPatch1);
  146. int unlinkRes2 = _unlink(pathToPatch2);
  147. while ((unlinkRes1!=0 || unlinkRes2!=0) && RakNet::GetTimeUS() < stopWaiting)
  148. {
  149. RakSleep(1000);
  150. if (unlinkRes1!=0)
  151. unlinkRes1 = _unlink(pathToPatch1);
  152. if (unlinkRes2!=0)
  153. unlinkRes2 = _unlink(pathToPatch2);
  154. }
  155. if (unlinkRes1!=0)
  156. printf("\nWARNING: unlink %s failed.\nerr=%i (%s)\n", pathToPatch1, errno, strerror(errno));
  157. if (unlinkRes2!=0)
  158. printf("\nWARNING: unlink %s failed.\nerr=%i (%s)\n", pathToPatch2, errno, strerror(errno));
  159. return PC_WRITE_FILE;
  160. }
  161. }
  162. } transferCallback;
  163. int main(int argc, char **argv)
  164. {
  165. if (argc<8)
  166. {
  167. printf("Arguments: serverIP, pathToGame, gameName, patchImmediately, localPort, serverPort, fullScan");
  168. return 0;
  169. }
  170. RakNet::SystemAddress TCPServerAddress=RakNet::UNASSIGNED_SYSTEM_ADDRESS;
  171. RakNet::AutopatcherClient autopatcherClient;
  172. RakNet::FileListTransfer fileListTransfer;
  173. RakNet::CloudClient cloudClient;
  174. autopatcherClient.SetFileListTransferPlugin(&fileListTransfer);
  175. bool didRebalance=false; // So we only reconnect to a lower load server once, for load balancing
  176. bool fullScan = argv[7][0]=='1';
  177. unsigned short localPort;
  178. localPort=atoi(argv[5]);
  179. unsigned short serverPort=atoi(argv[6]);
  180. RakNet::PacketizedTCP packetizedTCP;
  181. if (packetizedTCP.Start(localPort,1)==false)
  182. {
  183. printf("Failed to start TCP. Is the port already in use?");
  184. return 1;
  185. }
  186. packetizedTCP.AttachPlugin(&autopatcherClient);
  187. packetizedTCP.AttachPlugin(&fileListTransfer);
  188. RakNet::RakPeerInterface *rakPeer;
  189. rakPeer = RakNet::RakPeerInterface::GetInstance();
  190. RakNet::SocketDescriptor socketDescriptor(localPort,0);
  191. rakPeer->Startup(1,&socketDescriptor, 1);
  192. rakPeer->AttachPlugin(&cloudClient);
  193. DataStructures::List<RakNet::RakNetSocket2* > sockets;
  194. rakPeer->GetSockets(sockets);
  195. printf("Started on port %i\n", sockets[0]->GetBoundAddress().GetPort());
  196. char buff[512];
  197. strcpy(buff, argv[1]);
  198. rakPeer->Connect(buff, serverPort, 0, 0);
  199. printf("Connecting...\n");
  200. char appDir[512];
  201. strcpy(appDir, argv[2]);
  202. char appName[512];
  203. strcpy(appName, argv[3]);
  204. bool patchImmediately=argc>=5 && argv[4][0]=='1';
  205. RakNet::Packet *p;
  206. while (1)
  207. {
  208. RakNet::SystemAddress notificationAddress;
  209. notificationAddress=packetizedTCP.HasCompletedConnectionAttempt();
  210. if (notificationAddress!=RakNet::UNASSIGNED_SYSTEM_ADDRESS)
  211. {
  212. printf("ID_CONNECTION_REQUEST_ACCEPTED\n");
  213. TCPServerAddress=notificationAddress;
  214. }
  215. notificationAddress=packetizedTCP.HasNewIncomingConnection();
  216. if (notificationAddress!=RakNet::UNASSIGNED_SYSTEM_ADDRESS)
  217. printf("ID_NEW_INCOMING_CONNECTION\n");
  218. notificationAddress=packetizedTCP.HasLostConnection();
  219. if (notificationAddress!=RakNet::UNASSIGNED_SYSTEM_ADDRESS)
  220. printf("ID_CONNECTION_LOST\n");
  221. notificationAddress=packetizedTCP.HasFailedConnectionAttempt();
  222. if (notificationAddress!=RakNet::UNASSIGNED_SYSTEM_ADDRESS)
  223. {
  224. printf("ID_CONNECTION_ATTEMPT_FAILED TCP\n");
  225. autopatcherClient.SetFileListTransferPlugin(0);
  226. autopatcherClient.Clear();
  227. packetizedTCP.Stop();
  228. rakPeer->Shutdown(500,0);
  229. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  230. return 0;
  231. }
  232. p=packetizedTCP.Receive();
  233. while (p)
  234. {
  235. if (p->data[0]==ID_AUTOPATCHER_REPOSITORY_FATAL_ERROR)
  236. {
  237. char buff[256];
  238. RakNet::BitStream temp(p->data, p->length, false);
  239. temp.IgnoreBits(8);
  240. RakNet::StringCompressor::Instance()->DecodeString(buff, 256, &temp);
  241. printf("ID_AUTOPATCHER_REPOSITORY_FATAL_ERROR\n");
  242. printf("%s\n", buff);
  243. autopatcherClient.SetFileListTransferPlugin(0);
  244. autopatcherClient.Clear();
  245. packetizedTCP.Stop();
  246. rakPeer->Shutdown(500,0);
  247. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  248. return 0;
  249. }
  250. else if (p->data[0]==ID_AUTOPATCHER_CANNOT_DOWNLOAD_ORIGINAL_UNMODIFIED_FILES)
  251. {
  252. printf("ID_AUTOPATCHER_CANNOT_DOWNLOAD_ORIGINAL_UNMODIFIED_FILES\n");
  253. autopatcherClient.SetFileListTransferPlugin(0);
  254. autopatcherClient.Clear();
  255. packetizedTCP.Stop();
  256. rakPeer->Shutdown(500,0);
  257. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  258. return 0;
  259. }
  260. else if (p->data[0]==ID_AUTOPATCHER_FINISHED)
  261. {
  262. printf("ID_AUTOPATCHER_FINISHED with server time %f\n", autopatcherClient.GetServerDate());
  263. double srvDate=autopatcherClient.GetServerDate();
  264. FILE *fp = fopen("srvDate", "wb");
  265. fwrite(&srvDate,sizeof(double),1,fp);
  266. fclose(fp);
  267. autopatcherClient.SetFileListTransferPlugin(0);
  268. autopatcherClient.Clear();
  269. packetizedTCP.Stop();
  270. rakPeer->Shutdown(500,0);
  271. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  272. return 0;
  273. }
  274. else if (p->data[0]==ID_AUTOPATCHER_RESTART_APPLICATION)
  275. {
  276. printf("ID_AUTOPATCHER_RESTART_APPLICATION");
  277. autopatcherClient.SetFileListTransferPlugin(0);
  278. autopatcherClient.Clear();
  279. packetizedTCP.Stop();
  280. rakPeer->Shutdown(500,0);
  281. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  282. return 0;
  283. }
  284. // Launch \"AutopatcherClientRestarter.exe autopatcherRestart.txt\"\nQuit this application immediately after to unlock files.\n");
  285. packetizedTCP.DeallocatePacket(p);
  286. p=packetizedTCP.Receive();
  287. }
  288. p=rakPeer->Receive();
  289. while (p)
  290. {
  291. if (p->data[0]==ID_CONNECTION_REQUEST_ACCEPTED)
  292. {
  293. // UploadInstanceToCloud(&cloudClient, p->guid);
  294. // GetClientSubscription(&cloudClient, p->guid);
  295. GetServers(&cloudClient, p->guid);
  296. break;
  297. }
  298. else if (p->data[0]==ID_CONNECTION_ATTEMPT_FAILED)
  299. {
  300. printf("ID_CONNECTION_ATTEMPT_FAILED UDP\n");
  301. autopatcherClient.SetFileListTransferPlugin(0);
  302. autopatcherClient.Clear();
  303. packetizedTCP.Stop();
  304. rakPeer->Shutdown(500,0);
  305. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  306. return 0;
  307. }
  308. else if (p->data[0]==ID_CLOUD_GET_RESPONSE)
  309. {
  310. RakNet::CloudQueryResult cloudQueryResult;
  311. cloudClient.OnGetReponse(&cloudQueryResult, p);
  312. unsigned int rowIndex;
  313. const bool wasCallToGetServers=cloudQueryResult.cloudQuery.keys[0].primaryKey=="CloudConnCount";
  314. printf("\n");
  315. if (wasCallToGetServers)
  316. printf("Downloaded server list. %i servers.\n", cloudQueryResult.rowsReturned.Size());
  317. unsigned short connectionsOnOurServer=65535;
  318. unsigned short lowestConnectionsServer=65535;
  319. RakNet::SystemAddress lowestConnectionAddress;
  320. for (rowIndex=0; rowIndex < cloudQueryResult.rowsReturned.Size(); rowIndex++)
  321. {
  322. RakNet::CloudQueryRow *row = cloudQueryResult.rowsReturned[rowIndex];
  323. if (wasCallToGetServers)
  324. {
  325. unsigned short connCount;
  326. RakNet::BitStream bsIn(row->data, row->length, false);
  327. bsIn.Read(connCount);
  328. printf("%i. Server found at %s with %i connections\n", rowIndex+1, row->serverSystemAddress.ToString(true), connCount);
  329. unsigned short connectionsExcludingOurselves;
  330. if (row->serverGUID==p->guid)
  331. connectionsExcludingOurselves=connCount-1;
  332. else
  333. connectionsExcludingOurselves=connCount;
  334. // Find the lowest load server (optional)
  335. if (p->guid==row->serverGUID)
  336. {
  337. connectionsOnOurServer=connectionsExcludingOurselves;
  338. }
  339. else if (connectionsExcludingOurselves < lowestConnectionsServer)
  340. {
  341. lowestConnectionsServer=connectionsExcludingOurselves;
  342. lowestConnectionAddress=row->serverSystemAddress;
  343. }
  344. }
  345. }
  346. // Do load balancing by reconnecting to lowest load server (optional)
  347. if (didRebalance==false && wasCallToGetServers)
  348. {
  349. if (cloudQueryResult.rowsReturned.Size()>0 && connectionsOnOurServer>lowestConnectionsServer)
  350. {
  351. printf("Reconnecting to lower load server %s\n", lowestConnectionAddress.ToString(false));
  352. rakPeer->CloseConnection(p->guid, true);
  353. // Wait for the thread to close, otherwise will immediately get back ID_CONNECTION_ATTEMPT_FAILED because no free outgoing connection slots
  354. // Alternatively, just call Startup() with 2 slots instead of 1
  355. RakSleep(500);
  356. rakPeer->Connect(lowestConnectionAddress.ToString(false), lowestConnectionAddress.GetPort(), 0, 0);
  357. // TCP Connect to new IP address
  358. packetizedTCP.Connect(lowestConnectionAddress.ToString(false),serverPort,false);
  359. }
  360. else
  361. {
  362. // TCP Connect to original IP address
  363. packetizedTCP.Connect(buff,serverPort,false);
  364. }
  365. didRebalance=true;
  366. }
  367. cloudClient.DeallocateWithDefaultAllocator(&cloudQueryResult);
  368. }
  369. rakPeer->DeallocatePacket(p);
  370. p=rakPeer->Receive();
  371. }
  372. if (TCPServerAddress!=RakNet::UNASSIGNED_SYSTEM_ADDRESS && patchImmediately==true)
  373. {
  374. patchImmediately=false;
  375. char restartFile[512];
  376. strcpy(restartFile, appDir);
  377. strcat(restartFile, "/autopatcherRestart.txt");
  378. double lastUpdateDate;
  379. if (fullScan==false)
  380. {
  381. FILE *fp = fopen("srvDate", "rb");
  382. if (fp)
  383. {
  384. fread(&lastUpdateDate, sizeof(lastUpdateDate), 1, fp);
  385. fclose(fp);
  386. }
  387. else
  388. lastUpdateDate=0;
  389. }
  390. else
  391. lastUpdateDate=0;
  392. if (autopatcherClient.PatchApplication(appName, appDir, lastUpdateDate, TCPServerAddress, &transferCallback, restartFile, argv[0]))
  393. {
  394. printf("Patching process starting.\n");
  395. }
  396. else
  397. {
  398. printf("Failed to start patching.\n");
  399. autopatcherClient.SetFileListTransferPlugin(0);
  400. autopatcherClient.Clear();
  401. packetizedTCP.Stop();
  402. rakPeer->Shutdown(500,0);
  403. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  404. return 0;
  405. }
  406. }
  407. RakSleep(30);
  408. }
  409. // Dereference so the destructor doesn't crash
  410. autopatcherClient.SetFileListTransferPlugin(0);
  411. autopatcherClient.Clear();
  412. packetizedTCP.Stop();
  413. rakPeer->Shutdown(500,0);
  414. RakNet::RakPeerInterface::DestroyInstance(rakPeer);
  415. return 1;
  416. }
  417. void UploadInstanceToCloud(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
  418. {
  419. RakNet::CloudKey cloudKey(CLOUD_CLIENT_PRIMARY_KEY,0);
  420. RakNet::BitStream bs;
  421. bs.Write("Hello World"); // This could be anything such as player list, game name, etc.
  422. cloudClient->Post(&cloudKey, bs.GetData(), bs.GetNumberOfBytesUsed(), serverGuid);
  423. }
  424. void GetClientSubscription(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
  425. {
  426. RakNet::CloudQuery cloudQuery;
  427. cloudQuery.keys.Push(RakNet::CloudKey(CLOUD_CLIENT_PRIMARY_KEY,0),_FILE_AND_LINE_);
  428. cloudQuery.subscribeToResults=false;
  429. cloudClient->Get(&cloudQuery, serverGuid);
  430. }
  431. void GetServers(RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
  432. {
  433. RakNet::CloudQuery cloudQuery;
  434. cloudQuery.keys.Push(RakNet::CloudKey("CloudConnCount",0),_FILE_AND_LINE_); // CloudConnCount is defined at the top of CloudServerHelper.cpp
  435. cloudQuery.subscribeToResults=false;
  436. cloudClient->Get(&cloudQuery, serverGuid);
  437. }
粤ICP备19079148号