UserSettingsCommand.cs (10188B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="UserSettingsCommand.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace Microsoft.WinGet.Client.Engine.Commands 8 { 9 using System; 10 using System.Collections; 11 using System.IO; 12 using System.Linq; 13 using System.Management.Automation; 14 using Microsoft.WinGet.Client.Engine.Commands.Common; 15 using Microsoft.WinGet.Client.Engine.Common; 16 using Microsoft.WinGet.Client.Engine.Exceptions; 17 using Microsoft.WinGet.Client.Engine.Helpers; 18 using Microsoft.WinGet.Common.Command; 19 using Newtonsoft.Json; 20 using Newtonsoft.Json.Linq; 21 22 /// <summary> 23 /// Class used by the user settings cmdlets. 24 /// </summary> 25 public sealed class UserSettingsCommand : BaseCommand 26 { 27 private const string SchemaKey = "$schema"; 28 private const string SchemaValue = "https://aka.ms/winget-settings.schema.json"; 29 30 private static string? winGetSettingsFilePath; 31 32 /// <summary> 33 /// Initializes a new instance of the <see cref="UserSettingsCommand"/> class. 34 /// </summary> 35 /// <param name="psCmdlet">PSCmdlet.</param> 36 public UserSettingsCommand(PSCmdlet psCmdlet) 37 : base(psCmdlet) 38 { 39 // Doing it in the static constructor will show the user running in system context: 40 // The type initializer for 'Microsoft.WinGet.Client.Engine.Commands.UserSettingsCommand' threw an exception. 41 // Here would be "The specified method is not supported." 42 if (winGetSettingsFilePath == null) 43 { 44 var wingetCliWrapper = new WingetCLIWrapper(); 45 var settingsResult = wingetCliWrapper.RunCommand(this, "settings", "export"); 46 47 // Read the user settings file property. 48 var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile"); 49 winGetSettingsFilePath = (string)userSettingsFile; 50 } 51 } 52 53 /// <summary> 54 /// Get-WinGetUserSetting. 55 /// </summary> 56 public void Get() 57 { 58 this.Write(StreamType.Object, this.GetLocalSettingsAsHashtable()); 59 } 60 61 /// <summary> 62 /// Test-WinGetUserSetting. 63 /// </summary> 64 /// <param name="userSettings">Input user settings.</param> 65 /// <param name="ignoreNotSet">Ignore comparing settings that are not part of the input.</param> 66 public void Test(Hashtable userSettings, bool ignoreNotSet) 67 { 68 this.Write(StreamType.Object, this.CompareUserSettings(userSettings, ignoreNotSet)); 69 } 70 71 /// <summary> 72 /// Set-WinGetUserSetting. 73 /// </summary> 74 /// <param name="userSettings">Input user settings.</param> 75 /// <param name="merge">Merge the current user settings and the input settings.</param> 76 public void Set(Hashtable userSettings, bool merge) 77 { 78 var newSettings = HashtableToJObject(userSettings); 79 80 // Merge settings. 81 if (merge) 82 { 83 var currentSettings = this.LocalSettingsFileToJObject(); 84 85 // To make the input settings triumph, they need to be merged into the existing settings. 86 currentSettings.Merge(newSettings, new JsonMergeSettings 87 { 88 MergeArrayHandling = MergeArrayHandling.Union, 89 MergeNullValueHandling = MergeNullValueHandling.Ignore, 90 }); 91 92 newSettings = currentSettings; 93 } 94 95 // Add schema if not there. 96 if (!newSettings.ContainsKey(SchemaKey)) 97 { 98 newSettings.Add(SchemaKey, SchemaValue); 99 } 100 101 var orderedSettings = this.CreateAlphabeticallyOrderedJObject(newSettings); 102 103 // Write settings. 104 var settingsJson = orderedSettings.ToString(Formatting.Indented); 105 File.WriteAllText( 106 winGetSettingsFilePath!, 107 settingsJson); 108 109 this.Write(StreamType.Object, Utilities.ConvertToHashtable(settingsJson)); 110 } 111 112 private static JObject HashtableToJObject(Hashtable hashtable) 113 { 114 return (JObject)JToken.FromObject(hashtable); 115 } 116 117 private Hashtable GetLocalSettingsAsHashtable() 118 { 119 var content = File.Exists(winGetSettingsFilePath) ? 120 File.ReadAllText(winGetSettingsFilePath) : 121 string.Empty; 122 123 return Utilities.ConvertToHashtable(content); 124 } 125 126 private JObject LocalSettingsFileToJObject() 127 { 128 try 129 { 130 return File.Exists(winGetSettingsFilePath) ? 131 JObject.Parse(File.ReadAllText(winGetSettingsFilePath)) : 132 new JObject(); 133 } 134 catch (JsonReaderException e) 135 { 136 this.Write(StreamType.Verbose, e.Message); 137 throw new UserSettingsReadException(e); 138 } 139 } 140 141 private bool CompareUserSettings(Hashtable userSettings, bool ignoreNotSet) 142 { 143 try 144 { 145 var currentSettings = this.LocalSettingsFileToJObject(); 146 var newSettings = HashtableToJObject(userSettings); 147 148 // Don't fail because of the schema. 149 if (currentSettings.ContainsKey(SchemaKey)) 150 { 151 currentSettings.Remove(SchemaKey); 152 } 153 154 if (newSettings.ContainsKey(SchemaKey)) 155 { 156 newSettings.Remove(SchemaKey); 157 } 158 159 if (ignoreNotSet) 160 { 161 return this.PartialDeepEquals(newSettings, currentSettings); 162 } 163 164 return JToken.DeepEquals(newSettings, currentSettings); 165 } 166 catch (Exception e) 167 { 168 this.Write(StreamType.Verbose, e.Message); 169 return false; 170 } 171 } 172 173 /// <summary> 174 /// Partially compares json. All properties and values of json must exist and have the same value 175 /// as otherJson. 176 /// This doesn't support deep JArray object comparison, but we don't have arrays of type object so far :). 177 /// </summary> 178 /// <param name="json">Main json.</param> 179 /// <param name="otherJson">otherJson.</param> 180 /// <returns>True is otherJson partially contains json.</returns> 181 private bool PartialDeepEquals(JToken json, JToken? otherJson) 182 { 183 if (JToken.DeepEquals(json, otherJson)) 184 { 185 return true; 186 } 187 188 if (otherJson == null) 189 { 190 return false; 191 } 192 193 // If they are a JValue (string, integer, date, etc) or they are a JArray and DeepEquals fails then not equal. 194 if ((json is JValue && otherJson is JValue) || 195 (json is JArray && otherJson is JArray)) 196 { 197 this.Write( 198 StreamType.Verbose, 199 $"'{json.ToString(Formatting.None)}' != '{otherJson.ToString(Formatting.None)}'"); 200 return false; 201 } 202 203 // If its not the same type then don't bother. 204 if (json.Type != otherJson.Type) 205 { 206 this.Write( 207 StreamType.Verbose, 208 $"Mismatch types '{json.ToString(Formatting.None)}' '{otherJson.ToString(Formatting.None)}'"); 209 return false; 210 } 211 212 // Look deeply. 213 if (json.Type == JTokenType.Object) 214 { 215 var jObject = (JObject)json; 216 var otherJObject = (JObject)otherJson; 217 218 var properties = jObject.Properties(); 219 foreach (var property in properties) 220 { 221 // If the property is not there then give up. 222 if (!otherJObject.ContainsKey(property.Name)) 223 { 224 this.Write(StreamType.Verbose, $"{property.Name} not found."); 225 return false; 226 } 227 228 if (!this.PartialDeepEquals(property.Value, otherJObject.GetValue(property.Name))) 229 { 230 // Found inequality within a property. We are done. 231 return false; 232 } 233 } 234 } 235 236 return true; 237 } 238 239 /// <summary> 240 /// Helper method to order alphabetically properties. Newtonsoft doesn't have a nice way 241 /// to do it via a custom JsonConverter. 242 /// </summary> 243 /// <param name="jObject">JObject.</param> 244 /// <returns>New ordered JObject.</returns> 245 private JObject CreateAlphabeticallyOrderedJObject(JObject jObject) 246 { 247 JObject newJObject = new (); 248 var orderedProperties = jObject.Properties().OrderBy(p => p.Name, StringComparer.Ordinal); 249 foreach (var property in orderedProperties) 250 { 251 if (property.Value.Type == JTokenType.Object) 252 { 253 newJObject.Add( 254 property.Name, 255 this.CreateAlphabeticallyOrderedJObject((JObject)property.Value)); 256 } 257 else 258 { 259 newJObject.Add(property); 260 } 261 } 262 263 return newJObject; 264 } 265 } 266 }