SFSClientTool.cpp (16654B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 4 #include <sfsclient/SFSClient.h> 5 6 #include <exception> 7 #include <filesystem> 8 #include <fstream> 9 #include <iomanip> 10 #include <iostream> 11 #include <sstream> 12 #include <time.h> 13 #include <vector> 14 15 #include <nlohmann/json.hpp> 16 17 using namespace SFS; 18 using json = nlohmann::json; 19 20 namespace 21 { 22 void DisplayUsage() 23 { 24 std::cout 25 << "Usage: SFSClientTool --product <identifier> --accountId <id> [options]" << std::endl 26 << std::endl 27 << "Required:" << std::endl 28 << " --product <identifier>\tName or GUID of the product to be retrieved" << std::endl 29 << " --accountId <id>\t\tAccount ID of the SFS service, used to identify the caller" << std::endl 30 << std::endl 31 << "Options:" << std::endl 32 << " -h, --help\t\t\tDisplay this help message" << std::endl 33 << " -v, --version\t\t\tDisplay the library version" << std::endl 34 << " -o, --outputFile <path>\t\tWhen specified, the JSON output is saved to this file" << std::endl 35 << " --isApp\t\t\tIndicates the specific product is an App" << std::endl 36 << " --instanceId <id>\t\tA custom SFS instance ID" << std::endl 37 << " --namespace <ns>\t\tA custom SFS namespace" << std::endl 38 << " --customUrl <url>\t\tA custom URL for the SFS service. Library must have been built with SFS_ENABLE_OVERRIDES" 39 << std::endl 40 << std::endl 41 << "Example:" << std::endl 42 << " SFSClientTool --product msedge-stable-win-x64 --accountId msedge" << std::endl; 43 } 44 45 void DisplayHelp() 46 { 47 std::cout << "SFSClient Tool" << std::endl 48 << "Copyright (c) Microsoft Corporation. All rights reserved." << std::endl 49 << std::endl 50 << "Use to interact with the SFS service through the SFS Client library." << std::endl 51 << std::endl; 52 DisplayUsage(); 53 } 54 55 void DisplayVersion() 56 { 57 std::cout << "SFSClient Tool " << SFSClient::GetVersion() << std::endl; 58 } 59 60 const std::string c_boldRedStart = "\033[1;31m"; 61 const std::string c_cyanStart = "\033[0;36m"; 62 const std::string c_darkGreyStart = "\033[0;90m"; 63 const std::string c_colorEnd = "\033[0m"; 64 65 void PrintError(std::string_view message) 66 { 67 std::cout << c_boldRedStart << message << c_colorEnd << std::endl; 68 } 69 70 void PrintLog(std::string_view message) 71 { 72 std::cout << c_cyanStart << message << c_colorEnd << std::endl; 73 } 74 75 bool AreEqualI(std::string_view a, std::string_view b) 76 { 77 return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) { 78 return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); 79 }); 80 } 81 82 struct Settings 83 { 84 bool displayHelp{true}; 85 bool displayVersion{false}; 86 bool isApp{false}; 87 std::string product; 88 std::string accountId; 89 std::string instanceId; 90 std::string nameSpace; 91 std::string customUrl; 92 std::string outputFile; 93 94 bool ShouldOutputToFile() const 95 { 96 return !outputFile.empty(); 97 } 98 }; 99 100 void ParseArguments(const std::vector<std::string_view>& args, Settings& settings) 101 { 102 settings = {}; 103 settings.displayHelp = args.size() == 1; 104 105 const size_t argsSize = args.size(); 106 auto validateArg = 107 [&argsSize](const size_t index, const std::string& switchName, const std::string_view& argValue) -> void { 108 if (argsSize <= index + 1) 109 { 110 throw std::runtime_error("Missing argument of --" + switchName); 111 } 112 if (!argValue.empty()) 113 { 114 throw std::runtime_error("--" + switchName + " can only be specified once"); 115 } 116 }; 117 118 auto matchArg = 119 [](std::string_view arg, const std::string& shortSwitchName, const std::string& longSwitchName) -> bool { 120 return (!shortSwitchName.empty() && AreEqualI(arg, shortSwitchName)) || AreEqualI(arg, longSwitchName); 121 }; 122 123 auto matchLongArg = [](std::string_view arg, const std::string& longSwitchName) -> bool { 124 return AreEqualI(arg, longSwitchName); 125 }; 126 127 for (size_t i = 1; i < args.size(); ++i) 128 { 129 if (matchArg(args[i], "-h", "--help")) 130 { 131 settings.displayHelp = true; 132 } 133 else if (matchArg(args[i], "-v", "--version")) 134 { 135 settings.displayVersion = true; 136 } 137 else if (matchArg(args[i], "-o", "--outputFile")) 138 { 139 validateArg(i, "outputFile", settings.outputFile); 140 settings.outputFile = args[++i]; 141 } 142 else if (matchLongArg(args[i], "--isApp")) 143 { 144 settings.isApp = true; 145 } 146 else if (matchLongArg(args[i], "--product")) 147 { 148 validateArg(i, "product", settings.product); 149 settings.product = args[++i]; 150 } 151 else if (matchLongArg(args[i], "--accountId")) 152 { 153 validateArg(i, "accountId", settings.accountId); 154 settings.accountId = args[++i]; 155 } 156 else if (matchLongArg(args[i], "--instanceId")) 157 { 158 validateArg(i, "instanceId", settings.instanceId); 159 settings.instanceId = args[++i]; 160 } 161 else if (matchLongArg(args[i], "--namespace")) 162 { 163 validateArg(i, "namespace", settings.nameSpace); 164 settings.nameSpace = args[++i]; 165 } 166 else if (matchLongArg(args[i], "--customUrl")) 167 { 168 validateArg(i, "customUrl", settings.customUrl); 169 settings.customUrl = args[++i]; 170 } 171 else 172 { 173 throw std::runtime_error("Unknown option " + std::string(args[i])); 174 } 175 } 176 177 if (!settings.displayHelp && (settings.product.empty() || settings.accountId.empty())) 178 { 179 throw std::runtime_error("--product and --accountId are required and cannot be empty"); 180 } 181 } 182 183 constexpr std::string_view ToString(HashType type) 184 { 185 switch (type) 186 { 187 case HashType::Sha1: 188 return "Sha1"; 189 case HashType::Sha256: 190 return "Sha256"; 191 } 192 return ""; 193 } 194 195 constexpr std::string_view ToString(Architecture type) 196 { 197 switch (type) 198 { 199 case Architecture::None: 200 return "None"; 201 case Architecture::Amd64: 202 return "amd64"; 203 case Architecture::Arm: 204 return "arm"; 205 case Architecture::Arm64: 206 return "arm64"; 207 case Architecture::x86: 208 return "x86"; 209 } 210 return ""; 211 } 212 213 json ContentsToJson(const std::vector<Content>& contents) 214 { 215 json out = json::array(); 216 for (const auto& content : contents) 217 { 218 json j = json::object(); 219 j["ContentId"]["Namespace"] = content.GetContentId().GetNameSpace(); 220 j["ContentId"]["Name"] = content.GetContentId().GetName(); 221 j["ContentId"]["Version"] = content.GetContentId().GetVersion(); 222 223 j["Files"] = json::array(); 224 for (const auto& file : content.GetFiles()) 225 { 226 json fileJson = json::object(); 227 fileJson["FileId"] = file.GetFileId(); 228 fileJson["Url"] = file.GetUrl(); 229 fileJson["SizeInBytes"] = file.GetSizeInBytes(); 230 json hashes = json::object(); 231 for (const auto& hash : file.GetHashes()) 232 { 233 hashes[ToString(hash.first)] = hash.second; 234 } 235 fileJson["Hashes"] = hashes; 236 j["Files"].push_back(fileJson); 237 } 238 out.push_back(j); 239 } 240 241 return out; 242 } 243 244 json AppFileToJson(const AppFile& file) 245 { 246 json fileJson = json::object(); 247 fileJson["FileId"] = file.GetFileId(); 248 fileJson["Url"] = file.GetUrl(); 249 fileJson["SizeInBytes"] = file.GetSizeInBytes(); 250 fileJson["FileMoniker"] = file.GetFileMoniker(); 251 json hashes = json::object(); 252 for (const auto& hash : file.GetHashes()) 253 { 254 hashes[ToString(hash.first)] = hash.second; 255 } 256 fileJson["Hashes"] = hashes; 257 258 fileJson["ApplicabilityDetails"] = json::object(); 259 fileJson["ApplicabilityDetails"]["Architectures"] = json::array(); 260 for (const auto& arch : file.GetApplicabilityDetails().GetArchitectures()) 261 { 262 fileJson["ApplicabilityDetails"]["Architectures"].push_back(ToString(arch)); 263 } 264 fileJson["ApplicabilityDetails"]["PlatformApplicabilityForPackage"] = json::array(); 265 for (const auto& app : file.GetApplicabilityDetails().GetPlatformApplicabilityForPackage()) 266 { 267 fileJson["ApplicabilityDetails"]["PlatformApplicabilityForPackage"].push_back(app); 268 } 269 270 return fileJson; 271 } 272 273 json AppContentsToJson(const std::vector<AppContent>& contents) 274 { 275 json out = json::array(); 276 for (const auto& content : contents) 277 { 278 json j = json::object(); 279 j["ContentId"]["Namespace"] = content.GetContentId().GetNameSpace(); 280 j["ContentId"]["Name"] = content.GetContentId().GetName(); 281 j["ContentId"]["Version"] = content.GetContentId().GetVersion(); 282 j["UpdateId"] = content.GetUpdateId(); 283 284 j["Files"] = json::array(); 285 for (const auto& file : content.GetFiles()) 286 { 287 j["Files"].push_back(AppFileToJson(file)); 288 } 289 290 j["Prerequisites"] = json::array(); 291 for (const auto& prereq : content.GetPrerequisites()) 292 { 293 json prereqJson = json::object(); 294 prereqJson["ContentId"]["Namespace"] = prereq.GetContentId().GetNameSpace(); 295 prereqJson["ContentId"]["Name"] = prereq.GetContentId().GetName(); 296 prereqJson["ContentId"]["Version"] = prereq.GetContentId().GetVersion(); 297 298 prereqJson["Files"] = json::array(); 299 for (const auto& file : prereq.GetFiles()) 300 { 301 prereqJson["Files"].push_back(AppFileToJson(file)); 302 } 303 304 j["Prerequisites"].push_back(prereqJson); 305 } 306 out.push_back(j); 307 } 308 309 return out; 310 } 311 312 void DisplayResults(const json& results) 313 { 314 if (results.empty()) 315 { 316 return; 317 } 318 319 PrintLog("Content found:"); 320 PrintLog(results.dump(2 /*indent*/)); 321 } 322 323 Result JSONToFile(const json& results, const std::string& filename) 324 { 325 if (results.empty()) 326 { 327 return Result::Unexpected; 328 } 329 330 const std::filesystem::path filepath = std::filesystem::absolute(filename); 331 try 332 { 333 std::filesystem::create_directories(filepath.parent_path()); 334 } 335 catch (const std::exception& e) 336 { 337 PrintError("Failed to create parent directories for filepath: " + filepath.string() + ". Error: " + e.what()); 338 return Result::Unexpected; 339 } 340 341 std::ofstream file(filepath, std::ios::out | std::ios::trunc); 342 if (!file.is_open()) 343 { 344 PrintError("Failed to open file for writing: " + filename); 345 return Result::Unexpected; 346 } 347 348 file << results.dump(2 /*indent*/); 349 350 PrintLog("Content found. Saved to file " + filepath.string()); 351 352 return Result::Success; 353 } 354 355 void LogResult(const SFS::Result& result) 356 { 357 std::cout << " Result code: " << ToString(result.GetCode()); 358 if (!result.GetMsg().empty()) 359 { 360 std::cout << ". Message: " << result.GetMsg(); 361 } 362 std::cout << std::endl; 363 } 364 365 std::string TimestampToString(std::chrono::time_point<std::chrono::system_clock> time) 366 { 367 using namespace std::chrono; 368 369 // get number of milliseconds for the current second 370 // (remainder after division into seconds) 371 auto ms = duration_cast<milliseconds>(time.time_since_epoch()) % 1000; 372 373 auto timer = system_clock::to_time_t(time); 374 375 std::stringstream timeStream; 376 struct tm gmTime; 377 #ifdef _WIN32 378 gmtime_s(&gmTime, &timer); // gmtime_s is the safe version of gmtime, not available on Linux 379 #else 380 gmTime = (*std::gmtime(&timer)); 381 #endif 382 timeStream << std::put_time(&gmTime, "%F %X"); // yyyy-mm-dd HH:MM:SS 383 timeStream << '.' << std::setfill('0') << std::setw(3) << ms.count(); 384 385 return timeStream.str(); 386 } 387 388 void LoggingCallback(const SFS::LogData& logData) 389 { 390 std::cout << c_darkGreyStart << "Log: " << TimestampToString(logData.time) << " [" << ToString(logData.severity) 391 << "]" 392 << " " << std::filesystem::path(logData.file).filename().string() << ":" << logData.line << " " 393 << logData.message << c_colorEnd << std::endl; 394 } 395 396 bool SetEnv(const std::string& varName, const std::string& value) 397 { 398 if (varName.empty() || value.empty()) 399 { 400 return false; 401 } 402 #ifdef _WIN32 403 return _putenv_s(varName.c_str(), value.c_str()) == 0; 404 #else 405 return setenv(varName.c_str(), value.c_str(), 1 /*overwrite*/) == 0; 406 #endif 407 } 408 409 Result GetLatestDownloadInfo(const SFSClient& sfsClient, const Settings& settings, json& out) 410 { 411 PrintLog("Getting latest download info for product: " + settings.product); 412 RequestParams params; 413 params.productRequests = {{settings.product, {}}}; 414 415 if (settings.isApp) 416 { 417 std::vector<AppContent> appContents; 418 auto result = sfsClient.GetLatestAppDownloadInfo(params, appContents); 419 if (!result) 420 { 421 PrintError("Failed to get latest download info for app."); 422 LogResult(result); 423 return result.GetCode(); 424 } 425 426 out = AppContentsToJson(appContents); 427 } 428 else 429 { 430 std::vector<Content> contents; 431 auto result = sfsClient.GetLatestDownloadInfo(params, contents); 432 if (!result) 433 { 434 PrintError("Failed to get latest download info."); 435 LogResult(result); 436 return result.GetCode(); 437 } 438 439 out = ContentsToJson(contents); 440 } 441 442 if (out.empty()) 443 { 444 PrintError("No results found."); 445 return Result::Unexpected; 446 } 447 448 return Result::Success; 449 } 450 451 Result HandleJSONContents(const Settings& settings, const json& contents) 452 { 453 if (settings.ShouldOutputToFile()) 454 { 455 auto result = JSONToFile(contents, settings.outputFile); 456 if (!result) 457 { 458 PrintError("Failed to save to file."); 459 return result.GetCode(); 460 } 461 } 462 else 463 { 464 DisplayResults(contents); 465 return Result::Success; 466 } 467 468 return Result::Success; 469 } 470 } // namespace 471 472 int main(int argc, char* argv[]) 473 { 474 Settings settings; 475 try 476 { 477 ParseArguments(std::vector<std::string_view>(argv, argv + argc), settings); 478 } 479 catch (const std::runtime_error& e) 480 { 481 PrintError(std::string(e.what()) + "\n"); 482 DisplayUsage(); 483 return 1; 484 } 485 486 if (settings.displayVersion) 487 { 488 DisplayVersion(); 489 return 0; 490 } 491 492 if (settings.displayHelp) 493 { 494 DisplayHelp(); 495 return 0; 496 } 497 498 if (!settings.customUrl.empty()) 499 { 500 SetEnv("SFS_TEST_OVERRIDE_BASE_URL", settings.customUrl); 501 PrintLog("Using custom URL: " + settings.customUrl); 502 PrintLog("Note that the library must have been built with SFS_ENABLE_OVERRIDES to use a custom URL."); 503 } 504 505 // Initialize SFSClient 506 PrintLog("Initializing SFSClient with accountId: " + settings.accountId + 507 (settings.instanceId.empty() ? "" : ", instanceId: " + settings.instanceId) + 508 (settings.nameSpace.empty() ? "" : ", namespace: " + settings.nameSpace)); 509 510 ClientConfig config; 511 config.accountId = settings.accountId; 512 if (!settings.instanceId.empty()) 513 { 514 config.instanceId = settings.instanceId; 515 } 516 if (!settings.nameSpace.empty()) 517 { 518 config.nameSpace = settings.nameSpace; 519 } 520 config.logCallbackFn = LoggingCallback; 521 522 std::unique_ptr<SFSClient> sfsClient; 523 auto result = SFSClient::Make(config, sfsClient); 524 if (!result) 525 { 526 PrintError("Failed to initialize SFSClient."); 527 LogResult(result); 528 return result.GetCode(); 529 } 530 531 // Perform operations using SFSClient 532 json contents; 533 result = GetLatestDownloadInfo(*sfsClient, settings, contents); 534 if (!result) 535 { 536 LogResult(result); 537 return result.GetCode(); 538 } 539 540 result = HandleJSONContents(settings, contents); 541 if (!result) 542 { 543 LogResult(result); 544 return result.GetCode(); 545 } 546 547 return 0; 548 }