// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // using Microsoft.PackageManagement.Internal.Utility.Platform; namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Threading; using Microsoft.PackageManagement.Internal.Utility.Extensions; using Microsoft.PackageManagement.Internal.Utility.Platform; using Messages = Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages; using System.Collections.Concurrent; public class PowerShellProviderBase : IDisposable { private object _lock = new Object(); protected PSModuleInfo _module; private PowerShell _powershell; private ManualResetEvent _reentrancyLock = new ManualResetEvent(true); private readonly Dictionary _allCommands = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _methods = new Dictionary(StringComparer.OrdinalIgnoreCase); public PowerShellProviderBase(PowerShell ps, PSModuleInfo module) { if (module == null) { throw new ArgumentNullException("module"); } _powershell = ps; _module = module; // combine all the cmdinfos we care about // but normalize the keys as we go (remove any '-' '_' chars) foreach (var k in _module.ExportedAliases.Keys) { _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedAliases[k]); } foreach (var k in _module.ExportedCmdlets.Keys) { _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedCmdlets[k]); } foreach (var k in _module.ExportedFunctions.Keys) { _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedFunctions[k]); } } public string ModulePath { get { return _module.Path; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { if (_powershell != null) { _powershell.Dispose(); _powershell = null; } if (_reentrancyLock != null) { _reentrancyLock.Dispose(); _reentrancyLock = null; } _module = null; } } internal CommandInfo GetMethod(string methodName) { return _methods.GetOrAdd(methodName, () => { if (_allCommands.ContainsKey(methodName)) { return _allCommands[methodName]; } // try simple plurals to single if (methodName.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { var meth = methodName.Substring(0, methodName.Length - 1); if (_allCommands.ContainsKey(meth)) { return _allCommands[meth]; } } // try words like Dependencies to Dependency if (methodName.EndsWith("cies", StringComparison.OrdinalIgnoreCase)) { var meth = methodName.Substring(0, methodName.Length - 4) + "cy"; if (_allCommands.ContainsKey(meth)) { return _allCommands[meth]; } } // try IsFoo to Test-IsFoo if (methodName.IndexOf("Is", StringComparison.OrdinalIgnoreCase) == 0) { var meth = "test" + methodName; if (_allCommands.ContainsKey(meth)) { return _allCommands[meth]; } } if (methodName.IndexOf("add", StringComparison.OrdinalIgnoreCase) == 0) { // try it with 'register' instead var result = GetMethod("register" + methodName.Substring(3)); if (result != null) { return result; } } if (methodName.IndexOf("remove", StringComparison.OrdinalIgnoreCase) == 0) { // try it with 'register' instead var result = GetMethod("unregister" + methodName.Substring(6)); if (result != null) { return result; } } // can't find one, return null. return null; }); // hmm, it is possible to get the parameter types to match better when binding. // module.ExportedFunctions.FirstOrDefault().Value.Parameters.Values.First().ParameterType } internal object CallPowerShellWithoutRequest(string method, params object[] args) { var cmdInfo = GetMethod(method); if (cmdInfo == null) { return null; } var result = _powershell.InvokeFunction(cmdInfo.Name, null, null, args); if (result == null) { // failure! throw new Exception(Messages.PowershellScriptFunctionReturnsNull.format(_module.Name, method)); } return result; } // lock is on this instance only internal void ReportErrors(PsRequest request, IEnumerable errors) { foreach (var error in errors) { request.Error(error.FullyQualifiedErrorId, error.CategoryInfo.Category.ToString(), error.TargetObject == null ? null : error.TargetObject.ToString(), error.ErrorDetails == null ? error.Exception.Message : error.ErrorDetails.Message); if (!string.IsNullOrWhiteSpace(error.Exception.StackTrace)) { // give a debug hint if we have a script stack trace. How nice of us. // the exception stack trace gives better stack than the script stack trace request.Debug(Constants.ScriptStackTrace, error.Exception.StackTrace); } } } private IAsyncResult _stopResult; private object _stopLock = new object(); internal void CancelRequest() { if (!_reentrancyLock.WaitOne(0)) { // it's running right now. #if DEBUG NativeMethods.OutputDebugString("[Cmdlet:debugging] -- Stopping powershell script."); #endif lock (_stopLock) { if (_stopResult == null) { _stopResult = _powershell.BeginStop(ar => { }, null); } } } } internal object CallPowerShell(PsRequest request, params object[] args) { // the lock ensures that we're not re-entrant into the same powershell runspace lock (_lock) { if (!_reentrancyLock.WaitOne(0)) { // this lock is set to false, meaning we're still in a call **ON THIS THREAD** // this is bad karma -- powershell won't let us call into the runspace again // we're going to throw an error here because this indicates that the currently // running powershell call is calling back into PM, and it has called back // into this provider. That's just bad bad bad. throw new Exception("Re-entrancy Violation in powershell module"); } try { // otherwise, this is the first time we've been here during this call. _reentrancyLock.Reset(); _powershell.SetVariable("request", request); _powershell.Streams.ClearStreams(); object finalValue = null; ConcurrentBag errors = new ConcurrentBag(); request.Debug("INVOKING PowerShell Fn {0} with args {1} that has length {2}", request.CommandInfo.Name, String.Join(", ", args), args.Length); var result = _powershell.InvokeFunction(request.CommandInfo.Name, (sender, e) => output_DataAdded(sender, e, request, ref finalValue), (sender, e) => error_DataAdded(sender, e, request, errors), args); if (result == null) { // result is null but it does not mean that the call fails because the command may return nothing request.Debug(Messages.PowershellScriptFunctionReturnsNull.format(_module.Name, request.CommandInfo.Name)); } if (errors.Count > 0) { // report the error if there are any ReportErrors(request, errors); _powershell.Streams.Error.Clear(); } return finalValue; } catch (CmdletInvocationException cie) { var error = cie.ErrorRecord; request.Error(error.FullyQualifiedErrorId, error.CategoryInfo.Category.ToString(), error.TargetObject == null ? null : error.TargetObject.ToString(), error.ErrorDetails == null ? error.Exception.Message : error.ErrorDetails.Message); } finally { lock (_stopLock) { if (_stopResult != null){ _powershell.EndStop(_stopResult); _stopResult = null; } } _powershell.Clear(); _powershell.SetVariable("request", null); // it's ok if someone else calls into this module now. request.Debug("Done calling powershell", request.CommandInfo.Name, _module.Name); _reentrancyLock.Set(); } return null; } } private void error_DataAdded(object sender, DataAddedEventArgs e, PsRequest request, ConcurrentBag errors) { PSDataCollection errorStream = sender as PSDataCollection; if (errorStream == null) { return; } var error = errorStream[e.Index]; if (error != null) { // add the error so we can report them later errors.Add(error); } } private void output_DataAdded(object sender, DataAddedEventArgs e, PsRequest request, ref object finalValue) { PSDataCollection outputstream = sender as PSDataCollection; if (outputstream == null) { return; } PSObject psObject = outputstream[e.Index]; if (psObject != null) { var value = psObject.ImmediateBaseObject; var y = value as Yieldable; if (y != null) { // yield it to stream the result gradually y.YieldResult(request); } else { finalValue = value; return; } } } } }