├── nativelib ├── .gitignore ├── CMakeLists.txt └── nativelib.cpp ├── flat ├── NativeMethods.txt ├── lib │ ├── greetings.dll │ ├── libgreetings.so │ └── libgreetings.dylib ├── hello.cs ├── webapi.settings.json ├── apphost.settings.json ├── cswin32sandbox.cs ├── cat.run.json ├── pinvoke.run.json ├── spaceinvaders.run.json ├── envvars.cs ├── apphost.cs ├── webapi.run.json ├── webapi.cs ├── filepath.cs ├── usersecrets.cs ├── apphost.run.json ├── playwright.cs ├── cat.cs ├── wait.cs ├── pinvoke.cs ├── spaceinvaders.cs └── The Tell-Tale Heart.txt ├── razorapp ├── wwwroot │ ├── favicon.png │ └── app.css ├── Home.razor ├── razorapp.settings.json ├── _Imports.razor ├── razorapp.cs ├── razorapp.run.json └── App.razor ├── .devcontainer └── devcontainer.json ├── global.json ├── helloblazor ├── wwwroot │ └── app.css ├── helloblazor.settings.json ├── helloblazor.cs ├── pages │ └── Index.razor └── helloblazor.run.json ├── classlibproject ├── ClassLib │ ├── Greeter.cs │ └── ClassLib.csproj └── samples │ └── hello.cs ├── nuget.config ├── README.md ├── .editorconfig ├── LICENSE ├── THIRDPARTYNOTICES ├── .github └── workflows │ └── ci.yml ├── .gitignore └── verify.cs /nativelib/.gitignore: -------------------------------------------------------------------------------- 1 | /out/ 2 | -------------------------------------------------------------------------------- /flat/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | GetTickCount 2 | GetTickCount64 -------------------------------------------------------------------------------- /flat/lib/greetings.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/greetings.dll -------------------------------------------------------------------------------- /flat/lib/libgreetings.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/libgreetings.so -------------------------------------------------------------------------------- /flat/lib/libgreetings.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/flat/lib/libgreetings.dylib -------------------------------------------------------------------------------- /razorapp/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DamianEdwards/runfile/HEAD/razorapp/wwwroot/favicon.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": ".NET 10 dev container", 3 | "image": "mcr.microsoft.com/dotnet/sdk:10.0" 4 | } -------------------------------------------------------------------------------- /flat/hello.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | var name = args.Length > 0 ? args[0] : "World"; 4 | Console.WriteLine($"Hello, {name}!"); 5 | -------------------------------------------------------------------------------- /razorapp/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": true 6 | } 7 | } -------------------------------------------------------------------------------- /helloblazor/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } -------------------------------------------------------------------------------- /classlibproject/ClassLib/Greeter.cs: -------------------------------------------------------------------------------- 1 | namespace ClassLib; 2 | 3 | public class Greeter 4 | { 5 | public string Greet(string name) 6 | { 7 | return $"Hello, {name}!"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /flat/webapi.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /razorapp/razorapp.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /helloblazor/helloblazor.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /classlibproject/samples/hello.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | #:project ../ClassLib 3 | 4 | var greeter = new ClassLib.Greeter(); 5 | var greeting = greeter.Greet(args.Length > 0 ? args[0] : "World"); 6 | Console.WriteLine(greeting); 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runfile Playground 2 | 3 | Playground of file-based C# apps for `dotnet run file.cs` feature added in .NET 10. 4 | 5 | Requires [.NET SDK version 10.0.100](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) or later. 6 | -------------------------------------------------------------------------------- /flat/apphost.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Aspire.Hosting.Dcp": "Debug", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.ASPIRECSHARPAPPS001.severity = none 3 | dotnet_diagnostic.CA1050.severity = none 4 | dotnet_diagnostic.SP0001.severity = none 5 | dotnet_diagnostic.IL2026.severity = none 6 | csharp_prefer_braces = true:error 7 | -------------------------------------------------------------------------------- /classlibproject/ClassLib/ClassLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /flat/cswin32sandbox.cs: -------------------------------------------------------------------------------- 1 | #:property TargetFramework=net10.0-windows 2 | #:package Microsoft.Windows.CsWin32@0.3.242 3 | #:package Humanizer@3.0.0-rc.30 4 | 5 | using Windows.Win32; 6 | using Humanizer; 7 | 8 | var ticks = PInvoke.GetTickCount64(); 9 | Console.WriteLine($"System has been running for {TimeSpan.FromMilliseconds((long)ticks).Humanize(precision: 4)}."); 10 | -------------------------------------------------------------------------------- /flat/cat.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "default": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true 7 | }, 8 | "verify": { 9 | "commandName": "Project", 10 | "dotnetRunMessages": true, 11 | "commandLineArgs": "apphost.cs" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /flat/pinvoke.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "default": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true 7 | }, 8 | "verify": { 9 | "commandName": "Project", 10 | "dotnetRunMessages": true, 11 | "commandLineArgs": "--console" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /razorapp/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using razorapp 10 | -------------------------------------------------------------------------------- /flat/spaceinvaders.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "default": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true 7 | }, 8 | "verify": { 9 | "commandName": "Project", 10 | "dotnetRunMessages": true, 11 | "commandLineArgs": "--demo" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /helloblazor/helloblazor.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | #:sdk Microsoft.NET.Sdk.Web 4 | #:property PublishAot=false 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | builder.Services.AddRazorComponents(); 9 | 10 | var app = builder.Build(); 11 | 12 | app.UseHttpsRedirection(); 13 | app.UseAntiforgery(); 14 | 15 | app.MapStaticAssets(); 16 | app.MapRazorComponents(); 17 | 18 | app.Run(); 19 | -------------------------------------------------------------------------------- /flat/envvars.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | #:property DefineConstants=$(DefineConstants);CUSTOM_CONSTANT;$(EnvVarConstant) 3 | 4 | #if DEBUG 5 | Console.WriteLine("Hello from Debug!"); 6 | #endif 7 | 8 | #if RELEASE 9 | Console.WriteLine("Hello from Release!"); 10 | #endif 11 | 12 | #if CUSTOM_CONSTANT 13 | Console.WriteLine("CUSTOM_CONSTANT is enabled!"); 14 | #endif 15 | 16 | #if ENV_VAR_CONSTANT 17 | Console.WriteLine("EnvVarConstant is enabled!"); 18 | #endif 19 | -------------------------------------------------------------------------------- /nativelib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(greetings VERSION 1.0.0 LANGUAGES CXX) 3 | 4 | # Set C++ standard 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | 9 | # Library target 10 | add_library(greetings SHARED nativelib.cpp) 11 | 12 | # Installation rules 13 | install(TARGETS greetings 14 | EXPORT greetingsTargets 15 | LIBRARY DESTINATION lib 16 | ARCHIVE DESTINATION lib 17 | RUNTIME DESTINATION bin 18 | ) 19 | -------------------------------------------------------------------------------- /helloblazor/pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Hello, world!

16 |

17 | Welcome to your new app. 18 |

19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /flat/apphost.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env aspire 2 | 3 | #:sdk Aspire.AppHost.Sdk@13.0.0 4 | #:package Aspire.Hosting.Redis@13.0.0 5 | 6 | var builder = DistributedApplication.CreateBuilder(args); 7 | 8 | var webapi = builder.AddCSharpApp("webapi", "./webapi.cs"); 9 | 10 | builder.AddCSharpApp("razorapp", "../razorapp/razorapp.cs") 11 | .WithReference(webapi).WaitFor(webapi); 12 | 13 | if (!string.Equals(builder.Configuration["DOTNET_LAUNCH_PROFILE"], "verify", StringComparison.OrdinalIgnoreCase)) 14 | { 15 | var redis = builder.AddRedis("redis"); 16 | webapi.WithReference(redis).WaitFor(redis); 17 | } 18 | 19 | builder.Build().Run(); 20 | -------------------------------------------------------------------------------- /flat/webapi.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://localhost:5202", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://localhost:7276;http://localhost:5202", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /razorapp/razorapp.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | #:sdk Microsoft.NET.Sdk.Web 4 | #:property BlazorDisableThrowNavigationException=true 5 | #:property PublishAot=false 6 | 7 | using razorapp; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | 11 | builder.Services.AddRazorComponents(); 12 | 13 | var app = builder.Build(); 14 | 15 | if (!app.Environment.IsDevelopment()) 16 | { 17 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 18 | app.UseHsts(); 19 | } 20 | app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); 21 | 22 | app.UseHttpsRedirection(); 23 | app.UseAntiforgery(); 24 | 25 | app.MapStaticAssets(); 26 | app.MapRazorComponents(); 27 | 28 | app.Run(); 29 | -------------------------------------------------------------------------------- /razorapp/razorapp.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://razorapp.dev.localhost:5149", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://razorapp.dev.localhost:7293;http://razorapp.dev.localhost:5149", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helloblazor/helloblazor.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://helloblazor.dev.localhost:5150", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://helloblazor.dev.localhost:7301;http://helloblazor.dev.localhost:5150", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /razorapp/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /flat/webapi.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | #:sdk Microsoft.NET.Sdk.Web 4 | #:package Microsoft.AspNetCore.OpenApi@10.0.0 5 | 6 | using System.Text.Json.Serialization; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | builder.Services.ConfigureHttpJsonOptions(options => 11 | { 12 | options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); 13 | }); 14 | 15 | builder.Services.AddOpenApi(); 16 | 17 | var app = builder.Build(); 18 | 19 | if (app.Environment.IsDevelopment()) 20 | { 21 | app.MapOpenApi(); 22 | } 23 | 24 | app.MapGet("/", () => new HelloResponse { Message = "Hello, World!" }) 25 | .WithName("HelloWorld"); 26 | 27 | app.Run(); 28 | 29 | class HelloResponse 30 | { 31 | public string Message { get; set; } = "Hello, World!"; 32 | } 33 | 34 | [JsonSerializable(typeof(HelloResponse))] 35 | partial class AppJsonSerializerContext : JsonSerializerContext 36 | { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /nativelib/nativelib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef _MSC_VER 6 | #include 7 | #define EXPORT __declspec(dllexport) 8 | 9 | char* char_alloc_for_dotnet(size_t size) 10 | { 11 | return static_cast(::CoTaskMemAlloc(size)); 12 | } 13 | 14 | #else 15 | #define EXPORT __attribute__((visibility("default"))) 16 | 17 | char* char_alloc_for_dotnet(size_t size) 18 | { 19 | return static_cast(::malloc(size)); 20 | } 21 | 22 | #endif 23 | 24 | extern "C" EXPORT 25 | char const* Greetings(char const* name) 26 | { 27 | char const* greeting_format = "Hello, %s!"; 28 | if (name == nullptr) 29 | name = "World"; 30 | 31 | // This represents more space than is needed. 32 | size_t greeting_length = std::strlen(greeting_format) + std::strlen(name) + 1; // +1 for null terminator 33 | 34 | char* greeting = char_alloc_for_dotnet(greeting_length); 35 | if (greeting == nullptr) 36 | return nullptr; // Allocation failed 37 | 38 | std::snprintf(greeting, greeting_length, greeting_format, name); 39 | return greeting; 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Damian Edwards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flat/filepath.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | Console.WriteLine("From [CallerFilePath] attribute:"); 4 | Console.WriteLine($" - Entry-point path: {Path.EntryPointFilePath()}"); 5 | Console.WriteLine($" - Entry-point directory: {Path.EntryPointFileDirectoryPath()}"); 6 | 7 | Console.WriteLine("From AppContext data:"); 8 | Console.WriteLine($" - Entry-point path: {AppContext.EntryPointFilePath()}"); 9 | Console.WriteLine($" - Entry-point directory: {AppContext.EntryPointFileDirectoryPath()}"); 10 | 11 | static class PathEntryPointExtensions 12 | { 13 | extension(Path) 14 | { 15 | public static string EntryPointFilePath() => EntryPointImpl(); 16 | 17 | public static string EntryPointFileDirectoryPath() => Path.GetDirectoryName(EntryPointImpl()) ?? ""; 18 | 19 | private static string EntryPointImpl([System.Runtime.CompilerServices.CallerFilePath] string filePath = "") => filePath; 20 | } 21 | } 22 | 23 | static class AppContextExtensions 24 | { 25 | extension(AppContext) 26 | { 27 | public static string? EntryPointFilePath() => AppContext.GetData("EntryPointFilePath") as string; 28 | public static string? EntryPointFileDirectoryPath() => AppContext.GetData("EntryPointFileDirectoryPath") as string; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flat/usersecrets.cs: -------------------------------------------------------------------------------- 1 | #:package Microsoft.Extensions.Configuration.UserSecrets@10.0.0 2 | 3 | using System.Reflection; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Configuration.UserSecrets; 6 | 7 | var key = args.Length > 0 ? args[0] : "MySecret"; 8 | 9 | var configuration = new ConfigurationBuilder() 10 | .AddUserSecrets() 11 | .Build(); 12 | 13 | Console.WriteLine(configuration[key] ?? $""" 14 | Value for secret '{key}' was not found. Run the following command to set one: 15 | dotnet user-secrets set {key} "This is a secret!" --file {Path.GetFileName((string)AppContext.GetData("EntryPointFilePath")!)} 16 | """); 17 | 18 | static class UserSecretsExtensions 19 | { 20 | extension(IConfigurationBuilder builder) 21 | { 22 | /// 23 | /// Adds user secrets configuration source to based on the user secrets ID returned by . 24 | /// 25 | public IConfigurationBuilder AddUserSecrets() 26 | { 27 | return builder.AddUserSecrets(GetDefaultUserSecretsId()); 28 | } 29 | 30 | private static string GetDefaultUserSecretsId() 31 | { 32 | var userSecretsId = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.UserSecretsId; 33 | if (userSecretsId is not null) 34 | { 35 | // Use the UserSecretsId from the attribute 36 | return userSecretsId; 37 | } 38 | 39 | return "global"; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /flat/apphost.run.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://apphost.dev.localhost:17123;http://apphost.dev.localhost:15234", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21456", 13 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22678" 14 | } 15 | }, 16 | "http": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "http://apphost.dev.localhost:15234", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "DOTNET_ENVIRONMENT": "Development", 24 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19456", 25 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20678" 26 | } 27 | }, 28 | "verify": { 29 | "commandName": "Project", 30 | "dotnetRunMessages": true, 31 | "applicationUrl": "https://apphost-verify.dev.localhost:17123;http://apphost-verify.dev.localhost:15234", 32 | "environmentVariables": { 33 | "ASPNETCORE_ENVIRONMENT": "Development", 34 | "DOTNET_ENVIRONMENT": "Development", 35 | "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21456", 36 | "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22678" 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /THIRDPARTYNOTICES: -------------------------------------------------------------------------------- 1 | # THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 2 | 3 | This project incorporates material from the project(s) listed below (collectively, "Third Party Code"). 4 | This Third Party Code is licensed to you under their original license terms set forth below. 5 | 6 | ================================================================================ 7 | 8 | 1. dotnet-stop 9 | https://github.com/devlooped/dotnet-stop 10 | 11 | -------------------------------------------------------------------------------- 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) Daniel Cazzulino and Contributors 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | ================================================================================ 36 | -------------------------------------------------------------------------------- /flat/playwright.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | #:package Microsoft.NET.Test.Sdk@18.0.0 3 | #:package TUnit.Playwright@1.1.10 4 | #:property PublishAot=false 5 | 6 | using TUnit.Playwright; 7 | using System.Diagnostics; 8 | using System.Text.RegularExpressions; 9 | 10 | public partial class Tests : PageTest 11 | { 12 | [Test] 13 | public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() 14 | { 15 | await Page.GotoAsync("https://playwright.dev"); 16 | 17 | // Expect a title "to contain" a substring. 18 | await Expect(Page).ToHaveTitleAsync(PlaywrightRegex()); 19 | 20 | // create a locator 21 | var getStarted = Page.Locator("text=Get Started"); 22 | 23 | // Expect an attribute "to be strictly equal" to the value. 24 | await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro"); 25 | 26 | // Click the get started link. 27 | await getStarted.ClickAsync(); 28 | 29 | // Expects the URL to contain intro. 30 | await Expect(Page).ToHaveURLAsync(IntroRegex()); 31 | } 32 | 33 | [GeneratedRegex("Playwright")] 34 | public static partial Regex PlaywrightRegex(); 35 | 36 | [GeneratedRegex(".*intro")] 37 | public static partial Regex IntroRegex(); 38 | } 39 | 40 | public partial class GlobalHooks 41 | { 42 | [Before(TestSession)] 43 | public static void InstallPlaywright() 44 | { 45 | Console.WriteLine("Installing Playwright browsers..."); 46 | 47 | if (Debugger.IsAttached) 48 | { 49 | Environment.SetEnvironmentVariable("PWDEBUG", "1"); 50 | } 51 | 52 | // Install Playwright browsers and dependencies 53 | // This handles cross-platform installation automatically 54 | var exitCode = Microsoft.Playwright.Program.Main(["install", "--with-deps"]); 55 | 56 | if (exitCode != 0) 57 | { 58 | Console.WriteLine($"Warning: Playwright install exited with code {exitCode}"); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /flat/cat.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | #:package System.CommandLine@2.0.0 3 | 4 | using System.CommandLine; 5 | using System.IO; 6 | 7 | var fileArg = new Argument(name: "file") 8 | { 9 | Description = "The file to read and display on the console.", 10 | Arity = ArgumentArity.ExactlyOne 11 | }; 12 | fileArg.AcceptExistingOnly(); 13 | 14 | var rootCommand = new RootCommand("C# implementation of the Unix 'cat' command") 15 | { 16 | fileArg 17 | }; 18 | 19 | rootCommand.SetAction((parseResult, ct) => 20 | { 21 | var result = 0; 22 | var fileInfo = parseResult.GetValue(fileArg); 23 | 24 | if (fileInfo?.Exists != true) 25 | { 26 | var color = Console.ForegroundColor; 27 | Console.ForegroundColor = ConsoleColor.Red; 28 | Console.WriteLine("File not found or no file specified."); 29 | Console.ForegroundColor = color; 30 | result = 1; 31 | } 32 | else 33 | { 34 | var linesRead = FileHelpers.PrintFile(fileInfo, ct); 35 | } 36 | 37 | return Task.FromResult(result); 38 | }); 39 | 40 | var parseResult = rootCommand.Parse(args); 41 | return await parseResult.InvokeAsync(); 42 | 43 | public static class FileHelpers 44 | { 45 | public static long PrintFile(FileInfo file, CancellationToken cancellationToken) 46 | { 47 | Span buffer = stackalloc char[256]; 48 | 49 | using var readStream = File.OpenRead(file.FullName); 50 | using var reader = new StreamReader(readStream); 51 | long linesRead = 0; 52 | 53 | while (!reader.EndOfStream) 54 | { 55 | var charsRead = reader.ReadBlock(buffer); 56 | char? lastChar = null; 57 | 58 | for (var i = 0; i < charsRead; i++) 59 | { 60 | var c = buffer[i]; 61 | Console.Write(c); 62 | 63 | if (c == '\n' && (Environment.NewLine == "\n" || lastChar == '\r')) 64 | { 65 | linesRead++; 66 | } 67 | 68 | lastChar = c; 69 | } 70 | } 71 | 72 | return linesRead; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /flat/wait.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | #:package Humanizer@2.14.1 4 | 5 | using System.Text.RegularExpressions; 6 | using Humanizer; 7 | 8 | var delay = TimeSpan.FromSeconds(1); 9 | 10 | if (args.Length > 0) 11 | { 12 | var arg = string.Join(' ', args); 13 | if (int.TryParse(arg, out var ms)) 14 | { 15 | delay = TimeSpan.FromMilliseconds(ms); 16 | } 17 | else if (TimeSpan.TryParse(arg, out var ts)) 18 | { 19 | delay = ts; 20 | } 21 | else if (TryParseDuration(arg, out var ts2)) 22 | { 23 | delay = ts2; 24 | } 25 | else 26 | { 27 | Console.Error.WriteLine($"Invalid argument: {arg}"); 28 | return 1; 29 | } 30 | } 31 | 32 | if (delay < TimeSpan.Zero) 33 | { 34 | Console.Error.WriteLine("Negative wait value is not allowed."); 35 | return 2; 36 | } 37 | else if (delay == TimeSpan.Zero) 38 | { 39 | Console.WriteLine("Wait value is zero, exiting immediately."); 40 | } 41 | else 42 | { 43 | Console.WriteLine($"Waiting for {delay.Humanize(5)}…"); 44 | await Task.Delay(delay); 45 | } 46 | 47 | return 0; 48 | 49 | static bool TryParseDuration(string input, out TimeSpan result) 50 | { 51 | var tokens = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); 52 | 53 | result = TimeSpan.Zero; 54 | 55 | foreach (var token in tokens) 56 | { 57 | var match = RegexParsers.Duration().Match(token); 58 | 59 | if (!match.Success) 60 | { 61 | return false; 62 | } 63 | 64 | var value = int.Parse(match.Groups["value"].Value); 65 | var unit = match.Groups["unit"].Value.ToLowerInvariant(); 66 | var now = DateTime.Now; 67 | 68 | result += unit switch 69 | { 70 | "y" or "year" => now.AddYears(value) - now, 71 | "mo" or "month" => now.AddMonths(value) - now, 72 | "w" or "week" => TimeSpan.FromDays(value * 7), 73 | "d" or "day" => TimeSpan.FromDays(value), 74 | "h" or "hr" or "hour" => TimeSpan.FromHours(value), 75 | "m" or "min" or "minute" => TimeSpan.FromMinutes(value), 76 | "s" or "sec" or "second" => TimeSpan.FromSeconds(value), 77 | "ms" or "millisecond" => TimeSpan.FromMilliseconds(value), 78 | "microsecond" => TimeSpan.FromMicroseconds(value), 79 | _ => throw new InvalidOperationException($"Unknown time unit: {unit}") 80 | }; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | internal static partial class RegexParsers 87 | { 88 | [GeneratedRegex(@"^(?\d+)(?!\.\d)\s*(?ms|y|year|mo|month|w|wk|week|d|day|h|hr|hour|m|min|minute|s|sec|second|millisecond|microsecond)s?$", RegexOptions.IgnoreCase, "en-US")] 89 | public static partial Regex Duration(); 90 | } -------------------------------------------------------------------------------- /razorapp/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | 53 | .form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { 54 | color: var(--bs-secondary-color); 55 | text-align: end; 56 | } 57 | 58 | .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { 59 | text-align: start; 60 | } -------------------------------------------------------------------------------- /flat/pinvoke.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | #:property AllowUnsafeBlocks=True 3 | 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | 7 | if (args.Contains("--debug")) 8 | { 9 | NativeMethods.Debug = true; 10 | Console.WriteLine("Debug mode enabled."); 11 | } 12 | 13 | NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, NativeMethods.DllImportResolver); 14 | 15 | var greeting = NativeMethods.Greetings("run-file"); 16 | 17 | if (OperatingSystem.IsWindows() && !args.Contains("--console")) 18 | { 19 | NativeMethods.MessageBoxW(IntPtr.Zero, greeting, "Attention!", 0); 20 | } 21 | else 22 | { 23 | Console.WriteLine(greeting); 24 | } 25 | 26 | internal partial class NativeMethods 27 | { 28 | public static bool Debug { get; set; } = false; 29 | 30 | public static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) 31 | { 32 | 33 | // Try to load the native library from the current directory. 34 | var libraryPath = Path.Join(AppContext.GetData("EntryPointFileDirectoryPath")?.ToString() ?? Environment.CurrentDirectory, GetLibraryName(libraryName)); 35 | if (Debug) 36 | { 37 | Console.WriteLine($"Attempting to load library: {Path.NormalizeSeparators(libraryPath)}"); 38 | } 39 | return NativeLibrary.TryLoad(libraryPath, out var handle) 40 | ? handle 41 | : IntPtr.Zero; // Fallback to the default import resolver. 42 | } 43 | 44 | private static string GetLibraryName(string libraryName) => OperatingSystem.IsWindows() ? libraryName : PrefixFileName("lib", libraryName); 45 | 46 | private static string PrefixFileName(string prefix, string path) 47 | { 48 | var dir = Path.GetDirectoryName(path); 49 | var fileName = $"{prefix}{Path.GetFileName(path)}"; 50 | var ext = OperatingSystem.IsWindows() 51 | ? ".dll" 52 | : OperatingSystem.IsLinux() 53 | ? ".so" 54 | : OperatingSystem.IsMacOS() 55 | ? ".dylib" 56 | : throw new PlatformNotSupportedException("Unsupported platform for native library loading."); 57 | var result = Path.Join(dir ?? string.Empty, $"{fileName}{ext}"); 58 | 59 | if (Debug) 60 | { 61 | Console.WriteLine($"Current dir: {Environment.CurrentDirectory}"); 62 | Console.WriteLine($"Library dir: {dir}"); 63 | Console.WriteLine($"Library file name: {fileName}"); 64 | Console.WriteLine($"Prefixed result: {result}"); 65 | } 66 | 67 | return result; 68 | } 69 | 70 | [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] 71 | public static partial int MessageBoxW(IntPtr hWnd, string lpText, string lpCaption, uint uType); 72 | 73 | [LibraryImport("lib/greetings", StringMarshalling = StringMarshalling.Utf8)] 74 | public static partial string Greetings(string name); 75 | } 76 | 77 | static class PathEntryPointExtensions 78 | { 79 | extension(Path) 80 | { 81 | public static string NormalizeSeparators(string path) => path.Replace( 82 | Path.DirectorySeparatorChar switch 83 | { 84 | '/' => '\\', 85 | '\\' => '/', 86 | _ => throw new InvalidOperationException("Unsupported directory separator character.") 87 | }, 88 | Path.DirectorySeparatorChar); 89 | } 90 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-and-verify: 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, windows-latest, macos-latest] 22 | 23 | runs-on: ${{ matrix.os }} 24 | 25 | env: 26 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 27 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 28 | DOTNET_NOLOGO: true 29 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 30 | DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false 31 | DOTNET_MULTILEVEL_LOOKUP: 0 32 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: true 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Check for nativelib changes 42 | id: check-nativelib 43 | run: | 44 | NEED_BUILD=false 45 | 46 | # Check if nativelib source files changed 47 | if [ "${{ github.event_name }}" == "pull_request" ]; then 48 | if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q '^nativelib/'; then 49 | NEED_BUILD=true 50 | fi 51 | else 52 | if git diff --name-only HEAD~1 HEAD | grep -q '^nativelib/'; then 53 | NEED_BUILD=true 54 | fi 55 | fi 56 | 57 | # Check if output files are missing 58 | if [ "${{ runner.os }}" == "Windows" ] && [ ! -f "flat/lib/greetings.dll" ]; then 59 | NEED_BUILD=true 60 | fi 61 | if [ "${{ runner.os }}" == "Linux" ] && [ ! -f "flat/lib/libgreetings.so" ]; then 62 | NEED_BUILD=true 63 | fi 64 | if [ "${{ runner.os }}" == "macOS" ] && [ ! -f "flat/lib/libgreetings.dylib" ]; then 65 | NEED_BUILD=true 66 | fi 67 | 68 | if [ "$NEED_BUILD" == "true" ]; then 69 | echo "changed=true" >> $GITHUB_OUTPUT 70 | else 71 | echo "changed=false" >> $GITHUB_OUTPUT 72 | fi 73 | shell: bash 74 | 75 | - name: Setup .NET SDK 76 | uses: actions/setup-dotnet@v4 77 | with: 78 | global-json-file: global.json 79 | 80 | - name: Install HTTPS development certificate 81 | run: dotnet dev-certs https --verbose 82 | continue-on-error: true 83 | 84 | - name: Install CMake (Ubuntu) 85 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Linux' 86 | run: | 87 | sudo apt-get update 88 | sudo apt-get install -y cmake build-essential 89 | 90 | - name: Install CMake (macOS) 91 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'macOS' 92 | run: | 93 | brew install cmake 94 | 95 | - name: Install CMake (Windows) 96 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Windows' 97 | run: | 98 | choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y 99 | 100 | - name: Build nativelib 101 | if: steps.check-nativelib.outputs.changed == 'true' 102 | run: | 103 | cmake -S nativelib -B nativelib/build -DCMAKE_BUILD_TYPE=Release 104 | cmake --build nativelib/build --config Release 105 | 106 | - name: Copy nativelib output to flat/lib (Windows) 107 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Windows' 108 | run: | 109 | mkdir -p flat/lib 110 | cp nativelib/build/Release/greetings.dll flat/lib/ 111 | shell: bash 112 | 113 | - name: Copy nativelib output to flat/lib (Linux) 114 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'Linux' 115 | run: | 116 | mkdir -p flat/lib 117 | cp nativelib/build/libgreetings.so flat/lib/ 118 | 119 | - name: Copy nativelib output to flat/lib (macOS) 120 | if: steps.check-nativelib.outputs.changed == 'true' && runner.os == 'macOS' 121 | run: | 122 | mkdir -p flat/lib 123 | cp nativelib/build/libgreetings.dylib flat/lib/ 124 | 125 | - name: Commit and push nativelib artifacts 126 | if: steps.check-nativelib.outputs.changed == 'true' && github.event_name == 'pull_request' 127 | run: | 128 | git config user.name "github-actions[bot]" 129 | git config user.email "github-actions[bot]@users.noreply.github.com" 130 | git add flat/lib/ 131 | if ! git diff --staged --quiet; then 132 | git commit -m "Update nativelib artifacts for ${{ runner.os }}" 133 | git push origin HEAD:${{ github.head_ref }} 134 | fi 135 | shell: bash 136 | 137 | - name: Run verification script 138 | run: dotnet verify.cs --parallel --timeout 400 139 | working-directory: ${{ github.workspace }} 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea/ 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | -------------------------------------------------------------------------------- /flat/spaceinvaders.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | // Check for --demo mode 4 | var demoMode = args.Contains("--demo", StringComparer.OrdinalIgnoreCase); 5 | 6 | if (demoMode) 7 | { 8 | // In demo mode, just create the game, render the initial screen, and exit 9 | var game = new Game(); 10 | game.RenderInitialScreen(); 11 | return 0; 12 | } 13 | else 14 | { 15 | Console.Clear(); 16 | new Game().Run(); 17 | return 0; 18 | } 19 | 20 | class Game 21 | { 22 | private readonly int width; 23 | private readonly int height; 24 | private Player player; 25 | private readonly List enemies; 26 | private readonly List bullets; 27 | private int enemyDirection; // 1 for right, -1 for left 28 | private bool gameOver; 29 | private bool playerWon; 30 | private int frameCount = 0; 31 | 32 | public Game(int width = 40, int height = 20) 33 | { 34 | this.width = width; 35 | this.height = height; 36 | // Initialize player at bottom center. 37 | player = new Player(width / 2, height - 1); 38 | bullets = []; 39 | enemies = []; 40 | // Create a grid of enemies. 41 | int rows = 3; 42 | int cols = 8; 43 | int startX = 5; 44 | int startY = 0; 45 | for (int r = 0; r < rows; r++) 46 | { 47 | for (int c = 0; c < cols; c++) 48 | { 49 | enemies.Add(new Enemy(startX + c, startY + r)); 50 | } 51 | } 52 | enemyDirection = 1; 53 | gameOver = false; 54 | playerWon = false; 55 | } 56 | 57 | public void RenderInitialScreen() 58 | { 59 | // Render the initial game state for demo/verification purposes 60 | // Don't use Console.Clear() or Console.CursorVisible as they fail when redirected 61 | RenderToConsole(); 62 | Console.WriteLine("\nDemo mode - initial game state rendered successfully."); 63 | } 64 | 65 | private void RenderToConsole() 66 | { 67 | // Create a buffer for the inner game area. 68 | char[,] buffer = new char[height, width]; 69 | for (int r = 0; r < height; r++) 70 | { 71 | for (int c = 0; c < width; c++) 72 | { 73 | buffer[r, c] = ' '; 74 | } 75 | } 76 | 77 | char enemyChar = 'X'; 78 | char playerChar = '^'; 79 | 80 | // Draw player. 81 | if (player.Y >= 0 && player.Y < height && player.X >= 0 && player.X < width) 82 | { 83 | buffer[player.Y, player.X] = playerChar; 84 | } 85 | // Draw enemies. 86 | foreach (var enemy in enemies) 87 | { 88 | if (enemy.Y >= 0 && enemy.Y < height && enemy.X >= 0 && enemy.X < width) 89 | { 90 | buffer[enemy.Y, enemy.X] = enemyChar; 91 | } 92 | } 93 | 94 | // Draw the border. 95 | string topBorder = "+" + new string('-', width) + "+"; 96 | Console.WriteLine(topBorder); 97 | for (int r = 0; r < height; r++) 98 | { 99 | Console.Write("|"); 100 | for (int c = 0; c < width; c++) 101 | { 102 | Console.Write(buffer[r, c]); 103 | } 104 | Console.WriteLine("|"); 105 | } 106 | string bottomBorder = "+" + new string('-', width) + "+"; 107 | Console.WriteLine(bottomBorder); 108 | } 109 | 110 | public void Run() 111 | { 112 | Console.CursorVisible = false; 113 | while (!gameOver) 114 | { 115 | HandleInput(); 116 | UpdateGame(); 117 | Render(); 118 | Thread.Sleep(100); 119 | } 120 | Console.CursorVisible = true; 121 | Console.Clear(); 122 | // Display end message with color. 123 | if (playerWon) 124 | { 125 | Console.ForegroundColor = ConsoleColor.Green; 126 | Console.WriteLine("You win! All enemies defeated."); 127 | } 128 | else 129 | { 130 | Console.ForegroundColor = ConsoleColor.Red; 131 | Console.WriteLine("Game Over! An enemy reached the bottom."); 132 | } 133 | Console.ResetColor(); 134 | } 135 | 136 | private void HandleInput() 137 | { 138 | if (Console.KeyAvailable) 139 | { 140 | var keyInfo = Console.ReadKey(intercept: true); 141 | if (keyInfo.Key == ConsoleKey.LeftArrow) 142 | { 143 | player = player.MoveLeft(); 144 | } 145 | else if (keyInfo.Key == ConsoleKey.RightArrow) 146 | { 147 | player = player.MoveRight(width); 148 | } 149 | else if (keyInfo.Key == ConsoleKey.Spacebar) 150 | { 151 | // Fire a bullet from just above the player. 152 | bullets.Add(new Bullet(player.X, player.Y - 1)); 153 | } 154 | // Flush any additional key presses. 155 | while (Console.KeyAvailable) 156 | { 157 | Console.ReadKey(true); 158 | } 159 | } 160 | } 161 | 162 | private void UpdateGame() 163 | { 164 | // Update bullets: move them upward. 165 | for (int i = bullets.Count - 1; i >= 0; i--) 166 | { 167 | bullets[i] = bullets[i].MoveUp(); 168 | if (bullets[i].Y < 0) 169 | { 170 | bullets.RemoveAt(i); 171 | } 172 | } 173 | 174 | // Determine enemy boundaries. 175 | bool hitEdge = false; 176 | int minX = int.MaxValue, maxX = int.MinValue; 177 | foreach (var enemy in enemies) 178 | { 179 | if (enemy.X < minX) 180 | { 181 | minX = enemy.X; 182 | } 183 | 184 | if (enemy.X > maxX) 185 | { 186 | maxX = enemy.X; 187 | } 188 | } 189 | if ((enemyDirection == 1 && maxX >= width - 1) || 190 | (enemyDirection == -1 && minX <= 0)) 191 | { 192 | hitEdge = true; 193 | } 194 | 195 | // Update enemies. 196 | if (hitEdge) 197 | { 198 | for (int i = 0; i < enemies.Count; i++) 199 | { 200 | enemies[i] = new Enemy(enemies[i].X, enemies[i].Y + 1); 201 | } 202 | enemyDirection *= -1; 203 | } 204 | else 205 | { 206 | for (int i = 0; i < enemies.Count; i++) 207 | { 208 | enemies[i] = new Enemy(enemies[i].X + enemyDirection, enemies[i].Y); 209 | } 210 | } 211 | 212 | // Collision detection: bullet vs enemy. 213 | for (int i = bullets.Count - 1; i >= 0; i--) 214 | { 215 | for (int j = enemies.Count - 1; j >= 0; j--) 216 | { 217 | if (bullets[i].X == enemies[j].X && bullets[i].Y == enemies[j].Y) 218 | { 219 | bullets.RemoveAt(i); 220 | enemies.RemoveAt(j); 221 | break; 222 | } 223 | } 224 | } 225 | 226 | // Check win condition. 227 | if (enemies.Count == 0) 228 | { 229 | playerWon = true; 230 | gameOver = true; 231 | } 232 | else 233 | { 234 | // Lose condition: any enemy reaches the player's row. 235 | foreach (var enemy in enemies) 236 | { 237 | if (enemy.Y >= player.Y) 238 | { 239 | gameOver = true; 240 | break; 241 | } 242 | } 243 | } 244 | } 245 | 246 | private void Render() 247 | { 248 | // Move cursor to top-left. 249 | Console.SetCursorPosition(0, 0); 250 | // Create a buffer for the inner game area. 251 | char[,] buffer = new char[height, width]; 252 | for (int r = 0; r < height; r++) 253 | { 254 | for (int c = 0; c < width; c++) 255 | { 256 | buffer[r, c] = ' '; 257 | } 258 | } 259 | 260 | // Determine animated characters. 261 | char enemyChar = (frameCount % 2 == 0) ? 'X' : 'x'; 262 | char playerChar = (frameCount % 2 == 0) ? '^' : 'A'; 263 | char bulletChar = (frameCount % 2 == 0) ? '|' : '!'; 264 | 265 | // Draw bullets. 266 | foreach (var bullet in bullets) 267 | { 268 | if (bullet.Y >= 0 && bullet.Y < height && bullet.X >= 0 && bullet.X < width) 269 | { 270 | buffer[bullet.Y, bullet.X] = bulletChar; 271 | } 272 | } 273 | // Draw player. 274 | if (player.Y >= 0 && player.Y < height && player.X >= 0 && player.X < width) 275 | { 276 | buffer[player.Y, player.X] = playerChar; 277 | } 278 | // Draw enemies. 279 | foreach (var enemy in enemies) 280 | { 281 | if (enemy.Y >= 0 && enemy.Y < height && enemy.X >= 0 && enemy.X < width) 282 | { 283 | buffer[enemy.Y, enemy.X] = enemyChar; 284 | } 285 | } 286 | 287 | // Draw the border. 288 | // Top border. 289 | string topBorder = "+" + new string('-', width) + "+"; 290 | Console.WriteLine(topBorder); 291 | // For each row, draw left border, row content, then right border. 292 | for (int r = 0; r < height; r++) 293 | { 294 | Console.Write("|"); 295 | for (int c = 0; c < width; c++) 296 | { 297 | char ch = buffer[r, c]; 298 | // Set color based on entity. 299 | Console.ForegroundColor = ch switch 300 | { 301 | '^' or 'A' => ConsoleColor.Green, 302 | 'X' or 'x' => ConsoleColor.Red, 303 | '|' or '!' => ConsoleColor.Yellow, 304 | _ => ConsoleColor.Gray, 305 | }; 306 | Console.Write(ch); 307 | } 308 | Console.ResetColor(); 309 | Console.WriteLine("|"); 310 | } 311 | // Bottom border. 312 | string bottomBorder = "+" + new string('-', width) + "+"; 313 | Console.WriteLine(bottomBorder); 314 | 315 | frameCount++; 316 | } 317 | 318 | public record Player(int X, int Y) 319 | { 320 | // Return a new Player with updated X when moving. 321 | public Player MoveLeft() => this with { X = X > 0 ? X - 1 : 0 }; 322 | public Player MoveRight(int maxWidth) => this with { X = X < maxWidth - 1 ? X + 1 : maxWidth - 1 }; 323 | } 324 | 325 | public record Enemy(int X, int Y); 326 | 327 | public record Bullet(int X, int Y) 328 | { 329 | // Move the bullet upward. 330 | public Bullet MoveUp() => this with { Y = Y - 1 }; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /flat/The Tell-Tale Heart.txt: -------------------------------------------------------------------------------- 1 | THE TELL-TALE HEART. 2 | Author: Edgar Allan Poe 3 | 4 | 5 | True!—nervous—very, very dreadfully nervous I had been and am; 6 | but why will you say that I am mad? The disease had sharpened my 7 | senses—not destroyed—not dulled them. Above all was the sense of 8 | hearing acute. I heard all things in the heaven and in the earth. 9 | I heard many things in hell. How, then, am I mad? Hearken! and 10 | observe how healthily—how calmly I can tell you the whole story. 11 | 12 | It is impossible to say how first the idea entered my brain; but 13 | once conceived, it haunted me day and night. Object there was 14 | none. Passion there was none. I loved the old man. He had never 15 | wronged me. He had never given me insult. For his gold I had no 16 | desire. I think it was his eye! yes, it was this! He had the eye 17 | of a vulture—a pale blue eye, with a film over it. Whenever it 18 | fell upon me, my blood ran cold; and so by degrees—very 19 | gradually—I made up my mind to take the life of the old man, and 20 | thus rid myself of the eye forever. 21 | 22 | Now this is the point. You fancy me mad. Madmen know nothing. But 23 | you should have seen me. You should have seen how wisely I 24 | proceeded—with what caution—with what foresight—with what 25 | dissimulation I went to work! I was never kinder to the old man 26 | than during the whole week before I killed him. And every night, 27 | about midnight, I turned the latch of his door and opened it—oh, 28 | so gently! And then, when I had made an opening sufficient for my 29 | head, I put in a dark lantern, all closed, closed, that no light 30 | shone out, and then I thrust in my head. Oh, you would have 31 | laughed to see how cunningly I thrust it in! I moved it 32 | slowly—very, very slowly, so that I might not disturb the old 33 | man’s sleep. It took me an hour to place my whole head within the 34 | opening so far that I could see him as he lay upon his bed. 35 | Ha!—would a madman have been so wise as this? And then, when my 36 | head was well in the room, I undid the lantern cautiously—oh, so 37 | cautiously—cautiously (for the hinges creaked)—I undid it just so 38 | much that a single thin ray fell upon the vulture eye. And this I 39 | did for seven long nights—every night just at midnight—but I 40 | found the eye always closed; and so it was impossible to do the 41 | work; for it was not the old man who vexed me, but his Evil Eye. 42 | And every morning, when the day broke, I went boldly into the 43 | chamber, and spoke courageously to him, calling him by name in a 44 | hearty tone, and inquiring how he has passed the night. So you 45 | see he would have been a very profound old man, indeed, to 46 | suspect that every night, just at twelve, I looked in upon him 47 | while he slept. 48 | 49 | Upon the eighth night I was more than usually cautious in opening 50 | the door. A watch’s minute hand moves more quickly than did mine. 51 | Never before that night had I felt the extent of my own powers—of 52 | my sagacity. I could scarcely contain my feelings of triumph. To 53 | think that there I was, opening the door, little by little, and 54 | he not even to dream of my secret deeds or thoughts. I fairly 55 | chuckled at the idea; and perhaps he heard me; for he moved on 56 | the bed suddenly, as if startled. Now you may think that I drew 57 | back—but no. His room was as black as pitch with the thick 58 | darkness, (for the shutters were close fastened, through fear of 59 | robbers,) and so I knew that he could not see the opening of the 60 | door, and I kept pushing it on steadily, steadily. 61 | 62 | I had my head in, and was about to open the lantern, when my 63 | thumb slipped upon the tin fastening, and the old man sprang up 64 | in bed, crying out—“Who’s there?” 65 | 66 | I kept quite still and said nothing. For a whole hour I did not 67 | move a muscle, and in the meantime I did not hear him lie down. 68 | He was still sitting up in the bed listening;—just as I have 69 | done, night after night, hearkening to the death watches in the 70 | wall. 71 | 72 | Presently I heard a slight groan, and I knew it was the groan of 73 | mortal terror. It was not a groan of pain or of grief—oh, no!—it 74 | was the low stifled sound that arises from the bottom of the soul 75 | when overcharged with awe. I knew the sound well. Many a night, 76 | just at midnight, when all the world slept, it has welled up from 77 | my own bosom, deepening, with its dreadful echo, the terrors that 78 | distracted me. I say I knew it well. I knew what the old man 79 | felt, and pitied him, although I chuckled at heart. I knew that 80 | he had been lying awake ever since the first slight noise, when 81 | he had turned in the bed. His fears had been ever since growing 82 | upon him. He had been trying to fancy them causeless, but could 83 | not. He had been saying to himself—“It is nothing but the wind in 84 | the chimney—it is only a mouse crossing the floor,” or “It is 85 | merely a cricket which has made a single chirp.” Yes, he had been 86 | trying to comfort himself with these suppositions: but he had 87 | found all in vain. All in vain; because Death, in approaching him 88 | had stalked with his black shadow before him, and enveloped the 89 | victim. And it was the mournful influence of the unperceived 90 | shadow that caused him to feel—although he neither saw nor 91 | heard—to feel the presence of my head within the room. 92 | 93 | When I had waited a long time, very patiently, without hearing 94 | him lie down, I resolved to open a little—a very, very little 95 | crevice in the lantern. So I opened it—you cannot imagine how 96 | stealthily, stealthily—until, at length a simple dim ray, like 97 | the thread of the spider, shot from out the crevice and fell full 98 | upon the vulture eye. 99 | 100 | It was open—wide, wide open—and I grew furious as I gazed upon 101 | it. I saw it with perfect distinctness—all a dull blue, with a 102 | hideous veil over it that chilled the very marrow in my bones; 103 | but I could see nothing else of the old man’s face or person: for 104 | I had directed the ray as if by instinct, precisely upon the 105 | damned spot. 106 | 107 | And have I not told you that what you mistake for madness is but 108 | over-acuteness of the sense?—now, I say, there came to my ears a 109 | low, dull, quick sound, such as a watch makes when enveloped in 110 | cotton. I knew that sound well, too. It was the beating of the 111 | old man’s heart. It increased my fury, as the beating of a drum 112 | stimulates the soldier into courage. 113 | 114 | But even yet I refrained and kept still. I scarcely breathed. I 115 | held the lantern motionless. I tried how steadily I could 116 | maintain the ray upon the eye. Meantime the hellish tattoo of the 117 | heart increased. It grew quicker and quicker, and louder and 118 | louder every instant. The old man’s terror must have been 119 | extreme! It grew louder, I say, louder every moment!—do you mark 120 | me well? I have told you that I am nervous: so I am. And now at 121 | the dead hour of the night, amid the dreadful silence of that old 122 | house, so strange a noise as this excited me to uncontrollable 123 | terror. Yet, for some minutes longer I refrained and stood still. 124 | But the beating grew louder, louder! I thought the heart must 125 | burst. And now a new anxiety seized me—the sound would be heard 126 | by a neighbour! The old man’s hour had come! With a loud yell, I 127 | threw open the lantern and leaped into the room. He shrieked 128 | once—once only. In an instant I dragged him to the floor, and 129 | pulled the heavy bed over him. I then smiled gaily, to find the 130 | deed so far done. But, for many minutes, the heart beat on with a 131 | muffled sound. This, however, did not vex me; it would not be 132 | heard through the wall. At length it ceased. The old man was 133 | dead. I removed the bed and examined the corpse. Yes, he was 134 | stone, stone dead. I placed my hand upon the heart and held it 135 | there many minutes. There was no pulsation. He was stone dead. 136 | His eye would trouble me no more. 137 | 138 | If still you think me mad, you will think so no longer when I 139 | describe the wise precautions I took for the concealment of the 140 | body. The night waned, and I worked hastily, but in silence. 141 | First of all I dismembered the corpse. I cut off the head and the 142 | arms and the legs. 143 | 144 | I then took up three planks from the flooring of the chamber, and 145 | deposited all between the scantlings. I then replaced the boards 146 | so cleverly, so cunningly, that no human eye—not even his—could 147 | have detected any thing wrong. There was nothing to wash out—no 148 | stain of any kind—no blood-spot whatever. I had been too wary for 149 | that. A tub had caught all—ha! ha! 150 | 151 | When I had made an end of these labors, it was four o’clock—still 152 | dark as midnight. As the bell sounded the hour, there came a 153 | knocking at the street door. I went down to open it with a light 154 | heart,—for what had I now to fear? There entered three men, who 155 | introduced themselves, with perfect suavity, as officers of the 156 | police. A shriek had been heard by a neighbour during the night; 157 | suspicion of foul play had been aroused; information had been 158 | lodged at the police office, and they (the officers) had been 159 | deputed to search the premises. 160 | 161 | I smiled,—for what had I to fear? I bade the gentlemen welcome. 162 | The shriek, I said, was my own in a dream. The old man, I 163 | mentioned, was absent in the country. I took my visitors all over 164 | the house. I bade them search—search well. I led them, at length, 165 | to his chamber. I showed them his treasures, secure, undisturbed. 166 | In the enthusiasm of my confidence, I brought chairs into the 167 | room, and desired them here to rest from their fatigues, while I 168 | myself, in the wild audacity of my perfect triumph, placed my own 169 | seat upon the very spot beneath which reposed the corpse of the 170 | victim. 171 | 172 | The officers were satisfied. My manner had convinced them. I was 173 | singularly at ease. They sat, and while I answered cheerily, they 174 | chatted of familiar things. But, ere long, I felt myself getting 175 | pale and wished them gone. My head ached, and I fancied a ringing 176 | in my ears: but still they sat and still chatted. The ringing 177 | became more distinct:—it continued and became more distinct: I 178 | talked more freely to get rid of the feeling: but it continued 179 | and gained definiteness—until, at length, I found that the noise 180 | was not within my ears. 181 | 182 | No doubt I now grew _very_ pale;—but I talked more fluently, and 183 | with a heightened voice. Yet the sound increased—and what could I 184 | do? It was a low, dull, quick sound—much such a sound as a watch 185 | makes when enveloped in cotton. I gasped for breath—and yet the 186 | officers heard it not. I talked more quickly—more vehemently; but 187 | the noise steadily increased. I arose and argued about trifles, 188 | in a high key and with violent gesticulations; but the noise 189 | steadily increased. Why would they not be gone? I paced the floor 190 | to and fro with heavy strides, as if excited to fury by the 191 | observations of the men—but the noise steadily increased. Oh God! 192 | what could I do? I foamed—I raved—I swore! I swung the chair upon 193 | which I had been sitting, and grated it upon the boards, but the 194 | noise arose over all and continually increased. It grew 195 | louder—louder—louder! And still the men chatted pleasantly, and 196 | smiled. Was it possible they heard not? Almighty God!—no, no! 197 | They heard!—they suspected!—they knew!—they were making a mockery 198 | of my horror!—this I thought, and this I think. But anything was 199 | better than this agony! Anything was more tolerable than this 200 | derision! I could bear those hypocritical smiles no longer! I 201 | felt that I must scream or die! and now—again!—hark! louder! 202 | louder! louder! _louder!_ 203 | 204 | “Villains!” I shrieked, “dissemble no more! I admit the 205 | deed!—tear up the planks!—here, here!—It is the beating of his 206 | hideous heart!” -------------------------------------------------------------------------------- /verify.cs: -------------------------------------------------------------------------------- 1 | #!/bin/usr/env dotnet 2 | #:package Spectre.Console@0.54.0 3 | #:property AllowUnsafeBlocks=true 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.InteropServices; 7 | using System.Text.Json; 8 | using Spectre.Console; 9 | 10 | // Parse command-line arguments 11 | var useParallel = args.Contains("--parallel", StringComparer.OrdinalIgnoreCase); 12 | var timeoutSeconds = 10; // Default timeout 13 | for (int i = 0; i < args.Length; i++) 14 | { 15 | if (args[i].Equals("--timeout", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) 16 | { 17 | if (int.TryParse(args[i + 1], out var parsed)) 18 | { 19 | timeoutSeconds = parsed; 20 | } 21 | } 22 | } 23 | 24 | // Get the path of this script file 25 | var scriptPath = AppContext.GetData("EntryPointFilePath")?.ToString(); 26 | if (string.IsNullOrEmpty(scriptPath)) 27 | { 28 | AnsiConsole.MarkupLine("[red]Error: Could not determine script path[/]"); 29 | return 1; 30 | } 31 | 32 | var scriptDir = Path.GetDirectoryName(scriptPath) ?? Environment.CurrentDirectory; 33 | AnsiConsole.MarkupLine($"[cyan]Scanning for .cs files from:[/] {scriptDir}"); 34 | AnsiConsole.MarkupLine($"[cyan]Execution mode:[/] {(useParallel ? "Parallel" : "Sequential")}"); 35 | AnsiConsole.MarkupLine($"[cyan]Timeout:[/] {timeoutSeconds}s"); 36 | AnsiConsole.WriteLine(); 37 | 38 | // Find all .cs files in subdirectories 39 | var csFiles = FindExecutableCsFiles(scriptDir); 40 | 41 | if (csFiles.Count == 0) 42 | { 43 | AnsiConsole.MarkupLine("[yellow]No executable .cs files found[/]"); 44 | return 0; 45 | } 46 | 47 | AnsiConsole.MarkupLine($"[green]Found {csFiles.Count} .cs file(s) to verify[/]"); 48 | AnsiConsole.WriteLine(); 49 | 50 | // Run verification 51 | var results = useParallel 52 | ? await VerifyFilesParallel(csFiles, timeoutSeconds) 53 | : await VerifyFilesSequential(csFiles, timeoutSeconds); 54 | 55 | // Display results in a table 56 | DisplayResults(results); 57 | 58 | // Return exit code based on results 59 | var failedCount = results.Count(r => !r.Success); 60 | return failedCount > 0 ? 1 : 0; 61 | 62 | // --- Helper Methods --- 63 | 64 | bool HasVerifyLaunchProfile(string csFilePath) 65 | { 66 | try 67 | { 68 | var directory = Path.GetDirectoryName(csFilePath); 69 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(csFilePath); 70 | var runJsonPath = Path.Combine(directory ?? ".", $"{fileNameWithoutExtension}.run.json"); 71 | 72 | if (!File.Exists(runJsonPath)) 73 | { 74 | return false; 75 | } 76 | 77 | var jsonContent = File.ReadAllText(runJsonPath); 78 | using var doc = JsonDocument.Parse(jsonContent, new JsonDocumentOptions { AllowTrailingCommas = true }); 79 | 80 | if (doc.RootElement.TryGetProperty("profiles", out var profiles)) 81 | { 82 | return profiles.TryGetProperty("verify", out _); 83 | } 84 | } 85 | catch 86 | { 87 | // If we can't read or parse the file, just proceed without the launch profile 88 | } 89 | 90 | return false; 91 | } 92 | 93 | bool ShouldSkipFile(string csFilePath) 94 | { 95 | try 96 | { 97 | // Read the file header (up to first code line) 98 | foreach (var line in File.ReadLines(csFilePath)) 99 | { 100 | var trimmed = line.Trim(); 101 | 102 | // Stop at first code line (non-comment, non-directive, non-blank) 103 | if (!string.IsNullOrWhiteSpace(trimmed) && 104 | !trimmed.StartsWith("#") && 105 | !trimmed.StartsWith("//") && 106 | !trimmed.StartsWith("/*")) 107 | { 108 | break; 109 | } 110 | 111 | // Check for TargetFramework property directive 112 | if (trimmed.StartsWith("#:property TargetFramework=", StringComparison.OrdinalIgnoreCase)) 113 | { 114 | var tfm = trimmed.Substring("#:property TargetFramework=".Length).Trim(); 115 | 116 | // Check if TFM is OS-specific and doesn't match current OS 117 | if (tfm.Contains("-windows", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsWindows()) 118 | { 119 | return true; 120 | } 121 | if (tfm.Contains("-linux", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsLinux()) 122 | { 123 | return true; 124 | } 125 | if (tfm.Contains("-macos", StringComparison.OrdinalIgnoreCase) && !OperatingSystem.IsMacOS()) 126 | { 127 | return true; 128 | } 129 | } 130 | } 131 | } 132 | catch 133 | { 134 | // If we can't read the file, don't skip it 135 | } 136 | 137 | return false; 138 | } 139 | 140 | List FindExecutableCsFiles(string rootDir) 141 | { 142 | var files = new List(); 143 | 144 | foreach (var dir in Directory.GetDirectories(rootDir)) 145 | { 146 | // Skip if directory contains a .csproj file 147 | if (Directory.GetFiles(dir, "*.csproj").Length > 0) 148 | { 149 | continue; 150 | } 151 | 152 | // Add all .cs files in this directory (that shouldn't be skipped) 153 | foreach (var file in Directory.GetFiles(dir, "*.cs")) 154 | { 155 | if (!ShouldSkipFile(file)) 156 | { 157 | files.Add(file); 158 | } 159 | } 160 | 161 | // Recursively search subdirectories 162 | files.AddRange(FindExecutableCsFiles(dir)); 163 | } 164 | 165 | return files; 166 | } 167 | 168 | async Task> VerifyFilesSequential(List files, int timeoutSeconds) 169 | { 170 | var results = new List(); 171 | 172 | await AnsiConsole.Progress() 173 | .Columns( 174 | new TaskDescriptionColumn(), 175 | new ProgressBarColumn(), 176 | new SpinnerColumn()) 177 | .StartAsync(async ctx => 178 | { 179 | var task = ctx.AddTask($"[green]Verifying files (0/{files.Count})[/]", maxValue: files.Count); 180 | 181 | foreach (var file in files) 182 | { 183 | var result = await VerifyFile(file, timeoutSeconds); 184 | results.Add(result); 185 | task.Increment(1); 186 | task.Description = $"[green]Verifying files ({results.Count}/{files.Count})[/]"; 187 | } 188 | }); 189 | 190 | return results; 191 | } 192 | 193 | async Task> VerifyFilesParallel(List files, int timeoutSeconds) 194 | { 195 | var results = new List(); 196 | var lockObj = new Lock(); 197 | 198 | await AnsiConsole.Progress() 199 | .Columns( 200 | new TaskDescriptionColumn(), 201 | new ProgressBarColumn(), 202 | new SpinnerColumn()) 203 | .StartAsync(async ctx => 204 | { 205 | var progressTask = ctx.AddTask($"[green]Verifying files (0/{files.Count})[/]", maxValue: files.Count); 206 | 207 | var tasks = files.Select(async file => 208 | { 209 | var result = await VerifyFile(file, timeoutSeconds); 210 | lock (lockObj) 211 | { 212 | results.Add(result); 213 | progressTask.Increment(1); 214 | progressTask.Description = $"[green]Verifying files ({results.Count}/{files.Count})[/]"; 215 | } 216 | }); 217 | 218 | await Task.WhenAll(tasks); 219 | }); 220 | 221 | return results; 222 | } 223 | 224 | async Task VerifyFile(string filePath, int timeoutSeconds) 225 | { 226 | var result = new VerificationResult 227 | { 228 | FilePath = filePath, 229 | FileName = Path.GetFileName(filePath) 230 | }; 231 | 232 | var startInfo = new ProcessStartInfo 233 | { 234 | FileName = "dotnet", 235 | RedirectStandardOutput = true, 236 | RedirectStandardError = true, 237 | RedirectStandardInput = true, 238 | UseShellExecute = false, 239 | CreateNoWindow = true, 240 | WorkingDirectory = Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory 241 | }; 242 | 243 | // Add arguments using ArgumentList to avoid escaping issues 244 | startInfo.ArgumentList.Add(filePath); 245 | 246 | // Check for .run.json with verify launch profile 247 | var hasVerifyProfile = HasVerifyLaunchProfile(filePath); 248 | if (hasVerifyProfile) 249 | { 250 | startInfo.ArgumentList.Add("--launch-profile"); 251 | startInfo.ArgumentList.Add("verify"); 252 | result.UsedVerifyProfile = true; 253 | } 254 | 255 | startInfo.Environment["VERIFY_MODE"] = "1"; 256 | 257 | if (OperatingSystem.IsWindows()) 258 | { 259 | startInfo.CreateNewProcessGroup = true; 260 | } 261 | 262 | var process = new Process { StartInfo = startInfo }; 263 | var output = new List(); 264 | var error = new List(); 265 | var shutdownMessageDetected = false; 266 | var shutdownTcs = new TaskCompletionSource(); 267 | 268 | process.OutputDataReceived += (s, e) => 269 | { 270 | if (e.Data != null) 271 | { 272 | lock (output) 273 | { 274 | output.Add(e.Data); 275 | if (e.Data.Contains("Press Ctrl+C to shut down.") && !shutdownMessageDetected) 276 | { 277 | shutdownMessageDetected = true; 278 | shutdownTcs.TrySetResult(true); 279 | } 280 | } 281 | } 282 | }; 283 | 284 | process.ErrorDataReceived += (s, e) => 285 | { 286 | if (e.Data != null) 287 | { 288 | lock (error) 289 | { 290 | error.Add(e.Data); 291 | } 292 | } 293 | }; 294 | 295 | var stopwatch = Stopwatch.StartNew(); 296 | 297 | try 298 | { 299 | process.Start(); 300 | process.BeginOutputReadLine(); 301 | process.BeginErrorReadLine(); 302 | 303 | // Wait for either: process exit, shutdown message detected, or timeout 304 | var timeout = TimeSpan.FromSeconds(timeoutSeconds); 305 | var timeoutTask = Task.Delay(timeout); 306 | var processTask = Task.Run(process.WaitForExit); 307 | 308 | var completedTask = await Task.WhenAny(processTask, shutdownTcs.Task, timeoutTask); 309 | 310 | if (completedTask == shutdownTcs.Task) 311 | { 312 | // Shutdown message detected - send shutdown signal 313 | try 314 | { 315 | process.Stop(); 316 | 317 | if (!process.HasExited) 318 | { 319 | process.Kill(); 320 | } 321 | 322 | stopwatch.Stop(); 323 | result.Success = true; 324 | result.ExitCode = 0; 325 | result.Duration = stopwatch.Elapsed; 326 | 327 | result.FullOutput = string.Join("\n", output); 328 | result.FullError = string.Join("\n", error); 329 | result.HasStderr = error.Count > 0; 330 | result.Message = result.HasStderr ? "App started & stopped successfully" : "App started & stopped successfully"; 331 | } 332 | catch 333 | { 334 | process.Kill(); 335 | stopwatch.Stop(); 336 | result.Success = false; 337 | result.Duration = stopwatch.Elapsed; 338 | result.Message = "Failed to gracefully stop app"; 339 | 340 | result.FullOutput = string.Join("\n", output); 341 | result.FullError = string.Join("\n", error); 342 | } 343 | } 344 | else if (completedTask == timeoutTask) 345 | { 346 | // Process timed out without shutdown message 347 | var allOutput = string.Join("\n", output); 348 | var allError = string.Join("\n", error); 349 | 350 | result.FullOutput = allOutput; 351 | result.FullError = allError; 352 | 353 | process.Kill(); 354 | stopwatch.Stop(); 355 | result.Success = false; 356 | result.Duration = stopwatch.Elapsed; 357 | result.Message = $"Timeout ({timeout.TotalSeconds}s)"; 358 | } 359 | else 360 | { 361 | // Process completed normally 362 | stopwatch.Stop(); 363 | result.ExitCode = process.ExitCode; 364 | result.Duration = stopwatch.Elapsed; 365 | result.Success = process.ExitCode == 0; 366 | 367 | result.FullOutput = string.Join("\n", output); 368 | result.FullError = string.Join("\n", error); 369 | result.HasStderr = error.Count > 0; 370 | 371 | if (result.Success) 372 | { 373 | result.Message = "Completed successfully"; 374 | } 375 | else if (error.Count > 0) 376 | { 377 | result.Message = string.Join("; ", error.Take(2)); 378 | } 379 | else 380 | { 381 | result.Message = $"Exit code: {process.ExitCode}"; 382 | } 383 | } 384 | } 385 | catch (Exception ex) 386 | { 387 | stopwatch.Stop(); 388 | result.Success = false; 389 | result.Duration = stopwatch.Elapsed; 390 | result.Message = $"Error: {ex.Message}"; 391 | } 392 | finally 393 | { 394 | if (!process.HasExited) 395 | { 396 | try { process.Kill(); } catch { } 397 | } 398 | process.Dispose(); 399 | } 400 | 401 | return result; 402 | } 403 | 404 | void DisplayResults(List results) 405 | { 406 | var table = new Table(); 407 | table.Border(TableBorder.Rounded); 408 | table.AddColumn("[bold]File[/]"); 409 | table.AddColumn("[bold]Status[/]"); 410 | table.AddColumn("[bold]Duration[/]"); 411 | table.AddColumn("[bold]Message[/]"); 412 | 413 | foreach (var result in results.OrderBy(r => r.FileName)) 414 | { 415 | var statusText = result.Success 416 | ? (result.HasStderr ? "[green]✓ Pass (stderr)[/]" : "[green]✓ Pass[/]") 417 | : $"[red]✗ Fail ({result.ExitCode})[/]"; 418 | 419 | var durationText = $"{result.Duration.TotalSeconds:F2}s"; 420 | 421 | var fileNameDisplay = result.UsedVerifyProfile 422 | ? $"{result.FileName} [dim](verify profile)[/]" 423 | : result.FileName; 424 | 425 | // For failed apps, show full error output; for successful apps, show brief message 426 | string messageText; 427 | if (!result.Success) 428 | { 429 | // Combine error and output for failed cases 430 | var fullMessage = !string.IsNullOrEmpty(result.FullError) 431 | ? result.FullError 432 | : result.FullOutput; 433 | 434 | messageText = !string.IsNullOrEmpty(fullMessage) 435 | ? fullMessage 436 | : result.Message; 437 | } 438 | else 439 | { 440 | messageText = result.Message.Length > 60 441 | ? result.Message.Substring(0, 57) + "..." 442 | : result.Message; 443 | } 444 | 445 | table.AddRow( 446 | fileNameDisplay, 447 | statusText, 448 | durationText, 449 | messageText.EscapeMarkup() 450 | ); 451 | } 452 | 453 | AnsiConsole.Write(table); 454 | AnsiConsole.WriteLine(); 455 | 456 | // Summary 457 | var totalCount = results.Count; 458 | var passCount = results.Count(r => r.Success); 459 | var failCount = totalCount - passCount; 460 | 461 | var summaryTable = new Table(); 462 | summaryTable.Border(TableBorder.None); 463 | summaryTable.HideHeaders(); 464 | summaryTable.AddColumn(""); 465 | summaryTable.AddColumn(""); 466 | 467 | summaryTable.AddRow("[bold]Total:[/]", totalCount.ToString()); 468 | summaryTable.AddRow("[green]Passed:[/]", passCount.ToString()); 469 | if (failCount > 0) 470 | { 471 | summaryTable.AddRow("[red]Failed:[/]", failCount.ToString()); 472 | } 473 | 474 | AnsiConsole.Write(summaryTable); 475 | 476 | if (failCount == 0) 477 | { 478 | AnsiConsole.WriteLine(); 479 | AnsiConsole.MarkupLine("[green bold]All tests passed! ✓[/]"); 480 | } 481 | } 482 | 483 | class VerificationResult 484 | { 485 | public string FilePath { get; set; } = ""; 486 | public string FileName { get; set; } = ""; 487 | public bool Success { get; set; } 488 | public int ExitCode { get; set; } 489 | public TimeSpan Duration { get; set; } 490 | public string Message { get; set; } = ""; 491 | public string FullOutput { get; set; } = ""; 492 | public string FullError { get; set; } = ""; 493 | public bool HasStderr { get; set; } 494 | public bool UsedVerifyProfile { get; set; } 495 | } 496 | 497 | internal static partial class ProcessExtensions 498 | { 499 | // Code in this class adapted from https://github.com/devlooped/dotnet-stop 500 | // See THIRDPARTYNOTICES for license information. 501 | extension(Process process) 502 | { 503 | public int Stop(TimeSpan? timeout = null, bool quiet = true) 504 | { 505 | timeout ??= TimeSpan.FromSeconds(1); 506 | if (OperatingSystem.IsWindows()) 507 | { 508 | return process.StopWindowsProcess(timeout.Value, quiet); 509 | } 510 | else 511 | { 512 | return process.StopUnixProcess(timeout.Value, quiet); 513 | } 514 | } 515 | 516 | int StopUnixProcess(TimeSpan timeout, bool quiet) 517 | { 518 | if (!quiet) 519 | { 520 | AnsiConsole.MarkupLine($"[yellow]Shutting down {process.ProcessName}:{process.Id}...[/]"); 521 | } 522 | 523 | var killProcess = new ProcessStartInfo("kill") 524 | { 525 | UseShellExecute = true 526 | }; 527 | killProcess.ArgumentList.Add("-s"); 528 | killProcess.ArgumentList.Add("SIGINT"); 529 | killProcess.ArgumentList.Add(process.Id.ToString()); 530 | Process.Start(killProcess)?.WaitForExit(); 531 | 532 | if (timeout != TimeSpan.Zero) 533 | { 534 | if (process.WaitForExit(timeout)) 535 | { 536 | return 0; 537 | } 538 | 539 | if (!quiet) 540 | { 541 | AnsiConsole.MarkupLine($"[red]Timed out waiting for process {process.ProcessName}:{process.Id} to exit[/]"); 542 | } 543 | 544 | return -1; 545 | } 546 | else 547 | { 548 | process.WaitForExit(); 549 | return 0; 550 | } 551 | } 552 | 553 | int StopWindowsProcess(TimeSpan timeout, bool quiet) 554 | { 555 | if (!quiet) 556 | { 557 | AnsiConsole.MarkupLine($"[yellow]Shutting down {process.ProcessName}:{process.Id}...[/]"); 558 | } 559 | 560 | // Send Ctrl+Break to the process group 561 | GenerateConsoleCtrlEvent(1, (uint)process.Id); 562 | 563 | if (timeout != TimeSpan.Zero) 564 | { 565 | if (process.WaitForExit(timeout)) 566 | { 567 | return 0; 568 | } 569 | 570 | if (!quiet) 571 | { 572 | AnsiConsole.MarkupLine($"[red]Timed out waiting for process {process.ProcessName}:{process.Id} to exit[/]"); 573 | } 574 | 575 | return -1; 576 | } 577 | else 578 | { 579 | process.WaitForExit(); 580 | return 0; 581 | } 582 | } 583 | } 584 | 585 | [LibraryImport("kernel32.dll")] 586 | [return: MarshalAs(UnmanagedType.Bool)] 587 | internal static partial bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); 588 | } 589 | --------------------------------------------------------------------------------