cmFileAPI.cxx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. file Copyright.txt or https://cmake.org/licensing for details. */
  3. #include "cmFileAPI.h"
  4. #include "cmAlgorithms.h"
  5. #include "cmCryptoHash.h"
  6. #include "cmSystemTools.h"
  7. #include "cmTimestamp.h"
  8. #include "cmake.h"
  9. #include "cmsys/Directory.hxx"
  10. #include "cmsys/FStream.hxx"
  11. #include <algorithm>
  12. #include <cassert>
  13. #include <chrono>
  14. #include <ctime>
  15. #include <iomanip>
  16. #include <sstream>
  17. #include <utility>
  18. cmFileAPI::cmFileAPI(cmake* cm)
  19. : CMakeInstance(cm)
  20. {
  21. this->APIv1 =
  22. this->CMakeInstance->GetHomeOutputDirectory() + "/.cmake/api/v1";
  23. Json::CharReaderBuilder rbuilder;
  24. rbuilder["collectComments"] = false;
  25. rbuilder["failIfExtra"] = true;
  26. rbuilder["rejectDupKeys"] = false;
  27. rbuilder["strictRoot"] = true;
  28. this->JsonReader =
  29. std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
  30. Json::StreamWriterBuilder wbuilder;
  31. wbuilder["indentation"] = "\t";
  32. this->JsonWriter =
  33. std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
  34. }
  35. void cmFileAPI::ReadQueries()
  36. {
  37. std::string const query_dir = this->APIv1 + "/query";
  38. this->QueryExists = cmSystemTools::FileIsDirectory(query_dir);
  39. if (!this->QueryExists) {
  40. return;
  41. }
  42. // Load queries at the top level.
  43. std::vector<std::string> queries = cmFileAPI::LoadDir(query_dir);
  44. // Read the queries and save for later.
  45. for (std::string& query : queries) {
  46. if (cmHasLiteralPrefix(query, "client-")) {
  47. this->ReadClient(query);
  48. } else if (!cmFileAPI::ReadQuery(query, this->TopQuery.Known)) {
  49. this->TopQuery.Unknown.push_back(std::move(query));
  50. }
  51. }
  52. }
  53. void cmFileAPI::WriteReplies()
  54. {
  55. if (this->QueryExists) {
  56. cmSystemTools::MakeDirectory(this->APIv1 + "/reply");
  57. this->WriteJsonFile(this->BuildReplyIndex(), "index", ComputeSuffixTime);
  58. }
  59. this->RemoveOldReplyFiles();
  60. }
  61. std::vector<std::string> cmFileAPI::LoadDir(std::string const& dir)
  62. {
  63. std::vector<std::string> files;
  64. cmsys::Directory d;
  65. d.Load(dir);
  66. for (unsigned long i = 0; i < d.GetNumberOfFiles(); ++i) {
  67. std::string f = d.GetFile(i);
  68. if (f != "." && f != "..") {
  69. files.push_back(std::move(f));
  70. }
  71. }
  72. std::sort(files.begin(), files.end());
  73. return files;
  74. }
  75. void cmFileAPI::RemoveOldReplyFiles()
  76. {
  77. std::string const reply_dir = this->APIv1 + "/reply";
  78. std::vector<std::string> files = this->LoadDir(reply_dir);
  79. for (std::string const& f : files) {
  80. if (this->ReplyFiles.find(f) == this->ReplyFiles.end()) {
  81. std::string file = reply_dir + "/" + f;
  82. cmSystemTools::RemoveFile(file);
  83. }
  84. }
  85. }
  86. bool cmFileAPI::ReadJsonFile(std::string const& file, Json::Value& value,
  87. std::string& error)
  88. {
  89. std::vector<char> content;
  90. cmsys::ifstream fin;
  91. if (!cmSystemTools::FileIsDirectory(file)) {
  92. fin.open(file.c_str(), std::ios::binary);
  93. }
  94. auto finEnd = fin.rdbuf()->pubseekoff(0, std::ios::end);
  95. if (finEnd > 0) {
  96. size_t finSize = finEnd;
  97. try {
  98. // Allocate a buffer to read the whole file.
  99. content.resize(finSize);
  100. // Now read the file from the beginning.
  101. fin.seekg(0, std::ios::beg);
  102. fin.read(content.data(), finSize);
  103. } catch (...) {
  104. fin.setstate(std::ios::failbit);
  105. }
  106. }
  107. fin.close();
  108. if (!fin) {
  109. value = Json::Value();
  110. error = "failed to read from file";
  111. return false;
  112. }
  113. // Parse our buffer as json.
  114. if (!this->JsonReader->parse(content.data(), content.data() + content.size(),
  115. &value, &error)) {
  116. value = Json::Value();
  117. return false;
  118. }
  119. return true;
  120. }
  121. std::string cmFileAPI::WriteJsonFile(
  122. Json::Value const& value, std::string const& prefix,
  123. std::string (*computeSuffix)(std::string const&))
  124. {
  125. std::string fileName;
  126. // Write the json file with a temporary name.
  127. std::string const& tmpFile = this->APIv1 + "/tmp.json";
  128. cmsys::ofstream ftmp(tmpFile.c_str());
  129. this->JsonWriter->write(value, &ftmp);
  130. ftmp << "\n";
  131. ftmp.close();
  132. if (!ftmp) {
  133. cmSystemTools::RemoveFile(tmpFile);
  134. return fileName;
  135. }
  136. // Compute the final name for the file.
  137. fileName = prefix + "-" + computeSuffix(tmpFile) + ".json";
  138. // Create the destination.
  139. std::string file = this->APIv1 + "/reply";
  140. cmSystemTools::MakeDirectory(file);
  141. file += "/";
  142. file += fileName;
  143. // If the final name already exists then assume it has proper content.
  144. // Otherwise, atomically place the reply file at its final name
  145. if (cmSystemTools::FileExists(file, true) ||
  146. !cmSystemTools::RenameFile(tmpFile.c_str(), file.c_str())) {
  147. cmSystemTools::RemoveFile(tmpFile);
  148. }
  149. // Record this among files we have just written.
  150. this->ReplyFiles.insert(fileName);
  151. return fileName;
  152. }
  153. Json::Value cmFileAPI::MaybeJsonFile(Json::Value in, std::string const& prefix)
  154. {
  155. Json::Value out;
  156. if (in.isObject() || in.isArray()) {
  157. out = Json::objectValue;
  158. out["jsonFile"] = this->WriteJsonFile(in, prefix);
  159. } else {
  160. out = std::move(in);
  161. }
  162. return out;
  163. }
  164. std::string cmFileAPI::ComputeSuffixHash(std::string const& file)
  165. {
  166. cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
  167. std::string hash = hasher.HashFile(file);
  168. hash.resize(20, '0');
  169. return hash;
  170. }
  171. std::string cmFileAPI::ComputeSuffixTime(std::string const&)
  172. {
  173. std::chrono::milliseconds ms =
  174. std::chrono::duration_cast<std::chrono::milliseconds>(
  175. std::chrono::system_clock::now().time_since_epoch());
  176. std::chrono::seconds s =
  177. std::chrono::duration_cast<std::chrono::seconds>(ms);
  178. std::time_t ts = s.count();
  179. std::size_t tms = ms.count() % 1000;
  180. cmTimestamp cmts;
  181. std::ostringstream ss;
  182. ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
  183. << std::setfill('0') << std::setw(4) << tms;
  184. return ss.str();
  185. }
  186. bool cmFileAPI::ReadQuery(std::string const& query,
  187. std::vector<Object>& objects)
  188. {
  189. // Parse the "<kind>-" syntax.
  190. std::string::size_type sep_pos = query.find('-');
  191. if (sep_pos == std::string::npos) {
  192. return false;
  193. }
  194. std::string kindName = query.substr(0, sep_pos);
  195. std::string verStr = query.substr(sep_pos + 1);
  196. if (kindName == ObjectKindName(ObjectKind::InternalTest)) {
  197. Object o;
  198. o.Kind = ObjectKind::InternalTest;
  199. if (verStr == "v1") {
  200. o.Version = 1;
  201. } else if (verStr == "v2") {
  202. o.Version = 2;
  203. } else {
  204. return false;
  205. }
  206. objects.push_back(o);
  207. return true;
  208. }
  209. return false;
  210. }
  211. void cmFileAPI::ReadClient(std::string const& client)
  212. {
  213. // Load queries for the client.
  214. std::string clientDir = this->APIv1 + "/query/" + client;
  215. std::vector<std::string> queries = this->LoadDir(clientDir);
  216. // Read the queries and save for later.
  217. ClientQuery& clientQuery = this->ClientQueries[client];
  218. for (std::string& query : queries) {
  219. if (query == "query.json") {
  220. clientQuery.HaveQueryJson = true;
  221. this->ReadClientQuery(client, clientQuery.QueryJson);
  222. } else if (!this->ReadQuery(query, clientQuery.DirQuery.Known)) {
  223. clientQuery.DirQuery.Unknown.push_back(std::move(query));
  224. }
  225. }
  226. }
  227. void cmFileAPI::ReadClientQuery(std::string const& client, ClientQueryJson& q)
  228. {
  229. // Read the query.json file.
  230. std::string queryFile = this->APIv1 + "/query/" + client + "/query.json";
  231. Json::Value query;
  232. if (!this->ReadJsonFile(queryFile, query, q.Error)) {
  233. return;
  234. }
  235. if (!query.isObject()) {
  236. q.Error = "query root is not an object";
  237. return;
  238. }
  239. Json::Value const& clientValue = query["client"];
  240. if (!clientValue.isNull()) {
  241. q.ClientValue = clientValue;
  242. }
  243. q.RequestsValue = std::move(query["requests"]);
  244. q.Requests = this->BuildClientRequests(q.RequestsValue);
  245. }
  246. Json::Value cmFileAPI::BuildReplyIndex()
  247. {
  248. Json::Value index(Json::objectValue);
  249. // Report information about this version of CMake.
  250. index["cmake"] = this->BuildCMake();
  251. // Reply to all queries that we loaded.
  252. Json::Value& reply = index["reply"] = this->BuildReply(this->TopQuery);
  253. for (auto const& client : this->ClientQueries) {
  254. std::string const& clientName = client.first;
  255. ClientQuery const& clientQuery = client.second;
  256. reply[clientName] = this->BuildClientReply(clientQuery);
  257. }
  258. // Move our index of generated objects into its field.
  259. Json::Value& objects = index["objects"] = Json::arrayValue;
  260. for (auto& entry : this->ReplyIndexObjects) {
  261. objects.append(std::move(entry.second)); // NOLINT(*)
  262. }
  263. return index;
  264. }
  265. Json::Value cmFileAPI::BuildCMake()
  266. {
  267. Json::Value cmake = Json::objectValue;
  268. cmake["version"] = this->CMakeInstance->ReportVersionJson();
  269. Json::Value& cmake_paths = cmake["paths"] = Json::objectValue;
  270. cmake_paths["cmake"] = cmSystemTools::GetCMakeCommand();
  271. cmake_paths["ctest"] = cmSystemTools::GetCTestCommand();
  272. cmake_paths["cpack"] = cmSystemTools::GetCPackCommand();
  273. cmake_paths["root"] = cmSystemTools::GetCMakeRoot();
  274. return cmake;
  275. }
  276. Json::Value cmFileAPI::BuildReply(Query const& q)
  277. {
  278. Json::Value reply = Json::objectValue;
  279. for (Object const& o : q.Known) {
  280. std::string const& name = ObjectName(o);
  281. reply[name] = this->AddReplyIndexObject(o);
  282. }
  283. for (std::string const& name : q.Unknown) {
  284. reply[name] = cmFileAPI::BuildReplyError("unknown query file");
  285. }
  286. return reply;
  287. }
  288. Json::Value cmFileAPI::BuildReplyError(std::string const& error)
  289. {
  290. Json::Value e = Json::objectValue;
  291. e["error"] = error;
  292. return e;
  293. }
  294. Json::Value const& cmFileAPI::AddReplyIndexObject(Object const& o)
  295. {
  296. Json::Value& indexEntry = this->ReplyIndexObjects[o];
  297. if (!indexEntry.isNull()) {
  298. // The reply object has already been generated.
  299. return indexEntry;
  300. }
  301. // Generate this reply object.
  302. Json::Value const& object = this->BuildObject(o);
  303. assert(object.isObject());
  304. // Populate this index entry.
  305. indexEntry = Json::objectValue;
  306. indexEntry["kind"] = object["kind"];
  307. indexEntry["version"] = object["version"];
  308. indexEntry["jsonFile"] = this->WriteJsonFile(object, ObjectName(o));
  309. return indexEntry;
  310. }
  311. const char* cmFileAPI::ObjectKindName(ObjectKind kind)
  312. {
  313. // Keep in sync with ObjectKind enum.
  314. static const char* objectKindNames[] = {
  315. "__test" //
  316. };
  317. return objectKindNames[size_t(kind)];
  318. }
  319. std::string cmFileAPI::ObjectName(Object const& o)
  320. {
  321. std::string name = ObjectKindName(o.Kind);
  322. name += "-v";
  323. name += std::to_string(o.Version);
  324. return name;
  325. }
  326. Json::Value cmFileAPI::BuildObject(Object const& object)
  327. {
  328. Json::Value value;
  329. switch (object.Kind) {
  330. case ObjectKind::InternalTest:
  331. value = this->BuildInternalTest(object);
  332. break;
  333. }
  334. return value;
  335. }
  336. cmFileAPI::ClientRequests cmFileAPI::BuildClientRequests(
  337. Json::Value const& requests)
  338. {
  339. ClientRequests result;
  340. if (requests.isNull()) {
  341. result.Error = "'requests' member missing";
  342. return result;
  343. }
  344. if (!requests.isArray()) {
  345. result.Error = "'requests' member is not an array";
  346. return result;
  347. }
  348. result.reserve(requests.size());
  349. for (Json::Value const& request : requests) {
  350. result.emplace_back(this->BuildClientRequest(request));
  351. }
  352. return result;
  353. }
  354. cmFileAPI::ClientRequest cmFileAPI::BuildClientRequest(
  355. Json::Value const& request)
  356. {
  357. ClientRequest r;
  358. if (!request.isObject()) {
  359. r.Error = "request is not an object";
  360. return r;
  361. }
  362. Json::Value const& kind = request["kind"];
  363. if (kind.isNull()) {
  364. r.Error = "'kind' member missing";
  365. return r;
  366. }
  367. if (!kind.isString()) {
  368. r.Error = "'kind' member is not a string";
  369. return r;
  370. }
  371. std::string const& kindName = kind.asString();
  372. if (kindName == this->ObjectKindName(ObjectKind::InternalTest)) {
  373. r.Kind = ObjectKind::InternalTest;
  374. } else {
  375. r.Error = "unknown request kind '" + kindName + "'";
  376. return r;
  377. }
  378. Json::Value const& version = request["version"];
  379. if (version.isNull()) {
  380. r.Error = "'version' member missing";
  381. return r;
  382. }
  383. std::vector<RequestVersion> versions;
  384. if (!cmFileAPI::ReadRequestVersions(version, versions, r.Error)) {
  385. return r;
  386. }
  387. switch (r.Kind) {
  388. case ObjectKind::InternalTest:
  389. this->BuildClientRequestInternalTest(r, versions);
  390. break;
  391. }
  392. return r;
  393. }
  394. Json::Value cmFileAPI::BuildClientReply(ClientQuery const& q)
  395. {
  396. Json::Value reply = this->BuildReply(q.DirQuery);
  397. if (!q.HaveQueryJson) {
  398. return reply;
  399. }
  400. Json::Value& reply_query_json = reply["query.json"];
  401. ClientQueryJson const& qj = q.QueryJson;
  402. if (!qj.Error.empty()) {
  403. reply_query_json = this->BuildReplyError(qj.Error);
  404. return reply;
  405. }
  406. if (!qj.ClientValue.isNull()) {
  407. reply_query_json["client"] = qj.ClientValue;
  408. }
  409. if (!qj.RequestsValue.isNull()) {
  410. reply_query_json["requests"] = qj.RequestsValue;
  411. }
  412. reply_query_json["responses"] = this->BuildClientReplyResponses(qj.Requests);
  413. return reply;
  414. }
  415. Json::Value cmFileAPI::BuildClientReplyResponses(
  416. ClientRequests const& requests)
  417. {
  418. Json::Value responses;
  419. if (!requests.Error.empty()) {
  420. responses = this->BuildReplyError(requests.Error);
  421. return responses;
  422. }
  423. responses = Json::arrayValue;
  424. for (ClientRequest const& request : requests) {
  425. responses.append(this->BuildClientReplyResponse(request));
  426. }
  427. return responses;
  428. }
  429. Json::Value cmFileAPI::BuildClientReplyResponse(ClientRequest const& request)
  430. {
  431. Json::Value response;
  432. if (!request.Error.empty()) {
  433. response = this->BuildReplyError(request.Error);
  434. return response;
  435. }
  436. response = this->AddReplyIndexObject(request);
  437. return response;
  438. }
  439. bool cmFileAPI::ReadRequestVersions(Json::Value const& version,
  440. std::vector<RequestVersion>& versions,
  441. std::string& error)
  442. {
  443. if (version.isArray()) {
  444. for (Json::Value const& v : version) {
  445. if (!ReadRequestVersion(v, /*inArray=*/true, versions, error)) {
  446. return false;
  447. }
  448. }
  449. } else {
  450. if (!ReadRequestVersion(version, /*inArray=*/false, versions, error)) {
  451. return false;
  452. }
  453. }
  454. return true;
  455. }
  456. bool cmFileAPI::ReadRequestVersion(Json::Value const& version, bool inArray,
  457. std::vector<RequestVersion>& result,
  458. std::string& error)
  459. {
  460. if (version.isUInt()) {
  461. RequestVersion v;
  462. v.Major = version.asUInt();
  463. result.push_back(v);
  464. return true;
  465. }
  466. if (!version.isObject()) {
  467. if (inArray) {
  468. error = "'version' array entry is not a non-negative integer or object";
  469. } else {
  470. error =
  471. "'version' member is not a non-negative integer, object, or array";
  472. }
  473. return false;
  474. }
  475. Json::Value const& major = version["major"];
  476. if (major.isNull()) {
  477. error = "'version' object 'major' member missing";
  478. return false;
  479. }
  480. if (!major.isUInt()) {
  481. error = "'version' object 'major' member is not a non-negative integer";
  482. return false;
  483. }
  484. RequestVersion v;
  485. v.Major = major.asUInt();
  486. Json::Value const& minor = version["minor"];
  487. if (minor.isUInt()) {
  488. v.Minor = minor.asUInt();
  489. } else if (!minor.isNull()) {
  490. error = "'version' object 'minor' member is not a non-negative integer";
  491. return false;
  492. }
  493. result.push_back(v);
  494. return true;
  495. }
  496. std::string cmFileAPI::NoSupportedVersion(
  497. std::vector<RequestVersion> const& versions)
  498. {
  499. std::ostringstream msg;
  500. msg << "no supported version specified";
  501. if (!versions.empty()) {
  502. msg << " among:";
  503. for (RequestVersion const& v : versions) {
  504. msg << " " << v.Major << "." << v.Minor;
  505. }
  506. }
  507. return msg.str();
  508. }
  509. // The "__test" object kind is for internal testing of CMake.
  510. static unsigned int const InternalTestV1Minor = 3;
  511. static unsigned int const InternalTestV2Minor = 0;
  512. void cmFileAPI::BuildClientRequestInternalTest(
  513. ClientRequest& r, std::vector<RequestVersion> const& versions)
  514. {
  515. // Select a known version from those requested.
  516. for (RequestVersion const& v : versions) {
  517. if ((v.Major == 1 && v.Minor <= InternalTestV1Minor) || //
  518. (v.Major == 2 && v.Minor <= InternalTestV2Minor)) {
  519. r.Version = v.Major;
  520. break;
  521. }
  522. }
  523. if (!r.Version) {
  524. r.Error = NoSupportedVersion(versions);
  525. }
  526. }
  527. Json::Value cmFileAPI::BuildInternalTest(Object const& object)
  528. {
  529. Json::Value test = Json::objectValue;
  530. test["kind"] = this->ObjectKindName(object.Kind);
  531. Json::Value& version = test["version"] = Json::objectValue;
  532. if (object.Version == 2) {
  533. version["major"] = 2;
  534. version["minor"] = InternalTestV2Minor;
  535. } else {
  536. version["major"] = 1;
  537. version["minor"] = InternalTestV1Minor;
  538. }
  539. return test;
  540. }