Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Provide a way to convert the output of native executables to objects #19257

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions experimental-feature-linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"PSCustomTableHeaderLabelDecoration",
"PSLoadAssemblyFromNativeCode",
"PSNativeCommandErrorActionPreference",
"PSNativeJsonAdapter",
"PSSubsystemPluginModel",
"PSModuleAutoLoadSkipOfflineFiles",
"PSFeedbackProvider",
Expand Down
1 change: 1 addition & 0 deletions experimental-feature-windows.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"PSCustomTableHeaderLabelDecoration",
"PSLoadAssemblyFromNativeCode",
"PSNativeCommandErrorActionPreference",
"PSNativeJsonAdapter",
"PSSubsystemPluginModel",
"PSModuleAutoLoadSkipOfflineFiles",
"PSFeedbackProvider",
Expand Down
24 changes: 24 additions & 0 deletions src/System.Management.Automation/engine/ApplicationInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ internal ApplicationInfo(string name, string path, ExecutionContext context) : b
Path = path;
Extension = System.IO.Path.GetExtension(path);
_context = context;

if (ExperimentalFeature.IsEnabled("PSNativeJsonAdapter"))
{
// Look for a json adapter.
// These take the shape of name-json.extension
FindJsonAdapter();
}
}

private readonly ExecutionContext _context;
Expand All @@ -65,6 +72,12 @@ internal ApplicationInfo(string name, string path, ExecutionContext context) : b
/// </summary>
public string Extension { get; } = string.Empty;

/// <summary>
/// The Json adapter for this application.
/// If this is not null, the adapter will be added to the pipeline following the application.
/// </summary>
public CommandInfo JsonAdapter { get; private set; } = null;

/// <summary>
/// Gets the path of the application file.
/// </summary>
Expand Down Expand Up @@ -138,5 +151,16 @@ public override ReadOnlyCollection<PSTypeName> OutputType
}

private ReadOnlyCollection<PSTypeName> _outputType = null;

/// <summary>
/// Search for a Json adapter for this application.
/// It will have the shape of name-json.extension, it can be any type of command.
/// </summary>
private void FindJsonAdapter()
{
string jsonAdapterName = string.Format("{0}-json", System.IO.Path.GetFileNameWithoutExtension(this.Path));
JsonAdapter = _context.SessionState.InvokeCommand.GetCommand(jsonAdapterName, CommandTypes.All);
return;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSCommandWithArgs,
description: "Enable `-CommandWithArgs` parameter for pwsh"),
new ExperimentalFeature(
name: "PSNativeJsonAdapter",
description: "Automatically call an adapter to convert output to Json when a native command is executed."),
};

EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
Expand Down
13 changes: 12 additions & 1 deletion src/System.Management.Automation/engine/hostifaces/History.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal HistoryInfo(long pipelineId, string cmdline, PipelineState status, Date
Dbg.Assert(cmdline != null, "caller should validate the parameter");
_pipelineId = pipelineId;
CommandLine = cmdline;
ConstructedPipeline = cmdline;
ExecutionStatus = status;
StartExecutionTime = startTime;
EndExecutionTime = endTime;
Expand All @@ -50,6 +51,7 @@ private HistoryInfo(HistoryInfo history)
Id = history.Id;
_pipelineId = history._pipelineId;
CommandLine = history.CommandLine;
ConstructedPipeline = history.ConstructedPipeline;
ExecutionStatus = history.ExecutionStatus;
StartExecutionTime = history.StartExecutionTime;
EndExecutionTime = history.EndExecutionTime;
Expand All @@ -68,6 +70,12 @@ private HistoryInfo(HistoryInfo history)
/// <value></value>
public string CommandLine { get; private set; }

/// <summary>
/// The executed pipeline.
/// This will include the json adapter.
/// </summary>
public string ConstructedPipeline { get; internal set; }

/// <summary>
/// Execution status of associated pipeline.
/// </summary>
Expand Down Expand Up @@ -191,14 +199,15 @@ internal History(ExecutionContext context)
/// </summary>
/// <param name="pipelineId"></param>
/// <param name="cmdline"></param>
/// <param name="constructedPipeline"></param>
/// <param name="status"></param>
/// <param name="startTime"></param>
/// <param name="endTime"></param>
/// <param name="skipIfLocked">If true, the entry will not be added when the history is locked.</param>
/// <returns>Id for the new created entry. Use this id to fetch the
/// entry. Returns -1 if the entry is not added.</returns>
/// <remarks>This function is thread safe</remarks>
internal long AddEntry(long pipelineId, string cmdline, PipelineState status, DateTime startTime, DateTime endTime, bool skipIfLocked)
internal long AddEntry(long pipelineId, string cmdline, string constructedPipeline, PipelineState status, DateTime startTime, DateTime endTime, bool skipIfLocked)
{
if (!System.Threading.Monitor.TryEnter(_syncRoot, skipIfLocked ? 0 : System.Threading.Timeout.Infinite))
{
Expand All @@ -210,6 +219,7 @@ internal long AddEntry(long pipelineId, string cmdline, PipelineState status, Da
ReallocateBufferIfNeeded();

HistoryInfo entry = new HistoryInfo(pipelineId, cmdline, status, startTime, endTime);
entry.ConstructedPipeline = constructedPipeline;
return Add(entry);
}
finally
Expand Down Expand Up @@ -1397,6 +1407,7 @@ void ProcessRecord()
(
0,
infoToAdd.CommandLine,
infoToAdd.ConstructedPipeline,
infoToAdd.ExecutionStatus,
infoToAdd.StartExecutionTime,
infoToAdd.EndExecutionTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ private void AddHistoryEntry(bool skipIfLocked)
// History id is greater than zero if entry was added to history
if (AddToHistory)
{
LocalRunspace.History.AddEntry(InstanceId, HistoryString, PipelineState, _pipelineStartTime, DateTime.Now, skipIfLocked);
LocalRunspace.History.AddEntry(InstanceId, HistoryString, ConstructedPipeline, PipelineState, _pipelineStartTime, DateTime.Now, skipIfLocked);
}
}

Expand Down Expand Up @@ -1030,7 +1030,7 @@ void AddHistoryEntryFromAddHistoryCmdlet()

if (AddToHistory)
{
_historyIdForThisPipeline = LocalRunspace.History.AddEntry(InstanceId, HistoryString, PipelineState, _pipelineStartTime, DateTime.Now, false);
_historyIdForThisPipeline = LocalRunspace.History.AddEntry(InstanceId, HistoryString, ConstructedPipeline, PipelineState, _pipelineStartTime, DateTime.Now, false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5250,6 +5250,12 @@ private void StopThreadProc(object state)
/// </summary>
public string HistoryString { get; set; }

/// <summary>
/// The string representing the constructed pipeline
/// which includes the json adapter.
/// </summary>
public string ConstructedPipeline { get; set; }

/// <summary>
/// Extra commands to run in a single invocation.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,11 @@ private void OnErrorStreamDataReady(object sender, EventArgs e)
/// by invoke-cmd to place correct string in history.</remarks>
internal string HistoryString { get; set; }

/// <summary>
/// String which represents the actual pipeline.
/// </summary>
public string ConstructedPipeline { get; set; }

#endregion history

#region misc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,71 @@ private static string GetParameterText(string parameterName)

CommandProcessorBase commandProcessor = null;
CommandRedirection[] commandRedirection = null;
// If we add a json adapter, we need to modify the history string to include the adapter name.
bool commandAdded = false;

for (int i = 0; i < pipeElements.Length; i++)
{
commandRedirection = commandRedirections?[i];
commandProcessor = AddCommand(pipelineProcessor, pipeElements[i], pipeElementAsts[i],
commandRedirection, context);
if (ExperimentalFeature.IsEnabled("PSNativeJsonAdapter") && commandProcessor.CommandInfo is ApplicationInfo && commandRedirection is null)
{
var applicationInfo = (ApplicationInfo)commandProcessor.CommandInfo;
// Handle the Json adapter if it is set.
if (applicationInfo.JsonAdapter != null)
{
Token[] tokenList;
ParseError[] errorList;
var ast = Parser.ParseInput(applicationInfo.JsonAdapter.Name, out tokenList, out errorList);
CommandBaseAst cmdAst = ast?.Find(a => a is CommandBaseAst, false) as CommandBaseAst;
CommandAst jsonCommandAst = ast?.Find(a => a is CommandAst, false) as CommandAst;

// We want to not add the json adapter if the next element in the pipeline is the json adapter.
// We peek at the next element of the pipeline to see if it is the json adapter, and
// if it is, we will not add the adapter to the pipeline automatically.
if (i + 1 < pipeElements.Length)
{
var nextCommand = pipeElementAsts[i + 1] as CommandAst;
if (nextCommand != null && nextCommand.GetCommandName().Equals(applicationInfo.JsonAdapter.Name, StringComparison.OrdinalIgnoreCase))
{
// The next element is the json adapter, so we will not add this one to the pipeline.
jsonCommandAst = null;
}
}

// Process the Json adapter and add it to the pipeline.
if (jsonCommandAst != null && errorList.Length == 0)
{
// We will attach the ast of the original native command
var commandParameters = new List<CommandParameterInternal>();
foreach (var commandElement in jsonCommandAst.CommandElements)
{
var commandParameterAst = commandElement as CommandParameterAst;
if (commandParameterAst != null)
{
commandParameters.Add(GetCommandParameter(commandParameterAst, true, context));
continue;
}

var exprAst = (ExpressionAst)commandElement;
var argument = Compiler.GetExpressionValue(exprAst, true, context);
var splatting = (exprAst is VariableExpressionAst && ((VariableExpressionAst)exprAst).Splatted);
commandParameters.Add(CommandParameterInternal.CreateArgument(argument, exprAst, splatting));
}

// Attach the parameters of the original native command as arguments for the adapter.
// Output from the original native command will be piped to the adapter.
foreach (var commandElement in pipeElements[i])
{
commandParameters.Add(commandElement);
}

commandProcessor = AddCommand(pipelineProcessor, commandParameters.ToArray(), cmdAst, commandRedirection, context);
commandAdded = true;
}
}
}
}

var cmdletInfo = commandProcessor?.CommandInfo as CmdletInfo;
Expand Down Expand Up @@ -490,6 +549,12 @@ private static string GetParameterText(string parameterName)
}
}

if (commandAdded)
{
// We've added a json adapter, update the history string.
AddConstructedPipeline(context, pipelineProcessor);
}

context.PushPipelineProcessor(pipelineProcessor);
try
{
Expand All @@ -507,6 +572,31 @@ private static string GetParameterText(string parameterName)
}
}

// Add the constructed pipeline in the case of a discovered JSON adapter.
// This is best effort, if an exception occurs that is not a fatal error.
internal static void AddConstructedPipeline(ExecutionContext context, PipelineProcessor pipelineProcessor)
{
try
{
var runningPipeline = context.CurrentRunspace.GetCurrentlyRunningPipeline() as LocalPipeline;
if (runningPipeline is null)
{
return;
}

List<string> commands = new List<string>();
foreach (var commandProcessor in pipelineProcessor.Commands)
{
commands.Add(commandProcessor.Command.InvocationExtent.Text);
}
runningPipeline.ConstructedPipeline = string.Join(" | ", commands);
}
catch (Exception)
{
// Ignore any exception.
}
}

internal static void InvokePipelineInBackground(
PipelineBaseAst pipelineAst,
FunctionContext funcContext)
Expand Down Expand Up @@ -3627,7 +3717,7 @@ internal static void LogMemberInvocation(string targetName, string name, object[
}
catch (PSSecurityException)
{
// ReportContent() will throw PSSecurityException if AMSI detects malware, which
// ReportContent() will throw PSSecurityException if AMSI detects malware, which
// must be propagated.
throw;
}
Expand Down