From d76fb8c5d3ba73ff52f655778f57b8bc2ab4e92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Sat, 30 Dec 2023 07:33:34 +0100 Subject: [PATCH] Reimplement terminal drivers in a native helper library. * Implemented a new native helper library, Vezel.Cathode.Native, using Vezel.Zig.Sdk. * Moved all Unix/Win32 interop to the native helper library. * Removed CsWin32 and Unix P/Invoke declarations from Vezel.Cathode. * Removed Vezel.Cathode.Hosting as the native helper library cleans up terminal configuration much more reliably. * Removed Vezel.Cathode.Analyzers since Vezel.Cathode.Hosting was removed. * Lots of cleanup on the C# side as a result of these changes. * Some minor optimizations throughout Vezel.Cathode. Closes #43. --- .gitignore | 1 + .vscode/extensions.json | 1 + .vscode/launch.json | 1 - Directory.Build.rsp | 4 +- Directory.Build.targets | 2 +- Directory.Packages.props | 9 +- PACKAGE.md | 2 - README.md | 4 - cathode.sln | 233 +++++----- global.json | 3 +- src/analyzers/DiagnosticDescriptors.cs | 52 --- src/analyzers/Hosting/EntryPointGenerator.cs | 72 --- src/analyzers/analyzers.cs | 2 - src/analyzers/analyzers.csproj | 18 - src/common/common.csproj | 1 - src/core/Native/TerminalInterop.cs | 145 ++++++ src/core/NativeMethods.json | 7 - src/core/NativeMethods.txt | 24 - src/core/SystemVirtualTerminal.cs | 78 ++-- src/core/Terminal.cs | 16 +- src/core/TerminalSignal.cs | 1 + ...nalReader`2.cs => NativeTerminalReader.cs} | 18 +- ...nalWriter`2.cs => NativeTerminalWriter.cs} | 18 +- src/core/Terminals/NativeVirtualTerminal.cs | 70 +++ src/core/Terminals/NativeVirtualTerminal`1.cs | 8 - .../Unix/Linux/LinuxVirtualTerminal.cs | 136 ------ .../Unix/MacOS/MacOSVirtualTerminal.cs | 136 ------ src/core/Terminals/Unix/PosixSignalGuard.cs | 27 -- .../Terminals/Unix/UnixCancellationPipe.cs | 18 +- src/core/Terminals/Unix/UnixTerminalReader.cs | 36 +- src/core/Terminals/Unix/UnixTerminalWriter.cs | 46 +- .../Terminals/Unix/UnixVirtualTerminal.cs | 96 +--- .../Windows/WindowsTerminalReader.cs | 24 +- .../Windows/WindowsTerminalUtility.cs | 20 - .../Windows/WindowsTerminalWriter.cs | 24 +- .../Windows/WindowsVirtualTerminal.cs | 214 +-------- src/core/Unix/Linux/LinuxPInvoke.cs | 167 ------- src/core/Unix/Linux/termios.cs | 23 - src/core/Unix/MacOS/MacOSPInvoke.cs | 171 ------- src/core/Unix/MacOS/termios.cs | 21 - src/core/Unix/UnixPInvoke.cs | 60 --- src/core/Unix/pollfd.cs | 12 - src/core/Unix/winsize.cs | 15 - src/core/VirtualTerminal.cs | 6 +- src/core/core.csproj | 28 +- src/hosting/BannedSymbols.txt | 6 - src/hosting/IProgram.cs | 6 - src/hosting/ProgramContext.cs | 82 ---- src/hosting/ProgramHost.cs | 67 --- src/hosting/hosting.cs | 2 - src/hosting/hosting.csproj | 31 -- src/hosting/hosting.targets | 9 - src/native/cathode.h | 16 + src/native/driver-unix.c | 434 ++++++++++++++++++ src/native/driver-unix.h | 5 + src/native/driver-windows.c | 342 ++++++++++++++ src/native/driver-windows.h | 5 + src/native/driver.c | 7 + src/native/driver.h | 52 +++ src/native/native.cproj | 53 +++ src/samples/Directory.Build.targets | 5 - src/samples/hosting/.globalconfig | 3 - src/samples/hosting/Program.cs | 20 - src/samples/hosting/hosting.csproj | 1 - src/trimming/trimming.csproj | 2 - 65 files changed, 1399 insertions(+), 1819 deletions(-) delete mode 100644 src/analyzers/DiagnosticDescriptors.cs delete mode 100644 src/analyzers/Hosting/EntryPointGenerator.cs delete mode 100644 src/analyzers/analyzers.cs delete mode 100644 src/analyzers/analyzers.csproj create mode 100644 src/core/Native/TerminalInterop.cs delete mode 100644 src/core/NativeMethods.json delete mode 100644 src/core/NativeMethods.txt rename src/core/Terminals/{NativeTerminalReader`2.cs => NativeTerminalReader.cs} (75%) rename src/core/Terminals/{NativeTerminalWriter`2.cs => NativeTerminalWriter.cs} (75%) create mode 100644 src/core/Terminals/NativeVirtualTerminal.cs delete mode 100644 src/core/Terminals/NativeVirtualTerminal`1.cs delete mode 100644 src/core/Terminals/Unix/Linux/LinuxVirtualTerminal.cs delete mode 100644 src/core/Terminals/Unix/MacOS/MacOSVirtualTerminal.cs delete mode 100644 src/core/Terminals/Unix/PosixSignalGuard.cs delete mode 100644 src/core/Terminals/Windows/WindowsTerminalUtility.cs delete mode 100644 src/core/Unix/Linux/LinuxPInvoke.cs delete mode 100644 src/core/Unix/Linux/termios.cs delete mode 100644 src/core/Unix/MacOS/MacOSPInvoke.cs delete mode 100644 src/core/Unix/MacOS/termios.cs delete mode 100644 src/core/Unix/UnixPInvoke.cs delete mode 100644 src/core/Unix/pollfd.cs delete mode 100644 src/core/Unix/winsize.cs delete mode 100644 src/hosting/BannedSymbols.txt delete mode 100644 src/hosting/IProgram.cs delete mode 100644 src/hosting/ProgramContext.cs delete mode 100644 src/hosting/ProgramHost.cs delete mode 100644 src/hosting/hosting.cs delete mode 100644 src/hosting/hosting.csproj delete mode 100644 src/hosting/hosting.targets create mode 100644 src/native/cathode.h create mode 100644 src/native/driver-unix.c create mode 100644 src/native/driver-unix.h create mode 100644 src/native/driver-windows.c create mode 100644 src/native/driver-windows.h create mode 100644 src/native/driver.c create mode 100644 src/native/driver.h create mode 100644 src/native/native.cproj delete mode 100644 src/samples/hosting/.globalconfig delete mode 100644 src/samples/hosting/Program.cs delete mode 100644 src/samples/hosting/hosting.csproj diff --git a/.gitignore b/.gitignore index 1954103..2f80563 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /out *.user +.clangd .idea .vs node_modules diff --git a/.vscode/extensions.json b/.vscode/extensions.json index dfae614..3f540c6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,6 +8,7 @@ "github.vscode-github-actions", "github.vscode-pull-request-github", "jock.svg", + "llvm-vs-code-extensions.vscode-clangd", "ms-dotnettools.csharp", "redhat.vscode-xml", "redhat.vscode-yaml", diff --git a/.vscode/launch.json b/.vscode/launch.json index e6ebb94..c43cb5e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,6 @@ "control", "cursor", "extensions", - "hosting", "processes", "raw", "resize", diff --git a/Directory.Build.rsp b/Directory.Build.rsp index becb644..cee4fb3 100644 --- a/Directory.Build.rsp +++ b/Directory.Build.rsp @@ -1,3 +1,5 @@ --err +# TODO: https://github.com/ziglang/zig/issues/13385 +# TODO: https://github.com/ziglang/zig/issues/15398 +#-err -nr:false -tl diff --git a/Directory.Build.targets b/Directory.Build.targets index b4d42de..84cb326 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -21,7 +21,7 @@ - + diff --git a/Directory.Packages.props b/Directory.Packages.props index 2a68c4e..6c34524 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,9 @@ + + + - - - `. diff --git a/cathode.sln b/cathode.sln index 1f8e929..c603528 100644 --- a/cathode.sln +++ b/cathode.sln @@ -3,49 +3,43 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AE5CD1FF-A7F0-4542-8D45-00B8A985E326}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "analyzers", "src\analyzers\analyzers.csproj", "{D5F55B6C-3AFF-4565-BF0A-87CD35257473}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "common", "src\common\common.csproj", "{4FC7807A-D826-42F2-8D08-3827C01DF4D3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "common", "src\common\common.csproj", "{90C16E16-B4E8-4578-A8A7-CA095A5CB063}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core", "src\core\core.csproj", "{8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core", "src\core\core.csproj", "{4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extensions", "src\extensions\extensions.csproj", "{AEFB7390-6B1B-4565-B24E-F8C9A55D4B64}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extensions", "src\extensions\extensions.csproj", "{11715F80-BF6A-47D4-97E6-A370EED518E2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "trimming", "src\trimming\trimming.csproj", "{A1CA4784-52FB-45BE-AF2B-B92690B56492}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hosting", "src\hosting\hosting.csproj", "{F6DF6898-9B92-4006-AEF5-FEA1AA734C18}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{16580376-0166-4529-82C3-1BC286BEEFCB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "trimming", "src\trimming\trimming.csproj", "{FD13CF06-248D-40C2-A14E-E35D2FF8AE94}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "attributes", "src\samples\attributes\attributes.csproj", "{ED897630-8969-48AF-86BE-C32124859698}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{AD90CBF5-2ABD-4295-993E-03E92E8F3F56}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cancellation", "src\samples\cancellation\cancellation.csproj", "{E2FBD559-A590-4D00-AEBA-B66D9C344102}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "attributes", "src\samples\attributes\attributes.csproj", "{B12D84A0-1C8B-4D80-9395-41F6D4DF584A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "control", "src\samples\control\control.csproj", "{8994E6B9-28B5-4942-B455-AF3A92D95281}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cancellation", "src\samples\cancellation\cancellation.csproj", "{824ECDD2-6B82-4A4C-A247-B3B5167B077A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cursor", "src\samples\cursor\cursor.csproj", "{58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "control", "src\samples\control\control.csproj", "{F63CF535-E697-4013-BED8-0AC527915B68}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extensions", "src\samples\extensions\extensions.csproj", "{2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cursor", "src\samples\cursor\cursor.csproj", "{FA896405-3DF8-4D0E-AB32-BB4FDF542DD9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "processes", "src\samples\processes\processes.csproj", "{087E163B-AC5F-4832-84D6-2B9E5454D987}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extensions", "src\samples\extensions\extensions.csproj", "{092A5F30-63B6-4071-A08E-3BC4705CAF16}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "raw", "src\samples\raw\raw.csproj", "{ED9D3157-1F1F-4AD4-BE6D-E52F6A240728}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hosting", "src\samples\hosting\hosting.csproj", "{5D3B1B24-349E-4D32-831F-C098F00846FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "resize", "src\samples\resize\resize.csproj", "{E6868C22-2254-4CD7-A670-296DB76778B5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "processes", "src\samples\processes\processes.csproj", "{2A6273D2-52D4-4C98-8DEE-CD88ABDE3279}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "screens", "src\samples\screens\screens.csproj", "{16AF9596-21A3-4A11-9C33-F538EE3DC6D4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "raw", "src\samples\raw\raw.csproj", "{CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "scrolling", "src\samples\scrolling\scrolling.csproj", "{8A243513-D890-4CFC-A94E-EDB0B1B9EC49}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "resize", "src\samples\resize\resize.csproj", "{004DAE75-5BA0-4B95-B2DB-EA4E46C523EA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "signals", "src\samples\signals\signals.csproj", "{1C7E7B3C-55D1-488C-9C92-4DCC461935EB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "screens", "src\samples\screens\screens.csproj", "{A2496E05-D61F-439A-A71B-4E4EE970ABE9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sounds", "src\samples\sounds\sounds.csproj", "{E9656395-0866-4F80-B0B6-89EA7A4570F4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "scrolling", "src\samples\scrolling\scrolling.csproj", "{70F538E0-0254-4021-9281-49B7F1B5EBFA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "signals", "src\samples\signals\signals.csproj", "{CEE734AA-CE10-48F0-8335-E5776EE93B6F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sounds", "src\samples\sounds\sounds.csproj", "{51581231-AD41-4360-BC57-DD61E8BAB821}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "width", "src\samples\width\width.csproj", "{0822F67D-02DD-4C6F-AE05-037BEC5FA08A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "width", "src\samples\width\width.csproj", "{CA919AE9-EB3A-4925-A950-C452F1847AEF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,114 +47,99 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D5F55B6C-3AFF-4565-BF0A-87CD35257473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5F55B6C-3AFF-4565-BF0A-87CD35257473}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5F55B6C-3AFF-4565-BF0A-87CD35257473}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5F55B6C-3AFF-4565-BF0A-87CD35257473}.Release|Any CPU.Build.0 = Release|Any CPU - {90C16E16-B4E8-4578-A8A7-CA095A5CB063}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90C16E16-B4E8-4578-A8A7-CA095A5CB063}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90C16E16-B4E8-4578-A8A7-CA095A5CB063}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90C16E16-B4E8-4578-A8A7-CA095A5CB063}.Release|Any CPU.Build.0 = Release|Any CPU - {4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D}.Release|Any CPU.Build.0 = Release|Any CPU - {11715F80-BF6A-47D4-97E6-A370EED518E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11715F80-BF6A-47D4-97E6-A370EED518E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11715F80-BF6A-47D4-97E6-A370EED518E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11715F80-BF6A-47D4-97E6-A370EED518E2}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DF6898-9B92-4006-AEF5-FEA1AA734C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DF6898-9B92-4006-AEF5-FEA1AA734C18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DF6898-9B92-4006-AEF5-FEA1AA734C18}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DF6898-9B92-4006-AEF5-FEA1AA734C18}.Release|Any CPU.Build.0 = Release|Any CPU - {FD13CF06-248D-40C2-A14E-E35D2FF8AE94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD13CF06-248D-40C2-A14E-E35D2FF8AE94}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD13CF06-248D-40C2-A14E-E35D2FF8AE94}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD13CF06-248D-40C2-A14E-E35D2FF8AE94}.Release|Any CPU.Build.0 = Release|Any CPU - {B12D84A0-1C8B-4D80-9395-41F6D4DF584A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B12D84A0-1C8B-4D80-9395-41F6D4DF584A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B12D84A0-1C8B-4D80-9395-41F6D4DF584A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B12D84A0-1C8B-4D80-9395-41F6D4DF584A}.Release|Any CPU.Build.0 = Release|Any CPU - {824ECDD2-6B82-4A4C-A247-B3B5167B077A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {824ECDD2-6B82-4A4C-A247-B3B5167B077A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {824ECDD2-6B82-4A4C-A247-B3B5167B077A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {824ECDD2-6B82-4A4C-A247-B3B5167B077A}.Release|Any CPU.Build.0 = Release|Any CPU - {F63CF535-E697-4013-BED8-0AC527915B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F63CF535-E697-4013-BED8-0AC527915B68}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F63CF535-E697-4013-BED8-0AC527915B68}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F63CF535-E697-4013-BED8-0AC527915B68}.Release|Any CPU.Build.0 = Release|Any CPU - {FA896405-3DF8-4D0E-AB32-BB4FDF542DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA896405-3DF8-4D0E-AB32-BB4FDF542DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA896405-3DF8-4D0E-AB32-BB4FDF542DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA896405-3DF8-4D0E-AB32-BB4FDF542DD9}.Release|Any CPU.Build.0 = Release|Any CPU - {092A5F30-63B6-4071-A08E-3BC4705CAF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {092A5F30-63B6-4071-A08E-3BC4705CAF16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {092A5F30-63B6-4071-A08E-3BC4705CAF16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {092A5F30-63B6-4071-A08E-3BC4705CAF16}.Release|Any CPU.Build.0 = Release|Any CPU - {5D3B1B24-349E-4D32-831F-C098F00846FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D3B1B24-349E-4D32-831F-C098F00846FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D3B1B24-349E-4D32-831F-C098F00846FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D3B1B24-349E-4D32-831F-C098F00846FE}.Release|Any CPU.Build.0 = Release|Any CPU - {2A6273D2-52D4-4C98-8DEE-CD88ABDE3279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A6273D2-52D4-4C98-8DEE-CD88ABDE3279}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A6273D2-52D4-4C98-8DEE-CD88ABDE3279}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A6273D2-52D4-4C98-8DEE-CD88ABDE3279}.Release|Any CPU.Build.0 = Release|Any CPU - {CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142}.Release|Any CPU.Build.0 = Release|Any CPU - {004DAE75-5BA0-4B95-B2DB-EA4E46C523EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {004DAE75-5BA0-4B95-B2DB-EA4E46C523EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {004DAE75-5BA0-4B95-B2DB-EA4E46C523EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {004DAE75-5BA0-4B95-B2DB-EA4E46C523EA}.Release|Any CPU.Build.0 = Release|Any CPU - {A2496E05-D61F-439A-A71B-4E4EE970ABE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2496E05-D61F-439A-A71B-4E4EE970ABE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2496E05-D61F-439A-A71B-4E4EE970ABE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2496E05-D61F-439A-A71B-4E4EE970ABE9}.Release|Any CPU.Build.0 = Release|Any CPU - {70F538E0-0254-4021-9281-49B7F1B5EBFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70F538E0-0254-4021-9281-49B7F1B5EBFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70F538E0-0254-4021-9281-49B7F1B5EBFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70F538E0-0254-4021-9281-49B7F1B5EBFA}.Release|Any CPU.Build.0 = Release|Any CPU - {CEE734AA-CE10-48F0-8335-E5776EE93B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CEE734AA-CE10-48F0-8335-E5776EE93B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CEE734AA-CE10-48F0-8335-E5776EE93B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CEE734AA-CE10-48F0-8335-E5776EE93B6F}.Release|Any CPU.Build.0 = Release|Any CPU - {51581231-AD41-4360-BC57-DD61E8BAB821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51581231-AD41-4360-BC57-DD61E8BAB821}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51581231-AD41-4360-BC57-DD61E8BAB821}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51581231-AD41-4360-BC57-DD61E8BAB821}.Release|Any CPU.Build.0 = Release|Any CPU - {0822F67D-02DD-4C6F-AE05-037BEC5FA08A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0822F67D-02DD-4C6F-AE05-037BEC5FA08A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0822F67D-02DD-4C6F-AE05-037BEC5FA08A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0822F67D-02DD-4C6F-AE05-037BEC5FA08A}.Release|Any CPU.Build.0 = Release|Any CPU + {4FC7807A-D826-42F2-8D08-3827C01DF4D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FC7807A-D826-42F2-8D08-3827C01DF4D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FC7807A-D826-42F2-8D08-3827C01DF4D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FC7807A-D826-42F2-8D08-3827C01DF4D3}.Release|Any CPU.Build.0 = Release|Any CPU + {8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC}.Release|Any CPU.Build.0 = Release|Any CPU + {AEFB7390-6B1B-4565-B24E-F8C9A55D4B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEFB7390-6B1B-4565-B24E-F8C9A55D4B64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEFB7390-6B1B-4565-B24E-F8C9A55D4B64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEFB7390-6B1B-4565-B24E-F8C9A55D4B64}.Release|Any CPU.Build.0 = Release|Any CPU + {A1CA4784-52FB-45BE-AF2B-B92690B56492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1CA4784-52FB-45BE-AF2B-B92690B56492}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1CA4784-52FB-45BE-AF2B-B92690B56492}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1CA4784-52FB-45BE-AF2B-B92690B56492}.Release|Any CPU.Build.0 = Release|Any CPU + {ED897630-8969-48AF-86BE-C32124859698}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED897630-8969-48AF-86BE-C32124859698}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED897630-8969-48AF-86BE-C32124859698}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED897630-8969-48AF-86BE-C32124859698}.Release|Any CPU.Build.0 = Release|Any CPU + {E2FBD559-A590-4D00-AEBA-B66D9C344102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2FBD559-A590-4D00-AEBA-B66D9C344102}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2FBD559-A590-4D00-AEBA-B66D9C344102}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2FBD559-A590-4D00-AEBA-B66D9C344102}.Release|Any CPU.Build.0 = Release|Any CPU + {8994E6B9-28B5-4942-B455-AF3A92D95281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8994E6B9-28B5-4942-B455-AF3A92D95281}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8994E6B9-28B5-4942-B455-AF3A92D95281}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8994E6B9-28B5-4942-B455-AF3A92D95281}.Release|Any CPU.Build.0 = Release|Any CPU + {58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC}.Release|Any CPU.Build.0 = Release|Any CPU + {2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C}.Release|Any CPU.Build.0 = Release|Any CPU + {087E163B-AC5F-4832-84D6-2B9E5454D987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {087E163B-AC5F-4832-84D6-2B9E5454D987}.Debug|Any CPU.Build.0 = Debug|Any CPU + {087E163B-AC5F-4832-84D6-2B9E5454D987}.Release|Any CPU.ActiveCfg = Release|Any CPU + {087E163B-AC5F-4832-84D6-2B9E5454D987}.Release|Any CPU.Build.0 = Release|Any CPU + {ED9D3157-1F1F-4AD4-BE6D-E52F6A240728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED9D3157-1F1F-4AD4-BE6D-E52F6A240728}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED9D3157-1F1F-4AD4-BE6D-E52F6A240728}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED9D3157-1F1F-4AD4-BE6D-E52F6A240728}.Release|Any CPU.Build.0 = Release|Any CPU + {E6868C22-2254-4CD7-A670-296DB76778B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6868C22-2254-4CD7-A670-296DB76778B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6868C22-2254-4CD7-A670-296DB76778B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6868C22-2254-4CD7-A670-296DB76778B5}.Release|Any CPU.Build.0 = Release|Any CPU + {16AF9596-21A3-4A11-9C33-F538EE3DC6D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16AF9596-21A3-4A11-9C33-F538EE3DC6D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16AF9596-21A3-4A11-9C33-F538EE3DC6D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16AF9596-21A3-4A11-9C33-F538EE3DC6D4}.Release|Any CPU.Build.0 = Release|Any CPU + {8A243513-D890-4CFC-A94E-EDB0B1B9EC49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A243513-D890-4CFC-A94E-EDB0B1B9EC49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A243513-D890-4CFC-A94E-EDB0B1B9EC49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A243513-D890-4CFC-A94E-EDB0B1B9EC49}.Release|Any CPU.Build.0 = Release|Any CPU + {1C7E7B3C-55D1-488C-9C92-4DCC461935EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C7E7B3C-55D1-488C-9C92-4DCC461935EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C7E7B3C-55D1-488C-9C92-4DCC461935EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C7E7B3C-55D1-488C-9C92-4DCC461935EB}.Release|Any CPU.Build.0 = Release|Any CPU + {E9656395-0866-4F80-B0B6-89EA7A4570F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9656395-0866-4F80-B0B6-89EA7A4570F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9656395-0866-4F80-B0B6-89EA7A4570F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9656395-0866-4F80-B0B6-89EA7A4570F4}.Release|Any CPU.Build.0 = Release|Any CPU + {CA919AE9-EB3A-4925-A950-C452F1847AEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA919AE9-EB3A-4925-A950-C452F1847AEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA919AE9-EB3A-4925-A950-C452F1847AEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA919AE9-EB3A-4925-A950-C452F1847AEF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D5F55B6C-3AFF-4565-BF0A-87CD35257473} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {90C16E16-B4E8-4578-A8A7-CA095A5CB063} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {4CB1B75E-0A2D-4F67-B2AD-F471E2B42B0D} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {11715F80-BF6A-47D4-97E6-A370EED518E2} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {F6DF6898-9B92-4006-AEF5-FEA1AA734C18} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {FD13CF06-248D-40C2-A14E-E35D2FF8AE94} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} = {44ADCCD2-3DD4-48D5-9F63-5C2D61F889A7} - {B12D84A0-1C8B-4D80-9395-41F6D4DF584A} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {824ECDD2-6B82-4A4C-A247-B3B5167B077A} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {F63CF535-E697-4013-BED8-0AC527915B68} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {FA896405-3DF8-4D0E-AB32-BB4FDF542DD9} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {092A5F30-63B6-4071-A08E-3BC4705CAF16} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {5D3B1B24-349E-4D32-831F-C098F00846FE} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {2A6273D2-52D4-4C98-8DEE-CD88ABDE3279} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {CFBB8BE0-2126-4CB7-BBF2-0C3D998BD142} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {004DAE75-5BA0-4B95-B2DB-EA4E46C523EA} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {A2496E05-D61F-439A-A71B-4E4EE970ABE9} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {70F538E0-0254-4021-9281-49B7F1B5EBFA} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {CEE734AA-CE10-48F0-8335-E5776EE93B6F} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {51581231-AD41-4360-BC57-DD61E8BAB821} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} - {0822F67D-02DD-4C6F-AE05-037BEC5FA08A} = {AD90CBF5-2ABD-4295-993E-03E92E8F3F56} + {4FC7807A-D826-42F2-8D08-3827C01DF4D3} = {AE5CD1FF-A7F0-4542-8D45-00B8A985E326} + {8E998059-A6AB-46A8-8BF1-9AA7CBF46BFC} = {AE5CD1FF-A7F0-4542-8D45-00B8A985E326} + {AEFB7390-6B1B-4565-B24E-F8C9A55D4B64} = {AE5CD1FF-A7F0-4542-8D45-00B8A985E326} + {A1CA4784-52FB-45BE-AF2B-B92690B56492} = {AE5CD1FF-A7F0-4542-8D45-00B8A985E326} + {16580376-0166-4529-82C3-1BC286BEEFCB} = {AE5CD1FF-A7F0-4542-8D45-00B8A985E326} + {ED897630-8969-48AF-86BE-C32124859698} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {E2FBD559-A590-4D00-AEBA-B66D9C344102} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {8994E6B9-28B5-4942-B455-AF3A92D95281} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {58A3ABF6-A3B4-4BF3-85A1-B5E05DFA82BC} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {2B0ADB7B-4A1B-4892-B0E7-4A7663EEA26C} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {087E163B-AC5F-4832-84D6-2B9E5454D987} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {ED9D3157-1F1F-4AD4-BE6D-E52F6A240728} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {E6868C22-2254-4CD7-A670-296DB76778B5} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {16AF9596-21A3-4A11-9C33-F538EE3DC6D4} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {8A243513-D890-4CFC-A94E-EDB0B1B9EC49} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {1C7E7B3C-55D1-488C-9C92-4DCC461935EB} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {E9656395-0866-4F80-B0B6-89EA7A4570F4} = {16580376-0166-4529-82C3-1BC286BEEFCB} + {CA919AE9-EB3A-4925-A950-C452F1847AEF} = {16580376-0166-4529-82C3-1BC286BEEFCB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9DB5CF5E-DB10-4F49-B69A-8C0A98144F23} + SolutionGuid = {948C4912-2DBE-40C5-BFC5-76C9A958922D} EndGlobalSection EndGlobal diff --git a/global.json b/global.json index f2ef584..150fa0f 100644 --- a/global.json +++ b/global.json @@ -6,6 +6,7 @@ "allowPrelease": false }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "4.1.0" + "Microsoft.Build.Traversal": "4.1.0", + "Vezel.Zig.Sdk": "4.2.16" } } diff --git a/src/analyzers/DiagnosticDescriptors.cs b/src/analyzers/DiagnosticDescriptors.cs deleted file mode 100644 index 72ec781..0000000 --- a/src/analyzers/DiagnosticDescriptors.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Vezel.Cathode.Analyzers; - -internal static class DiagnosticDescriptors -{ - [AttributeUsage(AttributeTargets.Property, Inherited = false)] - private sealed class DiagnosticAttribute : Attribute - { - public string Title { get; } - - public string Message { get; } - - public DiagnosticSeverity Severity { get; } - - public DiagnosticAttribute(string title, string message, DiagnosticSeverity severity) - { - Title = title; - Message = message; - Severity = severity; - } - } - - [Diagnostic( - "Avoid multiple types implementing 'IProgram'", - "There must only be one type implementing 'IProgram' within an assembly", - DiagnosticSeverity.Error)] - public static DiagnosticDescriptor AvoidMultipleProgramTypes { get; private set; } = null!; - - [Diagnostic( - "Avoid manually implementing an entry point", - "Manually-implemented entry point method '{0}' conflicts with the generated entry point", - DiagnosticSeverity.Error)] - public static DiagnosticDescriptor AvoidImplementingEntryPoint { get; private set; } = null!; - - static DiagnosticDescriptors() - { - var id = 1000; - - foreach (var p in typeof(DiagnosticDescriptors) - .GetProperties(BindingFlags.Static | BindingFlags.Public) - .Where(p => p.PropertyType == typeof(DiagnosticDescriptor)) - .OrderBy(p => p.MetadataToken)) - { - var attr = p.GetCustomAttribute(); - - p.SetValue( - null, - new DiagnosticDescriptor($"CATH{id}", attr.Title, attr.Message, "Vezel.Cathode", attr.Severity, true)); - - id++; - } - } -} diff --git a/src/analyzers/Hosting/EntryPointGenerator.cs b/src/analyzers/Hosting/EntryPointGenerator.cs deleted file mode 100644 index 5d2612c..0000000 --- a/src/analyzers/Hosting/EntryPointGenerator.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace Vezel.Cathode.Analyzers.Hosting; - -[Generator] -public sealed class EntryPointGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterSourceOutput( - context.CompilationProvider - .Select(static (c, ct) => c.GetEntryPoint(ct)) - .Combine( - context.SyntaxProvider - .CreateSyntaxProvider( - static (node, _) => node is TypeDeclarationSyntax, - static (ctx, _) => (node: (TypeDeclarationSyntax)ctx.Node, model: ctx.SemanticModel)) - .Combine( - context.MetadataReferencesProvider - .Combine(context.CompilationProvider) - .Select(static (tup, _) => tup.Right.GetAssemblyOrModuleSymbol(tup.Left)) - .Where(static sym => sym is IAssemblySymbol { Name: "Vezel.Cathode.Hosting" }) - .Collect() - .Select(static (arr, _) => - ((IAssemblySymbol?)arr.FirstOrDefault())?.GetTypeByMetadataName( - "Vezel.Cathode.Hosting.IProgram"))) - .Select(static (tup, ct) => - (Interface: tup.Right, Candidate: tup.Left.model.GetDeclaredSymbol(tup.Left.node, ct))) - .Where(static tup => - tup is (not null, not null) && - tup.Candidate.AllInterfaces.Any(iface => - iface.Equals(tup.Interface, SymbolEqualityComparer.Default))) - .Select(static (tup, _) => tup.Candidate) - .Collect()), - static (ctx, tup) => - { - var syms = tup.Right; - - // Is the project using the terminal hosting APIs? - if (syms.IsEmpty) - return; - - if (syms.Length != 1) - foreach (var sym in syms) - foreach (var loc in sym!.Locations) - ctx.ReportDiagnostic( - Diagnostic.Create(DiagnosticDescriptors.AvoidMultipleProgramTypes, loc)); - - if (tup.Left is IMethodSymbol entry) - { - foreach (var loc in entry.Locations) - ctx.ReportDiagnostic( - Diagnostic.Create(DiagnosticDescriptors.AvoidImplementingEntryPoint, loc, entry)); - - return; - } - - var name = syms[0]!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - ctx.AddSource( - "GeneratedProgram.g.cs", - $$""" - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] - file static class GeneratedProgram - { - private static global::System.Threading.Tasks.Task Main(string[] args) - { - return global::Vezel.Cathode.Hosting.ProgramHost.RunAsync<{{name}}>(args); - } - } - """); - }); - } -} diff --git a/src/analyzers/analyzers.cs b/src/analyzers/analyzers.cs deleted file mode 100644 index ae5cefb..0000000 --- a/src/analyzers/analyzers.cs +++ /dev/null @@ -1,2 +0,0 @@ -[assembly: DisableRuntimeMarshalling] -[module: SkipLocalsInit] diff --git a/src/analyzers/analyzers.csproj b/src/analyzers/analyzers.csproj deleted file mode 100644 index 6196471..0000000 --- a/src/analyzers/analyzers.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - Vezel.Cathode.Analyzers - true - Vezel.Cathode.Analyzers - netstandard2.0 - - - - - - - - - - - - diff --git a/src/common/common.csproj b/src/common/common.csproj index 23b0eea..0e08eb7 100644 --- a/src/common/common.csproj +++ b/src/common/common.csproj @@ -12,6 +12,5 @@ This package provides common functionality used by all Cathode packages. - diff --git a/src/core/Native/TerminalInterop.cs b/src/core/Native/TerminalInterop.cs new file mode 100644 index 0000000..788a145 --- /dev/null +++ b/src/core/Native/TerminalInterop.cs @@ -0,0 +1,145 @@ +namespace Vezel.Cathode.Native; + +internal static unsafe partial class TerminalInterop +{ + public enum TerminalException + { + None, + ArgumentOutOfRange, + PlatformNotSupported, + TerminalNotAttached, + TerminalConfiguration, + Terminal, + } + + [StructLayout(LayoutKind.Sequential)] + public struct TerminalResult + { + public TerminalException Exception; + + public char* Message; + + public int Error; + + public readonly void ThrowIfError() + { + // For when ArgumentOutOfRangeException is not expected. + ThrowIfError((object?)null); + } + + public readonly void ThrowIfError(in T value, [CallerArgumentExpression(nameof(value))] string? name = null) + { + _ = value; + + if (Exception == TerminalException.None) + return; + + switch (Exception) + { + case TerminalException.ArgumentOutOfRange: + throw new ArgumentOutOfRangeException(name); + case TerminalException.PlatformNotSupported: + throw new PlatformNotSupportedException(); + case TerminalException.TerminalNotAttached: + throw new TerminalNotAttachedException(); + case TerminalException.TerminalConfiguration: + throw new TerminalConfigurationException($"{new(Message)} {new Win32Exception(Error).Message}"); + case TerminalException.Terminal: + throw new IO.TerminalException($"{new(Message)} {new Win32Exception(Error).Message}"); + } + } + } + + private const string Library = "Vezel.Cathode.Native"; + + static TerminalInterop() + { + NativeLibrary.SetDllImportResolver( +#pragma warning disable CS0436 + typeof(ThisAssembly).Assembly, +#pragma warning restore CS0436 + static (name, asm, paths) => + { + // First try the normal search algorithm that takes into account the application's configuration and + // static dependency information. + if (NativeLibrary.TryLoad(name, asm, paths, out var handle)) + return handle; + + // If someone is trying to load some unknown library through our assembly, at this point, there is + // nothing more that we can do. + if (name != Library) + return 0; + + // It is now likely that someone is trying to use Cathode without static dependency information, so the + // runtime has no idea how to find Vezel.Cathode.Native. In this case, it is likely to either sit right + // next to Vezel.Cathode.dll, or in runtimes//native. + + var directory = AppContext.BaseDirectory; + var fileName = OperatingSystem.IsWindows() + ? $"{name}.dll" + : OperatingSystem.IsMacOS() + ? $"lib{name}.dylib" + : $"lib{name}.so"; + + bool TryLoad(out nint handle, params string[] paths) + { + return NativeLibrary.TryLoad(Path.Combine([directory, .. paths, fileName]), out handle); + } + + return TryLoad(out handle) + ? handle + : TryLoad(out handle, "runtimes", RuntimeInformation.RuntimeIdentifier, "native") + ? handle + : 0; + }); + } + + [LibraryImport(Library, EntryPoint = "cathode_initialize")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void Initialize(); + + [LibraryImport(Library, EntryPoint = "cathode_get_handles")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void GetHandles(nuint* stdIn, nuint* stdOut, nuint* stdErr, nuint* ttyIn, nuint* ttyOut); + + [LibraryImport(Library, EntryPoint = "cathode_is_valid")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool IsValid(nuint handle, [MarshalAs(UnmanagedType.U1)] bool write); + + [LibraryImport(Library, EntryPoint = "cathode_is_interactive")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool IsInteractive(nuint handle); + + [LibraryImport(Library, EntryPoint = "cathode_query_size")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool QuerySize(int* width, int* height); + + [LibraryImport(Library, EntryPoint = "cathode_get_mode")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool GetMode(); + + [LibraryImport(Library, EntryPoint = "cathode_set_mode")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial TerminalResult SetMode( + [MarshalAs(UnmanagedType.U1)] bool raw, [MarshalAs(UnmanagedType.U1)] bool flush); + + [LibraryImport(Library, EntryPoint = "cathode_generate_signal")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial TerminalResult GenerateSignal(TerminalSignal signal); + + [LibraryImport(Library, EntryPoint = "cathode_read")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial TerminalResult Read(nuint handle, byte* buffer, int length, int* progress); + + [LibraryImport(Library, EntryPoint = "cathode_write")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial TerminalResult Write(nuint handle, byte* buffer, int length, int* progress); + + [LibraryImport(Library, EntryPoint = "cathode_poll")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial void Poll([MarshalAs(UnmanagedType.U1)] bool write, nuint* handles, bool* results, int count); +} diff --git a/src/core/NativeMethods.json b/src/core/NativeMethods.json deleted file mode 100644 index d749ca5..0000000 --- a/src/core/NativeMethods.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json", - "className": "WindowsPInvoke", - "allowMarshaling": false, - "wideCharOnly": false, - "emitSingleFile": true -} diff --git a/src/core/NativeMethods.txt b/src/core/NativeMethods.txt deleted file mode 100644 index 9cf33cf..0000000 --- a/src/core/NativeMethods.txt +++ /dev/null @@ -1,24 +0,0 @@ -CreateFileW -FlushConsoleInputBuffer -GenerateConsoleCtrlEvent -GetConsoleCP -GetConsoleMode -GetConsoleOutputCP -GetConsoleScreenBufferInfo -GetFileType -GetStdHandle -ReadConsoleW -ReadFile -SetConsoleCP -SetConsoleMode -SetConsoleOutputCP -WriteFile - -FILE_ACCESS_RIGHTS -FILE_TYPE -WIN32_ERROR - -CTRL_BREAK_EVENT -CTRL_C_EVENT -CTRL_CLOSE_EVENT -CTRL_SHUTDOWN_EVENT diff --git a/src/core/SystemVirtualTerminal.cs b/src/core/SystemVirtualTerminal.cs index c3fd9b9..ea9ec35 100644 --- a/src/core/SystemVirtualTerminal.cs +++ b/src/core/SystemVirtualTerminal.cs @@ -5,7 +5,7 @@ namespace Vezel.Cathode; [SuppressMessage("", "CA1001")] public abstract class SystemVirtualTerminal : VirtualTerminal { - public override event Action? Resized + public override sealed event Action? Resized { add { @@ -30,7 +30,7 @@ public override event Action? Resized } } - public override event Action? Signaled + public override sealed event Action? Signaled { add { @@ -41,6 +41,7 @@ public override event Action? Signaled if (_signaled == null) return; + [SuppressMessage("", "CA1065")] void HandleSignal(PosixSignalContext context) { var ctx = new TerminalSignalContext( @@ -50,7 +51,7 @@ void HandleSignal(PosixSignalContext context) PosixSignal.SIGINT => TerminalSignal.Interrupt, PosixSignal.SIGQUIT => TerminalSignal.Quit, PosixSignal.SIGTERM => TerminalSignal.Terminate, - _ => throw new NotSupportedException($"Received unexpected signal: {context.Signal}"), + _ => throw new UnreachableException(), }); _signaled?.Invoke(ctx); @@ -89,10 +90,17 @@ void HandleSignal(PosixSignalContext context) public TerminalControl Control { get; } = new(); - public override bool IsRawMode => _rawMode; + public override sealed bool IsRawMode + { + get + { + lock (_rawLock) + return GetMode(); + } + } [SuppressMessage("", "CA1065")] - public override Size Size + public override sealed Size Size { get { @@ -138,14 +146,15 @@ public TimeSpan SizePollingInterval private PosixSignalRegistration? _sigTerm; - private bool _rawMode; - private Size? _size; private TimeSpan _sizeInterval = TimeSpan.FromMilliseconds(100); private protected SystemVirtualTerminal() { + // Try to get the terminal size as early as reasonably possible. + RefreshSize(); + var thread = new Thread(() => { while (true) @@ -165,9 +174,9 @@ private protected SystemVirtualTerminal() thread.Start(); } - protected abstract Size? QuerySize(); + private protected abstract Size? QuerySize(); - protected void RefreshSize() + private protected void RefreshSize() { if (QuerySize() is not { } size) { @@ -196,41 +205,45 @@ protected void RefreshSize() // Do this on the thread pool to avoid breaking internals if an event handler misbehaves. _ = ThreadPool.UnsafeQueueUserWorkItem( - static tup => Unsafe.As(tup.terminal)._resized?.Invoke(tup.size), - (terminal: this, size), - true); + static tup => Unsafe.As(tup.This)._resized?.Invoke(tup.Size), + (This: this, Size: size), + preferLocal: true); } - protected abstract void SetMode(bool raw); + private protected abstract bool GetMode(); - public override void EnableRawMode() - { - using var guard = Control.Guard(); + private protected abstract void SetMode(bool raw, bool flush); + private protected void ChangeRawMode(bool raw, bool flush, Action? check) + { lock (_rawLock) { - Check.Operation( - _processes.Count == 0, $"Cannot enable raw mode with non-redirected child processes running."); - - SetMode(true); + check?.Invoke(this); - _rawMode = true; + SetMode(raw, flush); } } - public override void DisableRawMode() + public override sealed void EnableRawMode() { using var guard = Control.Guard(); - lock (_rawLock) - { - Check.Operation( - _processes.Count == 0, $"Cannot disable raw mode with non-redirected child processes running."); + ChangeRawMode( + raw: true, + flush: true, + static @this => Check.Operation( + @this._processes.Count == 0, $"Cannot enable raw mode with non-redirected child processes running.")); + } - SetMode(false); + public override sealed void DisableRawMode() + { + using var guard = Control.Guard(); - _rawMode = false; - } + ChangeRawMode( + raw: false, + flush: true, + static @this => Check.Operation( + @this._processes.Count == 0, $"Cannot disable raw mode with non-redirected child processes running.")); } internal void StartProcess(Func starter) @@ -239,7 +252,7 @@ internal void StartProcess(Func starter) { // The vast majority of programs expect to start in cooked mode. Enforce that we are in cooked mode while // any child processes that could be using the terminal are running. - Check.Operation(!_rawMode, $"Cannot start non-redirected child processes in raw mode."); + Check.Operation(!IsRawMode, $"Cannot start non-redirected child processes in raw mode."); // Guard here since this locks us into cooked mode until all non-redirected processes are gone. using var guard = Control.Guard(); @@ -260,14 +273,11 @@ internal void ReapProcess(ChildProcess process) try { // Child processes may have messed up the terminal settings. Restore them just in case. - SetMode(false); + SetMode(raw: false, flush: true); } catch (Exception e) when (e is TerminalNotAttachedException or TerminalConfigurationException) { } } } - - [EditorBrowsable(EditorBrowsableState.Never)] - public abstract void DangerousRestoreSettings(); } diff --git a/src/core/Terminal.cs b/src/core/Terminal.cs index 576299f..62fd0ab 100644 --- a/src/core/Terminal.cs +++ b/src/core/Terminal.cs @@ -1,5 +1,4 @@ -using Vezel.Cathode.Terminals.Unix.Linux; -using Vezel.Cathode.Terminals.Unix.MacOS; +using Vezel.Cathode.Terminals.Unix; using Vezel.Cathode.Terminals.Windows; namespace Vezel.Cathode; @@ -27,10 +26,9 @@ public static event Action? Resumed public static Encoding Encoding { get; } = new UTF8Encoding(false); public static SystemVirtualTerminal System { get; } = - OperatingSystem.IsLinux() ? LinuxVirtualTerminal.Instance : - OperatingSystem.IsMacOS() ? MacOSVirtualTerminal.Instance : - OperatingSystem.IsWindows() ? WindowsVirtualTerminal.Instance : - throw new PlatformNotSupportedException(); + OperatingSystem.IsWindows() + ? WindowsVirtualTerminal.Instance + : UnixVirtualTerminal.Instance; public static TerminalControl Control => System.Control; @@ -239,10 +237,4 @@ public static ValueTask ErrorLineAsync(T value, CancellationToken cancellatio { return System.ErrorLineAsync(value, cancellationToken); } - - [EditorBrowsable(EditorBrowsableState.Never)] - public static void DangerousRestoreSettings() - { - System.DangerousRestoreSettings(); - } } diff --git a/src/core/TerminalSignal.cs b/src/core/TerminalSignal.cs index 6dde8a4..c726ba7 100644 --- a/src/core/TerminalSignal.cs +++ b/src/core/TerminalSignal.cs @@ -1,5 +1,6 @@ namespace Vezel.Cathode; +// Keep in sync with src/native/driver.h. public enum TerminalSignal { Close, diff --git a/src/core/Terminals/NativeTerminalReader`2.cs b/src/core/Terminals/NativeTerminalReader.cs similarity index 75% rename from src/core/Terminals/NativeTerminalReader`2.cs rename to src/core/Terminals/NativeTerminalReader.cs index 76c22aa..153d712 100644 --- a/src/core/Terminals/NativeTerminalReader`2.cs +++ b/src/core/Terminals/NativeTerminalReader.cs @@ -1,17 +1,16 @@ +using Vezel.Cathode.Native; + namespace Vezel.Cathode.Terminals; -internal abstract class NativeTerminalReader : TerminalReader - where TTerminal : NativeVirtualTerminal +internal abstract class NativeTerminalReader : TerminalReader { // Note that the buffer size used affects how many characters the Windows console host will allow the user to type // in a single line (in cooked mode). private const int ReadBufferSize = 4096; - public TTerminal Terminal { get; } - - public string Name { get; } + public NativeVirtualTerminal Terminal { get; } - public THandle Handle { get; } + public nuint Handle { get; } public override sealed Stream Stream { get; } @@ -21,17 +20,16 @@ internal abstract class NativeTerminalReader : TerminalReade public override sealed bool IsInteractive { get; } - protected NativeTerminalReader(TTerminal terminal, string name, THandle handle) + protected NativeTerminalReader(NativeVirtualTerminal terminal, nuint handle) { Terminal = terminal; - Name = name; Handle = handle; Stream = new SynchronizedStream(new TerminalInputStream(this)); TextReader = new SynchronizedTextReader( new StreamReader(Stream, Cathode.Terminal.Encoding, false, ReadBufferSize, true)); - IsValid = terminal.IsHandleValid(handle, false); - IsInteractive = terminal.IsHandleInteractive(handle); + IsValid = TerminalInterop.IsValid(handle, write: false); + IsInteractive = TerminalInterop.IsInteractive(handle); } protected abstract int ReadPartialNative(scoped Span buffer, CancellationToken cancellationToken); diff --git a/src/core/Terminals/NativeTerminalWriter`2.cs b/src/core/Terminals/NativeTerminalWriter.cs similarity index 75% rename from src/core/Terminals/NativeTerminalWriter`2.cs rename to src/core/Terminals/NativeTerminalWriter.cs index 076dbbf..b827a37 100644 --- a/src/core/Terminals/NativeTerminalWriter`2.cs +++ b/src/core/Terminals/NativeTerminalWriter.cs @@ -1,16 +1,15 @@ +using Vezel.Cathode.Native; + namespace Vezel.Cathode.Terminals; -internal abstract class NativeTerminalWriter : TerminalWriter - where TTerminal : NativeVirtualTerminal +internal abstract class NativeTerminalWriter : TerminalWriter { // Unlike NativeTerminalReader, the buffer size here is arbitrary and only has performance implications. private const int WriteBufferSize = 256; - public TTerminal Terminal { get; } - - public string Name { get; } + public NativeVirtualTerminal Terminal { get; } - public THandle Handle { get; } + public nuint Handle { get; } public override sealed Stream Stream { get; } @@ -20,10 +19,9 @@ internal abstract class NativeTerminalWriter : TerminalWrite public override sealed bool IsInteractive { get; } - protected NativeTerminalWriter(TTerminal terminal, string name, THandle handle) + protected NativeTerminalWriter(NativeVirtualTerminal terminal, nuint handle) { Terminal = terminal; - Name = name; Handle = handle; Stream = new SynchronizedStream(new TerminalOutputStream(this)); TextWriter = @@ -31,8 +29,8 @@ protected NativeTerminalWriter(TTerminal terminal, string name, THandle handle) { AutoFlush = true, }); - IsValid = terminal.IsHandleValid(handle, true); - IsInteractive = terminal.IsHandleInteractive(handle); + IsValid = TerminalInterop.IsValid(handle, write: true); + IsInteractive = TerminalInterop.IsInteractive(handle); } protected abstract int WritePartialNative(scoped ReadOnlySpan buffer, CancellationToken cancellationToken); diff --git a/src/core/Terminals/NativeVirtualTerminal.cs b/src/core/Terminals/NativeVirtualTerminal.cs new file mode 100644 index 0000000..2c86742 --- /dev/null +++ b/src/core/Terminals/NativeVirtualTerminal.cs @@ -0,0 +1,70 @@ +using Vezel.Cathode.Native; + +namespace Vezel.Cathode.Terminals; + +internal abstract class NativeVirtualTerminal : SystemVirtualTerminal +{ + public override sealed NativeTerminalReader StandardIn { get; } + + public override sealed NativeTerminalWriter StandardOut { get; } + + public override sealed NativeTerminalWriter StandardError { get; } + + public override sealed NativeTerminalReader TerminalIn { get; } + + public override sealed NativeTerminalWriter TerminalOut { get; } + + [SuppressMessage("", "CA2000")] + [SuppressMessage("", "CA2214")] + private protected unsafe NativeVirtualTerminal() + { + // Ensure that the native library is fully loaded and initialized before we do anything terminal-related. + TerminalInterop.Initialize(); + + var inLock = new SemaphoreSlim(1, 1); + var outLock = new SemaphoreSlim(1, 1); + + nuint stdIn; + nuint stdOut; + nuint stdErr; + nuint ttyIn; + nuint ttyOut; + + TerminalInterop.GetHandles(&stdIn, &stdOut, &stdErr, &ttyIn, &ttyOut); + + StandardIn = CreateReader(stdIn, inLock); + StandardOut = CreateWriter(stdOut, outLock); + StandardError = CreateWriter(stdErr, outLock); + TerminalIn = CreateReader(ttyIn, inLock); + TerminalOut = CreateWriter(ttyOut, outLock); + } + + protected abstract NativeTerminalReader CreateReader(nuint handle, SemaphoreSlim semaphore); + + protected abstract NativeTerminalWriter CreateWriter(nuint handle, SemaphoreSlim semaphore); + + private protected override sealed unsafe Size? QuerySize() + { + int width; + int height; + + return TerminalInterop.QuerySize(&width, &height) ? new(width, height) : null; + } + + private protected override sealed bool GetMode() + { + return TerminalInterop.GetMode(); + } + + private protected override sealed void SetMode(bool raw, bool flush) + { + TerminalInterop.SetMode(raw, flush).ThrowIfError(); + } + + public override sealed void GenerateSignal(TerminalSignal signal) + { + using var guard = Control.Guard(); + + TerminalInterop.GenerateSignal(signal).ThrowIfError(signal); + } +} diff --git a/src/core/Terminals/NativeVirtualTerminal`1.cs b/src/core/Terminals/NativeVirtualTerminal`1.cs deleted file mode 100644 index 632cb29..0000000 --- a/src/core/Terminals/NativeVirtualTerminal`1.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Vezel.Cathode.Terminals; - -internal abstract class NativeVirtualTerminal : SystemVirtualTerminal -{ - public abstract bool IsHandleValid(THandle handle, bool write); - - public abstract bool IsHandleInteractive(THandle handle); -} diff --git a/src/core/Terminals/Unix/Linux/LinuxVirtualTerminal.cs b/src/core/Terminals/Unix/Linux/LinuxVirtualTerminal.cs deleted file mode 100644 index 1a31d6b..0000000 --- a/src/core/Terminals/Unix/Linux/LinuxVirtualTerminal.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Vezel.Cathode.Unix; -using Vezel.Cathode.Unix.Linux; -using static Vezel.Cathode.Unix.Linux.LinuxPInvoke; -using static Vezel.Cathode.Unix.UnixPInvoke; - -namespace Vezel.Cathode.Terminals.Unix.Linux; - -internal sealed class LinuxVirtualTerminal : UnixVirtualTerminal -{ - // Keep this class in sync with the MacOSVirtualTerminal class. - - public static LinuxVirtualTerminal Instance { get; } = new(); - - private Termios? _original; - - private LinuxVirtualTerminal() - { - } - - protected override Size? QuerySize() - { - return ioctl(TerminalOut.Handle, TIOCGWINSZ, out var w) == 0 ? new(w.ws_col, w.ws_row) : null; - } - - protected override unsafe void SetModeCore(bool raw, bool flush) - { - if (tcgetattr(TerminalOut.Handle, out var termios) == -1) - throw new TerminalNotAttachedException(); - - // Stash away the original settings the first time we are successfully called. - _original ??= termios; - - // These values are usually the default, but we set them just to be safe since UnixTerminalReader would not - // behave as expected by callers if these values differ. - termios.c_cc[VTIME] = 0; - termios.c_cc[VMIN] = 1; - - // Turn off some features that make little or no sense for virtual terminals. - termios.c_iflag &= ~(IGNBRK | IGNPAR | PARMRK | INPCK | ISTRIP | IXOFF | IMAXBEL); - termios.c_oflag &= ~(OFILL | OFDEL | NLDLY | CRDLY | TABDLY | BSDLY | VTDLY | FFDLY); - termios.c_oflag |= NL0 | CR0 | TAB0 | BS0 | VT0 | FF0; - termios.c_cflag &= ~(CSTOPB | PARENB | PARODD | HUPCL | CLOCAL | CMSPAR | CRTSCTS); - termios.c_lflag &= ~(FLUSHO | EXTPROC); - - // Set up some sensible defaults. - termios.c_iflag &= ~(IGNCR | INLCR | IUCLC | IXANY); - termios.c_iflag |= IUTF8; - termios.c_oflag &= ~(OLCUC | OCRNL | ONOCR | ONLRET); - termios.c_cflag &= ~CSIZE; - termios.c_cflag |= CS8 | CREAD; - termios.c_lflag &= ~(XCASE | ECHONL | NOFLSH | ECHOPRT | PENDIN); - - var iflag = BRKINT | ICRNL | IXON; - var oflag = OPOST | ONLCR; - var lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN; - - // Finally, enable/disable features that depend on raw/cooked mode. - if (raw) - { - termios.c_iflag &= ~iflag; - termios.c_oflag &= ~oflag; - termios.c_lflag &= ~lflag; - termios.c_lflag |= TOSTOP; - } - else - { - termios.c_iflag |= iflag; - termios.c_oflag |= oflag; - termios.c_lflag |= lflag; - termios.c_lflag &= ~TOSTOP; - } - - int ret; - var err = 0; - - using (var guard = raw ? null : new PosixSignalGuard(PosixSignal.SIGTTOU)) - { - while ((ret = tcsetattr(TerminalOut.Handle, flush ? TCSAFLUSH : TCSANOW, termios)) == -1 && - (err = Marshal.GetLastPInvokeError()) == EINTR) - { - // Retry in case we get interrupted by a signal. If we are trying to switch to cooked mode and we saw - // SIGTTOU, it means we are a background process. We will trust that, by the time we actually read or - // write anything, we will be in cooked mode. - if (guard?.Signaled == true) - return; - } - } - - if (ret != 0) - throw new TerminalConfigurationException( - $"Could not change terminal mode: {new Win32Exception(err).Message}"); - } - - public override int OpenTerminalHandle(string name) - { - return open(name, O_RDWR | O_NOCTTY | O_CLOEXEC); - } - - public override bool PollHandles(int? error, short events, scoped Span handles) - { - if (error is int err && err != EAGAIN) - return false; - - var fds = (stackalloc Pollfd[handles.Length]); - - for (var i = 0; i < handles.Length; i++) - fds[i] = new Pollfd - { - fd = handles[i], - events = events, - }; - - int ret; - - while ((ret = poll(fds, (nuint)fds.Length, -1)) == -1 && Marshal.GetLastPInvokeError() == EINTR) - { - // Retry in case we get interrupted by a signal. - } - - if (ret == -1) - return false; - - for (var i = 0; i < handles.Length; i++) - handles[i] = fds[i].revents; - - return true; - } - - public override void DangerousRestoreSettings() - { - using var guard = Control.Guard(); - - if (_original is Termios tios) - _ = tcsetattr(TerminalOut.Handle, TCSAFLUSH, tios); - } -} diff --git a/src/core/Terminals/Unix/MacOS/MacOSVirtualTerminal.cs b/src/core/Terminals/Unix/MacOS/MacOSVirtualTerminal.cs deleted file mode 100644 index 9f0869e..0000000 --- a/src/core/Terminals/Unix/MacOS/MacOSVirtualTerminal.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Vezel.Cathode.Unix; -using Vezel.Cathode.Unix.MacOS; -using static Vezel.Cathode.Unix.MacOS.MacOSPInvoke; -using static Vezel.Cathode.Unix.UnixPInvoke; - -namespace Vezel.Cathode.Terminals.Unix.MacOS; - -internal sealed class MacOSVirtualTerminal : UnixVirtualTerminal -{ - // Keep this class in sync with the LinuxVirtualTerminal class. - - public static MacOSVirtualTerminal Instance { get; } = new(); - - private Termios? _original; - - private MacOSVirtualTerminal() - { - } - - protected override Size? QuerySize() - { - return ioctl(TerminalOut.Handle, TIOCGWINSZ, out var w) == 0 ? new(w.ws_col, w.ws_row) : null; - } - - protected override unsafe void SetModeCore(bool raw, bool flush) - { - if (tcgetattr(TerminalOut.Handle, out var termios) == -1) - throw new TerminalNotAttachedException(); - - // Stash away the original settings the first time we are successfully called. - _original ??= termios; - - // These values are usually the default, but we set them just to be safe since UnixTerminalReader would not - // behave as expected by callers if these values differ. - termios.c_cc[VTIME] = 0; - termios.c_cc[VMIN] = 1; - - // Turn off some features that make little or no sense for virtual terminals. - termios.c_iflag &= ~(IGNBRK | IGNPAR | PARMRK | INPCK | ISTRIP | IXOFF | IMAXBEL); - termios.c_oflag &= ~(OFILL | OFDEL | NLDLY | CRDLY | TABDLY | BSDLY | VTDLY | FFDLY); - termios.c_oflag |= NL0 | CR0 | TAB0 | BS0 | VT0 | FF0; - termios.c_cflag &= ~(CSTOPB | PARENB | PARODD | HUPCL | CLOCAL | CRTSCTS | CDTR_IFLOW | CDSR_OFLOW | MDMBUF); - termios.c_lflag &= ~(FLUSHO | EXTPROC); - - // Set up some sensible defaults. - termios.c_iflag &= ~(IGNCR | INLCR | IXANY); - termios.c_iflag |= IUTF8; - termios.c_oflag &= ~(ONOEOT | OCRNL | ONOCR | ONLRET); - termios.c_cflag &= ~CSIZE; - termios.c_cflag |= CS8 | CREAD; - termios.c_lflag &= ~(ECHONL | ALTWERASE | NOFLSH | ECHOPRT | PENDIN); - - var iflag = BRKINT | ICRNL | IXON; - var oflag = OPOST | ONLCR; - var lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN; - - // Finally, enable/disable features that depend on raw/cooked mode. - if (raw) - { - termios.c_iflag &= ~iflag; - termios.c_oflag &= ~oflag; - termios.c_lflag &= ~lflag; - termios.c_lflag |= TOSTOP | NOKERNINFO; - } - else - { - termios.c_iflag |= iflag; - termios.c_oflag |= oflag; - termios.c_lflag |= lflag; - termios.c_lflag &= ~(TOSTOP | NOKERNINFO); - } - - int ret; - var err = 0; - - using (var guard = raw ? null : new PosixSignalGuard(PosixSignal.SIGTTOU)) - { - while ((ret = tcsetattr(TerminalOut.Handle, flush ? TCSAFLUSH : TCSANOW, termios)) == -1 && - (err = Marshal.GetLastPInvokeError()) == EINTR) - { - // Retry in case we get interrupted by a signal. If we are trying to switch to cooked mode and we saw - // SIGTTOU, it means we are a background process. We will trust that, by the time we actually read or - // write anything, we will be in cooked mode. - if (guard?.Signaled == true) - return; - } - } - - if (ret != 0) - throw new TerminalConfigurationException( - $"Could not change terminal mode: {new Win32Exception(err).Message}"); - } - - public override int OpenTerminalHandle(string name) - { - return open(name, O_RDWR | O_NOCTTY | O_CLOEXEC); - } - - public override bool PollHandles(int? error, short events, scoped Span handles) - { - if (error is int err && err != EAGAIN) - return false; - - var fds = (stackalloc Pollfd[handles.Length]); - - for (var i = 0; i < handles.Length; i++) - fds[i] = new Pollfd - { - fd = handles[i], - events = events, - }; - - int ret; - - while ((ret = poll(fds, (uint)fds.Length, -1)) == -1 && Marshal.GetLastPInvokeError() == EINTR) - { - // Retry in case we get interrupted by a signal. - } - - if (ret == -1) - return false; - - for (var i = 0; i < handles.Length; i++) - handles[i] = fds[i].revents; - - return true; - } - - public override void DangerousRestoreSettings() - { - using var guard = Control.Guard(); - - if (_original is Termios tios) - _ = tcsetattr(TerminalOut.Handle, TCSAFLUSH, tios); - } -} diff --git a/src/core/Terminals/Unix/PosixSignalGuard.cs b/src/core/Terminals/Unix/PosixSignalGuard.cs deleted file mode 100644 index dfb6bcb..0000000 --- a/src/core/Terminals/Unix/PosixSignalGuard.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Vezel.Cathode.Terminals.Unix; - -internal sealed class PosixSignalGuard : IDisposable -{ - public bool Signaled => _signaled; - - private readonly PosixSignalRegistration _registration; - - private volatile bool _signaled; - - public PosixSignalGuard(PosixSignal signal) - { - _registration = PosixSignalRegistration.Create(signal, _ => _signaled = true); - } - - ~PosixSignalGuard() - { - Dispose(); - } - - public void Dispose() - { - _registration?.Dispose(); - - GC.SuppressFinalize(this); - } -} diff --git a/src/core/Terminals/Unix/UnixCancellationPipe.cs b/src/core/Terminals/Unix/UnixCancellationPipe.cs index 34ba708..17b49fa 100644 --- a/src/core/Terminals/Unix/UnixCancellationPipe.cs +++ b/src/core/Terminals/Unix/UnixCancellationPipe.cs @@ -1,4 +1,4 @@ -using static Vezel.Cathode.Unix.UnixPInvoke; +using Vezel.Cathode.Native; namespace Vezel.Cathode.Terminals.Unix; @@ -18,7 +18,7 @@ public UnixCancellationPipe(UnixVirtualTerminal terminal) _client = new(PipeDirection.In, _server.ClientSafePipeHandle); } - public unsafe void PollWithCancellation(int handle, CancellationToken cancellationToken) + public unsafe void PollWithCancellation(nuint handle, CancellationToken cancellationToken) { // Note that the runtime sets up a SIGPIPE handler for us. @@ -29,15 +29,19 @@ public unsafe void PollWithCancellation(int handle, CancellationToken cancellati try { - var handles = (stackalloc[] { (int)pipeHandle.DangerousGetHandle(), handle }); + var handles = stackalloc[] + { + (nuint)pipeHandle.DangerousGetHandle(), + handle, + }; + var results = stackalloc bool[2]; using (var registration = cancellationToken.UnsafeRegister( - static state => Unsafe.As(state!)._server.WriteByte(42), this)) - if (!_terminal.PollHandles(null, POLLIN, handles)) - return; + static @this => Unsafe.As(@this!)._server.WriteByte(42), this)) + TerminalInterop.Poll(write: false, handles, results, count: 2); // Were we canceled? - if ((handles[0] & POLLIN) != 0) + if (results[0]) { // Read the dummy byte that was written to indicate cancellation. _ = _client.ReadByte(); diff --git a/src/core/Terminals/Unix/UnixTerminalReader.cs b/src/core/Terminals/Unix/UnixTerminalReader.cs index 82e8e13..3c2b4f6 100644 --- a/src/core/Terminals/Unix/UnixTerminalReader.cs +++ b/src/core/Terminals/Unix/UnixTerminalReader.cs @@ -1,56 +1,40 @@ -using static Vezel.Cathode.Unix.UnixPInvoke; +using Vezel.Cathode.Native; namespace Vezel.Cathode.Terminals.Unix; -internal sealed class UnixTerminalReader : NativeTerminalReader +internal sealed class UnixTerminalReader : NativeTerminalReader { private readonly SemaphoreSlim _semaphore; private readonly UnixCancellationPipe _cancellationPipe; public UnixTerminalReader( - UnixVirtualTerminal terminal, - string name, - int handle, - UnixCancellationPipe cancellationPipe, - SemaphoreSlim semaphore) - : base(terminal, name, handle) + UnixVirtualTerminal terminal, nuint handle, UnixCancellationPipe cancellationPipe, SemaphoreSlim semaphore) + : base(terminal, handle) { _semaphore = semaphore; _cancellationPipe = cancellationPipe; } - protected override int ReadPartialNative(scoped Span buffer, CancellationToken cancellationToken) + protected override unsafe int ReadPartialNative(scoped Span buffer, CancellationToken cancellationToken) { using var guard = Terminal.Control.Guard(); // If the descriptor is invalid, just present the illusion to the user that it has been redirected to /dev/null // or something along those lines, i.e. return EOF. - if (buffer.IsEmpty || !IsValid) + if (buffer is [] || !IsValid) return 0; using (_semaphore.Enter(cancellationToken)) { _cancellationPipe.PollWithCancellation(Handle, cancellationToken); - nint ret; + int progress; - // Note that this call may get us suspended by way of a SIGTTIN signal if we are a background process and - // the handle refers to a terminal. - while ((ret = read(Handle, buffer, (nuint)buffer.Length)) == -1 && Marshal.GetLastPInvokeError() == EINTR) - { - // Retry in case we get interrupted by a signal. - } + fixed (byte* p = buffer) + TerminalInterop.Read(Handle, p, buffer.Length, &progress).ThrowIfError(); - if (ret != -1) - return (int)ret; - - var err = Marshal.GetLastPInvokeError(); - - // EPIPE means the descriptor was probably redirected to a program that ended. - return err == EPIPE - ? 0 - : throw new TerminalException($"Could not read from {Name}: {new Win32Exception(err).Message}"); + return progress; } } } diff --git a/src/core/Terminals/Unix/UnixTerminalWriter.cs b/src/core/Terminals/Unix/UnixTerminalWriter.cs index 784a26b..71ce82a 100644 --- a/src/core/Terminals/Unix/UnixTerminalWriter.cs +++ b/src/core/Terminals/Unix/UnixTerminalWriter.cs @@ -1,56 +1,34 @@ -using static Vezel.Cathode.Unix.UnixPInvoke; +using Vezel.Cathode.Native; namespace Vezel.Cathode.Terminals.Unix; -internal sealed class UnixTerminalWriter : NativeTerminalWriter +internal sealed class UnixTerminalWriter : NativeTerminalWriter { private readonly SemaphoreSlim _semaphore; - public UnixTerminalWriter(UnixVirtualTerminal terminal, string name, int handle, SemaphoreSlim semaphore) - : base(terminal, name, handle) + public UnixTerminalWriter(UnixVirtualTerminal terminal, nuint handle, SemaphoreSlim semaphore) + : base(terminal, handle) { _semaphore = semaphore; } - protected override int WritePartialNative(scoped ReadOnlySpan buffer, CancellationToken cancellationToken) + protected override unsafe int WritePartialNative(scoped ReadOnlySpan buffer, CancellationToken cancellationToken) { using var guard = Terminal.Control.Guard(); // If the descriptor is invalid, just present the illusion to the user that it has been redirected to /dev/null // or something along those lines, i.e. pretend we wrote everything. - if (buffer.IsEmpty || !IsValid) + if (buffer is [] || !IsValid) return buffer.Length; using (_semaphore.Enter(cancellationToken)) { - while (true) - { - nint ret; - - // Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, - // the handle refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that - // it could get set anyway). - while ((ret = write(Handle, buffer, (nuint)buffer.Length)) == -1 && - Marshal.GetLastPInvokeError() == EINTR) - { - } - - if (ret != -1) - return (int)ret; - - var err = Marshal.GetLastPInvokeError(); - - // EPIPE means the descriptor was probably redirected to a program that ended. - if (err == EPIPE) - return 0; - - // The file descriptor might have been configured as non-blocking. Instead of busily trying to write - // over and over, poll until we can write and then try again. - if (Terminal.PollHandles(err, POLLOUT, [Handle])) - continue; - - throw new TerminalException($"Could not write to {Name}: {new Win32Exception(err).Message}"); - } + int progress; + + fixed (byte* p = buffer) + TerminalInterop.Write(Handle, p, buffer.Length, &progress).ThrowIfError(); + + return progress; } } } diff --git a/src/core/Terminals/Unix/UnixVirtualTerminal.cs b/src/core/Terminals/Unix/UnixVirtualTerminal.cs index a8f06a9..e0ac81f 100644 --- a/src/core/Terminals/Unix/UnixVirtualTerminal.cs +++ b/src/core/Terminals/Unix/UnixVirtualTerminal.cs @@ -1,20 +1,10 @@ -using static Vezel.Cathode.Unix.UnixPInvoke; - namespace Vezel.Cathode.Terminals.Unix; -internal abstract class UnixVirtualTerminal : NativeVirtualTerminal +internal sealed class UnixVirtualTerminal : NativeVirtualTerminal { public override event Action? Resumed; - public override sealed UnixTerminalReader StandardIn { get; } - - public override sealed UnixTerminalWriter StandardOut { get; } - - public override sealed UnixTerminalWriter StandardError { get; } - - public override sealed UnixTerminalReader TerminalIn { get; } - - public override sealed UnixTerminalWriter TerminalOut { get; } + public static UnixVirtualTerminal Instance { get; } = new(); [SuppressMessage("", "IDE0052")] private readonly PosixSignalRegistration _sigWinch; @@ -25,34 +15,8 @@ internal abstract class UnixVirtualTerminal : NativeVirtualTerminal [SuppressMessage("", "IDE0052")] private readonly PosixSignalRegistration _sigChld; - private readonly object _rawLock = new(); - - [SuppressMessage("", "CA2214")] - protected UnixVirtualTerminal() + public unsafe UnixVirtualTerminal() { - var inLock = new SemaphoreSlim(1, 1); - var outLock = new SemaphoreSlim(1, 1); - - StandardIn = new(this, "standard input", STDIN_FILENO, new(this), inLock); - StandardOut = new(this, "standard output", STDOUT_FILENO, outLock); - StandardError = new(this, "standard error", STDERR_FILENO, outLock); - - var tty = OpenTerminalHandle("/dev/tty"); - - TerminalIn = new(this, "terminal input", tty, new(this), inLock); - TerminalOut = new(this, "terminal output", tty, outLock); - - try - { - // Start in cooked mode. - SetModeCore(false, false); - } - catch (Exception e) when (e is TerminalNotAttachedException or TerminalConfigurationException) - { - } - - RefreshSize(); - void HandleSignal(PosixSignalContext context) { // If we are being restored from the background (SIGCONT), it is possible and likely that terminal settings @@ -60,13 +24,12 @@ void HandleSignal(PosixSignalContext context) // // This is a best-effort thing. The reality is that, since this signal handler method gets called in a // thread after the process has fully woken up, other code may already be trying to interact with the - // terminal again. There is currently nothing we can really do about this race condition. + // terminal again. There is nothing we can really do about this race condition. if (context.Signal == PosixSignal.SIGCONT) { try { - lock (_rawLock) - SetModeCore(IsRawMode, false); + ChangeRawMode(IsRawMode, flush: false, check: null); } catch (Exception e) when (e is TerminalNotAttachedException or TerminalConfigurationException) { @@ -76,13 +39,13 @@ void HandleSignal(PosixSignalContext context) } // Do this on the thread pool to avoid breaking internals if an event handler misbehaves. - _ = ThreadPool.UnsafeQueueUserWorkItem(static term => term.Resumed?.Invoke(), this, true); + _ = ThreadPool.UnsafeQueueUserWorkItem( + static @this => @this.Resumed?.Invoke(), this, preferLocal: true); } - // Terminal width/height will definitely have changed for SIGWINCH, and might have changed for SIGCONT. On - // Unix systems, SIGWINCH lets us respond much more quickly to a change in terminal size. - if (context.Signal is PosixSignal.SIGWINCH or PosixSignal.SIGCONT) - RefreshSize(); + // Terminal width/height will definitely have changed for SIGWINCH, and might have changed for SIGCONT and + // SIGCHLD. On Unix systems, signals let us respond much more quickly to a change in terminal size. + RefreshSize(); // Prevent System.Native from overwriting our terminal settings. context.Cancel = true; @@ -94,44 +57,13 @@ void HandleSignal(PosixSignalContext context) _sigChld = PosixSignalRegistration.Create(PosixSignal.SIGCHLD, HandleSignal); } - public override sealed void GenerateSignal(TerminalSignal signal) - { - using var guard = Control.Guard(); - - _ = kill( - 0, - signal switch - { - TerminalSignal.Close => SIGHUP, - TerminalSignal.Interrupt => SIGINT, - TerminalSignal.Quit => SIGQUIT, - TerminalSignal.Terminate => SIGTERM, - _ => throw new ArgumentOutOfRangeException(nameof(signal)), - }); - } - - protected abstract void SetModeCore(bool raw, bool flush); - - protected override sealed void SetMode(bool raw) - { - // We can be called from signal handlers so we need an additional lock here. - lock (_rawLock) - SetModeCore(raw, true); - } - - public abstract int OpenTerminalHandle(string name); - - public abstract bool PollHandles(int? error, short events, scoped Span handles); - - public override sealed bool IsHandleValid(int handle, bool write) + protected override UnixTerminalReader CreateReader(nuint handle, SemaphoreSlim semaphore) { - // We might obtain a negative descriptor (-1) if we fail to open /dev/tty, for example. - return handle >= 0; + return new(this, handle, new(this), semaphore); } - public override sealed bool IsHandleInteractive(int handle) + protected override UnixTerminalWriter CreateWriter(nuint handle, SemaphoreSlim semaphore) { - // Note that this also returns false for invalid descriptors. - return isatty(handle) == 1; + return new(this, handle, semaphore); } } diff --git a/src/core/Terminals/Windows/WindowsTerminalReader.cs b/src/core/Terminals/Windows/WindowsTerminalReader.cs index 11db1f7..2f6d819 100644 --- a/src/core/Terminals/Windows/WindowsTerminalReader.cs +++ b/src/core/Terminals/Windows/WindowsTerminalReader.cs @@ -1,14 +1,13 @@ -using static Windows.Win32.WindowsPInvoke; +using Vezel.Cathode.Native; namespace Vezel.Cathode.Terminals.Windows; -internal sealed class WindowsTerminalReader : NativeTerminalReader +internal sealed class WindowsTerminalReader : NativeTerminalReader { private readonly SemaphoreSlim _semaphore; - public WindowsTerminalReader( - WindowsVirtualTerminal terminal, string name, SafeHandle handle, SemaphoreSlim semaphore) - : base(terminal, name, handle) + public WindowsTerminalReader(WindowsVirtualTerminal terminal, nuint handle, SemaphoreSlim semaphore) + : base(terminal, handle) { _semaphore = semaphore; } @@ -19,18 +18,17 @@ protected override unsafe int ReadPartialNative(scoped Span buffer, Cancel // If the handle is invalid, just present the illusion to the user that it has been redirected to /dev/null or // something along those lines, i.e. return EOF. - if (buffer.IsEmpty || !IsValid) + if (buffer is [] || !IsValid) return 0; - bool result; - uint read; - using (_semaphore.Enter(cancellationToken)) - result = ReadFile(Handle, buffer, &read, null); + { + int progress; - if (!result && read == 0) - WindowsTerminalUtility.ThrowIfUnexpected($"Could not read from {Name}"); + fixed (byte* p = buffer) + TerminalInterop.Read(Handle, p, buffer.Length, &progress).ThrowIfError(); - return (int)read; + return progress; + } } } diff --git a/src/core/Terminals/Windows/WindowsTerminalUtility.cs b/src/core/Terminals/Windows/WindowsTerminalUtility.cs deleted file mode 100644 index 9f324e5..0000000 --- a/src/core/Terminals/Windows/WindowsTerminalUtility.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Windows.Win32.Foundation; - -namespace Vezel.Cathode.Terminals.Windows; - -internal static class WindowsTerminalUtility -{ - public static void ThrowIfUnexpected(string message) - { - var err = Marshal.GetLastPInvokeError(); - - // See comments in UnixTerminalWriter for the error handling rationale. - switch ((WIN32_ERROR)err) - { - case WIN32_ERROR.ERROR_HANDLE_EOF or WIN32_ERROR.ERROR_BROKEN_PIPE or WIN32_ERROR.ERROR_NO_DATA: - break; - default: - throw new TerminalException($"{message}: {new Win32Exception(err).Message}"); - } - } -} diff --git a/src/core/Terminals/Windows/WindowsTerminalWriter.cs b/src/core/Terminals/Windows/WindowsTerminalWriter.cs index f8c0652..406a5b0 100644 --- a/src/core/Terminals/Windows/WindowsTerminalWriter.cs +++ b/src/core/Terminals/Windows/WindowsTerminalWriter.cs @@ -1,14 +1,13 @@ -using static Windows.Win32.WindowsPInvoke; +using Vezel.Cathode.Native; namespace Vezel.Cathode.Terminals.Windows; -internal sealed class WindowsTerminalWriter : NativeTerminalWriter +internal sealed class WindowsTerminalWriter : NativeTerminalWriter { private readonly SemaphoreSlim _semaphore; - public WindowsTerminalWriter( - WindowsVirtualTerminal terminal, string name, SafeHandle handle, SemaphoreSlim semaphore) - : base(terminal, name, handle) + public WindowsTerminalWriter(WindowsVirtualTerminal terminal, nuint handle, SemaphoreSlim semaphore) + : base(terminal, handle) { _semaphore = semaphore; } @@ -20,18 +19,17 @@ protected override unsafe int WritePartialNative( // If the handle is invalid, just present the illusion to the user that it has been redirected to /dev/null or // something along those lines, i.e. pretend we wrote everything. - if (buffer.IsEmpty || !IsValid) + if (buffer is [] || !IsValid) return buffer.Length; - bool result; - uint written; - using (_semaphore.Enter(cancellationToken)) - result = WriteFile(Handle, buffer, &written, null); + { + int progress; - if (!result && written == 0) - WindowsTerminalUtility.ThrowIfUnexpected($"Could not write to {Name}"); + fixed (byte* p = buffer) + TerminalInterop.Write(Handle, p, buffer.Length, &progress).ThrowIfError(); - return (int)written; + return progress; + } } } diff --git a/src/core/Terminals/Windows/WindowsVirtualTerminal.cs b/src/core/Terminals/Windows/WindowsVirtualTerminal.cs index 9ff03d6..01e8d84 100644 --- a/src/core/Terminals/Windows/WindowsVirtualTerminal.cs +++ b/src/core/Terminals/Windows/WindowsVirtualTerminal.cs @@ -1,31 +1,7 @@ -using Windows.Win32.Security; -using Windows.Win32.Storage.FileSystem; -using Windows.Win32.System.Console; -using static Windows.Win32.WindowsPInvoke; - namespace Vezel.Cathode.Terminals.Windows; -internal sealed class WindowsVirtualTerminal : NativeVirtualTerminal +internal sealed class WindowsVirtualTerminal : NativeVirtualTerminal { - private sealed class ConsoleState - { - public CONSOLE_MODE InMode { get; } - - public CONSOLE_MODE OutMode { get; } - - public uint InCodePage { get; } - - public uint OutCodePage { get; } - - public ConsoleState(CONSOLE_MODE inMode, CONSOLE_MODE outMode, uint inCodePage, uint outCodePage) - { - InMode = inMode; - OutMode = outMode; - InCodePage = inCodePage; - OutCodePage = outCodePage; - } - } - public override event Action? Resumed { add @@ -40,197 +16,17 @@ public override event Action? Resumed public static WindowsVirtualTerminal Instance { get; } = new(); - public override WindowsTerminalReader StandardIn { get; } - - public override WindowsTerminalWriter StandardOut { get; } - - public override WindowsTerminalWriter StandardError { get; } - - public override WindowsTerminalReader TerminalIn { get; } - - public override WindowsTerminalWriter TerminalOut { get; } - - private ConsoleState? _original; - - [SuppressMessage("", "CA2000")] private WindowsVirtualTerminal() { - var inLock = new SemaphoreSlim(1, 1); - var outLock = new SemaphoreSlim(1, 1); - - StandardIn = new(this, "standard input", GetStdHandle_SafeHandle(STD_HANDLE.STD_INPUT_HANDLE), inLock); - StandardOut = new(this, "standard output", GetStdHandle_SafeHandle(STD_HANDLE.STD_OUTPUT_HANDLE), outLock); - StandardError = new(this, "standard error", GetStdHandle_SafeHandle(STD_HANDLE.STD_ERROR_HANDLE), outLock); - - static SafeHandle OpenConsoleHandle(string name) - { - return CreateFileW( - name, - (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), - FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, - new SECURITY_ATTRIBUTES - { - bInheritHandle = true, - }, - FILE_CREATION_DISPOSITION.OPEN_EXISTING, - 0, - null); - } - - TerminalIn = new(this, "terminal input", OpenConsoleHandle("CONIN$"), inLock); - TerminalOut = new(this, "terminal output", OpenConsoleHandle("CONOUT$"), outLock); - - try - { - // Start in cooked mode. - SetModeCore(false, false); - } - catch (Exception e) when (e is TerminalNotAttachedException or TerminalConfigurationException) - { - } - } - - protected override Size? QuerySize() - { - return GetConsoleScreenBufferInfo(TerminalOut.Handle, out var info) - ? new(info.srWindow.Right - info.srWindow.Left + 1, info.srWindow.Bottom - info.srWindow.Top + 1) - : null; - } - - public override void GenerateSignal(TerminalSignal signal) - { - using var guard = Control.Guard(); - - _ = GenerateConsoleCtrlEvent( - signal switch - { - TerminalSignal.Interrupt => CTRL_C_EVENT, - TerminalSignal.Quit => CTRL_BREAK_EVENT, - TerminalSignal.Close or TerminalSignal.Terminate => throw new PlatformNotSupportedException(), - _ => throw new ArgumentOutOfRangeException(nameof(signal)), - }, - 0); - } - - private void SetModeCore(bool raw, bool flush) - { - uint inCP; - uint outCP; - - if (!GetConsoleMode(TerminalIn.Handle, out var inMode) || - !GetConsoleMode(TerminalOut.Handle, out var outMode) || - (inCP = GetConsoleCP()) == 0 || - (outCP = GetConsoleOutputCP()) == 0) - throw new TerminalNotAttachedException(); - - // Stash away the original modes the first time we are successfully called. - _original ??= new(inMode, outMode, inCP, outCP); - - var origIn = inMode; - var origOut = outMode; - - // Set up some sensible defaults. - inMode &= ~( - CONSOLE_MODE.ENABLE_WINDOW_INPUT | - CONSOLE_MODE.ENABLE_MOUSE_INPUT | - CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE); - inMode |= - CONSOLE_MODE.ENABLE_INSERT_MODE | - CONSOLE_MODE.ENABLE_EXTENDED_FLAGS | - CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT; - outMode &= ~CONSOLE_MODE.ENABLE_LVB_GRID_WORLDWIDE; - outMode |= - CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT | - CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT | - CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING; - - var inExtra = - CONSOLE_MODE.ENABLE_PROCESSED_INPUT | - CONSOLE_MODE.ENABLE_LINE_INPUT | - CONSOLE_MODE.ENABLE_ECHO_INPUT; - var outExtra = CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN; - - // Enable/disable features that depend on cooked/raw mode. - if (!raw) - { - inMode |= inExtra; - outMode |= outExtra; - } - else - { - inMode &= ~inExtra; - outMode &= ~outExtra; - } - - try - { - var utf8 = (uint)Encoding.UTF8.CodePage; - - if (!SetConsoleCP(utf8) || !SetConsoleOutputCP(utf8)) - throw new TerminalConfigurationException( - $"Could not change console code page: {new Win32Exception().Message}"); - - if (!SetConsoleMode(TerminalIn.Handle, inMode) || !SetConsoleMode(TerminalOut.Handle, outMode)) - throw new TerminalConfigurationException( - $"Could not change console mode: {new Win32Exception().Message}"); - - if (flush && !FlushConsoleInputBuffer(TerminalIn.Handle)) - throw new TerminalConfigurationException( - $"Could not flush input buffer: {new Win32Exception().Message}"); - } - catch (TerminalConfigurationException) - { - // If we failed to configure the console, try to undo partial configuration (if any). - - _ = SetConsoleMode(TerminalIn.Handle, origIn); - _ = SetConsoleMode(TerminalOut.Handle, origOut); - - _ = SetConsoleCP(inCP); - _ = SetConsoleOutputCP(outCP); - - throw; - } - } - - protected override void SetMode(bool raw) - { - SetModeCore(raw, true); - } - - public override void DangerousRestoreSettings() - { - using var guard = Control.Guard(); - - if (_original != null) - { - _ = SetConsoleMode(TerminalIn.Handle, _original.InMode); - _ = SetConsoleMode(TerminalOut.Handle, _original.OutMode); - - _ = SetConsoleCP(_original.InCodePage); - _ = SetConsoleOutputCP(_original.OutCodePage); - } } - public override unsafe bool IsHandleValid(SafeHandle handle, bool write) + protected override WindowsTerminalReader CreateReader(nuint handle, SemaphoreSlim semaphore) { - if (handle.IsInvalid) - return false; - - // Apparently, for Windows GUI programs, the standard I/O handles will appear to be valid (i.e. not -1 or 0) but - // will not actually be usable. So do a zero-byte write to figure out if the handle is actually valid. - if (write) - { - var dummy = 42u; - - return WriteFile(handle, default, &dummy, null); - } - - return true; + return new(this, handle, semaphore); } - public override bool IsHandleInteractive(SafeHandle handle) + protected override WindowsTerminalWriter CreateWriter(nuint handle, SemaphoreSlim semaphore) { - // Note that this also returns true for invalid handles. - return GetFileType(handle) == FILE_TYPE.FILE_TYPE_CHAR && GetConsoleMode(handle, out _); + return new(this, handle, semaphore); } } diff --git a/src/core/Unix/Linux/LinuxPInvoke.cs b/src/core/Unix/Linux/LinuxPInvoke.cs deleted file mode 100644 index 3ef9a7a..0000000 --- a/src/core/Unix/Linux/LinuxPInvoke.cs +++ /dev/null @@ -1,167 +0,0 @@ -namespace Vezel.Cathode.Unix.Linux; - -[SuppressMessage("", "SA1300")] -[SuppressMessage("", "SA1310")] -internal static unsafe partial class LinuxPInvoke -{ - // c_iflag - - public const uint IGNBRK = 0x1; - - public const uint BRKINT = 0x2; - - public const uint IGNPAR = 0x4; - - public const uint PARMRK = 0x8; - - public const uint INPCK = 0x10; - - public const uint ISTRIP = 0x20; - - public const uint INLCR = 0x40; - - public const uint IGNCR = 0x80; - - public const uint ICRNL = 0x100; - - public const uint IUCLC = 0x200; - - public const uint IXON = 0x400; - - public const uint IXANY = 0x800; - - public const uint IXOFF = 0x1000; - - public const uint IMAXBEL = 0x2000; - - public const uint IUTF8 = 0x4000; - - // c_oflag - - public const uint OPOST = 0x1; - - public const uint OLCUC = 0x2; - - public const uint ONLCR = 0x4; - - public const uint OCRNL = 0x8; - - public const uint ONOCR = 0x10; - - public const uint ONLRET = 0x20; - - public const uint OFILL = 0x40; - - public const uint OFDEL = 0x80; - - public const uint NLDLY = 0x100; - - public const uint NL0 = 0x0; - - public const uint CRDLY = 0x600; - - public const uint CR0 = 0x0; - - public const uint TABDLY = 0x1800; - - public const uint TAB0 = 0x0; - - public const uint BSDLY = 0x2000; - - public const uint BS0 = 0x0; - - public const uint VTDLY = 0x4000; - - public const uint VT0 = 0x0; - - public const uint FFDLY = 0x8000; - - public const uint FF0 = 0x0; - - // c_cflag - - public const uint CSTOPB = 0x40; - - public const uint CREAD = 0x80; - - public const uint PARENB = 0x100; - - public const uint PARODD = 0x200; - - public const uint HUPCL = 0x400; - - public const uint CLOCAL = 0x800; - - public const uint CMSPAR = 0x40000000; - - public const uint CRTSCTS = 0x80000000; - - public const uint CSIZE = 0x30; - - public const uint CS8 = 0x30; - - // c_lflag - - public const uint ISIG = 0x1; - - public const uint ICANON = 0x2; - - public const uint XCASE = 0x4; - - public const uint ECHO = 0x8; - - public const uint ECHOE = 0x10; - - public const uint ECHOK = 0x20; - - public const uint ECHONL = 0x40; - - public const uint NOFLSH = 0x80; - - public const uint TOSTOP = 0x100; - - public const uint ECHOCTL = 0x200; - - public const uint ECHOPRT = 0x400; - - public const uint ECHOKE = 0x800; - - public const uint FLUSHO = 0x1000; - - public const uint PENDIN = 0x4000; - - public const uint IEXTEN = 0x8000; - - public const uint EXTPROC = 0x10000; - - // c_cc - - public const int VTIME = 5; - - public const int VMIN = 6; - - // flags - - public const int O_RDWR = 0x2; - - public const int O_NOCTTY = 0x100; - - public const int O_CLOEXEC = 0x80000; - - // request - - public const nuint TIOCGWINSZ = 0x5413; - - // errno - - public const int EAGAIN = 11; - - [LibraryImport("c", SetLastError = true)] - public static partial int poll(Span fds, nuint nfds, int timeout); - - [LibraryImport("c", SetLastError = true)] - public static partial int tcgetattr(int fildes, out Termios termios_p); - - [LibraryImport("c", SetLastError = true)] - public static partial int tcsetattr(int fildes, int optional_actions, in Termios termios_p); -} diff --git a/src/core/Unix/Linux/termios.cs b/src/core/Unix/Linux/termios.cs deleted file mode 100644 index e9bdf8b..0000000 --- a/src/core/Unix/Linux/termios.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Vezel.Cathode.Unix.Linux; - -[StructLayout(LayoutKind.Sequential)] -[SuppressMessage("", "SA1307")] -[SuppressMessage("", "SA1310")] -internal struct Termios -{ - public uint c_iflag; - - public uint c_oflag; - - public uint c_cflag; - - public uint c_lflag; - - public byte c_line; - - public unsafe fixed byte c_cc[32]; - - public uint c_ispeed; - - public uint c_ospeed; -} diff --git a/src/core/Unix/MacOS/MacOSPInvoke.cs b/src/core/Unix/MacOS/MacOSPInvoke.cs deleted file mode 100644 index 6d6f4c6..0000000 --- a/src/core/Unix/MacOS/MacOSPInvoke.cs +++ /dev/null @@ -1,171 +0,0 @@ -namespace Vezel.Cathode.Unix.MacOS; - -[SuppressMessage("", "SA1300")] -[SuppressMessage("", "SA1310")] -internal static unsafe partial class MacOSPInvoke -{ - // c_iflag - - public const nuint IGNBRK = 0x1; - - public const nuint BRKINT = 0x2; - - public const nuint IGNPAR = 0x4; - - public const nuint PARMRK = 0x8; - - public const nuint INPCK = 0x10; - - public const nuint ISTRIP = 0x20; - - public const nuint INLCR = 0x40; - - public const nuint IGNCR = 0x80; - - public const nuint ICRNL = 0x100; - - public const nuint IXON = 0x200; - - public const nuint IXOFF = 0x400; - - public const nuint IXANY = 0x800; - - public const nuint IMAXBEL = 0x2000; - - public const nuint IUTF8 = 0x4000; - - // c_oflag - - public const nuint OPOST = 0x1; - - public const nuint ONLCR = 0x2; - - public const nuint ONOEOT = 0x8; - - public const nuint OCRNL = 0x10; - - public const nuint ONOCR = 0x20; - - public const nuint ONLRET = 0x40; - - public const nuint OFILL = 0x80; - - public const nuint OFDEL = 0x20000; - - public const nuint NLDLY = 0x300; - - public const nuint NL0 = 0x0; - - public const nuint TABDLY = 0xc04; - - public const nuint TAB0 = 0x0; - - public const nuint CRDLY = 0x3000; - - public const nuint CR0 = 0x0; - - public const nuint FFDLY = 0x4000; - - public const nuint FF0 = 0x0; - - public const nuint BSDLY = 0x8000; - - public const nuint BS0 = 0x0; - - public const nuint VTDLY = 0x10000; - - public const nuint VT0 = 0x0; - - // c_cflag - - public const nuint CSTOPB = 0x400; - - public const nuint CREAD = 0x800; - - public const nuint PARENB = 0x1000; - - public const nuint PARODD = 0x2000; - - public const nuint HUPCL = 0x4000; - - public const nuint CLOCAL = 0x8000; - - public const nuint CRTSCTS = 0x30000; - - public const nuint CDTR_IFLOW = 0x40000; - - public const nuint CDSR_OFLOW = 0x80000; - - public const nuint MDMBUF = 0x100000; - - public const nuint CSIZE = 0x300; - - public const nuint CS8 = 0x300; - - // c_lflag - - public const nuint ECHOKE = 0x1; - - public const nuint ECHOE = 0x2; - - public const nuint ECHOK = 0x4; - - public const nuint ECHO = 0x8; - - public const nuint ECHONL = 0x10; - - public const nuint ECHOPRT = 0x20; - - public const nuint ECHOCTL = 0x40; - - public const nuint ISIG = 0x80; - - public const nuint ICANON = 0x100; - - public const nuint ALTWERASE = 0x200; - - public const nuint IEXTEN = 0x400; - - public const nuint EXTPROC = 0x800; - - public const nuint TOSTOP = 0x400000; - - public const nuint FLUSHO = 0x800000; - - public const nuint NOKERNINFO = 0x2000000; - - public const nuint PENDIN = 0x20000000; - - public const nuint NOFLSH = 0x80000000; - - // c_cc - - public const int VMIN = 16; - - public const int VTIME = 17; - - // flags - - public const int O_RDWR = 0x2; - - public const int O_NOCTTY = 0x20000; - - public const int O_CLOEXEC = 0x1000000; - - // request - - public const nuint TIOCGWINSZ = 0x40087468; - - // errno - - public const int EAGAIN = 35; - - [LibraryImport("c", SetLastError = true)] - public static partial int poll(Span fds, uint nfds, int timeout); - - [LibraryImport("c", SetLastError = true)] - public static partial int tcgetattr(int fildes, out Termios termios_p); - - [LibraryImport("c", SetLastError = true)] - public static partial int tcsetattr(int fildes, int optional_actions, in Termios termios_p); -} diff --git a/src/core/Unix/MacOS/termios.cs b/src/core/Unix/MacOS/termios.cs deleted file mode 100644 index 643c5e5..0000000 --- a/src/core/Unix/MacOS/termios.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Vezel.Cathode.Unix.MacOS; - -[StructLayout(LayoutKind.Sequential)] -[SuppressMessage("", "SA1307")] -[SuppressMessage("", "SA1310")] -internal struct Termios -{ - public nuint c_iflag; - - public nuint c_oflag; - - public nuint c_cflag; - - public nuint c_lflag; - - public unsafe fixed byte c_cc[20]; - - public nuint c_ispeed; - - public nuint c_ospeed; -} diff --git a/src/core/Unix/UnixPInvoke.cs b/src/core/Unix/UnixPInvoke.cs deleted file mode 100644 index 04e5fc4..0000000 --- a/src/core/Unix/UnixPInvoke.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Vezel.Cathode.Unix; - -[SuppressMessage("", "SA1300")] -[SuppressMessage("", "SA1310")] -internal static unsafe partial class UnixPInvoke -{ - // fd - - public const int STDIN_FILENO = 0; - - public const int STDOUT_FILENO = 1; - - public const int STDERR_FILENO = 2; - - // events, revents - - public const short POLLIN = 0x1; - - public const short POLLOUT = 0x4; - - // optional_actions - - public const int TCSANOW = 0; - - public const int TCSAFLUSH = 2; - - // sig - - public const int SIGHUP = 1; - - public const int SIGINT = 2; - - public const int SIGQUIT = 3; - - public const int SIGTERM = 15; - - // errno - - public const int EINTR = 4; - - public const int EPIPE = 32; - - [LibraryImport("c", SetLastError = true)] - public static partial int isatty(int fildes); - - [LibraryImport("c", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)] - public static partial int open(string path, int oflag); - - [LibraryImport("c", SetLastError = true)] - public static partial nint read(int fildes, Span buf, nuint nbyte); - - [LibraryImport("c", SetLastError = true)] - public static partial nint write(int fildes, ReadOnlySpan buf, nuint nbyte); - - [LibraryImport("c", SetLastError = true)] - public static partial int ioctl(int fildes, nuint request, out Winsize argp); - - [LibraryImport("c", SetLastError = true)] - public static partial int kill(int pid, int sig); -} diff --git a/src/core/Unix/pollfd.cs b/src/core/Unix/pollfd.cs deleted file mode 100644 index fd8e868..0000000 --- a/src/core/Unix/pollfd.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Vezel.Cathode.Unix; - -[StructLayout(LayoutKind.Sequential)] -[SuppressMessage("", "SA1307")] -internal struct Pollfd -{ - public int fd; - - public short events; - - public short revents; -} diff --git a/src/core/Unix/winsize.cs b/src/core/Unix/winsize.cs deleted file mode 100644 index f66e035..0000000 --- a/src/core/Unix/winsize.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Vezel.Cathode.Unix; - -[StructLayout(LayoutKind.Sequential)] -[SuppressMessage("", "SA1307")] -[SuppressMessage("", "SA1310")] -internal struct Winsize -{ - public ushort ws_row; - - public ushort ws_col; - - public ushort ws_xpixel; - - public ushort ws_ypixel; -} diff --git a/src/core/VirtualTerminal.cs b/src/core/VirtualTerminal.cs index cdb6ee1..533826d 100644 --- a/src/core/VirtualTerminal.cs +++ b/src/core/VirtualTerminal.cs @@ -18,16 +18,16 @@ public abstract class VirtualTerminal public abstract TerminalWriter TerminalOut { get; } - public abstract bool IsRawMode { get; } - public abstract Size Size { get; } - public abstract void GenerateSignal(TerminalSignal signal); + public abstract bool IsRawMode { get; } public abstract void EnableRawMode(); public abstract void DisableRawMode(); + public abstract void GenerateSignal(TerminalSignal signal); + public int Read(scoped Span value) { return StandardIn.ReadPartial(value); diff --git a/src/core/core.csproj b/src/core/core.csproj index 85312b3..89ab47c 100644 --- a/src/core/core.csproj +++ b/src/core/core.csproj @@ -1,7 +1,6 @@ Vezel.Cathode - _PackAnalyzer true $(PackageDescription) @@ -28,36 +27,31 @@ This package provides the core terminal API. - + - - - - - + + + ItemName="_NativeLibrary" /> - + diff --git a/src/hosting/BannedSymbols.txt b/src/hosting/BannedSymbols.txt deleted file mode 100644 index 7e32bca..0000000 --- a/src/hosting/BannedSymbols.txt +++ /dev/null @@ -1,6 +0,0 @@ -E:System.AppDomain.ProcessExit;Use Vezel.Cathode.Hosting.ProgramContext.ProcessExit instead -E:System.AppDomain.UnhandledException;Use Vezel.Cathode.Hosting.ProgramContext.UnhandledException instead -M:System.Environment.Exit(System.Int32);Use Vezel.Cathode.Hosting.ProgramContext.ExitCode instead -M:System.Environment.FailFast(System.String);May leave the terminal in an unusable state -M:System.Environment.FailFast(System.String,System.Exception);May leave the terminal in an unusable state -P:System.Environment.ExitCode;Use Vezel.Cathode.Hosting.ProgramContext.ExitCode instead diff --git a/src/hosting/IProgram.cs b/src/hosting/IProgram.cs deleted file mode 100644 index c8fbc31..0000000 --- a/src/hosting/IProgram.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Vezel.Cathode.Hosting; - -public interface IProgram -{ - public static abstract Task RunAsync(ProgramContext context); -} diff --git a/src/hosting/ProgramContext.cs b/src/hosting/ProgramContext.cs deleted file mode 100644 index 3715a4d..0000000 --- a/src/hosting/ProgramContext.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Vezel.Cathode.Hosting; - -public sealed class ProgramContext -{ - public event Action? UnhandledException; - - public event Action? ProcessExiting; - - public ReadOnlyMemory Arguments { get; } - - public CancellationToken CancellationToken => _cancellationToken.Value; - - public int ExitCode { get; set; } - - private readonly Lazy _cancellationToken = new(() => - { - // We create the token lazily when the caller wants it because hooking the Signaled event can have undesirable - // side effects when running under a debugger. - - var cts = new CancellationTokenSource(); - - Terminal.Signaled += ctx => - { - // We just kind of assume that any event handlers that come after us will not flip it back to false. Not - // much we can do in that case. - ctx.Cancel = true; - - cts.Cancel(); - }; - - return cts.Token; - }); - - internal ProgramContext(ReadOnlyMemory arguments) - { - Arguments = arguments; - } - - // The following methods must never throw. - - [SuppressMessage("", "CA1031")] - internal bool RaiseUnhandledException(Exception exception) - { - var ev = UnhandledException; - - if (ev == null) - return false; - - foreach (var dg in ev.GetInvocationList()) - { - try - { - Unsafe.As>(dg).Invoke(exception); - } - catch (Exception) - { - } - } - - return true; - } - - [SuppressMessage("", "CA1031")] - internal void RaiseProcessExit() - { - var ev = ProcessExiting; - - if (ev == null) - return; - - foreach (var dg in ev.GetInvocationList()) - { - try - { - Unsafe.As(dg).Invoke(); - } - catch (Exception) - { - } - } - } -} diff --git a/src/hosting/ProgramHost.cs b/src/hosting/ProgramHost.cs deleted file mode 100644 index c3766fc..0000000 --- a/src/hosting/ProgramHost.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace Vezel.Cathode.Hosting; - -[EditorBrowsable(EditorBrowsableState.Never)] -[SuppressMessage("", "RS0030")] -public static class ProgramHost -{ - // This class is only meant to be used by generated code. - - private static int _running; - - public static async Task RunAsync(ReadOnlyMemory arguments) - where TProgram : IProgram - { - Check.All(arguments.Span, static arg => arg != null); - Check.Operation(Interlocked.Exchange(ref _running, 1) == 0); - - var context = new ProgramContext(arguments); - var domain = AppDomain.CurrentDomain; - - domain.ProcessExit += (_, e) => - { - context.RaiseProcessExit(); - - // From this point on, invoking any terminal-related API will basically have undefined behavior. The only - // way for that to happen is if a user has hooked into some of the framework events that we have banned. - Terminal.DangerousRestoreSettings(); - - Environment.ExitCode = context.ExitCode; - }; - - domain.UnhandledException += (_, e) => - { - var ex = (Exception)e.ExceptionObject; - - // The terminal might be in raw mode when this happens and checking IsRawMode is racey. To ensure sensible - // output, just always use CRLF here. - if (!context.RaiseUnhandledException(ex)) - Terminal.Error($"Unhandled exception. {ex.ToString().ReplaceLineEndings("\r\n")}\r\n"); - - // Most users expect ProcessExit to run on both normal and abnormal termination. This call makes that happen - // and also ensures that the terminal cleanup we do in ProcessExit runs. One unfortunate downside of this - // approach is that finally blocks will not run - see the comment in the catch block below. - // - // The choice of exit code 127 is just to match CoreCLR behavior. It has no special meaning. We set it on - // the ProgramContext so that we overwrite any exit code set by the user prior to this point, but the user - // may still overwrite it in a ProcessExit handler. - Environment.Exit(context.ExitCode = 127); - }; - - try - { - await TProgram.RunAsync(context).ConfigureAwait(false); - } - catch (Exception) - { - // This seemingly pointless catching and throwing actually has a purpose: Since we call Environment.Exit in - // the UnhandledException handler, any finally blocks on the call stack will not be invoked. We cannot do - // anything about that on arbitrary threads, but here in the main thread we can at least catch the exception - // such that UnhandledException is not raised (causing finally blocks to run properly), and then throw it - // again to actually trigger UnhandledException since we know there are no finally blocks above us. - throw; - } - - // At this point, we will exit normally (raising ProcessExit) or abnormally (raising UnhandledException). Either - // way, event handlers will run and terminal cleanup will happen. - } -} diff --git a/src/hosting/hosting.cs b/src/hosting/hosting.cs deleted file mode 100644 index ae5cefb..0000000 --- a/src/hosting/hosting.cs +++ /dev/null @@ -1,2 +0,0 @@ -[assembly: DisableRuntimeMarshalling] -[module: SkipLocalsInit] diff --git a/src/hosting/hosting.csproj b/src/hosting/hosting.csproj deleted file mode 100644 index 341c6e1..0000000 --- a/src/hosting/hosting.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - Vezel.Cathode.Hosting - true - $(PackageDescription) - -This package provides the terminal hosting model. - Vezel.Cathode.Hosting - Vezel.Cathode.Hosting - - - - - - - - - - - - - - - - - - - - diff --git a/src/hosting/hosting.targets b/src/hosting/hosting.targets deleted file mode 100644 index 67371a3..0000000 --- a/src/hosting/hosting.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/native/cathode.h b/src/native/cathode.h new file mode 100644 index 0000000..55e55d1 --- /dev/null +++ b/src/native/cathode.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include + +#define atomic _Atomic +#define nonnull _Nonnull +#define nullable _Nullable + +#if defined(ZIG_OS_WINDOWS) +# define CATHODE_API [[gnu::dllexport]] +#else +# define CATHODE_API [[gnu::visibility("default")]] +#endif diff --git a/src/native/driver-unix.c b/src/native/driver-unix.c new file mode 100644 index 0000000..7bee5f4 --- /dev/null +++ b/src/native/driver-unix.c @@ -0,0 +1,434 @@ +#if !defined(ZIG_OS_WINDOWS) + +#include +#include +#include +#include +#include +#include +#include + +#include "driver-unix.h" + +static int tty_fd; +static struct termios original_termios; +static bool original_termios_saved; +static bool raw_mode; +static struct sigaction original_ttou; +static atomic bool ttou_seen; + +[[gnu::constructor]] +static void constructor(void) +{ + tty_fd = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); +} + +[[gnu::destructor]] +static void destructor(void) +{ + if (original_termios_saved) + tcsetattr(tty_fd, TCSAFLUSH, &original_termios); + + close(tty_fd); +} + +void cathode_get_handles( + size_t *nonnull std_in, + size_t *nonnull std_out, + size_t *nonnull std_err, + size_t *nonnull tty_in, + size_t *nonnull tty_out) +{ + assert(std_in); + assert(std_out); + assert(std_err); + assert(tty_in); + assert(tty_out); + + *std_in = STDIN_FILENO; + *std_out = STDOUT_FILENO; + *std_err = STDERR_FILENO; + *tty_in = (size_t)tty_fd; + *tty_out = (size_t)tty_fd; +} + +bool cathode_is_valid(size_t handle, bool) +{ + return (int)handle >= 0; +} + +bool cathode_is_interactive(size_t handle) +{ + return isatty((int)handle) == 1; +} + +bool cathode_query_size(int32_t *nonnull width, int32_t *nonnull height) +{ + assert(width); + assert(height); + + struct winsize size; + + if (ioctl(tty_fd, TIOCGWINSZ, &size)) + return false; + + *width = size.ws_col; + *height = size.ws_row; + + return true; +} + +bool cathode_get_mode(void) +{ + return raw_mode; +} + +static bool is_extended(const struct sigaction *nonnull action) +{ + assert(action); + + return action->sa_flags & SA_SIGINFO; +} + +static bool is_default(const struct sigaction *nonnull action) +{ + assert(action); + + return ((const void *)&action->sa_handler == &action->sa_sigaction || !is_extended(action)) && + action->sa_handler == SIG_DFL; +} + +static bool is_ignored(const struct sigaction *nonnull action) +{ + assert(action); + + return ((const void *)&action->sa_handler == &action->sa_sigaction || !is_extended(action)) && + action->sa_handler == SIG_IGN; +} + +static void ttou_handler(int signo, siginfo_t *info, void *context) +{ + ttou_seen = true; + + // If a non-default handler already existed, we should chain to it. + if (is_default(&original_ttou)) + return; + + if (is_extended(&original_ttou)) + original_ttou.sa_sigaction(signo, info, context); + else + original_ttou.sa_handler(signo); +} + +TerminalResult cathode_set_mode(bool raw, bool flush) +{ + struct termios termios; + + if (tcgetattr(tty_fd, &termios) == -1) + return (TerminalResult) + { + .exception = TerminalException_TerminalNotAttached, + }; + + // Stash away the original settings the first time we are successfully called. + if (!original_termios_saved) + { + original_termios = termios; + + original_termios_saved = true; + } + + // These values are usually the default, but we set them just to be safe since UnixTerminalReader would not behave + // as expected by callers if these values differ. + termios.c_cc[VTIME] = 0; + termios.c_cc[VMIN] = 1; + + // Turn off some features that make little or no sense for virtual terminals. + termios.c_iflag &= (tcflag_t)~(IGNBRK | IGNPAR | PARMRK | INPCK | ISTRIP | IXOFF | IMAXBEL); + termios.c_oflag &= (tcflag_t)~(OFILL | OFDEL | NLDLY | CRDLY | TABDLY | BSDLY | VTDLY | FFDLY); + termios.c_oflag |= NL0 | CR0 | TAB0 | BS0 | VT0 | FF0; + termios.c_cflag &= (tcflag_t)~(CSTOPB | PARENB | PARODD | HUPCL | CLOCAL | CRTSCTS); +#if defined(ZIG_OS_LINUX) + termios.c_cflag &= (tcflag_t)~CMSPAR; +#endif +#if defined(ZIG_OS_MACOS) + termios.c_cflag &= (tcflag_t)~(CDTR_IFLOW | CDSR_OFLOW | MDMBUF); +#endif + termios.c_lflag &= (tcflag_t)~(FLUSHO | EXTPROC); + + // Set up some sensible defaults. + termios.c_iflag &= (tcflag_t)~(IGNCR | INLCR | IXANY); +#if defined(ZIG_OS_LINUX) + termios.c_iflag &= (tcflag_t)~IUCLC; +#endif + termios.c_iflag |= IUTF8; + termios.c_oflag &= (tcflag_t)~(OCRNL | ONOCR | ONLRET); +#if defined(ZIG_OS_LINUX) + termios.c_oflag &= (tcflag_t)~OLCUC; +#endif +#if defined(ZIG_OS_MACOS) + termios.c_oflag &= (tcflag_t)~ONOEOT; +#endif + termios.c_cflag &= (tcflag_t)~CSIZE; + termios.c_cflag |= CS8 | CREAD; + termios.c_lflag &= (tcflag_t)~(ECHONL | NOFLSH | ECHOPRT | PENDIN); +#if defined(ZIG_OS_LINUX) + termios.c_lflag &= (tcflag_t)~XCASE; +#endif +#if defined(ZIG_OS_MACOS) + termios.c_lflag &= (tcflag_t)~ALTWERASE; +#endif + + tcflag_t iflag = BRKINT | ICRNL | IXON; + tcflag_t oflag = OPOST | ONLCR; + tcflag_t lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN; + + // Finally, enable/disable features that depend on raw/cooked mode. + if (raw) + { + termios.c_iflag &= ~iflag; + termios.c_oflag &= ~oflag; + termios.c_lflag &= ~lflag; + termios.c_lflag |= TOSTOP; +#if defined(ZIG_OS_MACOS) + termios.c_lflag |= NOKERNINFO; +#endif + } + else + { + termios.c_iflag |= iflag; + termios.c_oflag |= oflag; + termios.c_lflag |= lflag; + termios.c_lflag &= (tcflag_t)~TOSTOP; +#if defined(ZIG_OS_MACOS) + termios.c_lflag &= (tcflag_t)~NOKERNINFO; +#endif + } + + if (!raw) + { + sigaction(SIGTTOU, nullptr, &original_ttou); + + // If SIGTTOU is ignored, we do not need to do anything. + if (!is_ignored(&original_ttou)) + { + struct sigaction ttou; + + if (is_default(&original_ttou)) + { + sigemptyset(&ttou.sa_mask); + + ttou.sa_flags = 0; + } + else + { + // If a non-default handler already exists, maintain its mask and flags (although we must remove + // SA_RESTART as that would defeat the purpose of this whole exercise). + ttou.sa_mask = original_ttou.sa_mask; + ttou.sa_flags = original_ttou.sa_flags & ~SA_RESTART; + } + + // Ensure that we receive full signal information (SA_SIGINFO) so that we can chain to an existing handler. + // Also, only handle the signal once (SA_RESETHAND). + ttou.sa_flags |= SA_SIGINFO | SA_RESETHAND; + ttou.sa_sigaction = ttou_handler; + + sigaction(SIGTTOU, &ttou, nullptr); + } + + ttou_seen = false; + } + + int ret; + + while ((ret = tcsetattr(tty_fd, flush ? TCSAFLUSH : TCSANOW, &termios)) == -1 && errno == EINTR) + { + // Retry in case we get interrupted by a signal. If we are trying to switch to cooked mode and we saw SIGTTOU, + // it means we are a background process. We will trust that, by the time we actually read or write anything, we + // will be in cooked mode. + if (ttou_seen) + { + ret = 0; + + break; + } + } + + if (!raw) + sigaction(SIGTTOU, &original_ttou, nullptr); + + if (ret) + return (TerminalResult) + { + .exception = TerminalException_TerminalConfiguration, + .message = u"Could not change terminal mode.", + .error = errno, + }; + + raw_mode = raw; + + return (TerminalResult) + { + .exception = TerminalException_None, + }; +} + +TerminalResult cathode_generate_signal(TerminalSignal signal) +{ + int signo; + + switch (signal) + { + case TerminalSignal_Close: + signo = SIGHUP; + break; + case TerminalSignal_Interrupt: + signo = SIGINT; + break; + case TerminalSignal_Quit: + signo = SIGQUIT; + break; + case TerminalSignal_Terminate: + signo = SIGTERM; + break; + default: + return (TerminalResult) + { + .exception = TerminalException_ArgumentOutOfRange, + }; + } + + kill(0, signo); + + return (TerminalResult) + { + .exception = TerminalException_None, + }; +} + +void cathode_poll(bool write, const size_t *nonnull handles, bool *nullable results, int count) +{ + assert(handles); + assert(count); + + struct pollfd fds[count]; // count is only ever expected to be 1 or 2. + + for (int i = 0; i < count; i++) + fds[i] = (struct pollfd) + { + .fd = (int)handles[i], + .events = write ? POLLOUT : POLLIN, + }; + + int ret; + + while ((ret = poll(fds, (nfds_t)count, -1)) == -1 && errno == EINTR) + { + // Retry in case we get interrupted by a signal. + } + + if (results) + for (int i = 0; i < count; i++) + results[i] = fds[i].revents & (write ? POLLOUT : POLLIN); +} + +TerminalResult cathode_read(size_t handle, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress) +{ + assert(buffer); + assert(progress); + + while (true) + { + ssize_t ret; + + // Note that this call may get us suspended by way of a SIGTTIN signal if we are a background process and the + // handle refers to a terminal. + while ((ret = read((int)handle, buffer, (size_t)length)) == -1 && errno == EINTR) + { + // Retry in case we get interrupted by a signal. + } + + bool success = true; + + // EPIPE means the descriptor was probably redirected to a program that ended. + if (ret != -1) + *progress = (int32_t)ret; + else if (errno == EPIPE) + *progress = 0; + else + success = false; + + // EAGAIN means the descriptor was configured as non-blocking. Instead of busily trying to read over and over, + // poll until something happens (we can read, or an error occurs) and loop around again. + if (!success && errno == EAGAIN) + { + cathode_poll(false, &handle, nullptr, 1); + + continue; + } + + return success + ? (TerminalResult) + { + .exception = TerminalException_None, + } + : (TerminalResult) + { + .exception = TerminalException_Terminal, + .message = u"Could not read from input handle.", + .error = errno, + }; + } +} + +TerminalResult cathode_write(size_t handle, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress) +{ + assert(buffer); + assert(progress); + + while (true) + { + ssize_t ret; + + // Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, the handle + // refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that it could get set + // anyway). + while ((ret = write((int)handle, buffer, (size_t)length)) == -1 && errno == EINTR) + { + // Retry in case we get interrupted by a signal. + } + + bool success = true; + + // EPIPE means the descriptor was probably redirected to a program that ended. + if (ret != -1) + *progress = (int32_t)ret; + else if (errno == EPIPE) + *progress = 0; + else + success = false; + + // EAGAIN means the descriptor was configured as non-blocking. Instead of busily trying to write over and over, + // poll until something happens (we can write, or an error occurs) and loop around again. + if (!success && errno == EAGAIN) + { + cathode_poll(true, &handle, nullptr, 1); + + continue; + } + + return success + ? (TerminalResult) + { + .exception = TerminalException_None, + } + : (TerminalResult) + { + .exception = TerminalException_Terminal, + .message = u"Could not write to output handle.", + .error = errno, + }; + } +} + +#endif diff --git a/src/native/driver-unix.h b/src/native/driver-unix.h new file mode 100644 index 0000000..1c71aab --- /dev/null +++ b/src/native/driver-unix.h @@ -0,0 +1,5 @@ +#pragma once + +#include "driver.h" + +CATHODE_API void cathode_poll(bool write, const size_t *nonnull handles, bool *nullable results, int count); diff --git a/src/native/driver-windows.c b/src/native/driver-windows.c new file mode 100644 index 0000000..e3b00fe --- /dev/null +++ b/src/native/driver-windows.c @@ -0,0 +1,342 @@ +#if defined(ZIG_OS_WINDOWS) + +#include + +#include "driver-windows.h" + +typedef struct { + HANDLE handle; + DWORD original_mode; + UINT original_code_page; +} ConsoleState; + +static HANDLE stdio_in; +static HANDLE stdio_out; +static HANDLE stdio_err; +static ConsoleState in_state; +static ConsoleState out_state; +static bool original_state_saved; +static bool raw_mode; + +static HANDLE open_stdio_handle(DWORD type) +{ + HANDLE handle = GetStdHandle(type); + + // Convert a -1 handle to 0 so other code only has to worry about 0. + return handle != INVALID_HANDLE_VALUE ? handle : nullptr; +} + +static HANDLE open_console_handle(const wchar_t *nonnull name) +{ + assert(name); + + SECURITY_ATTRIBUTES attrs = + { + .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = nullptr, + .bInheritHandle = true, + }; + + HANDLE handle = CreateFileW( + name, + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + &attrs, + OPEN_EXISTING, + 0, + nullptr); + + // Convert a -1 handle to 0 so other code only has to worry about 0. + return handle != INVALID_HANDLE_VALUE ? handle : nullptr; +} + +[[gnu::constructor]] +static void constructor(void) +{ + stdio_in = open_stdio_handle(STD_INPUT_HANDLE); + stdio_out = open_stdio_handle(STD_OUTPUT_HANDLE); + stdio_err = open_stdio_handle(STD_ERROR_HANDLE); + in_state.handle = open_console_handle(u"CONIN$"); + out_state.handle = open_console_handle(u"CONOUT$"); +} + +[[gnu::destructor]] +static void destructor(void) +{ + if (original_state_saved) + { + SetConsoleMode(in_state.handle, in_state.original_mode); + SetConsoleMode(out_state.handle, out_state.original_mode); + + SetConsoleCP(in_state.original_code_page); + SetConsoleOutputCP(out_state.original_code_page); + } + + // CloseHandle will throw an exception under a debugger if the given handle is invalid, so check first. + + if (in_state.handle) + CloseHandle(in_state.handle); + + if (out_state.handle) + CloseHandle(out_state.handle); +} + +void cathode_get_handles( + size_t *nonnull std_in, + size_t *nonnull std_out, + size_t *nonnull std_err, + size_t *nonnull tty_in, + size_t *nonnull tty_out) +{ + assert(std_in); + assert(std_out); + assert(std_err); + assert(tty_in); + assert(tty_out); + + *std_in = (size_t)stdio_in; + *std_out = (size_t)stdio_out; + *std_err = (size_t)stdio_err; + *tty_in = (size_t)in_state.handle; + *tty_out = (size_t)out_state.handle; +} + +bool cathode_is_valid(size_t handle, bool write) +{ + if (!handle) + return false; + + // Apparently, for Windows GUI programs, the standard I/O handles will appear to be valid (i.e. not -1 or 0) but + // will not actually be usable. So do a zero-byte write to figure out if the handle is actually valid. + if (write) + { + DWORD written; + + return WriteFile((HANDLE)handle, nullptr, 0, &written, nullptr); + } + + return true; +} + +bool cathode_is_interactive(size_t handle) +{ + DWORD mode; + + // Note that this also returns true for invalid handles. + return GetFileType((HANDLE)handle) == FILE_TYPE_CHAR && GetConsoleMode((HANDLE)handle, &mode); +} + +bool cathode_query_size(int32_t *nonnull width, int32_t *nonnull height) +{ + assert(width); + assert(height); + + CONSOLE_SCREEN_BUFFER_INFO info; + + if (!GetConsoleScreenBufferInfo(out_state.handle, &info)) + return false; + + *width = info.srWindow.Right - info.srWindow.Left + 1; + *height = info.srWindow.Bottom - info.srWindow.Top + 1; + + return true; +} + +bool cathode_get_mode(void) +{ + return raw_mode; +} + +TerminalResult cathode_set_mode(bool raw, bool flush) +{ + DWORD in_mode; + DWORD out_mode; + + UINT in_code_page; + UINT out_code_page; + + if (!GetConsoleMode(in_state.handle, &in_mode) || + !GetConsoleMode(out_state.handle, &out_mode) || + !(in_code_page = GetConsoleCP()) || + !(out_code_page = GetConsoleOutputCP())) + return (TerminalResult) + { + .exception = TerminalException_TerminalNotAttached, + }; + + // Stash away the original modes the first time we are successfully called. + if (!original_state_saved) + { + in_state.original_mode = in_mode; + out_state.original_mode = out_mode; + + in_state.original_code_page = in_code_page; + out_state.original_code_page = out_code_page; + + original_state_saved = true; + } + + DWORD orig_in_mode = in_mode; + DWORD orig_out_mode = out_mode; + + // Set up some sensible defaults. + in_mode &= (DWORD)~(ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_QUICK_EDIT_MODE); + in_mode |= ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT; + out_mode &= (DWORD)~ENABLE_LVB_GRID_WORLDWIDE; + out_mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + DWORD in_mode_extra = ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT; + DWORD out_mode_extra = DISABLE_NEWLINE_AUTO_RETURN; + + // Enable/disable features that depend on cooked/raw mode. + if (!raw) + { + in_mode |= in_mode_extra; + out_mode |= out_mode_extra; + } + else + { + in_mode &= ~in_mode_extra; + out_mode &= ~out_mode_extra; + } + + TerminalResult result; + + if (!SetConsoleCP(CP_UTF8) || !SetConsoleOutputCP(CP_UTF8)) + { + result = (TerminalResult) + { + .exception = TerminalException_TerminalConfiguration, + .message = u"Could not change console code page.", + .error = (int32_t)GetLastError(), + }; + + goto done; + } + + if (!SetConsoleMode(in_state.handle, in_mode) || !SetConsoleMode(out_state.handle, out_mode)) + { + result = (TerminalResult) + { + .exception = TerminalException_TerminalConfiguration, + .message = u"Could not change console mode.", + .error = (int32_t)GetLastError(), + }; + + goto done; + } + + if (flush && !FlushConsoleInputBuffer(in_state.handle)) + { + result = (TerminalResult) + { + .exception = TerminalException_TerminalConfiguration, + .message = u"Could not flush console input buffer.", + .error = (int32_t)GetLastError(), + }; + + goto done; + } + + result = (TerminalResult) + { + .exception = TerminalException_None, + }; + +done: + if (result.exception != TerminalException_None) + { + result.error = (int32_t)GetLastError(); + + // If we failed to configure the console, try to undo partial configuration (if any). + + SetConsoleMode(in_state.handle, orig_in_mode); + SetConsoleMode(out_state.handle, orig_out_mode); + + SetConsoleCP(in_code_page); + SetConsoleOutputCP(out_code_page); + } + else + raw_mode = raw; + + return result; +} + +TerminalResult cathode_generate_signal(TerminalSignal signal) +{ + DWORD event; + + switch (signal) + { + case TerminalSignal_Interrupt: + event = CTRL_C_EVENT; + break; + case TerminalSignal_Quit: + event = CTRL_BREAK_EVENT; + break; + case TerminalSignal_Close: + case TerminalSignal_Terminate: + return (TerminalResult) + { + .exception = TerminalException_PlatformNotSupported, + }; + default: + return (TerminalResult) + { + .exception = TerminalException_ArgumentOutOfRange, + }; + } + + GenerateConsoleCtrlEvent(event, 0); + + return (TerminalResult) + { + .exception = TerminalException_None, + }; +} + +TerminalResult cathode_read(size_t handle, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress) +{ + assert(buffer); + assert(progress); + + BOOL result = ReadFile((HANDLE)handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr); + DWORD error = GetLastError(); + + // See driver-unix.c for the error handling rationale. + return result || *progress || error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA + ? (TerminalResult) + { + .exception = TerminalException_None, + } + : (TerminalResult) + { + .exception = TerminalException_Terminal, + .message = u"Could not read from input handle.", + .error = (int32_t)error, + }; +} + +TerminalResult cathode_write(size_t handle, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress) +{ + assert(buffer); + assert(progress); + + BOOL result = WriteFile((HANDLE)handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr); + DWORD error = GetLastError(); + + // See driver-unix.c for the error handling rationale. + return result || *progress || error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA + ? (TerminalResult) + { + .exception = TerminalException_None, + } + : (TerminalResult) + { + .exception = TerminalException_Terminal, + .message = u"Could not write to output handle.", + .error = (int32_t)error, + }; +} + +#endif diff --git a/src/native/driver-windows.h b/src/native/driver-windows.h new file mode 100644 index 0000000..b84f6ca --- /dev/null +++ b/src/native/driver-windows.h @@ -0,0 +1,5 @@ +#pragma once + +#include "driver.h" + +// Currently no OS-specific APIs. diff --git a/src/native/driver.c b/src/native/driver.c new file mode 100644 index 0000000..04fb1cf --- /dev/null +++ b/src/native/driver.c @@ -0,0 +1,7 @@ +#include "driver.h" + +void cathode_initialize(void) +{ + // Do our best to start in cooked mode. + cathode_set_mode(false, false); +} diff --git a/src/native/driver.h b/src/native/driver.h new file mode 100644 index 0000000..e93ece3 --- /dev/null +++ b/src/native/driver.h @@ -0,0 +1,52 @@ +#pragma once + +typedef enum { + TerminalException_None, + TerminalException_ArgumentOutOfRange, + TerminalException_PlatformNotSupported, + TerminalException_TerminalNotAttached, + TerminalException_TerminalConfiguration, + TerminalException_Terminal, +} TerminalException; + +typedef struct { + TerminalException exception; + const uint16_t *nullable message; // TODO: This should be char16_t. + int32_t error; +} TerminalResult; + +// Keep in sync with src/core/TerminalSignal.cs (public API). +typedef enum { + TerminalSignal_Close, + TerminalSignal_Interrupt, + TerminalSignal_Quit, + TerminalSignal_Terminate, +} TerminalSignal; + +CATHODE_API void cathode_initialize(void); + +CATHODE_API void cathode_get_handles( + size_t *nonnull std_in, + size_t *nonnull std_out, + size_t *nonnull std_err, + size_t *nonnull tty_in, + size_t *nonnull tty_out); + +CATHODE_API bool cathode_is_valid(size_t handle, bool write); + +CATHODE_API bool cathode_is_interactive(size_t handle); + +CATHODE_API bool cathode_query_size(int32_t *nonnull width, int32_t *nonnull height); + +CATHODE_API bool cathode_get_mode(void); + +// Requires synchronization by the caller. +CATHODE_API TerminalResult cathode_set_mode(bool raw, bool flush); + +CATHODE_API TerminalResult cathode_generate_signal(TerminalSignal signal); + +CATHODE_API TerminalResult cathode_read( + size_t handle, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress); + +CATHODE_API TerminalResult cathode_write( + size_t handle, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress); diff --git a/src/native/native.cproj b/src/native/native.cproj new file mode 100644 index 0000000..9003168 --- /dev/null +++ b/src/native/native.cproj @@ -0,0 +1,53 @@ + + + Vezel.Cathode.Native + + $(DefineConstants); + _GNU_SOURCE; + _UNICODE; + UNICODE; + WIN32_LEAN_AND_MEAN + + + false + + + linux-arm; + linux-arm64; + linux-x64; + osx-arm64; + osx-x64; + win-arm64; + win-x86; + win-x64 + + + + + + + + + + <_NativeLibrary Include="$(TargetPath)" + RuntimeIdentifier="$(RuntimeIdentifier)" /> + + + + + + <_RuntimeIdentifiers Include="$(RuntimeIdentifiers)" /> + <_Projects Include="$(MSBuildProjectFullPath)" + Properties="RuntimeIdentifier=%(_RuntimeIdentifiers.Identity)" /> + + + + + + + diff --git a/src/samples/Directory.Build.targets b/src/samples/Directory.Build.targets index e1c8f9f..1fa14b8 100644 --- a/src/samples/Directory.Build.targets +++ b/src/samples/Directory.Build.targets @@ -11,12 +11,8 @@ This reference is required since we are not consuming the library as a PackageReference item. --> - - - diff --git a/src/samples/hosting/.globalconfig b/src/samples/hosting/.globalconfig deleted file mode 100644 index 69e3f80..0000000 --- a/src/samples/hosting/.globalconfig +++ /dev/null @@ -1,3 +0,0 @@ -global_level = 2 - -dotnet_diagnostic.CA1812.severity = none # TODO: https://github.com/dotnet/roslyn-analyzers/issues/6218 diff --git a/src/samples/hosting/Program.cs b/src/samples/hosting/Program.cs deleted file mode 100644 index 40eab1a..0000000 --- a/src/samples/hosting/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -internal sealed class Program : IProgram -{ - public static async Task RunAsync(ProgramContext context) - { - context.ProcessExiting += () => OutLine("Exiting..."); - context.UnhandledException += e => OutLine($"Unhandled exception event: {e}"); - - await OutLineAsync($"Arguments: {string.Join(' ', context.Arguments.ToArray())}"); - await OutLineAsync("Switching to raw mode..."); - - try - { - EnableRawMode(); - } - catch (TerminalNotAttachedException) - { - // Expected in CI. - } - } -} diff --git a/src/samples/hosting/hosting.csproj b/src/samples/hosting/hosting.csproj deleted file mode 100644 index 6b512ec..0000000 --- a/src/samples/hosting/hosting.csproj +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/trimming/trimming.csproj b/src/trimming/trimming.csproj index 7616732..a219c13 100644 --- a/src/trimming/trimming.csproj +++ b/src/trimming/trimming.csproj @@ -18,13 +18,11 @@ - -