winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }