diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 0000000000..f112c225a7 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,33 @@ +# image_ps +# +# VERSION 0.0.2 + +FROM ubuntu:14.04 +MAINTAINER Peter Honeder + +RUN apt-get update +RUN apt-get install -y openssh-server wget libstdc++6 libunwind8 +RUN mkdir /var/run/sshd +#RUN echo 'root:asldkfj23l4kj2lkajsdf' | chpasswd +RUN echo 'root:pass' | chpasswd +RUN adduser --shell /opt/app_base/runps.sh --disabled-password --gecos "" test1 +RUN echo 'test1:pass' | chpasswd +RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config + +RUN cd /root && wget https://github.com/anilgulecha/shellinabox/releases/download/2.14.2/shellinabox_2.14-1_amd64.deb +RUN dpkg -i /root/shellinabox_2.14-1_amd64.deb + +# SSH login fix. Otherwise user is kicked off after login +RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd + +ENV NOTVISIBLE "in users profile" +RUN echo "export VISIBLE=now" >> /etc/profile + +COPY exec_env/app_base/ /opt/app_base/ +RUN chmod -R ugo+rX /opt/app_base + +EXPOSE 22 +EXPOSE 4201 +#CMD /usr/sbin/sshd && /usr/bin/shellinaboxd -s /:AUTH:HOME:/home/test1/app_base/runps.sh -p 4201 -t -v +CMD /usr/sbin/sshd && /usr/bin/shellinaboxd -s /:LOGIN -p 4201 -t -v + diff --git a/scripts/Makefile b/scripts/Makefile index 339a696628..b18b43dafd 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -21,6 +21,9 @@ include platform.mk # builds unit tests include tests.mk +# powershell-run is the main powershell executable +include powershell-run.mk + # These are the main PS dlls: # - System.Management.Automation.dll (the main PS dll) # - commands/modules (they contain cmdlets): management and utility @@ -54,7 +57,7 @@ SRCS_ALL=${STRING_RESOURCES} ${SRCS} CSC=mono Microsoft.Net.ToolsetCompilers.*/tools/csc.exe MCS=mcs -all: dotnetlibs/System.Management.Automation.dll dotnetlibs/powershell-run.exe dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/Microsoft.PowerShell.Commands.Utility.dll dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) +all: dotnetlibs/System.Management.Automation.dll $(POWERSHELL_RUN_TARGETS) dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/Microsoft.PowerShell.Commands.Utility.dll dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) # this is the build rule for SMA.dll dotnetlibs/System.Management.Automation.dll: ${SYS_AUTO_SRCS} dotnetlibs/Microsoft.Management.Infrastructure.dll ../src/assembly-info/System.Management.Automation.assembly-info.cs dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) $(PLATFORM_SRCS) ${SYS_AUTO_RES_SRCS} ${SYS_AUTO_RES_CS_SRCS} @@ -98,9 +101,6 @@ TypeCatalogGen.exe: ../src/monad/monad/nttargets/assemblies/core/PSAssemblyLoadC CorePsTypeCatalog.cs: powershell-linux.inc TypeCatalogGen.exe System.Reflection.Metadata.dll System.Collections.Immutable.dll LD_LIBRARY_PATH=. mono TypeCatalogGen.exe powershell-linux.inc $@ $(MONAD_EXT)/coreclr/TargetingPack -dotnetlibs/powershell-run.exe: ../src/powershell-run/powershell-run.cs dotnetlibs/System.Management.Automation.dll dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) - $(CSC) -out:$@ -noconfig -nostdlib -target:exe -r:dotnetlibs/System.Management.Automation.dll -r:dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) ${COREREF} ../src/powershell-run/powershell-run.cs - # the pinvoke library libps.so libps-build/Makefile: ../src/monad-native/src/CMakeLists.txt @@ -137,7 +137,7 @@ prepare: nuget install Microsoft.Net.ToolsetCompilers -pre # this is an internal target, it's not intended to be called manually -internal-prepare-exec_env: +internal-prepare-exec_env: runps.sh rm -rf exec_env mkdir exec_env cp -r ../src/monad_app_base/app_base exec_env @@ -145,17 +145,23 @@ internal-prepare-exec_env: cp -r dotnetlibs/*.exe exec_env/app_base cp -r dotnetlibs/lib* exec_env/app_base cp -r dotnetlibs/corerun exec_env/app_base + cp runps.sh exec_env/app_base -run: dotnetlibs/powershell-run.exe dotnetlibs/corerun dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/Microsoft.PowerShell.Commands.Utility.dll dotnetlibs/api-ms-win-core-registry-l1-1-0.dll internal-prepare-exec_env +run: $(POWERSHELL_RUN_TARGETS) dotnetlibs/corerun dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/Microsoft.PowerShell.Commands.Utility.dll dotnetlibs/api-ms-win-core-registry-l1-1-0.dll internal-prepare-exec_env # check if corerun is the right one (could be the debug version) if ! diff dotnetlibs/corerun $(MONAD_EXT)/coreclr/Release/corerun; then cp -r $(MONAD_EXT)/coreclr/Release/* dotnetlibs; fi #cd exec_env/app_base && PSMODULEPATH=. LD_LIBRARY_PATH=. ./corerun powershell-run.exe "\"test blah\"" #cd exec_env/app_base && PSMODULEPATH=. LD_LIBRARY_PATH=. ./corerun powershell-run.exe "get-module -listavailable" #cd exec_env/app_base && PSMODULEPATH=. LD_LIBRARY_PATH=. ./corerun powershell-run.exe "get-module -listavailable -all" #cd exec_env/app_base && PSMODULEPATH=$(shell pwd)/exec_env/app_base/Modules LD_LIBRARY_PATH=. ./corerun powershell-run.exe "import-module -name Microsoft.PowerShell.Utility" - cd exec_env/app_base && PSMODULEPATH=$(shell pwd)/exec_env/app_base/Modules LD_LIBRARY_PATH=. ./corerun powershell-run.exe "import-module -name Microsoft.PowerShell.Utility" '"a","b","c","a","a" | Select-Object -Unique' + cd exec_env/app_base && PSMODULEPATH=$(shell pwd)/exec_env/app_base/Modules LD_LIBRARY_PATH=. ./corerun powershell-simple.exe "import-module -name Microsoft.PowerShell.Utility" '"a","b","c","a","a" | Select-Object -Unique' #LD_LIBRARY_PATH=exec_env/app_base exec_env/app_base/corerun exec_env/app_base/powershell-run.exe "\"Hello World!\" |out-default" +run-interactive: $(POWERSHELL_RUN_TARGETS) dotnetlibs/corerun dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/Microsoft.PowerShell.Commands.Utility.dll dotnetlibs/api-ms-win-core-registry-l1-1-0.dll internal-prepare-exec_env + # check if corerun is the right one (could be the debug version) + if ! diff dotnetlibs/corerun $(MONAD_EXT)/coreclr/Release/corerun; then cp -r $(MONAD_EXT)/coreclr/Release/* dotnetlibs; fi + cd exec_env/app_base && ./runps.sh + test: dotnetlibs/ps_test_runner.exe dotnetlibs/corerun dotnetlibs/api-ms-win-core-registry-l1-1-0.dll cd dotnetlibs && LD_LIBRARY_PATH=. ./corerun ps_test_runner.exe @@ -184,3 +190,17 @@ clean: cleanall: clean rm -rf System.Reflection.Metadata.* System.Collections.Immutable.* +docker-build: + sudo docker build --no-cache=false -t image_ps . + $(eval INSTANCE_ID := $(shell sudo docker inspect -f '{{.Id}}' image_ps)) + rm -rf + +docker-run: docker-build + sudo docker run -d -P --name test_ps image_ps + sudo docker port test_ps 22 + sudo docker port test_ps 4201 + +docker-stop: + sudo docker stop test_ps + sudo docker rm test_ps + diff --git a/scripts/powershell-run.mk b/scripts/powershell-run.mk new file mode 100644 index 0000000000..31cca1edd4 --- /dev/null +++ b/scripts/powershell-run.mk @@ -0,0 +1,22 @@ +# This is the makefile for all kinds of powershell hosts +# +# Currently there is powershell-run.exe, which is a very generic interactive and +# non-interactive host +# + +POWERSHELL_RUN_FOLDER=../src/powershell-run +POWERSHELL_RUN_SRCS=$(addprefix $(POWERSHELL_RUN_FOLDER)/, main.cs host.cs ui.cs rawui.cs readline.cs) + +# direct dependencies to be linked in +POWERSHELL_RUN_DEPS=dotnetlibs/System.Management.Automation.dll dotnetlibs/Microsoft.PowerShell.Commands.Management.dll dotnetlibs/$(ASSEMBLY_LOAD_CONTEXT_TARGET) +POWERSHELL_RUN_REFS=$(addprefix -r:,$(POWERSHELL_RUN_DEPS)) + +POWERSHELL_RUN_TARGETS=dotnetlibs/powershell-run.exe dotnetlibs/powershell-simple.exe + +dotnetlibs/powershell-run.exe: $(POWERSHELL_RUN_SRCS) $(POWERSHELL_RUN_DEPS) + $(CSC) -out:$@ -noconfig -nostdlib -target:exe $(POWERSHELL_RUN_REFS) $(COREREF) $(POWERSHELL_RUN_SRCS) + +dotnetlibs/powershell-simple.exe: $(POWERSHELL_RUN_FOLDER)/powershell-run.cs $(POWERSHELL_RUN_DEPS) + $(CSC) -out:$@ -noconfig -nostdlib -target:exe $(POWERSHELL_RUN_REFS) $(COREREF) $(POWERSHELL_RUN_FOLDER)/powershell-run.cs + + diff --git a/scripts/runps.sh b/scripts/runps.sh new file mode 100755 index 0000000000..6845d1795a --- /dev/null +++ b/scripts/runps.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPTDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +cd $SCRIPTDIR +echo "launching PS now" +PSMODULEPATH=$SCRIPTDIR/Modules LD_LIBRARY_PATH=. ./corerun powershell-run.exe + diff --git a/src/monad-ext b/src/monad-ext index 9fb89ce1df..4bb973e987 160000 --- a/src/monad-ext +++ b/src/monad-ext @@ -1 +1 @@ -Subproject commit 9fb89ce1df302c38fff19acc47f8974d495b6814 +Subproject commit 4bb973e9877ec10536ecc6868c251d11ca606226 diff --git a/src/powershell-run/host.cs b/src/powershell-run/host.cs new file mode 100644 index 0000000000..0e8ecb96d3 --- /dev/null +++ b/src/powershell-run/host.cs @@ -0,0 +1,217 @@ +namespace Microsoft.Samples.PowerShell.Host +{ + using System; + using System.Globalization; + using System.Management.Automation.Host; + using System.Management.Automation.Runspaces; + + /// + /// This is a sample implementation of the PSHost abstract class for + /// console applications. Not all members are implemented. Those that + /// are not implemented throw a NotImplementedException exception or + /// return nothing. + /// + internal class MyHost : PSHost, IHostSupportsInteractiveSession + { + public MyHost(PSListenerConsoleSample program) + { + this.program = program; + } + + /// + /// A reference to the PSHost implementation. + /// + private PSListenerConsoleSample program; + + /// + /// The culture information of the thread that created + /// this object. + /// + private CultureInfo originalCultureInfo = CultureInfo.CurrentCulture; + + /// + /// The UI culture information of the thread that created + /// this object. + /// + private CultureInfo originalUICultureInfo = CultureInfo.CurrentCulture; + + /// + /// The identifier of this PSHost implementation. + /// + private static Guid instanceId = Guid.NewGuid(); + + /// + /// A reference to the implementation of the PSHostUserInterface + /// class for this application. + /// + private MyHostUserInterface myHostUserInterface = new MyHostUserInterface(); + + /// + /// A reference to the runspace used to start an interactive session. + /// + public Runspace pushedRunspace = null; + + + /// + /// Gets the culture information to use. This implementation + /// returns a snapshot of the culture information of the thread + /// that created this object. + /// + public override CultureInfo CurrentCulture + { + get { return this.originalCultureInfo; } + } + + /// + /// Gets the UI culture information to use. This implementation + /// returns a snapshot of the UI culture information of the thread + /// that created this object. + /// + public override CultureInfo CurrentUICulture + { + get { return this.originalUICultureInfo; } + } + + /// + /// Gets an identifier for this host. This implementation always + /// returns the GUID allocated at instantiation time. + /// + public override Guid InstanceId + { + get { return instanceId; } + } + + /// + /// Gets a string that contains the name of this host implementation. + /// Keep in mind that this string may be used by script writers to + /// identify when your host is being used. + /// + public override string Name + { + get { return "MySampleConsoleHostImplementation"; } + } + + /// + /// Gets an instance of the implementation of the PSHostUserInterface + /// class for this application. This instance is allocated once at startup time + /// and returned every time thereafter. + /// + public override PSHostUserInterface UI + { + get { return this.myHostUserInterface; } + } + + /// + /// Gets the version object for this application. Typically this + /// should match the version resource in the application. + /// + public override Version Version + { + get { return new Version(1, 0, 0, 0); } + } + +#region IHostSupportsInteractiveSession Properties + + /// + /// Gets a value indicating whether a request + /// to open a PSSession has been made. + /// + public bool IsRunspacePushed + { + get { return this.pushedRunspace != null; } + } + + /// + /// Gets or sets the runspace used by the PSSession. + /// + public Runspace Runspace + { + get { return this.program.myRunSpace; } + internal set { this.program.myRunSpace = value; } + } +#endregion IHostSupportsInteractiveSession Properties + + /// + /// This API Instructs the host to interrupt the currently running + /// pipeline and start a new nested input loop. In this example this + /// functionality is not needed so the method throws a + /// NotImplementedException exception. + /// + public override void EnterNestedPrompt() + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This API instructs the host to exit the currently running input loop. + /// In this example this functionality is not needed so the method + /// throws a NotImplementedException exception. + /// + public override void ExitNestedPrompt() + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This API is called before an external application process is + /// started. Typically it is used to save state so that the parent + /// can restore state that has been modified by a child process (after + /// the child exits). In this example this functionality is not + /// needed so the method returns nothing. + /// + public override void NotifyBeginApplication() + { + return; + } + + /// + /// This API is called after an external application process finishes. + /// Typically it is used to restore state that a child process has + /// altered. In this example, this functionality is not needed so + /// the method returns nothing. + /// + public override void NotifyEndApplication() + { + return; + } + + /// + /// Indicate to the host application that exit has + /// been requested. Pass the exit code that the host + /// application should use when exiting the process. + /// + /// The exit code that the + /// host application should use. + public override void SetShouldExit(int exitCode) + { + this.program.ShouldExit = true; + this.program.ExitCode = exitCode; + } + +#region IHostSupportsInteractiveSession Methods + + /// + /// Requests to close a PSSession. + /// + public void PopRunspace() + { + Runspace = this.pushedRunspace; + this.pushedRunspace = null; + } + + /// + /// Requests to open a PSSession. + /// + /// Runspace to use. + public void PushRunspace(Runspace runspace) + { + this.pushedRunspace = Runspace; + Runspace = runspace; + } + +#endregion IHostSupportsInteractiveSession Methods + } +} + diff --git a/src/powershell-run/main.cs b/src/powershell-run/main.cs new file mode 100644 index 0000000000..9b16bdbe9b --- /dev/null +++ b/src/powershell-run/main.cs @@ -0,0 +1,366 @@ +namespace Microsoft.Samples.PowerShell.Host +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Management.Automation.Runspaces; + using System.Text; + using PowerShell = System.Management.Automation.PowerShell; + + /// + /// This sample shows how to implement a basic read-evaluate-print + /// loop (or 'listener') that allowing you to interactively work + /// with the Windows PowerShell engine. + /// + internal class PSListenerConsoleSample + { + /// + /// Used to read user input. + /// + internal ConsoleReadLine consoleReadLine = new ConsoleReadLine(); + + /// + /// Holds a reference to the runspace for this interpeter. + /// + internal Runspace myRunSpace; + + /// + /// Indicator to tell the host application that it should exit. + /// + private bool shouldExit; + + /// + /// The exit code that the host application will use to exit. + /// + private int exitCode; + + /// + /// Holds a reference to the PSHost implementation for this interpreter. + /// + private MyHost myHost; + + /// + /// Holds a reference to the currently executing pipeline so that it can be + /// stopped by the control-C handler. + /// + private PowerShell currentPowerShell; + + /// + /// Used to serialize access to instance data. + /// + private object instanceLock = new object(); + + /// + /// Gets or sets a value indicating whether the host application + /// should exit. + /// + public bool ShouldExit + { + get { return this.shouldExit; } + set { this.shouldExit = value; } + } + + /// + /// Gets or sets a value indicating whether the host application + /// should exit. + /// + public int ExitCode + { + get { return this.exitCode; } + set { this.exitCode = value; } + } + + public static void init() + { + string psBasePath = System.IO.Directory.GetCurrentDirectory(); + PowerShellAssemblyLoadContextInitializer.SetPowerShellAssemblyLoadContext(psBasePath); + } + + /// + /// Creates and initiates the listener instance. + /// + /// This parameter is not used. + private static void Main(string[] args) + { + init(); + + // Display the welcome message. + Console.WriteLine(); + Console.WriteLine("PowerShell for Linux interactive console"); + Console.WriteLine("========================================"); + Console.WriteLine(); + Console.WriteLine("Current status:"); + Console.WriteLine("- Type 'exit' to exit"); + Console.WriteLine("- Utility and Management cmdlet modules are loadable"); + Console.WriteLine(); + //Console.ForegroundColor = oldFg; + + // Create the listener and run it. This method never returns. + PSListenerConsoleSample listener = new PSListenerConsoleSample(); + listener.Run(); + } + + /// + /// Initializes a new instance of the PSListenerConsoleSample class. + /// + public PSListenerConsoleSample() + { + // Create the host and runspace instances for this interpreter. + // Note that this application does not support console files so + // only the default snap-ins will be available. + this.myHost = new MyHost(this); + InitialSessionState iss = InitialSessionState.CreateDefault2(); + this.myRunSpace = RunspaceFactory.CreateRunspace(this.myHost,iss); + this.myRunSpace.Open(); + + // Create a PowerShell object to run the commands used to create + // $profile and load the profiles. + lock (this.instanceLock) + { + this.currentPowerShell = PowerShell.Create(); + } + + try + { + this.currentPowerShell.Runspace = this.myRunSpace; + +/* PSCommand[] profileCommands = Microsoft.Samples.PowerShell.Host.HostUtilities.GetProfileCommands("SampleHost06"); + foreach (PSCommand command in profileCommands) + { + this.currentPowerShell.Commands = command; + this.currentPowerShell.Invoke(); + }*/ + } + finally + { + // Dispose the PowerShell object and set currentPowerShell + // to null. It is locked because currentPowerShell may be + // accessed by the ctrl-C handler. + lock (this.instanceLock) + { + this.currentPowerShell.Dispose(); + this.currentPowerShell = null; + } + } + } + + /// + /// A helper class that builds and executes a pipeline that writes + /// to the default output path. Any exceptions that are thrown are + /// just passed to the caller. Since all output goes to the default + /// outter, this method does not return anything. + /// + /// The script to run. + /// Any input arguments to pass to the script. + /// If null then nothing is passed in. + private void executeHelper(string cmd, object input) + { + // Ignore empty command lines. + if (String.IsNullOrEmpty(cmd)) + { + return; + } + + // Create the pipeline object and make it available to the + // ctrl-C handle through the currentPowerShell instance + // variable. + lock (this.instanceLock) + { + this.currentPowerShell = PowerShell.Create(); + } + + // Add a script and command to the pipeline and then run the pipeline. Place + // the results in the currentPowerShell variable so that the pipeline can be + // stopped. + try + { + this.currentPowerShell.Runspace = this.myRunSpace; + + this.currentPowerShell.AddScript(cmd); + + // Add the default outputter to the end of the pipe and then call the + // MergeMyResults method to merge the output and error streams from the + // pipeline. This will result in the output being written using the PSHost + // and PSHostUserInterface classes instead of returning objects to the host + // application. + this.currentPowerShell.AddCommand("out-default"); + this.currentPowerShell.Commands.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output); + + // If there is any input pass it in, otherwise just invoke the + // the pipeline. + if (input != null) + { + this.currentPowerShell.Invoke(new object[] { input }); + } + else + { + this.currentPowerShell.Invoke(); + } + } + finally + { + // Dispose the PowerShell object and set currentPowerShell to null. + // It is locked because currentPowerShell may be accessed by the + // ctrl-C handler. + lock (this.instanceLock) + { + this.currentPowerShell.Dispose(); + this.currentPowerShell = null; + } + } + } + + /// + /// To display an exception using the display formatter, + /// run a second pipeline passing in the error record. + /// The runtime will bind this to the $input variable, + /// which is why $input is being piped to the Out-String + /// cmdlet. The WriteErrorLine method is called to make sure + /// the error gets displayed in the correct error color. + /// + /// The exception to display. + private void ReportException(Exception e) + { + if (e != null) + { + object error; + IContainsErrorRecord icer = e as IContainsErrorRecord; + if (icer != null) + { + error = icer.ErrorRecord; + } + else + { + error = (object)new ErrorRecord(e, "Host.ReportException", ErrorCategory.NotSpecified, null); + } + + lock (this.instanceLock) + { + this.currentPowerShell = PowerShell.Create(); + } + + this.currentPowerShell.Runspace = this.myRunSpace; + + try + { + this.currentPowerShell.AddScript("$input").AddCommand("out-string"); + + // Do not merge errors, this function will swallow errors. + Collection result; + PSDataCollection inputCollection = new PSDataCollection(); + inputCollection.Add(error); + inputCollection.Complete(); + result = this.currentPowerShell.Invoke(inputCollection); + + if (result.Count > 0) + { + string str = result[0].BaseObject as string; + if (!string.IsNullOrEmpty(str)) + { + // Remove \r\n, which is added by the Out-String cmdlet. + this.myHost.UI.WriteErrorLine(str.Substring(0, str.Length - 2)); + } + } + } + finally + { + // Dispose of the pipeline and set it to null, locking it because + // currentPowerShell may be accessed by the ctrl-C handler. + lock (this.instanceLock) + { + this.currentPowerShell.Dispose(); + this.currentPowerShell = null; + } + } + } + } + + /// + /// Basic script execution routine. Any runtime exceptions are + /// caught and passed back to the Windows PowerShell engine to + /// display. + /// + /// Script to run. + private void Execute(string cmd) + { + try + { + // Run the command with no input. + this.executeHelper(cmd, null); + } + catch (RuntimeException rte) + { + this.ReportException(rte); + } + } + + /// + /// Method used to handle control-C's from the user. It calls the + /// pipeline Stop() method to stop execution. If any exceptions occur + /// they are printed to the console but otherwise ignored. + /// + /// See sender property documentation of + /// ConsoleCancelEventHandler. + /// See e property documentation of + /// ConsoleCancelEventHandler. + private void HandleControlC(object sender, ConsoleCancelEventArgs e) + { + Console.WriteLine("HandleControlC"); + try + { + lock (this.instanceLock) + { + if (this.currentPowerShell != null && this.currentPowerShell.InvocationStateInfo.State == PSInvocationState.Running) + { + this.currentPowerShell.Stop(); + } + } + + e.Cancel = true; + } + catch (Exception exception) + { + this.myHost.UI.WriteErrorLine(exception.ToString()); + } + } + + /// + /// Implements the basic listener loop. It sets up the ctrl-C handler, then + /// reads a command from the user, executes it and repeats until the ShouldExit + /// flag is set. + /// + private void Run() + { + // Set up the control-C handler. + Console.CancelKeyPress += new ConsoleCancelEventHandler(this.HandleControlC); + //Console.TreatControlCAsInput = false; + + // Read commands and run them until the ShouldExit flag is set by + // the user calling "exit". + while (!this.ShouldExit) + { + string prompt; + if (this.myHost.IsRunspacePushed) + { + prompt = string.Format("\n[{0}] PSConsoleSample: ", this.myRunSpace.ConnectionInfo.ComputerName); + } + else + { + prompt = "\nPSConsoleSample: "; + } + + this.myHost.UI.Write(ConsoleColor.White, ConsoleColor.Black, prompt); +// string cmd = this.consoleReadLine.Read(); + string cmd = Console.ReadLine(); + this.Execute(cmd); + } + + // Exit with the desired exit code that was set by the exit command. + // The exit code is set in the host by the MyHost.SetShouldExit() method. + //Environment.Exit(this.ExitCode); + } + } +} + diff --git a/src/powershell-run/rawui.cs b/src/powershell-run/rawui.cs new file mode 100644 index 0000000000..97d93e5bc3 --- /dev/null +++ b/src/powershell-run/rawui.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Management.Automation.Host; +using System.Globalization; +using System.Reflection; + +namespace Microsoft.Samples.PowerShell.Host +{ + // this is all from https://msdn.microsoft.com/en-us/library/ee706570%28v=vs.85%29.aspx + + internal class MyRawUserInterface : PSHostRawUserInterface + { + /// + /// Gets or sets the background color of the displayed text. + /// This maps to the corresponding Console.Background property. + /// + public override ConsoleColor BackgroundColor + { + get { return Console.BackgroundColor; } + set { Console.BackgroundColor = value; } + } + + /// + /// Gets or sets the size of the host buffer. In this example the + /// buffer size is adapted from the Console buffer size members. + /// + public override Size BufferSize + { + get { return new Size(200,500); } + set { } + //get { return new Size(Console.BufferWidth, Console.BufferHeight); } + //set { Console.SetBufferSize(value.Width, value.Height); } + } + + /// + /// Gets or sets the cursor position. In this example this + /// functionality is not needed so the property throws a + /// NotImplementException exception. + /// + public override Coordinates CursorPosition + { + get { throw new NotImplementedException( + "The method or operation is not implemented."); } + set { throw new NotImplementedException( + "The method or operation is not implemented."); } + } + + /// + /// Gets or sets the size of the displayed cursor. In this example + /// the cursor size is taken directly from the Console.CursorSize + /// property. + /// + public override int CursorSize + { + get { return 12; } + set { } + //get { return Console.CursorSize; } + //set { Console.CursorSize = value; } + } + + /// + /// Gets or sets the foreground color of the displayed text. + /// This maps to the corresponding Console.ForgroundColor property. + /// + public override ConsoleColor ForegroundColor + { + get { return Console.ForegroundColor; } + set { Console.ForegroundColor = value; } + } + + /// + /// Gets a value indicating whether the user has pressed a key. This maps + /// to the corresponding Console.KeyAvailable property. + /// + public override bool KeyAvailable + { + get { return false; } + // get { return Console.KeyAvailable; } + } + + /// + /// Gets the dimensions of the largest window that could be + /// rendered in the current display, if the buffer was at the least + /// that large. This example uses the Console.LargestWindowWidth and + /// Console.LargestWindowHeight properties to determine the returned + /// value of this property. + /// + public override Size MaxPhysicalWindowSize + { + // get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + get { return new Size(1024,768); } + } + + /// + /// Gets the dimentions of the largest window size that can be + /// displayed. This example uses the Console.LargestWindowWidth and + /// console.LargestWindowHeight properties to determine the returned + /// value of this property. + /// + public override Size MaxWindowSize + { + // get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); } + get { return new Size(1024,768); } + } + + /// + /// Gets or sets the position of the displayed window. This example + /// uses the Console window position APIs to determine the returned + /// value of this property. + /// + public override Coordinates WindowPosition + { + // get { return new Coordinates(Console.WindowLeft, Console.WindowTop); } + // set { Console.SetWindowPosition(value.X, value.Y); } + get { return new Coordinates(0,0); } + set { } + } + + /// + /// Gets or sets the size of the displayed window. This example + /// uses the corresponding Console window size APIs to determine the + /// returned value of this property. + /// + public override Size WindowSize + { + // get { return new Size(Console.WindowWidth, Console.WindowHeight); } + // set { Console.SetWindowSize(value.Width, value.Height); } + get { return new Size(1024,768); } + set { } + } + + /// + /// Gets or sets the title of the displayed window. The example + /// maps the Console.Title property to the value of this property. + /// + public override string WindowTitle + { + // get { return Console.Title; } + // set { Console.Title = value; } + get { return "window title"; } + set { } + } + + /// + /// This API resets the input buffer. In this example this + /// functionality is not needed so the method returns nothing. + /// + public override void FlushInputBuffer() + { + } + + /// + /// This API returns a rectangular region of the screen buffer. In + /// this example this functionality is not needed so the method throws + /// a NotImplementException exception. + /// + /// Defines the size of the rectangle. + /// Throws a NotImplementedException exception. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This API reads a pressed, released, or pressed and released keystroke + /// from the keyboard device, blocking processing until a keystroke is + /// typed that matches the specified keystroke options. In this example + /// this functionality is not needed so the method throws a + /// NotImplementException exception. + /// + /// Options, such as IncludeKeyDown, used when + /// reading the keyboard. + /// Throws a NotImplementedException exception. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This API crops a region of the screen buffer. In this example + /// this functionality is not needed so the method throws a + /// NotImplementException exception. + /// + /// The region of the screen to be scrolled. + /// The region of the screen to receive the + /// source region contents. + /// The region of the screen to include in the operation. + /// The character and attributes to be used to fill all cell. + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This method copies an array of buffer cells into the screen buffer + /// at a specified location. In this example this functionality is + /// not needed so the method throws a NotImplementedException exception. + /// + /// The parameter is not used. + /// The parameter is not used. + public override void SetBufferContents(Coordinates origin, + BufferCell[,] contents) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// This method copies a given character, foreground color, and background + /// color to a region of the screen buffer. In this example this + /// functionality is not needed so the method throws a + /// NotImplementException exception./// + /// Defines the area to be filled. + /// Defines the fill character. + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + } + +} + diff --git a/src/powershell-run/readline.cs b/src/powershell-run/readline.cs new file mode 100644 index 0000000000..b1893029e7 --- /dev/null +++ b/src/powershell-run/readline.cs @@ -0,0 +1,485 @@ +namespace Microsoft.Samples.PowerShell.Host +{ + using System; + using System.Collections.ObjectModel; + using System.Management.Automation; + using System.Text; + + /// + /// This class is used to read the command line and color the text as + /// it is entered. Tokens are determined using the PSParser.Tokenize + /// method. + /// + internal class ConsoleReadLine + { + /// + /// The buffer used to edit. + /// + private StringBuilder buffer = new StringBuilder(); + + /// + /// The position of the cursor within the buffer. + /// + private int current; + + /// + /// The count of characters in buffer rendered. + /// + private int rendered; + + /// + /// Store the anchor and handle cursor movement + /// + //private Cursor cursor; + + /// + /// The array of colors for tokens, indexed by PSTokenType + /// + private ConsoleColor[] tokenColors; + + /// + /// We do not pick different colors for every token, those tokens + /// use this default. + /// + private ConsoleColor defaultColor = ConsoleColor.White; + + /// + /// Initializes a new instance of the ConsoleReadLine class. + /// + public ConsoleReadLine() + { + this.tokenColors = new ConsoleColor[] + { + this.defaultColor, // Unknown + ConsoleColor.Yellow, // Command + ConsoleColor.Green, // CommandParameter + ConsoleColor.Cyan, // CommandArgument + ConsoleColor.Cyan, // Number + ConsoleColor.Cyan, // String + ConsoleColor.Green, // Variable + this.defaultColor, // Member + this.defaultColor, // LoopLabel + ConsoleColor.DarkYellow, // Attribute + ConsoleColor.DarkYellow, // Type + ConsoleColor.DarkCyan, // Operator + this.defaultColor, // GroupStart + this.defaultColor, // GroupEnd + ConsoleColor.Magenta, // Keyword + ConsoleColor.Red, // Comment + ConsoleColor.DarkCyan, // StatementSeparator + this.defaultColor, // NewLine + this.defaultColor, // LineContinuation + this.defaultColor, // Position + }; + } + + /// + /// Read a line of text, colorizing while typing. + /// + /// The command line read + public string Read() + { + this.Initialize(); + + while (true) + { + // no support for Console.ReadKey in CoreCLR yet + int key = Console.Read(); + char keyChar = Convert.ToChar(key); + this.Insert(keyChar); +/* + ConsoleKeyInfo key = Console.ReadKey(true); + + switch (key.Key) + { + case ConsoleKey.Backspace: + this.OnBackspace(); + break; + case ConsoleKey.Delete: + this.OnDelete(); + break; + case ConsoleKey.Enter: + return this.OnEnter(); + case ConsoleKey.RightArrow: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.LeftArrow: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.Escape: + this.OnEscape(); + break; + case ConsoleKey.Home: + this.OnHome(); + break; + case ConsoleKey.End: + this.OnEnd(); + break; + case ConsoleKey.UpArrow: + case ConsoleKey.DownArrow: + case ConsoleKey.LeftWindows: + case ConsoleKey.RightWindows: + // ignore these + continue; + + default: + if (key.KeyChar == '\x0D') + { + goto case ConsoleKey.Enter; // Ctrl-M + } + + if (key.KeyChar == '\x08') + { + goto case ConsoleKey.Backspace; // Ctrl-H + } + + this.Insert(key); + break; + }*/ + } + } + + /// + /// Initializes the buffer. + /// + private void Initialize() + { + this.buffer.Length = 0; + this.current = 0; + this.rendered = 0; + //this.cursor = new Cursor(); + } + + /// + /// Inserts a key. + /// + /// The key to insert. + private void Insert(char key) + { + this.buffer.Insert(this.current, key); + this.current++; + if (key == '\n') + this.Render(); + } + +/* + /// + /// The End key was enetered.. + /// + private void OnEnd() + { + this.current = this.buffer.Length; + this.cursor.Place(this.rendered); + } + + /// + /// The Home key was eneterd. + /// + private void OnHome() + { + this.current = 0; + this.cursor.Reset(); + } + + /// + /// The Escape key was enetered. + /// + private void OnEscape() + { + this.buffer.Length = 0; + this.current = 0; + this.Render(); + } + + /// + /// Moves to the left of the cursor position. + /// + /// Enumeration for Alt, Control, + /// and Shift keys. + private void OnLeft(ConsoleModifiers consoleModifiers) + { + if ((consoleModifiers & ConsoleModifiers.Control) != 0) + { + // Move back to the start of the previous word. + if (this.buffer.Length > 0 && this.current != 0) + { + bool nonLetter = IsSeperator(this.buffer[this.current - 1]); + while (this.current > 0 && (this.current - 1 < this.buffer.Length)) + { + this.MoveLeft(); + + if (IsSeperator(this.buffer[this.current]) != nonLetter) + { + if (!nonLetter) + { + this.MoveRight(); + break; + } + + nonLetter = false; + } + } + } + } + else + { + this.MoveLeft(); + } + } +*/ + + /// + /// Determines if a character is a seperator. + /// + /// Character to investigate. + /// A value that indicates whether the character + /// is a seperator. + private static bool IsSeperator(char ch) + { + return !Char.IsLetter(ch); + } +/* + /// + /// Moves to what is to the right of the cursor position. + /// + /// Enumeration for Alt, Control, + /// and Shift keys. + private void OnRight(ConsoleModifiers consoleModifiers) + { + if ((consoleModifiers & ConsoleModifiers.Control) != 0) + { + // Move to the next word. + if (this.buffer.Length != 0 && this.current < this.buffer.Length) + { + bool nonLetter = IsSeperator(this.buffer[this.current]); + while (this.current < this.buffer.Length) + { + this.MoveRight(); + + if (this.current == this.buffer.Length) + { + break; + } + + if (IsSeperator(this.buffer[this.current]) != nonLetter) + { + if (nonLetter) + { + break; + } + + nonLetter = true; + } + } + } + } + else + { + this.MoveRight(); + } + } + + /// + /// Moves the cursor one character to the right. + /// + private void MoveRight() + { + if (this.current < this.buffer.Length) + { + char c = this.buffer[this.current]; + this.current++; + Cursor.Move(1); + } + } + + /// + /// Moves the cursor one character to the left. + /// + private void MoveLeft() + { + if (this.current > 0 && (this.current - 1 < this.buffer.Length)) + { + this.current--; + char c = this.buffer[this.current]; + Cursor.Move(-1); + } + } + + /// + /// The Enter key was entered. + /// + /// A newline character. + private string OnEnter() + { + Console.Out.Write("\n"); + return this.buffer.ToString(); + } + + /// + /// The delete key was entered. + /// + private void OnDelete() + { + if (this.buffer.Length > 0 && this.current < this.buffer.Length) + { + this.buffer.Remove(this.current, 1); + this.Render(); + } + } + + /// + /// The Backspace key was entered. + /// + private void OnBackspace() + { + if (this.buffer.Length > 0 && this.current > 0) + { + this.buffer.Remove(this.current - 1, 1); + this.current--; + this.Render(); + } + } +*/ + /// + /// Displays the line. + /// + private void Render() + { + string text = this.buffer.ToString(); + + // The PowerShell tokenizer is used to decide how to colorize + // the input. Any errors in the input are returned in 'errors', + // but we won't be looking at those here. + Collection errors = null; + Collection tokens = PSParser.Tokenize(text, out errors); + + if (tokens.Count > 0) + { + // We can skip rendering tokens that end before the cursor. + int i; + for (i = 0; i < tokens.Count; ++i) + { + if (this.current >= tokens[i].Start) + { + break; + } + } + + // Place the cursor at the start of the first token to render. The + // last edit may require changes to the colorization of characters + // preceding the cursor. + //this.cursor.Place(tokens[i].Start); + + for (; i < tokens.Count; ++i) + { + // Write out the token. We don't use tokens[i].Content, instead we + // use the actual text from our input because the content sometimes + // excludes part of the token, e.g. the quote characters of a string. + Console.ForegroundColor = this.tokenColors[(int)tokens[i].Type]; + Console.Out.Write(text.Substring(tokens[i].Start, tokens[i].Length)); + + // Whitespace doesn't show up in the array of tokens. Write it out here. + if (i != (tokens.Count - 1)) + { + Console.ForegroundColor = this.defaultColor; + for (int j = (tokens[i].Start + tokens[i].Length); j < tokens[i + 1].Start; ++j) + { + Console.Out.Write(text[j]); + } + } + } + + // It's possible there is text left over to output. This happens when there is + // some error during tokenization, e.g. a string literal is missing a closing quote. + Console.ForegroundColor = this.defaultColor; + for (int j = tokens[i - 1].Start + tokens[i - 1].Length; j < text.Length; ++j) + { + Console.Out.Write(text[j]); + } + } + else + { + // If tokenization completely failed, just redraw the whole line. This + // happens most frequently when the first token is incomplete, like a string + // literal missing a closing quote. + //this.cursor.Reset(); + Console.Out.Write(text); + } + + // If characters were deleted, we must write over previously written characters + if (text.Length < this.rendered) + { + Console.Out.Write(new string(' ', this.rendered - text.Length)); + } + + this.rendered = text.Length; + //this.cursor.Place(this.current); + } + +/* + /// + /// A helper class for maintaining the cursor while editing the command line. + /// + internal class Cursor + { + /// + /// The top anchor for reposition the cursor. + /// + private int anchorTop; + + /// + /// The left anchor for repositioning the cursor. + /// + private int anchorLeft; + + /// + /// Initializes a new instance of the Cursor class. + /// + public Cursor() + { + this.anchorTop = Console.CursorTop; + this.anchorLeft = Console.CursorLeft; + } + + /// + /// Moves the cursor. + /// + /// The number of characters to move. + internal static void Move(int delta) + { + int position = Console.CursorTop * Console.BufferWidth + Console.CursorLeft + delta; + + Console.CursorLeft = position % Console.BufferWidth; + Console.CursorTop = position / Console.BufferWidth; + } + + /// + /// Resets the cursor position. + /// + internal void Reset() + { + Console.CursorTop = this.anchorTop; + Console.CursorLeft = this.anchorLeft; + } + + /// + /// Moves the cursor to a specific position. + /// + /// The new position. + internal void Place(int position) + { + Console.CursorLeft = (this.anchorLeft + position) % Console.BufferWidth; + int cursorTop = this.anchorTop + (this.anchorLeft + position) / Console.BufferWidth; + if (cursorTop >= Console.BufferHeight) + { + this.anchorTop -= cursorTop - Console.BufferHeight + 1; + cursorTop = Console.BufferHeight - 1; + } + + Console.CursorTop = cursorTop; + } + } // End Cursor + */ + } +} + diff --git a/src/powershell-run/ui.cs b/src/powershell-run/ui.cs new file mode 100644 index 0000000000..f30aa544a0 --- /dev/null +++ b/src/powershell-run/ui.cs @@ -0,0 +1,509 @@ +namespace Microsoft.Samples.PowerShell.Host +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Text; + + /// + /// A sample implementation of the PSHostUserInterface abstract class for + /// console applications. Not all members are implemented. Those that are + /// not implemented throw a NotImplementedException exception or return + /// nothing. Members that are implemented include those that map easily to + /// Console APIs and a basic implementation of the prompt API provided. + /// + internal class MyHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection + { + /// + /// A reference to the PSRawUserInterface implementation. + /// + private MyRawUserInterface myRawUi = new MyRawUserInterface(); + + /// + /// Gets an instance of the PSRawUserInterface class for this host + /// application. + /// + public override PSHostRawUserInterface RawUI + { + get { return this.myRawUi; } + } + + /// + /// Prompts the user for input. + /// The caption or title of the prompt. + /// The text of the prompt. + /// A collection of FieldDescription objects + /// that describe each field of the prompt. + /// A dictionary object that contains the results of the user + /// prompts. + public override Dictionary Prompt( + string caption, + string message, + Collection descriptions) + { + this.Write( + ConsoleColor.White, + ConsoleColor.Black, + caption + "\n" + message + " "); + Dictionary results = + new Dictionary(); + foreach (FieldDescription fd in descriptions) + { + string[] label = GetHotkeyAndLabel(fd.Label); + this.WriteLine(label[1]); + string userData = Console.ReadLine(); + if (userData == null) + { + return null; + } + + results[fd.Name] = PSObject.AsPSObject(userData); + } + + return results; + } + + /// + + /// Provides a set of choices that enable the user to choose a + /// single option from a set of options. + /// + /// Text that proceeds (a title) the choices. + /// A message that describes the choice. + /// A collection of ChoiceDescription objects that + /// describ each choice. + /// The index of the label in the Choices + /// parameter collection. To indicate no default choice, set to -1. + /// The index of the Choices parameter collection element that + /// corresponds to the option that is selected by the user. + public override int PromptForChoice( + string caption, + string message, + Collection choices, + int defaultChoice) + { + // Write the caption and message strings in Blue. + this.WriteLine( + ConsoleColor.Blue, + ConsoleColor.Black, + caption + "\n" + message + "\n"); + + // Convert the choice collection into something that is + // easier to work with. See the BuildHotkeysAndPlainLabels + // method for details. + string[,] promptData = BuildHotkeysAndPlainLabels(choices); + + // Format the overall choice prompt string to display. + StringBuilder sb = new StringBuilder(); + for (int element = 0; element < choices.Count; element++) + { + sb.Append(String.Format( + CultureInfo.CurrentCulture, + "|{0}> {1} ", + promptData[0, element], + promptData[1, element])); + } + + sb.Append(String.Format( + CultureInfo.CurrentCulture, + "[Default is ({0}]", + promptData[0, defaultChoice])); + + // Read prompts until a match is made, the default is + // chosen, or the loop is interrupted with ctrl-C. + while (true) + { + this.WriteLine(ConsoleColor.Cyan, ConsoleColor.Black, sb.ToString()); + string data = Console.ReadLine().Trim().ToUpper(); + + // If the choice string was empty, use the default selection. + if (data.Length == 0) + { + return defaultChoice; + } + + // See if the selection matched and return the + // corresponding index if it did. + for (int i = 0; i < choices.Count; i++) + { + if (promptData[0, i] == data) + { + return i; + } + } + + this.WriteErrorLine("Invalid choice: " + data); + } + } + +#region IHostUISupportsMultipleChoiceSelection Members + + /// + /// Provides a set of choices that enable the user to choose a one or + /// more options from a set of options. + /// + /// Text that proceeds (a title) the choices. + /// A message that describes the choice. + /// A collection of ChoiceDescription objects that + /// describ each choice. + /// The index of the label in the Choices + /// parameter collection. To indicate no default choice, set to -1. + /// The index of the Choices parameter collection element that + /// corresponds to the option that is selected by the user. + public Collection PromptForChoice( + string caption, + string message, + Collection choices, + IEnumerable defaultChoices) + { + // Write the caption and message strings in Blue. + this.WriteLine( + ConsoleColor.Blue, + ConsoleColor.Black, + caption + "\n" + message + "\n"); + + // Convert the choice collection into something that is + // easier to work with. See the BuildHotkeysAndPlainLabels + // method for details. + string[,] promptData = BuildHotkeysAndPlainLabels(choices); + + // Format the overall choice prompt string to display. + StringBuilder sb = new StringBuilder(); + for (int element = 0; element < choices.Count; element++) + { + sb.Append(String.Format( + CultureInfo.CurrentCulture, + "|{0}> {1} ", + promptData[0, element], + promptData[1, element])); + } + + Collection defaultResults = new Collection(); + if (defaultChoices != null) + { + int countDefaults = 0; + foreach (int defaultChoice in defaultChoices) + { + ++countDefaults; + defaultResults.Add(defaultChoice); + } + + if (countDefaults != 0) + { + sb.Append(countDefaults == 1 ? "[Default choice is " : "[Default choices are "); + foreach (int defaultChoice in defaultChoices) + { + sb.AppendFormat( + CultureInfo.CurrentCulture, + "\"{0}\",", + promptData[0, defaultChoice]); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append("]"); + } + } + + this.WriteLine( + ConsoleColor.Cyan, + ConsoleColor.Black, + sb.ToString()); + // Read prompts until a match is made, the default is + // chosen, or the loop is interrupted with ctrl-C. + Collection results = new Collection(); + while (true) + { +ReadNext: + string prompt = string.Format(CultureInfo.CurrentCulture, "Choice[{0}]:", results.Count); + this.Write(ConsoleColor.Cyan, ConsoleColor.Black, prompt); + string data = Console.ReadLine().Trim().ToUpper(); + + // If the choice string was empty, no more choices have been made. + // If there were no choices made, return the defaults + if (data.Length == 0) + { + return (results.Count == 0) ? defaultResults : results; + } + + // See if the selection matched and return the + // corresponding index if it did. + for (int i = 0; i < choices.Count; i++) + { + if (promptData[0, i] == data) + { + results.Add(i); + goto ReadNext; + } + } + + this.WriteErrorLine("Invalid choice: " + data); + } + } + +#endregion + + /// + /// Prompts the user for credentials with a specified prompt window + /// caption, prompt message, user name, and target name. In this + /// example this functionality is not needed so the method throws a + /// NotImplementException exception. + /// + /// The caption for the message window. + /// The text of the message. + /// The user name whose credential is to be + /// prompted for. + /// The name of the target for which the + /// credential is collected. + /// Throws a NotImplementedException exception. + public override PSCredential PromptForCredential( + string caption, + string message, + string userName, + string targetName) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// Prompts the user for credentials by using a specified prompt window + /// caption, prompt message, user name and target name, credential + /// types allowed to be returned, and UI behavior options. In this + /// example this functionality is not needed so the method throws a + /// NotImplementException exception. + /// + /// The caption for the message window. + /// The text of the message. + /// The user name whose credential is to be + /// prompted for. + /// The name of the target for which the + /// credential is collected. + /// A PSCredentialTypes constant + /// that identifies the type of credentials that can be returned. + /// A PSCredentialUIOptions constant that + /// identifies the UI behavior when it gathers the credentials. + /// Throws a NotImplementedException exception. + public override PSCredential PromptForCredential( + string caption, + string message, + string userName, + string targetName, + PSCredentialTypes allowedCredentialTypes, + PSCredentialUIOptions options) + { + throw new NotImplementedException( + "The method or operation is not implemented."); + } + + /// + /// Reads characters that are entered by the user until a newline + /// (carriage return) is encountered. + /// + /// The characters that are entered by the user. + public override string ReadLine() + { + return Console.ReadLine(); + } + + /// + /// Reads characters entered by the user until a newline (carriage return) + /// is encountered and returns the characters as a secure string. In this + /// example this functionality is not needed so the method throws a + /// NotImplementException exception. + /// + /// Throws a NotImplemented exception. + public override System.Security.SecureString ReadLineAsSecureString() + { + throw new NotImplementedException("The method or operation is not implemented."); + } + + /// + /// Writes characters to the output display of the host. + /// + /// The characters to be written. + public override void Write(string value) + { + Console.Write(value); + } + + /// + /// Writes characters to the output display of the host with possible + /// foreground and background colors. + /// + /// The color of the characters. + /// The backgound color to use. + /// The characters to be written. + public override void Write( + ConsoleColor foregroundColor, + ConsoleColor backgroundColor, + string value) + { + Console.ForegroundColor = foregroundColor; + Console.BackgroundColor = backgroundColor; + Console.Write(value); + //Console.Write(value); + Console.ResetColor(); + } + + + /// + /// Writes a line of characters to the output display of the host + /// with foreground and background colors and appends a newline (carriage return). + /// + /// The forground color of the display. + /// The background color of the display. + /// The line to be written. + public override void WriteLine( + ConsoleColor foregroundColor, + ConsoleColor backgroundColor, + string value) + { + Console.ForegroundColor = foregroundColor; + Console.BackgroundColor = backgroundColor; + Console.WriteLine(value); + Console.ResetColor(); + } + + /// + /// Writes a debug message to the output display of the host. + /// + /// The debug message that is displayed. + public override void WriteDebugLine(string message) + { + this.WriteLine( + ConsoleColor.DarkYellow, + ConsoleColor.Black, + String.Format(CultureInfo.CurrentCulture, "DEBUG: {0}", message)); + } + + + /// + /// Writes an error message to the output display of the host. + /// + /// The error message that is displayed. + public override void WriteErrorLine(string value) + { + this.WriteLine( + ConsoleColor.Red, + ConsoleColor.Black, + value); + } + + /// + /// Writes a newline character (carriage return) + /// to the output display of the host. + /// + public override void WriteLine() + { + Console.WriteLine(); + } + + /// + /// Writes a line of characters to the output display of the host + /// and appends a newline character(carriage return). + /// + /// The line to be written. + public override void WriteLine(string value) + { + Console.WriteLine(value); + } + + /// + /// Writes a progress report to the output display of the host. + /// + /// Unique identifier of the source of the record. + /// A ProgressReport object. + public override void WriteProgress(long sourceId, ProgressRecord record) + { + + } + + /// + /// Writes a verbose message to the output display of the host. + /// + /// The verbose message that is displayed. + public override void WriteVerboseLine(string message) + { + this.WriteLine( + ConsoleColor.Green, + ConsoleColor.Black, + String.Format(CultureInfo.CurrentCulture, "VERBOSE: {0}", message)); + } + + /// + /// Writes a warning message to the output display of the host. + /// + /// The warning message that is displayed. + public override void WriteWarningLine(string message) + { + this.WriteLine( + ConsoleColor.Yellow, + ConsoleColor.Black, + String.Format(CultureInfo.CurrentCulture, "WARNING: {0}", message)); + } + + + /// + /// Parse a string containing a hotkey character. + /// Take a string of the form + /// Yes to &all + /// and returns a two-dimensional array split out as + /// "A", "Yes to all". + /// + /// The string to process + /// + /// A two dimensional array containing the parsed components. + /// + private static string[] GetHotkeyAndLabel(string input) + { + string[] result = new string[] { String.Empty, String.Empty }; + string[] fragments = input.Split('&'); + if (fragments.Length == 2) + { + if (fragments[1].Length > 0) + { + result[0] = fragments[1][0].ToString().ToUpper(); + } + + result[1] = (fragments[0] + fragments[1]).Trim(); + } + else + { + result[1] = input; + } + + return result; + } + + /// + /// This is a private worker function splits out the + /// accelerator keys from the menu and builds a two + /// dimentional array with the first access containing the + /// accelerator and the second containing the label string + /// with the & removed. + /// + /// The choice collection to process + /// + /// A two dimensional array containing the accelerator characters + /// and the cleaned-up labels + private static string[,] BuildHotkeysAndPlainLabels( + Collection choices) + { + // Allocate the result array + string[,] hotkeysAndPlainLabels = new string[2, choices.Count]; + + for (int i = 0; i < choices.Count; ++i) + { + string[] hotkeyAndLabel = GetHotkeyAndLabel(choices[i].Label); + hotkeysAndPlainLabels[0, i] = hotkeyAndLabel[0]; + hotkeysAndPlainLabels[1, i] = hotkeyAndLabel[1]; + } + + return hotkeysAndPlainLabels; + } + } +} +