better error handling

This commit is contained in:
Charlie Kindel
2022-11-08 18:07:30 -07:00
parent 9d7f985c8a
commit 495c4c6499
4 changed files with 126 additions and 179 deletions

View File

@@ -956,9 +956,10 @@ namespace Terminal.Gui {
public static bool Is_WSL_Platform ()
{
if (new CursesClipboard ().IsSupported) {
// If xclip is installed on Linux under WSL, this will return true.
return false;
}
var (exitCode, result) = BashRunner.Run ("uname -a", runCurses: false);
var (exitCode, result) = ClipboardProcessRunner.Bash ("uname -a", runCurses: false);
if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) {
return true;
}
@@ -1273,28 +1274,33 @@ namespace Terminal.Gui {
IsSupported = CheckSupport ();
}
string xclipPath = string.Empty;
public override bool IsSupported { get; }
bool CheckSupport ()
{
try {
var (exitCode, result) = BashRunner.Run ("which xclip", runCurses: false);
return (exitCode == 0 && result.FileExists ());
var (exitCode, result) = ClipboardProcessRunner.Bash ("which xclip", runCurses: false);
if (exitCode == 0 && result.FileExists ()) {
xclipPath = result;
return true;
}
} catch (Exception) {
// Permissions issue.
return false;
}
return false;
}
protected override string GetClipboardDataImpl ()
{
var tempFileName = System.IO.Path.GetTempFileName ();
try {
// BashRunner.Run ($"xsel -o --clipboard > {tempFileName}");
var (exitCode, result) = BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}");
var (exitCode, result) = ClipboardProcessRunner.Bash ($"{xclipPath} -selection clipboard -o > {tempFileName}");
if (exitCode == 0) {
return System.IO.File.ReadAllText (tempFileName);
}
} catch (Exception e) {
throw new NotSupportedException ($"{xclipPath} -selection clipboard -o failed.", e);
} finally {
System.IO.File.Delete (tempFileName);
}
@@ -1303,65 +1309,73 @@ namespace Terminal.Gui {
protected override void SetClipboardDataImpl (string text)
{
// var tempFileName = System.IO.Path.GetTempFileName ();
// System.IO.File.WriteAllText (tempFileName, text);
// try {
// // BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard");
// BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard");
// } finally {
// System.IO.File.Delete (tempFileName);
// }
BashRunner.Run ("xclip -selection clipboard -i", false, text);
try {
ClipboardProcessRunner.Bash ($"{xclipPath} - selection clipboard -i", false, text);
} catch (Exception e) {
throw new NotSupportedException ($"{xclipPath} -selection clipboard -o failed", e);
}
}
}
static class BashRunner {
public static (int exitCode, string result) Run (string commandLine, bool output = true, string inputText = "", bool runCurses = true)
internal static class ClipboardProcessRunner {
public static (int exitCode, string result) Bash (string commandLine, bool output = true, string inputText = "", bool runCurses = true)
{
var arguments = $"-c \"{commandLine}\"";
var (exitCode, result) = Process ("bash", arguments, inputText);
if (exitCode == 0) {
if (runCurses && Application.Driver is CursesDriver) {
Curses.raw ();
Curses.noecho ();
}
}
return (exitCode, result.TrimEnd ());
}
public static (int exitCode, string result) Process (string cmd, string arguments, string input = null)
{
var errorBuilder = new System.Text.StringBuilder ();
var outputBuilder = new System.Text.StringBuilder ();
using (var process = new System.Diagnostics.Process {
StartInfo = new System.Diagnostics.ProcessStartInfo {
FileName = "bash",
var output = string.Empty;
using (Process process = new Process {
StartInfo = new ProcessStartInfo {
FileName = cmd,
Arguments = arguments,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = false,
CreateNoWindow = true,
}
}) {
process.Start ();
if (output) {
process.StandardInput.Write (inputText);
if (!string.IsNullOrEmpty (input)) {
process.StandardInput.Write (input);
process.StandardInput.Close ();
}
process.StandardInput.Close ();
process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine (args.Data); };
process.BeginOutputReadLine ();
process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine (args.Data); };
process.BeginErrorReadLine ();
if (!process.DoubleWaitForExit ()) {
var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {arguments}.
if (!process.WaitForExit (5000)) {
var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
throw new TimeoutException (timeoutError);
}
if (process.ExitCode == 0) {
if (runCurses && Application.Driver is CursesDriver) {
Curses.raw ();
Curses.noecho ();
}
return (process.ExitCode, outputBuilder.ToString ().TrimEnd ());
}
var error = $@"Could not execute process. ExitCode: {process.ExitCode}, Command line: {process.StartInfo.FileName} {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
return (process.ExitCode, error);
if (process.ExitCode > 0) {
output = $@"Process failed to run. Command line: {cmd} {arguments}.
Output: {outputBuilder}
Error: {errorBuilder}";
} else {
output = outputBuilder.ToString ().TrimEnd ();
}
return (process.ExitCode, output);
}
}
@@ -1380,6 +1394,7 @@ namespace Terminal.Gui {
}
}
/// <summary>
/// A clipboard implementation for MacOSX.
/// This implementation uses the Mac clipboard API (via P/Invoke) to copy/paste.
@@ -1412,12 +1427,12 @@ namespace Terminal.Gui {
bool CheckSupport ()
{
var (exitCode, result) = BashRunner.Run ("which pbcopy");
if (!result.FileExists ()) {
var (exitCode, result) = ClipboardProcessRunner.Bash ("which pbcopy");
if (exitCode != 0 || !result.FileExists ()) {
return false;
}
(exitCode, result) = BashRunner.Run ("which pbpaste");
return result.FileExists ();
(exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste");
return exitCode == 0 && result.FileExists ();
}
protected override string GetClipboardDataImpl ()
@@ -1474,83 +1489,45 @@ namespace Terminal.Gui {
public override bool IsSupported { get; }
private string powershellCommand = string.Empty;
private string powershellPath = string.Empty;
bool CheckSupport ()
{
if (string.IsNullOrEmpty (powershellCommand)) {
if (string.IsNullOrEmpty (powershellPath)) {
// Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL)
var (exitCode, result) = BashRunner.Run ("which pwsh.exe");
var (exitCode, result) = ClipboardProcessRunner.Bash ("which pwsh.exe");
if (exitCode > 0) {
(exitCode, result) = BashRunner.Run ("which powershell.exe");
(exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe");
}
if (exitCode == 0) {
powershellCommand = result;
powershellPath = result;
}
}
return !string.IsNullOrEmpty (powershellCommand);
return !string.IsNullOrEmpty (powershellPath);
}
protected override string GetClipboardDataImpl ()
{
if (!IsSupported) return string.Empty;
using (var powershell = new System.Diagnostics.Process {
StartInfo = new System.Diagnostics.ProcessStartInfo {
RedirectStandardOutput = true,
FileName = powershellCommand,
Arguments = "-noprofile -command \"Get-Clipboard\"",
UseShellExecute = false,
CreateNoWindow = true
}
}) {
powershell.Start ();
if (!powershell.DoubleWaitForExit ()) {
var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}.
Output: {powershell.StandardOutput.ReadToEnd ()}
Error: {powershell.StandardError.ReadToEnd ()}";
throw new Exception (timeoutError);
}
var result = powershell.StandardOutput.ReadToEnd ();
powershell.StandardOutput.Close ();
var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, "-noprofile -command \"Get-Clipboard\"");
if (exitCode == 0) {
if (Application.Driver is CursesDriver) {
Curses.raw ();
Curses.noecho ();
}
if (result.EndsWith ("\r\n")) {
result = result.Substring (0, result.Length - 2);
if (output.EndsWith ("\r\n")) {
output = output.Substring (0, output.Length - 2);
}
return result;
return output;
}
return string.Empty;
}
protected override void SetClipboardDataImpl (string text)
{
if (!IsSupported) return;
using (var powershell = new System.Diagnostics.Process {
StartInfo = new System.Diagnostics.ProcessStartInfo {
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = powershellCommand,
Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\""
}
}) {
powershell.Start ();
powershell.WaitForExit ();
if (!powershell.DoubleWaitForExit ()) {
var timeoutError = $@"Process timed out. Command line: {powershell.StartInfo.FileName} {powershell.StartInfo.Arguments}.
Output: {powershell.StandardOutput.ReadToEnd ()}
Error: {powershell.StandardError.ReadToEnd ()}";
throw new TimeoutException (timeoutError);
}
if (powershell.ExitCode > 0) {
var setClipboardError = $@"Set-Clipboard failed. Command line: {powershell.StartInfo.FileName} {powershell.StartInfo.Arguments}.
Output: {powershell.StandardOutput.ReadToEnd ()}
Error: {powershell.StandardError.ReadToEnd ()}";
throw new System.InvalidOperationException (setClipboardError);
}
var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"");
if (exitCode == 0) {
if (Application.Driver is CursesDriver) {
Curses.raw ();
Curses.noecho ();

View File

@@ -27,28 +27,7 @@ namespace Terminal.Gui {
public override int Top => 0;
public override bool HeightAsBuffer { get; set; }
private IClipboard clipboard = null;
public override IClipboard Clipboard {
get {
if (clipboard == null) {
if (usingFakeClipboard) {
clipboard = new FakeClipboard ();
} else {
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
clipboard = new WindowsClipboard ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
clipboard = new MacOSXClipboard ();
} else {
if (CursesDriver.Is_WSL_Platform ()) {
clipboard = new WSLClipboard ();
} else {
clipboard = new CursesClipboard ();
}
}
}
}
return clipboard;
}
}
public override IClipboard Clipboard => clipboard;
// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
int [,,] contents;
@@ -83,6 +62,21 @@ namespace Terminal.Gui {
public FakeDriver (bool useFakeClipboard = true)
{
usingFakeClipboard = useFakeClipboard;
if (usingFakeClipboard) {
clipboard = new FakeClipboard ();
} else {
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
clipboard = new WindowsClipboard ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
clipboard = new MacOSXClipboard ();
} else {
if (CursesDriver.Is_WSL_Platform ()) {
clipboard = new WSLClipboard ();
} else {
clipboard = new CursesClipboard ();
}
}
}
}
bool needMove;
@@ -654,7 +648,7 @@ namespace Terminal.Gui {
}
#endregion
public class FakeClipboard : ClipboardBase {
public override bool IsSupported => true;

View File

@@ -15,9 +15,10 @@ namespace Terminal.Gui {
public abstract bool IsSupported { get; }
/// <summary>
/// Get the operation system clipboard.
/// Returns the contents of the OS clipboard if possible.
/// </summary>
/// <exception cref="NotSupportedException">Thrown if it was not possible to read the clipboard contents</exception>
/// <returns>The contents of the OS clipboard if successful.</returns>
/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
public string GetClipboardData ()
{
try {
@@ -28,35 +29,38 @@ namespace Terminal.Gui {
}
/// <summary>
/// Get the operation system clipboard.
/// Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
/// </summary>
/// <returns>The contents of the OS clipboard if successful.</returns>
/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
protected abstract string GetClipboardDataImpl ();
/// <summary>
/// Sets the operation system clipboard.
/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
/// </summary>
/// <param name="text"></param>
/// <exception cref="NotSupportedException">Thrown if it was not possible to set the clipboard contents</exception>
/// <param name="text">The text to paste to the OS clipboard.</param>
/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
public void SetClipboardData (string text)
{
try {
SetClipboardDataImpl (text);
} catch (Exception ex) {
throw new NotSupportedException ("Failed to write to clipboard.", ex);
throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex);
}
}
/// <summary>
/// Sets the operation system clipboard.
/// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
/// </summary>
/// <param name="text"></param>
/// <param name="text">The text to paste to the OS clipboard.</param>
/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
protected abstract void SetClipboardDataImpl (string text);
/// <summary>
/// Gets the operation system clipboard if possible.
/// Copies the contents of the OS clipboard to <paramref name="result"/> if possible.
/// </summary>
/// <param name="result">Clipboard contents read</param>
/// <returns>true if it was possible to read the OS clipboard.</returns>
/// <param name="result">The contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
/// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
public bool TryGetClipboardData (out string result)
{
// Don't even try to read because environment is not set up.
@@ -78,10 +82,10 @@ namespace Terminal.Gui {
}
/// <summary>
/// Sets the operation system clipboard if possible.
/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
/// </summary>
/// <param name="text"></param>
/// <returns>True if the clipboard content was set successfully</returns>
/// <param name="text">The text to paste to the OS clipboard.</param>
/// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
public bool TrySetClipboardData (string text)
{
// Don't even try to set because environment is not set up

View File

@@ -69,37 +69,6 @@ namespace Terminal.Gui.ConsoleDrivers {
}
}
private static string RunClipboardProcess (string cmd, string args, string writeText = null)
{
string output = string.Empty;
using (Process process = new Process {
StartInfo = new ProcessStartInfo {
FileName = cmd,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true
}
}) {
process.Start ();
if (string.IsNullOrEmpty (writeText)) {
process.StandardInput.Write (writeText);
process.StandardInput.Close ();
}
process.WaitForExit ();
if (process.ExitCode > 0) {
var error = $@"RunClipboardProcess failed. Command line: {cmd} {args}.
Output: {process.StandardOutput.ReadToEnd ()}
Error: {process.StandardError.ReadToEnd ()}";
throw new InvalidOperationException (error);
}
output = process.StandardOutput.ReadToEnd ().TrimEnd ();
process.StandardOutput.Close ();
}
return output;
}
[Fact, AutoInitShutdown (useFakeClipboard: false)]
public void Contents_Gets_From_OS_Clipboard ()
@@ -110,18 +79,18 @@ namespace Terminal.Gui.ConsoleDrivers {
Application.Iteration += () => {
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
RunClipboardProcess ("pwsh", $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\"");
ClipboardProcessRunner.Process ("pwsh", $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\"");
getClipText = Clipboard.Contents.ToString ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
RunClipboardProcess ("pbcopy", string.Empty, clipText);
ClipboardProcessRunner.Process ("pbcopy", string.Empty, clipText);
getClipText = Clipboard.Contents.ToString ();
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
if (Is_WSL_Platform ()) {
try {
// This runs the WINDOWS version of powershell.exe via WSL.
RunClipboardProcess ("powershell.exe", $"-noprofile -command \"Set-Clipboard -Value \\\"{clipText}\\\"\"");
ClipboardProcessRunner.Process ("powershell.exe", $"-noprofile -command \"Set-Clipboard -Value \\\"{clipText}\\\"\"");
} catch {
failed = true;
}
@@ -139,7 +108,7 @@ namespace Terminal.Gui.ConsoleDrivers {
}
// If we get here, powershell didn't work and xclip exists...
RunClipboardProcess ("bash", $"-c \"xclip -sel clip -i\"", clipText);
ClipboardProcessRunner.Process ("bash", $"-c \"xclip -sel clip -i\"", clipText);
if (!failed) {
getClipText = Clipboard.Contents.ToString ();
@@ -168,27 +137,30 @@ namespace Terminal.Gui.ConsoleDrivers {
Clipboard.Contents = clipText;
if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
clipReadText = RunClipboardProcess ("pwsh", "-noprofile -command \"Get-Clipboard\"");
(_, clipReadText) = ClipboardProcessRunner.Process ("pwsh", "-noprofile -command \"Get-Clipboard\"");
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
clipReadText = RunClipboardProcess ("pbpaste", "");
(_, clipReadText) = ClipboardProcessRunner.Process ("pbpaste", "");
} else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
var exitCode = 0;
if (Is_WSL_Platform ()) {
try {
clipReadText = RunClipboardProcess ("/opt/microsoft/powershell/7/pwsh", "-noprofile -command \"Get-Clipboard\"");
} catch {
failed = true;
(exitCode, clipReadText) = ClipboardProcessRunner.Process ("powershell.exe", "-noprofile -command \"Get-Clipboard\"");
if (exitCode == 0) {
Application.RequestStop ();
return;
}
Application.RequestStop ();
failed = true;
}
if (failed = xclipExists () == false) {
// xclip doesn't exist then exit.
Application.RequestStop ();
return;
}
clipReadText = RunClipboardProcess ("bash", $"-c \"xclip -sel clip -o\"");
(exitCode, clipReadText) = ClipboardProcessRunner.Process ("bash", $"-c \"xclip -sel clip -o\"");
Assert.Equal (0, exitCode);
}
Application.RequestStop ();
@@ -204,14 +176,14 @@ namespace Terminal.Gui.ConsoleDrivers {
bool Is_WSL_Platform ()
{
var result = RunClipboardProcess ("bash", $"-c \"uname -a\"");
var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"uname -a\"");
return result.Contains ("microsoft") && result.Contains ("WSL");
}
bool xclipExists ()
{
try {
var result = RunClipboardProcess ("bash", $"-c \"which xclip\"");
var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"which xclip\"");
return result.TrimEnd () != "";
} catch (System.Exception) {
return false;