Skip to content

Commit

Permalink
feat: 初步支持ssh控制台
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaitonn committed Aug 15, 2024
1 parent f281627 commit c570799
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 40 deletions.
1 change: 0 additions & 1 deletion src/Serein.Cli/Serein.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions src/Serein.Core/Serein.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageReference Include="NCrontab" Version="3.3.3" />
<PackageReference Include="Octokit" Version="11.0.1" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" PrivateAssets="all" />
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="WebSocket4Net" Version="0.15.2" />
</ItemGroup>
Expand Down
64 changes: 64 additions & 0 deletions src/Serein.Core/Services/Network/Ssh/Console/SshConsole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text;

using Serein.Core.Utils;

using Spectre.Console;
using Spectre.Console.Rendering;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshConsole : IAnsiConsole
{
private readonly object _lock = new();
private readonly SshPty _sshPty;

public SshConsole(SshPty sshPty)
{
_sshPty = sshPty;

Pipeline = new();
Input = new SshConsoleInput(_sshPty);
Cursor = new SshConsoleCursor(_sshPty);
Profile = new(new SshConsoleOutput(_sshPty), EncodingMap.UTF8);
ExclusivityMode = new SshExclusivityMode();

if (sshPty.WidthChars > 0)
Profile.Width = (int)sshPty.WidthChars;
if (sshPty.HeightChars > 0)
Profile.Height = (int)sshPty.HeightChars;
}

public Profile Profile { get; }
public IAnsiConsoleCursor Cursor { get; }
public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; }

public void Clear(bool home)
{
lock (_lock)
if (home)
_sshPty.Clear();
}

public void Write(IRenderable renderable)
{
var stringBuilder = new StringBuilder();

foreach (var segment in renderable.GetSegments(this))
{
// if (segment.IsControlCode)
// {
// stringBuilder.Append(segment.Text);
// continue;
// }

stringBuilder.Append(segment.Text);

// var parts = segment.Text.Normalize(NormalizationForm.FormC)
}

Profile.Out.Writer.Write(stringBuilder);
Profile.Out.Writer.Flush();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

using Serein.Core.Utils;

namespace Serein.Core.Services.Network.Ssh.Console;

public static class SshConsoleAnsiHandler
{
public static IEnumerable<ConsoleKeyInfo?> Handle(byte[] bytes)
{
if (bytes.Length == 0)
return [];

if (bytes[0] == '\x1b' && bytes.Length > 1)
return HandleAsCSISequences(bytes);

var text = EncodingMap.UTF8.GetString(bytes);

throw new NotImplementedException();
}

private static IEnumerable<ConsoleKeyInfo?> HandleAsCSISequences(byte[] bytes)
{
if (bytes.Length < 3 || bytes[1] != '[')
return [];

var func = Convert.ToChar(bytes[^1]);
ConsoleKeyInfo? info;

switch (func)
{
case 'A':
case 'B':
case 'C':
case 'D':
info = new(
'\x00',
func switch
{
'A' => ConsoleKey.UpArrow,
_ => throw new NotSupportedException()
},
false,
false,
false
);
if (bytes.Length == 3)
return [info];

if (bytes.Length == 4)
return Enumerable.Repeat(info, bytes[2]);

break;

}

throw new NotImplementedException();
}
}
26 changes: 26 additions & 0 deletions src/Serein.Core/Services/Network/Ssh/Console/SshConsoleCursor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Spectre.Console;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshConsoleCursor(SshPty sshPty) : IAnsiConsoleCursor
{
private readonly SshPty _sshPty = sshPty;

public void Move(CursorDirection direction, int steps)
{
_sshPty.MoveCursor(direction, steps);
}

public void SetPosition(int column, int line)
{
_sshPty.SetCursor(column, line);
}

public void Show(bool show)
{
if (show)
_sshPty.ShowCursor();
else
_sshPty.HideCursor();
}
}
53 changes: 53 additions & 0 deletions src/Serein.Core/Services/Network/Ssh/Console/SshConsoleInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Threading;
using System.Threading.Tasks;

using Spectre.Console;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshConsoleInput(SshPty sshPty) : IAnsiConsoleInput
{
private readonly SshPty _sshPty = sshPty;

public bool IsKeyAvailable() => true;

private ConsoleKeyInfo? WaitKey(bool intercept, CancellationToken cancellationToken = default)
{
var lockObj = new object();

using var handle = new EventWaitHandle(false, EventResetMode.ManualReset);
cancellationToken.Register(() => handle.Set());

ConsoleKeyInfo? consoleKeyInfo = null;
_sshPty.KeyRead += OnKeyRead;
handle.WaitOne();

return consoleKeyInfo;

void OnKeyRead(object? sender, ConsoleKeyInfo? keyInfo)
{
lock (lockObj)
{
_sshPty.KeyRead -= OnKeyRead;
consoleKeyInfo = keyInfo;
handle.Set();

if (!intercept && keyInfo.HasValue)
_sshPty.Send(keyInfo.Value.KeyChar.ToString());
}
}
}

public ConsoleKeyInfo? ReadKey(bool intercept)
{
return WaitKey(intercept);
}



public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
return Task.FromResult(WaitKey(intercept, cancellationToken));
}
}
21 changes: 21 additions & 0 deletions src/Serein.Core/Services/Network/Ssh/Console/SshConsoleOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.IO;
using System.Text;

using Spectre.Console;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshConsoleOutput(SshPty sshPty) : IAnsiConsoleOutput
{
private readonly SshPty _sshPty = sshPty;

public TextWriter Writer { get; } = new SshConsoleTextWriter(sshPty);

public bool IsTerminal => true;

public int Width => (int)_sshPty.WidthChars;

public int Height => (int)_sshPty.HeightChars;

public void SetEncoding(Encoding encoding) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.IO;
using System.Text;

using Serein.Core.Utils;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshConsoleTextWriter(SshPty sshPty) : TextWriter
{
private readonly SshPty _sshPty = sshPty;

public override Encoding Encoding { get; } = EncodingMap.UTF8;

private readonly List<string> _buffer = [];

public override void Write(string? value)
{
if (value != null)
lock (_buffer)
_buffer.Add(value);
}

public override void Flush()
{
lock (_buffer)
{
_sshPty.Send(string.Join(string.Empty, _buffer));
_buffer.Clear();
}
}
}
19 changes: 19 additions & 0 deletions src/Serein.Core/Services/Network/Ssh/Console/SshExclusivityMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;

using Spectre.Console;

namespace Serein.Core.Services.Network.Ssh.Console;

public class SshExclusivityMode : IExclusivityMode
{
public T Run<T>(Func<T> func)
{
return func();
}

public Task<T> RunAsync<T>(Func<Task<T>> func)
{
return func();
}
}
Loading

0 comments on commit c570799

Please sign in to comment.