forked from PowerShell/PowerShell
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPropertyAccessor.cs
More file actions
486 lines (432 loc) · 19.9 KB
/
Copy pathPropertyAccessor.cs
File metadata and controls
486 lines (432 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Xml;
using System.IO;
using System.Text;
using System.Reflection;
using System.Globalization;
using System.Threading;
using System.Management.Automation;
using Microsoft.Win32;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace System.Management.Automation
{
/// <summary>
/// Leverages the strategy pattern to abstract away the details of gathering properties from outside sources.
/// Note: This is a class so that it can be internal.
/// </summary>
internal abstract class ConfigPropertyAccessor
{
#region Statics
/// <summary>
/// Static constructor to instantiate an instance
/// </summary>
static ConfigPropertyAccessor()
{
Instance = new JsonConfigFileAccessor();
}
/// <summary>
/// The instance of the ConfigPropertyAccessor to use to interact with properties.
/// Derived classes should not be directly instantiated.
/// </summary>
internal static readonly ConfigPropertyAccessor Instance;
#endregion // Statics
#region Enums
/// <summary>
/// Describes the scope of the property query.
/// SystemWide properties apply to all users.
/// CurrentUser properties apply to the current user that is impersonated.
/// </summary>
internal enum PropertyScope
{
SystemWide = 0,
CurrentUser = 1
}
#endregion // Enums
#region Interface Methods
/// <summary>
/// Existing Key = HKLM:\System\CurrentControlSet\Control\Session Manager\Environment
/// Proposed value = %ProgramFiles%\PowerShell\Modules by default
///
/// Note: There is no setter because this value is immutable.
/// </summary>
/// <returns>Module path values from the config file.</returns>
internal abstract string GetModulePath(PropertyScope scope);
/// <summary>
/// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
/// Proposed value = Existing default execution policy if not already specified
/// </summary>
/// <param name="scope">Where it should check for the value.</param>
/// <param name="shellId">The shell associated with this policy. Typically, it is "Microsoft.PowerShell"</param>
/// <returns></returns>
internal abstract string GetExecutionPolicy(PropertyScope scope, string shellId);
internal abstract void RemoveExecutionPolicy(PropertyScope scope, string shellId);
internal abstract void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy);
/// <summary>
/// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds
/// Proposed value = existing default. Probably "1"
/// </summary>
/// <returns>Whether console prompting should happen.</returns>
internal abstract bool GetConsolePrompting();
internal abstract void SetConsolePrompting(bool shouldPrompt);
/// <summary>
/// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell
/// Proposed value = Existing default. Probably "0"
/// </summary>
/// <returns>Boolean indicating whether Update-Help should prompt</returns>
internal abstract bool GetDisablePromptToUpdateHelp();
internal abstract void SetDisablePromptToUpdateHelp(bool prompt);
/// <summary>
/// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp
/// Proposed value = blank.This should be supported though
/// </summary>
/// <returns></returns>
internal abstract string GetDefaultSourcePath();
internal abstract void SetDefaultSourcePath(string defaultPath);
#endregion // Interface Methods
}
/// <summary>
/// JSON configuration file accessor
///
/// Reads from and writes to configuration files. The values stored were
/// originally stored in the Windows registry.
/// </summary>
internal class JsonConfigFileAccessor : ConfigPropertyAccessor
{
private string psHomeConfigDirectory;
private string appDataConfigDirectory;
private const string configFileName = "PowerShellProperties.json";
/// <summary>
/// Lock used to enable multiple concurrent readers and singular write locks within a
/// single process.
/// TODO: This solution only works for IO from a single process. A more robust solution is needed to enable ReaderWriterLockSlim behavior between processes.
/// </summary>
private ReaderWriterLockSlim fileLock = new ReaderWriterLockSlim();
internal JsonConfigFileAccessor()
{
//
// Sets the system-wide configuration directory
//
Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly;
psHomeConfigDirectory = Path.GetDirectoryName(assembly.Location);
//
// Sets the per-user configuration directory
// Note: This directory may or may not exist depending upon the
// execution scenario. Writes will attempt to create the directory
// if it does not already exist.
//
appDataConfigDirectory = Utils.GetUserConfigurationDirectory();
}
/// <summary>
/// This value is not writable via the API and must be set using a text editor.
/// </summary>
/// <param name="scope"></param>
/// <returns>Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable().</returns>
internal override string GetModulePath(PropertyScope scope)
{
string scopeDirectory = psHomeConfigDirectory;
// Defaults to system wide.
if (PropertyScope.CurrentUser == scope)
{
scopeDirectory = appDataConfigDirectory;
}
string fileName = Path.Combine(scopeDirectory, configFileName);
string modulePath = ReadValueFromFile<string>(fileName, Constants.PSModulePathEnvVar);
if (!string.IsNullOrEmpty(modulePath))
{
modulePath = Environment.ExpandEnvironmentVariables(modulePath);
}
return modulePath;
}
/// <summary>
/// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
/// Proposed value = Existing default execution policy if not already specified
///
/// Schema:
/// {
/// "shell ID string","ExecutionPolicy" : "execution policy string"
/// }
///
/// TODO: In a single config file, it might be better to nest this. It is unnecessary complexity until a need arises for more nested values.
/// </summary>
/// <param name="scope">Whether this is a system-wide or per-user setting.</param>
/// <param name="shellId">The shell associated with this policy. Typically, it is "Microsoft.PowerShell"</param>
/// <returns>The execution policy if found. Null otherwise.</returns>
internal override string GetExecutionPolicy(PropertyScope scope, string shellId)
{
string execPolicy = null;
string scopeDirectory = psHomeConfigDirectory;
// Defaults to system wide.
if(PropertyScope.CurrentUser == scope)
{
scopeDirectory = appDataConfigDirectory;
}
string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
string rawExecPolicy = ReadValueFromFile<string>(fileName, valueName);
if (!String.IsNullOrEmpty(rawExecPolicy))
{
execPolicy = rawExecPolicy;
}
return execPolicy;
}
internal override void RemoveExecutionPolicy(PropertyScope scope, string shellId)
{
string scopeDirectory = psHomeConfigDirectory;
// Defaults to system wide.
if (PropertyScope.CurrentUser == scope)
{
scopeDirectory = appDataConfigDirectory;
}
string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
RemoveValueFromFile<string>(fileName, valueName);
}
internal override void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy)
{
string scopeDirectory = psHomeConfigDirectory;
// Defaults to system wide.
if (PropertyScope.CurrentUser == scope)
{
// Exceptions are not caught so that they will propagate to the
// host for display to the user.
// CreateDirectory will succeed if the directory already exists
// so there is no reason to check Directory.Exists().
Directory.CreateDirectory(appDataConfigDirectory);
scopeDirectory = appDataConfigDirectory;
}
string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
WriteValueToFile<string>(fileName, valueName, executionPolicy);
}
/// <summary>
/// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds
/// Proposed value = existing default. Probably "1"
///
/// Schema:
/// {
/// "ConsolePrompting" : bool
/// }
/// </summary>
/// <returns>Whether console prompting should happen. If the value cannot be read it defaults to false.</returns>
internal override bool GetConsolePrompting()
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
return ReadValueFromFile<bool>(fileName, "ConsolePrompting");
}
internal override void SetConsolePrompting(bool shouldPrompt)
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
WriteValueToFile<bool>(fileName, "ConsolePrompting", shouldPrompt);
}
/// <summary>
/// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell
/// Proposed value = Existing default. Probably "0"
///
/// Schema:
/// {
/// "DisablePromptToUpdateHelp" : bool
/// }
/// </summary>
/// <returns>Boolean indicating whether Update-Help should prompt. If the value cannot be read, it defaults to false.</returns>
internal override bool GetDisablePromptToUpdateHelp()
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
return ReadValueFromFile<bool>(fileName, "DisablePromptToUpdateHelp");
}
internal override void SetDisablePromptToUpdateHelp(bool prompt)
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
WriteValueToFile<bool>(fileName, "DisablePromptToUpdateHelp", prompt);
}
/// <summary>
/// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp
/// Proposed value = blank.This should be supported though
///
/// Schema:
/// {
/// "DefaultSourcePath" : "path to local updatable help location"
/// }
/// </summary>
/// <returns>The source path if found, null otherwise.</returns>
internal override string GetDefaultSourcePath()
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
string rawExecPolicy = ReadValueFromFile<string>(fileName, "DefaultSourcePath");
if (!String.IsNullOrEmpty(rawExecPolicy))
{
return rawExecPolicy;
}
return null;
}
internal override void SetDefaultSourcePath(string defaultPath)
{
string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
WriteValueToFile<string>(fileName, "DefaultSourcePath", defaultPath);
}
private T ReadValueFromFile<T>(string fileName, string key)
{
fileLock.EnterReadLock();
try
{
// Open file for reading, but allow multiple readers
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
using (StreamReader streamRdr = new StreamReader(fs))
using (JsonTextReader jsonReader = new JsonTextReader(streamRdr))
{
// Safely determines whether there is content to read from the file
bool isReadSuccess = jsonReader.Read();
if (isReadSuccess)
{
JObject jsonObject = (JObject) JToken.ReadFrom(jsonReader);
JToken value = jsonObject.GetValue(key);
if (null != value)
{
return value.ToObject<T>();
}
}
}
}
catch (FileNotFoundException)
{
// The file doesn't exist. Treat this the same way as if the
// key was not present in the file.
}
catch (DirectoryNotFoundException)
{
// A directory in the path does not exist. Treat this as if the
// key is not present in the file.
}
finally
{
fileLock.ExitReadLock();
}
return default(T);
}
/// <summary>
/// TODO: Should this return success fail or throw?
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="fileName"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="addValue">Whether the key-value pair should be added to or removed from the file</param>
private void UpdateValueInFile<T>(string fileName, string key, T value, bool addValue)
{
fileLock.EnterWriteLock();
try
{
// Since multiple properties can be in a single file, replacement
// is required instead of overwrite if a file already exists.
// Handling the read and write operations within a single FileStream
// prevents other processes from reading or writing the file while
// the update is in progress. It also locks out readers during write
// operations.
using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
JObject jsonObject = null;
// UTF8, BOM detection, and bufferSize are the same as the basic stream constructor.
// The most important parameter here is the last one, which keeps the StreamReader
// (and FileStream) open during Dispose so that it can be reused for the write
// operation.
using (StreamReader streamRdr = new StreamReader(fs, Encoding.UTF8, true, 1024, true))
using (JsonTextReader jsonReader = new JsonTextReader(streamRdr))
{
// Safely determines whether there is content to read from the file
bool isReadSuccess = jsonReader.Read();
if (isReadSuccess)
{
// Read the stream into a root JObject for manipulation
jsonObject = (JObject) JToken.ReadFrom(jsonReader);
JProperty propertyToModify = jsonObject.Property(key);
if (null == propertyToModify)
{
// The property doesn't exist, so add it
if (addValue)
{
jsonObject.Add(new JProperty(key, value));
}
// else the property doesn't exist so there is nothing to remove
}
// The property exists
else
{
if (addValue)
{
propertyToModify.Replace(new JProperty(key, value));
}
else
{
propertyToModify.Remove();
}
}
}
else
{
// The file doesn't already exist and we want to write to it
// or it exists with no content.
// A new file will be created that contains only this value.
// If the file doesn't exist and a we don't want to write to it, no
// action is necessary.
if (addValue)
{
jsonObject = new JObject(new JProperty(key, value));
}
else
{
return;
}
}
}
// Reset the stream position to the beginning so that the
// changes to the file can be written to disk
fs.Seek(0, SeekOrigin.Begin);
// Update the file with new content
using (StreamWriter streamWriter = new StreamWriter(fs))
using (JsonTextWriter jsonWriter = new JsonTextWriter(streamWriter))
{
// The entire document exists within the root JObject.
// I just need to write that object to produce the document.
jsonObject.WriteTo(jsonWriter);
// This trims the file if the file shrank. If the file grew,
// it is a no-op. The purpose is to trim extraneous characters
// from the file stream when the resultant JObject is smaller
// than the input JObject.
fs.SetLength(fs.Position);
}
}
}
finally
{
fileLock.ExitWriteLock();
}
}
/// <summary>
/// TODO: Should this return success, fail, or throw?
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="fileName"></param>
/// <param name="key"></param>
/// <param name="value"></param>
private void WriteValueToFile<T>(string fileName, string key, T value)
{
UpdateValueInFile<T>(fileName, key, value, true);
}
/// <summary>
/// TODO: Should this return success, fail, or throw?
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="fileName"></param>
/// <param name="key"></param>
private void RemoveValueFromFile<T>(string fileName, string key)
{
// Optimization: If the file doesn't exist, there is nothing to remove
if (File.Exists(fileName))
{
UpdateValueInFile<T>(fileName, key, default(T), false);
}
}
}
} // Namespace System.Management.Automation