Skip to content

Commit 9b02e80

Browse files
author
Kapil Borle
committed
Parse an array of module names for import-dscresource
1 parent 66eeb9d commit 9b02e80

File tree

3 files changed

+149
-21
lines changed

3 files changed

+149
-21
lines changed

Engine/Generic/ModuleDependencyHandler.cs

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ private void SetupPSSAAppData()
158158
SetupTempDir();
159159
}
160160

161-
private bool IsModulePresent(string moduleName)
161+
private bool IsModulePresentInTempModulePath(string moduleName)
162162
{
163163
foreach (var dir in Directory.EnumerateDirectories(TempModulePath))
164164
{
@@ -335,7 +335,7 @@ public bool TrySaveModule(string moduleName)
335335
public void SaveModule(string moduleName)
336336
{
337337
ThrowIfNull(moduleName, "moduleName");
338-
if (IsModulePresent(moduleName))
338+
if (IsModulePresentInTempModulePath(moduleName))
339339
{
340340
return;
341341
}
@@ -350,6 +350,60 @@ public void SaveModule(string moduleName)
350350
}
351351
}
352352

353+
/// <summary>
354+
/// Encapsulates Get-Module to check the availability of the module on the system
355+
/// </summary>
356+
/// <param name="moduleName"></param>
357+
/// <returns>True indicating the presence of the module, otherwise false</returns>
358+
public bool IsModuleAvailable(string moduleName)
359+
{
360+
ThrowIfNull(moduleName, "moduleName");
361+
IEnumerable<PSModuleInfo> availableModules;
362+
using (var ps = System.Management.Automation.PowerShell.Create())
363+
{
364+
ps.Runspace = runspace;
365+
availableModules = ps.AddCommand("Get-Module")
366+
.AddParameter("Name", moduleName)
367+
.AddParameter("ListAvailable")
368+
.Invoke<PSModuleInfo>();
369+
}
370+
return availableModules != null ? availableModules.Any() : false;
371+
}
372+
373+
/// <summary>
374+
/// Extracts out the module names from the error extent that are not available
375+
///
376+
/// This handles the following case.
377+
/// Import-DSCResourceModule -ModuleName ModulePresent,ModuleAbsent
378+
///
379+
/// ModulePresent is present in PSModulePath whereas ModuleAbsent is not.
380+
/// But the error exent coverts the entire extent and hence we need to check
381+
/// which module is actually not present so as to be downloaded
382+
/// </summary>
383+
/// <param name="error"></param>
384+
/// <param name="ast"></param>
385+
/// <returns>An enumeration over the module names that are not available</returns>
386+
public IEnumerable<string> GetUnavailableModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
387+
{
388+
ThrowIfNull(error, "error");
389+
ThrowIfNull(ast, "ast");
390+
var moduleNames = ModuleDependencyHandler.GetModuleNameFromErrorExtent(error, ast);
391+
if (moduleNames == null)
392+
{
393+
return null;
394+
}
395+
var unavailableModules = new List<string>();
396+
foreach (var moduleName in moduleNames)
397+
{
398+
if (!IsModuleAvailable(moduleName))
399+
{
400+
unavailableModules.Add(moduleName);
401+
}
402+
}
403+
//return moduleNames.Where(x => !IsModuleAvailable(x));
404+
return unavailableModules;
405+
}
406+
353407
/// <summary>
354408
/// Get the module name from the error extent
355409
///
@@ -362,7 +416,7 @@ public void SaveModule(string moduleName)
362416
/// <param name="error">Parse error</param>
363417
/// <param name="ast">AST of the script that contians the parse error</param>
364418
/// <returns>The name of the module that caused the parser to throw the error. Returns null if it cannot extract the module name.</returns>
365-
public static string GetModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
419+
public static IEnumerable<string> GetModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
366420
{
367421
ThrowIfNull(error, "error");
368422
ThrowIfNull(ast, "ast");
@@ -373,9 +427,10 @@ public static string GetModuleNameFromErrorExtent(ParseError error, ScriptBlockA
373427
return null;
374428
}
375429
// check if the command name is import-dscmodule
376-
// right now we handle only the following form
377-
// Import-DSCResource -ModuleName xActiveDirectory
378-
if (dynamicKywdAst.CommandElements.Count != 3)
430+
// right now we handle only the following forms
431+
// 1. Import-DSCResourceModule -ModuleName somemodule
432+
// 2. Import-DSCResourceModule -ModuleName somemodule1,somemodule2
433+
if (dynamicKywdAst.CommandElements.Count < 3)
379434
{
380435
return null;
381436
}
@@ -386,19 +441,56 @@ public static string GetModuleNameFromErrorExtent(ParseError error, ScriptBlockA
386441
return null;
387442
}
388443

389-
var paramAst = dynamicKywdAst.CommandElements[1] as CommandParameterAst;
390-
if (paramAst == null || !paramAst.ParameterName.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
444+
// find a parameter named modulename
445+
int k;
446+
for (k = 1; k < dynamicKywdAst.CommandElements.Count; k++)
391447
{
392-
return null;
448+
var paramAst = dynamicKywdAst.CommandElements[1] as CommandParameterAst;
449+
// TODO match the initial letters only
450+
if (paramAst == null || !paramAst.ParameterName.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
451+
{
452+
continue;
453+
}
454+
break;
393455
}
394-
395-
var paramValAst = dynamicKywdAst.CommandElements[2] as StringConstantExpressionAst;
396-
if (paramValAst == null)
456+
457+
if (k == dynamicKywdAst.CommandElements.Count)
397458
{
459+
// cannot find modulename
398460
return null;
399461
}
462+
var modules = new List<string>();
463+
464+
// k < count - 1, because only -ModuleName throws parse error and hence not possible
465+
var paramValAst = dynamicKywdAst.CommandElements[++k];
400466

401-
return paramValAst.Value;
467+
// import-dscresource -ModuleName module1
468+
var paramValStrConstExprAst = paramValAst as StringConstantExpressionAst;
469+
if (paramValStrConstExprAst != null)
470+
{
471+
modules.Add(paramValStrConstExprAst.Value);
472+
return modules;
473+
}
474+
475+
// import-dscresource -ModuleName module1,module2
476+
var paramValArrLtrlAst = paramValAst as ArrayLiteralAst;
477+
if (paramValArrLtrlAst != null)
478+
{
479+
foreach (var elem in paramValArrLtrlAst.Elements)
480+
{
481+
var elemStrConstExprAst = elem as StringConstantExpressionAst;
482+
if (elemStrConstExprAst != null)
483+
{
484+
modules.Add(elemStrConstExprAst.Value);
485+
}
486+
}
487+
if (modules.Count == 0)
488+
{
489+
return null;
490+
}
491+
return modules;
492+
}
493+
return null;
402494
}
403495

404496
/// <summary>

Engine/ScriptAnalyzer.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,36 +1165,43 @@ public IEnumerable<DiagnosticRecord> AnalyzePath(string path, bool searchRecursi
11651165
// is an optimization over doing the whole operation at once
11661166
// and calling .Concat on IEnumerables to join results.
11671167
this.BuildScriptPathList(path, searchRecursively, scriptFilePaths);
1168+
IEnumerable<DiagnosticRecord> diagnosticRecords;
11681169
if (resolveDscResourceDependency)
11691170
{
11701171
using (var rsp = RunspaceFactory.CreateRunspace())
11711172
{
11721173
rsp.Open();
11731174
using (var moduleHandler = new ModuleDependencyHandler(rsp))
11741175
{
1175-
return AnalyzePaths(scriptFilePaths, moduleHandler);
1176+
// cannot use IEnumerable because the execution is deferred
1177+
// which causes the code to go out of the "using" scope which
1178+
// in turn closes the runspace
1179+
diagnosticRecords = AnalyzePaths(scriptFilePaths, moduleHandler);
11761180
}
11771181
} // disposing the runspace also closes it if it not already closed
11781182
}
11791183
else
11801184
{
1181-
return AnalyzePaths(scriptFilePaths, null);
1185+
diagnosticRecords = AnalyzePaths(scriptFilePaths, null);
11821186
}
1187+
return diagnosticRecords;
11831188
}
11841189

1185-
private IEnumerable<DiagnosticRecord> AnalyzePaths(
1190+
private List<DiagnosticRecord> AnalyzePaths(
11861191
IEnumerable<string> scriptFilePaths,
11871192
ModuleDependencyHandler moduleHandler)
11881193
{
1194+
var diagnosticRecords = new List<DiagnosticRecord>();
11891195
foreach (string scriptFilePath in scriptFilePaths)
11901196
{
11911197
// Yield each record in the result so that the
11921198
// caller can pull them one at a time
11931199
foreach (var diagnosticRecord in this.AnalyzeFile(scriptFilePath, moduleHandler))
11941200
{
1195-
yield return diagnosticRecord;
1201+
diagnosticRecords.Add(diagnosticRecord);
11961202
}
11971203
}
1204+
return diagnosticRecords;
11981205
}
11991206

12001207
/// <summary>
@@ -1327,11 +1334,10 @@ private IEnumerable<DiagnosticRecord> AnalyzeFile(
13271334
{
13281335
foreach (ParseError error in errors.Where(IsModuleNotFoundError))
13291336
{
1330-
var moduleName = ModuleDependencyHandler.GetModuleNameFromErrorExtent(error, scriptAst);
1331-
if (moduleName != null
1332-
&& moduleHandler.TrySaveModule(moduleName))
1337+
var moduleNames = moduleHandler.GetUnavailableModuleNameFromErrorExtent(error, scriptAst);
1338+
if (moduleNames != null)
13331339
{
1334-
parseAgain = true;
1340+
parseAgain |= moduleNames.Any(x => moduleHandler.TrySaveModule(x));
13351341
}
13361342
}
13371343
}

Tests/Engine/ModuleDependencyHandler.tests.ps1

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,36 @@ Describe "Resolve DSC Resource Dependency" {
6666
{$moduleHandlerType::new($rsp)} | Should Throw
6767
$rsp.Dispose()
6868
}
69+
70+
It "Extracts 1 module name" {
71+
$sb = @"
72+
{Configuration SomeConfiguration
73+
{
74+
Import-DscResource -ModuleName SomeDscModule1
75+
}}
76+
"@
77+
$tokens = $null
78+
$parseError = $null
79+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$tokens, [ref]$parseError)
80+
$resultModuleNames = $moduleHandlerType::GetModuleNameFromErrorExtent($parseError[0], $ast).ToArray()
81+
$resultModuleNames[0] | Should Be 'SomeDscModule1'
82+
}
83+
84+
It "Extracts more than 1 module names" {
85+
$sb = @"
86+
{Configuration SomeConfiguration
87+
{
88+
Import-DscResource -ModuleName SomeDscModule1,SomeDscModule2,SomeDscModule3
89+
}}
90+
"@
91+
$tokens = $null
92+
$parseError = $null
93+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$tokens, [ref]$parseError)
94+
$resultModuleNames = $moduleHandlerType::GetModuleNameFromErrorExtent($parseError[0], $ast).ToArray()
95+
$resultModuleNames[0] | Should Be 'SomeDscModule1'
96+
$resultModuleNames[1] | Should Be 'SomeDscModule2'
97+
$resultModuleNames[2] | Should Be 'SomeDscModule3'
98+
}
6999
}
70100

71101
Context "Invoke-ScriptAnalyzer without switch" {

0 commit comments

Comments
 (0)