From 6e3a827e32da135404648e3bc595ff962828fb83 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Wed, 26 Aug 2020 13:45:11 -0500 Subject: [PATCH] plugin changes to support script plugins (#2315) * plugin changes to support script plugins * all platforms can have plugins with file extensions * added .cmd, .py, .ps1 for windows * added more trace! statements to document request and response * WIP * pretty much working, need to figure out sink plugins * added a home for scripting plugin examples, ran fmt * Had a visit with my good friend Clippy. We're on speaking terms again. * add viable plugin extensions * clippy * update to load plugins without extension in *nix/mac. * fmt --- crates/nu-cli/src/cli.rs | 70 +++-- crates/nu-cli/src/commands/plugin.rs | 74 ++++- .../powershell/nu_plugin_mylen_filter.ps1 | 261 ++++++++++++++++++ 3 files changed, 378 insertions(+), 27 deletions(-) create mode 100644 docs/sample_plugins/powershell/nu_plugin_mylen_filter.ps1 diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 7af47b10b7..48e93de0f9 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -24,16 +24,39 @@ use std::error::Error; use std::io::{BufRead, BufReader, Write}; use std::iter::Iterator; use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; use std::sync::atomic::Ordering; use rayon::prelude::*; fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), ShellError> { - let mut child = std::process::Command::new(path) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn() - .expect("Failed to spawn child process"); + let ext = path.extension(); + let ps1_file = match ext { + Some(ext) => ext == "ps1", + None => false, + }; + + let mut child: Child = if ps1_file { + Command::new("pwsh") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .args(&[ + "-NoLogo", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + &path.to_string_lossy(), + ]) + .spawn() + .expect("Failed to spawn PowerShell process") + } else { + Command::new(path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn child process") + }; let stdin = child.stdin.as_mut().expect("Failed to open stdin"); let stdout = child.stdout.as_mut().expect("Failed to open stdout"); @@ -42,13 +65,14 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel let request = JsonRpc::new("config", Vec::::new()); let request_raw = serde_json::to_string(&request)?; + trace!(target: "nu::load", "plugin infrastructure config -> path {:#?}, request {:?}", &path, &request_raw); stdin.write_all(format!("{}\n", request_raw).as_bytes())?; let path = dunce::canonicalize(path)?; let mut input = String::new(); let result = match reader.read_line(&mut input) { Ok(count) => { - trace!(target: "nu::load", "plugin infrastructure -> config response"); + trace!(target: "nu::load", "plugin infrastructure -> config response for {:#?}", &path); trace!(target: "nu::load", "plugin infrastructure -> processing response ({} bytes)", count); trace!(target: "nu::load", "plugin infrastructure -> response: {}", input); @@ -156,31 +180,35 @@ pub fn load_plugins(context: &mut Context) -> Result<(), ShellError> { } }; + // allow plugins with extensions on all platforms let is_valid_name = { - #[cfg(windows)] - { - bin_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.') - } - - #[cfg(not(windows))] - { - bin_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_') - } + bin_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.') }; let is_executable = { #[cfg(windows)] { - bin_name.ends_with(".exe") || bin_name.ends_with(".bat") + bin_name.ends_with(".exe") + || bin_name.ends_with(".bat") + || bin_name.ends_with(".cmd") + || bin_name.ends_with(".py") + || bin_name.ends_with(".ps1") } #[cfg(not(windows))] { - true + !bin_name.contains('.') + || (bin_name.ends_with('.') + || bin_name.ends_with(".py") + || bin_name.ends_with(".rb") + || bin_name.ends_with(".sh") + || bin_name.ends_with(".bash") + || bin_name.ends_with(".zsh") + || bin_name.ends_with(".pl") + || bin_name.ends_with(".awk") + || bin_name.ends_with(".ps1")) } }; diff --git a/crates/nu-cli/src/commands/plugin.rs b/crates/nu-cli/src/commands/plugin.rs index 033ba0e493..7fac1468a7 100644 --- a/crates/nu-cli/src/commands/plugin.rs +++ b/crates/nu-cli/src/commands/plugin.rs @@ -8,6 +8,8 @@ use serde::{self, Deserialize, Serialize}; use std::io::prelude::*; use std::io::BufReader; use std::io::Write; +use std::path::Path; +use std::process::{Child, Command, Stdio}; #[derive(Debug, Serialize, Deserialize)] pub struct JsonRpc { @@ -84,11 +86,34 @@ pub async fn filter_plugin( let args = args.evaluate_once_with_scope(®istry, &scope).await?; - let mut child = std::process::Command::new(path) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn() - .expect("Failed to spawn child process"); + let real_path = Path::new(&path); + let ext = real_path.extension(); + let ps1_file = match ext { + Some(ext) => ext == "ps1", + None => false, + }; + + let mut child: Child = if ps1_file { + Command::new("pwsh") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .args(&[ + "-NoLogo", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + &real_path.to_string_lossy(), + ]) + .spawn() + .expect("Failed to spawn PowerShell process") + } else { + Command::new(path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn child process") + }; let call_info = args.call_info.clone(); @@ -111,6 +136,7 @@ pub async fn filter_plugin( let request = JsonRpc::new("begin_filter", call_info.clone()); let request_raw = serde_json::to_string(&request); + trace!("begin_filter:request {:?}", &request_raw); match request_raw { Err(_) => { @@ -136,6 +162,8 @@ pub async fn filter_plugin( match reader.read_line(&mut input) { Ok(_) => { let response = serde_json::from_str::(&input); + trace!("begin_filter:response {:?}", &response); + match response { Ok(NuResult::response { params }) => match params { Ok(params) => futures::stream::iter(params).to_output_stream(), @@ -168,6 +196,7 @@ pub async fn filter_plugin( let request: JsonRpc> = JsonRpc::new("end_filter", vec![]); let request_raw = serde_json::to_string(&request); + trace!("end_filter:request {:?}", &request_raw); match request_raw { Err(_) => { @@ -193,6 +222,8 @@ pub async fn filter_plugin( let stream = match reader.read_line(&mut input) { Ok(_) => { let response = serde_json::from_str::(&input); + trace!("end_filter:response {:?}", &response); + match response { Ok(NuResult::response { params }) => match params { Ok(params) => futures::stream::iter(params).to_output_stream(), @@ -220,6 +251,7 @@ pub async fn filter_plugin( let request: JsonRpc> = JsonRpc::new("quit", vec![]); let request_raw = serde_json::to_string(&request); + trace!("quit:request {:?}", &request_raw); match request_raw { Ok(request_raw) => { @@ -246,6 +278,8 @@ pub async fn filter_plugin( let request = JsonRpc::new("filter", v); let request_raw = serde_json::to_string(&request); + trace!("filter:request {:?}", &request_raw); + match request_raw { Ok(request_raw) => { let _ = stdin.write(format!("{}\n", request_raw).as_bytes()); @@ -262,6 +296,8 @@ pub async fn filter_plugin( match reader.read_line(&mut input) { Ok(_) => { let response = serde_json::from_str::(&input); + trace!("filter:response {:?}", &response); + match response { Ok(NuResult::response { params }) => match params { Ok(params) => futures::stream::iter(params).to_output_stream(), @@ -335,7 +371,33 @@ pub async fn sink_plugin( let _ = writeln!(tmpfile, "{}", request_raw); let _ = tmpfile.flush(); - let child = std::process::Command::new(path).arg(tmpfile.path()).spawn(); + let real_path = Path::new(&path); + let ext = real_path.extension(); + let ps1_file = match ext { + Some(ext) => ext == "ps1", + None => false, + }; + + // TODO: This sink may not work in powershell, trying to find + // an example of what CallInfo would look like in this temp file + let child = if ps1_file { + Command::new("pwsh") + .args(&[ + "-NoLogo", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + &real_path.to_string_lossy(), + &tmpfile + .path() + .to_str() + .expect("Failed getting tmpfile path"), + ]) + .spawn() + } else { + Command::new(path).arg(&tmpfile.path()).spawn() + }; if let Ok(mut child) = child { let _ = child.wait(); diff --git a/docs/sample_plugins/powershell/nu_plugin_mylen_filter.ps1 b/docs/sample_plugins/powershell/nu_plugin_mylen_filter.ps1 new file mode 100644 index 0000000000..c909165da6 --- /dev/null +++ b/docs/sample_plugins/powershell/nu_plugin_mylen_filter.ps1 @@ -0,0 +1,261 @@ +#!/usr/bin/env pwsh + +# Created to demonstrate how to create a plugin with PowerShell +# Below is a list of other links to help with scripting language plugin creation +# https://vsoch.github.io/2019/nushell-plugin-golang/ +# Go https://github.com/vsoch/nushell-plugin-len +# Python https://github.com/vsoch/nushell-plugin-python +# Python https://github.com/vsoch/nushell-plugin-pokemon +# C# https://github.com/myty/nu-plugin-lib +# Ruby https://github.com/andrasio/nu_plugin + +# WIP 8/19/20 + +# def print_good_response(response): +# json_response = {"jsonrpc": "2.0", "method": "response", "params": {"Ok": response}} +# print(json.dumps(json_response)) +# sys.stdout.flush() + +function print_good_response { + param($response) + $json_response = @" +{"jsonrpc": "2.0", "method": "response", "params": {"Ok": $($response)}} +"@ + Write-Host $json_response +} + +# def get_length(string_value): +# string_len = len(string_value["item"]["Primitive"]["String"]) +# int_item = {"Primitive": {"Int": string_len}} +# int_value = string_value +# int_value["item"] = int_item +# return int_value + +# functino get_length { +# param($string_val) +# $string_len = $string_val[`"item`"][`"Primitive`"][`"String`"].Length +# } + +function config { + param ($json_rpc) + #Write-Host $json_rpc + + $response = '{ "jsonrpc": "2.0", "method": "response", "params": { "Ok": { "name": "mylen", "usage": "Return the length of a string", "positional": [], "rest_positional": null, "named": {}, "is_filter": true } } }' + Write-Host $response + return +} + +function begin_filter { + $response = '{"jsonrpc":"2.0","method":"response","params":{"Ok":[]}}' + Write-Host $response + return +} + +function run_filter { + param($input_data) + # param( + # [Parameter( + # # Position = 0, + # Mandatory = $true, + # ValueFromPipeline = $true, + # ValueFromPipelineByPropertyName = $true) + # ] + # [Alias('piped')] + # [String]$piped_input + # ) + + # param([string]$params) + # {"method":"filter", "params": {"item": {"Primitive": {"String": "oogabooga"}}, \ + # "tag":{"anchor":null,"span":{"end":10,"start":12}}}} + # Write-Error $piped_input + Write-TraceMessage "PIPED" $input_data + + $prim = "Primitive" + $method = $input_data | Select-Object "method" + $params = $input_data.params + $primitive = $input_data.params.value.$prim + $prim_type = "" + $len = 0 + + if (![String]::IsNullOrEmpty($input_data)) { + Write-TraceMessage "FJSON" $input_data + } + if (![String]::IsNullOrEmpty($method)) { + Write-TraceMessage "FMETHOD" $method + } + if (![String]::IsNullOrEmpty($params)) { + Write-TraceMessage "FPARAMS" $params + } + if (![String]::IsNullOrEmpty($primitive)) { + Write-TraceMessage "FPRIMITIVE" $primitive + # $prim_type = $primitive | Get-Member -MemberType NoteProperty | Select-Object Name + # switch ($prim_type.Name) { + # 'String' { $data.params.value.$prim.String } + # 'Int' { $data.params.value.$prim.Int } + # Default { "none-found" } + # } + } + + $prim_type = $primitive | Get-Member -MemberType NoteProperty | Select-Object Name + switch ($prim_type.Name) { + 'String' { $len = $input_data.params.value.$prim.String.Length } + 'Int' { $input_data.params.value.$prim.Int } + Default { $len = 0 } + } + + #Fake it til you make it + # $response = '{ "jsonrpc": "2.0", "method": "response", "params": { "Ok": [ { "Ok": { "Value": { "value": { "Primitive": { "Int": 9 } }, "tag": { "anchor": null, "span": { "end": 2, "start": 0 } } } } } ] } }' + # Write-Host $response + # $response = '{ "jsonrpc": "2.0", "method": "response", "params": { "Ok": [ { "Ok": { "Value": { "value": { "Primitive": { "Int": 3 } }, "tag": { "anchor": null, "span": { "start": 0, "end": 2 } } } } } ] } }' + # Write-Host $response + + $json_obj = [ordered]@{ + jsonrpc = "2.0" + method = "response" + params = [ordered]@{ + Ok = @( + [ordered]@{ + Ok = [ordered]@{ + Value = [ordered]@{ + value = [ordered]@{ + Primitive = [ordered]@{ + Int = $len + } + } + tag = [ordered]@{ + anchor = $null + span = @{ + end = 2 + start = 0 + } + } + } + } + } + ) + } + } + $response = $json_obj | ConvertTo-Json -Depth 100 -Compress + # Write-TraceMessage "RESULT" $($json_obj | ConvertTo-Json -Depth 100 -Compress) + Write-Host $response + + return +} + +function end_filter { + $response = '{"jsonrpc":"2.0","method":"response","params":{"Ok":[]}}' + Write-Host $response + return +} + +function Write-TraceMessage { + Param + ( + [Parameter(Mandatory = $false, Position = 0)] + [string] $label, + [Parameter(Mandatory = $false, Position = 1)] + [string] $message + ) + + [Console]::Error.WriteLine("$($label) $($message)") +} + +function run_loop { + param($data) + # param( + # [Parameter( + # Position = 0, + # Mandatory = $true, + # ValueFromPipeline = $true, + # ValueFromPipelineByPropertyName = $true) + # ] + # [Alias('thedata')] + # $data + # ) + $prim = "Primitive" + $method = $data | Select-Object "method" + $params = $data.params + $primitive = $data.params.value.$prim + # $prim_type = "" + # Write out some debug trace messages + if (![String]::IsNullOrEmpty($data)) { + Write-TraceMessage "JSON" $data + } + if (![String]::IsNullOrEmpty($method)) { + Write-TraceMessage "METHOD" $method + } + if (![String]::IsNullOrEmpty($params)) { + Write-TraceMessage "PARAMS" $params + } + if (![String]::IsNullOrEmpty($primitive)) { + Write-TraceMessage "PRIMITIVE" $primitive + # $prim_type = $primitive | Get-Member -MemberType NoteProperty | Select-Object Name + # switch ($prim_type.Name) { + # 'String' { $data.params.value.$prim.String } + # 'Int' { $data.params.value.$prim.Int } + # Default { "none-found" } + # } + } + + + if ($method[0].method -eq "config") { + # Write-TraceMessage "Received config method with: " $data + return config + } + elseif ($method[0].method -eq "begin_filter") { + return begin_filter + } + elseif ($method[0].method -eq "end_filter") { + return end_filter + } + elseif ($method[0].method -eq "filter") { + # return run_filter -piped $params + return run_filter -input_data $data + } +} + +function Get-PipedData { + param( + [Parameter( + Position = 0, + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true) + ] + [Alias('piped')] + [String]$piped_input + ) + + process { + # Write-Error $piped_input + Write-TraceMessage "BeforeJSON" $piped_input + $json = ConvertFrom-Json $piped_input + run_loop -data $json + } +} + +# $Input | % { Write-Host PSInput $_ } +# $Input | ForEach-Object { +# $json = ConvertFrom-Json $_ +# $method = $json -replace "`n", ", " | Select-Object "method" +# $params = $json -replace "`n", ", " | Select-Object "params" +# if ($method[0].method -eq "config") { +# config +# } elseif ($method[0].method -eq "begin_filter") { +# begin_filter +# } elseif ($method[0].method -eq "end_filter") { +# end_filter +# } elseif ($method[0].method -eq "filter") { +# run_filter -params $params +# } +# } + +# $prim = "Primitive" +# $j = $json | ConvertFrom-Json +# $j.params.value.$prim +# String +# ------ +# 123 + +# $Input | Get-PipedData +$Input | ForEach-Object { $_ | Get-PipedData } \ No newline at end of file