DscUserSettingsFileResource.cpp (9704B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "DscUserSettingsFileResource.h" 5 #include "DscComposableObject.h" 6 #include "Resources.h" 7 #include "AppInstallerStrings.h" 8 9 using namespace AppInstaller::Utility::literals; 10 using namespace AppInstaller::Settings; 11 12 #define ACTION_FULL "Full" 13 #define ACTION_PARTIAL "Partial" 14 15 namespace AppInstaller::CLI 16 { 17 namespace 18 { 19 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(SettingsProperty, Json::Value, Settings, "settings", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionUserSettingsFileSettings); 20 WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(ActionProperty, std::string, Action, "action", Resource::String::DscResourcePropertyDescriptionUserSettingsFileAction, ({ ACTION_PARTIAL, ACTION_FULL }), ACTION_PARTIAL); 21 22 using UserSettingsFileResourceObject = DscComposableObject<StandardInDesiredStateProperty, SettingsProperty, ActionProperty>; 23 24 struct UserSettingsFileFunctionData 25 { 26 UserSettingsFileFunctionData() 27 : UserSettingsFileFunctionData(std::nullopt, true) 28 { 29 } 30 31 UserSettingsFileFunctionData(const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) : 32 Input(json, ignoreFieldRequirements), 33 _userSettingsPath(UserSettings::SettingsFilePath()) 34 { 35 } 36 37 const UserSettingsFileResourceObject Input; 38 UserSettingsFileResourceObject Output; 39 40 void Get() 41 { 42 Output.Settings(GetUserSettings()); 43 } 44 45 bool Test() 46 { 47 return GetResolvedInput() == Output.Settings(); 48 } 49 50 Json::Value DiffJson() 51 { 52 Json::Value result{ Json::ValueType::arrayValue }; 53 54 if (!Test()) 55 { 56 result.append(std::string{ SettingsProperty::Name() }); 57 } 58 59 return result; 60 } 61 62 const Json::Value& GetResolvedInput() 63 { 64 THROW_HR_IF(E_UNEXPECTED, !Input.Settings().has_value()); 65 if (!_resolvedInputUserSettings) 66 { 67 if(Input.Action().has_value() && Utility::CaseInsensitiveEquals(Input.Action().value(), ACTION_FULL)) 68 { 69 Output.Action(ACTION_FULL); 70 _resolvedInputUserSettings = Input.Settings(); 71 } 72 else 73 { 74 Output.Action(ACTION_PARTIAL); 75 _resolvedInputUserSettings = MergeUserSettingsFiles(*Input.Settings()); 76 } 77 } 78 79 return *_resolvedInputUserSettings; 80 } 81 82 bool WriteOutput() 83 { 84 THROW_HR_IF(E_UNEXPECTED, !Output.Settings().has_value()); 85 std::ofstream file(_userSettingsPath, std::ios::binary); 86 if (file) 87 { 88 Json::StreamWriterBuilder writer; 89 writer["indentation"] = " "; 90 file << Json::writeString(writer, *Output.Settings()); 91 return true; 92 } 93 94 AICLI_LOG(Config, Error, << "Failed to open or create user settings file: " << _userSettingsPath); 95 return false; 96 } 97 98 private: 99 std::filesystem::path _userSettingsPath; 100 std::optional<Json::Value> _userSettings; 101 std::optional<Json::Value> _resolvedInputUserSettings; 102 103 Json::Value MergeUserSettingsFiles(const Json::Value& overlay) 104 { 105 Json::Value mergedUserSettingsFile = GetUserSettings(); 106 MergeUserSettingsFiles(mergedUserSettingsFile, overlay); 107 return mergedUserSettingsFile; 108 } 109 110 // Merges the overlay settings into the target settings. 111 void MergeUserSettingsFiles(Json::Value& target, const Json::Value& overlay) 112 { 113 // If either is not an object, we can't merge. 114 if (!overlay.isObject() || !target.isObject()) 115 { 116 return; 117 } 118 119 // Iterate through the overlay settings and merge them into the target. 120 for (const auto& overlayKey : overlay.getMemberNames()) 121 { 122 const Json::Value& overlayValue = overlay[overlayKey]; 123 if (target.isMember(overlayKey)) 124 { 125 Json::Value& targetValue = target[overlayKey]; 126 if (targetValue.isObject() && overlayValue.isObject()) 127 { 128 // Recursively merge the objects. 129 MergeUserSettingsFiles(targetValue, overlayValue); 130 } 131 else 132 { 133 // Replace the value in the target. 134 // Note: Arrays are not merged, they are replaced. 135 target[overlayKey] = overlayValue; 136 } 137 } 138 else 139 { 140 // Add the overlay key to the target. 141 target[overlayKey] = overlayValue; 142 } 143 } 144 } 145 146 const Json::Value& GetUserSettings() 147 { 148 if (!_userSettings) 149 { 150 _userSettings = Json::objectValue; 151 std::ifstream file(_userSettingsPath, std::ios::binary); 152 if (file) 153 { 154 Json::CharReaderBuilder builder; 155 std::string errs; 156 Json::Value jsonRoot; 157 if (Json::parseFromStream(builder, file, &jsonRoot, &errs)) 158 { 159 _userSettings = jsonRoot; 160 } 161 else 162 { 163 AICLI_LOG(Config, Warning, << "Failed to parse user settings file: " << _userSettingsPath << ", error: " << errs); 164 } 165 } 166 else 167 { 168 AICLI_LOG(Config, Warning, << "Failed to open user settings file: " << _userSettingsPath); 169 } 170 } 171 172 return *_userSettings; 173 } 174 }; 175 } 176 177 DscUserSettingsFileResource::DscUserSettingsFileResource(std::string_view parent) : 178 DscCommandBase(parent, "user-settings-file", DscResourceKind::Resource, 179 DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, 180 DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff) 181 { 182 } 183 184 Resource::LocString DscUserSettingsFileResource::ShortDescription() const 185 { 186 return Resource::String::DscUserSettingsFileShortDescription; 187 } 188 189 Resource::LocString DscUserSettingsFileResource::LongDescription() const 190 { 191 return Resource::String::DscUserSettingsFileLongDescription; 192 } 193 194 std::string DscUserSettingsFileResource::ResourceType() const 195 { 196 return "UserSettingsFile"; 197 } 198 199 void DscUserSettingsFileResource::ResourceFunctionGet(Execution::Context& context) const 200 { 201 ResourceFunctionExport(context); 202 } 203 204 void DscUserSettingsFileResource::ResourceFunctionSet(Execution::Context& context) const 205 { 206 if (auto json = GetJsonFromInput(context)) 207 { 208 UserSettingsFileFunctionData data{ json }; 209 210 data.Get(); 211 212 // Capture the diff before updating the output 213 auto diff = data.DiffJson(); 214 215 if (!data.Test()) 216 { 217 data.Output.Settings(data.GetResolvedInput()); 218 if (!data.WriteOutput()) 219 { 220 AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_OPEN_FAILED)); 221 return; 222 } 223 } 224 225 WriteJsonOutputLine(context, data.Output.ToJson()); 226 WriteJsonOutputLine(context, diff); 227 } 228 } 229 230 void DscUserSettingsFileResource::ResourceFunctionTest(Execution::Context& context) const 231 { 232 if (auto json = GetJsonFromInput(context)) 233 { 234 UserSettingsFileFunctionData data{ json }; 235 236 data.Get(); 237 data.Output.InDesiredState(data.Test()); 238 239 WriteJsonOutputLine(context, data.Output.ToJson()); 240 WriteJsonOutputLine(context, data.DiffJson()); 241 } 242 } 243 244 void DscUserSettingsFileResource::ResourceFunctionExport(Execution::Context& context) const 245 { 246 UserSettingsFileFunctionData data; 247 248 data.Get(); 249 250 WriteJsonOutputLine(context, data.Output.ToJson()); 251 } 252 253 void DscUserSettingsFileResource::ResourceFunctionSchema(Execution::Context& context) const 254 { 255 WriteJsonOutputLine(context, UserSettingsFileResourceObject::Schema(ResourceType())); 256 } 257 }