A:剩余提交
This commit is contained in:
1 parent
dd0681ba90
commit
2c83fc72e4
38 files changed
+3527
-68
No files matched your search
@@ -0,0 +1,341 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Sharp-Basic is a modular C# .NET 8 framework for building WPF and C# applications with reusable components. It provides a comprehensive toolkit with emphasis on:
|
||||||
|
- **Modularity**: Independent, reusable libraries that can be used selectively - "引入即用"
|
||||||
|
- **Quick Start**: Zero-configuration setup with sensible defaults - "约定优于配置"
|
||||||
|
- **Desktop Applications**: WPF-based UI toolkit with MVVM infrastructure
|
||||||
|
- **Industrial IoT**: PLC integration (Siemens) and serial port communication (optional)
|
||||||
|
- **Configuration as Code**: Multi-format (JSON/XML/INI/TOML) with hot-reload
|
||||||
|
- **Functional Programming**: Result<T> pattern for railway-oriented error handling
|
||||||
|
- **Opinionated**: Strong technology choices (EF Core, Serilog, etc.) for team consistency
|
||||||
|
|
||||||
|
**Design Philosophy**: "固执己见的框架" - Makes strong choices about technology stack to enable fast development. If you can't use EF Core, this framework may not be for you.
|
||||||
|
|
||||||
|
**Language**: All comments and documentation are in Chinese (中文).
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
**New to Sharp?** See `QUICKSTART.md` for zero-configuration setup guide.
|
||||||
|
|
||||||
|
### Minimal WPF Application (3 lines)
|
||||||
|
```csharp
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSharpEssentials(); // Config + Logging + Cache
|
||||||
|
services.AddSharpWpf(); // WPF Services (Dialog, Dispatcher)
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Database (1 more line)
|
||||||
|
```csharp
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Build the entire solution
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build specific project
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet build Client/Client.csproj # WPF demo application
|
||||||
|
dotnet build Common/Common.csproj # Core utilities
|
||||||
|
dotnet build Config/Config.csproj # Configuration management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet test Sharp.sln # All tests
|
||||||
|
dotnet test ../../test/Common.Test # Specific test project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the WPF Client application
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet run --project Client/Client.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean build artifacts
|
||||||
|
```bash
|
||||||
|
cd src/Sharp
|
||||||
|
dotnet clean Sharp.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Sharp/ # All source projects
|
||||||
|
├── Common/ # Core foundation (Result<T>, Global container, utilities)
|
||||||
|
├── Config/ # Multi-format configuration with hot-reload
|
||||||
|
├── Sharp.Infrastructure/ # Infrastructure abstractions
|
||||||
|
├── Wpf/ # WPF UI toolkit (MVVM, dialogs, converters) - NO UI library dependency
|
||||||
|
├── Wpf.HandyControl/ # HandyControl UI library integration (optional)
|
||||||
|
├── Data/ # EF Core wrapper with repository pattern
|
||||||
|
├── Repository/ # Repository implementations
|
||||||
|
├── Log/ # Serilog-based structured logging
|
||||||
|
├── Cache/ # Memory cache (planned)
|
||||||
|
├── EventBus/ # Event pub/sub (planned)
|
||||||
|
├── NetClient/ # HTTP client with Polly resilience
|
||||||
|
├── NetServer/ # Lightweight HTTP server with middleware
|
||||||
|
├── Ports/ # Serial port abstraction (RS232/RS485)
|
||||||
|
├── Plc/ # PLC abstraction interface
|
||||||
|
├── Plc.Siemens/ # Siemens S7 PLC implementation (S7.NET+)
|
||||||
|
├── Client/ # WPF demo application
|
||||||
|
└── Server/ # WPF server example
|
||||||
|
|
||||||
|
test/ # MSTest test projects
|
||||||
|
├── Common.Test/
|
||||||
|
├── Config.Test/
|
||||||
|
├── Data.Test/
|
||||||
|
├── Log.Test/
|
||||||
|
├── NetClient.Test/
|
||||||
|
└── NetServer.Test/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
#### 1. Result<T> Pattern (Railway-Oriented Programming)
|
||||||
|
- Eliminates exceptions as control flow
|
||||||
|
- Provides chainable operations: `Map`, `Bind`, `Match`, `Where`, `OnSuccess`, `OnFailure`
|
||||||
|
- Synchronous and async wrappers: `Result.Try()`, `Result.TryAsync()`
|
||||||
|
- Example usage:
|
||||||
|
```csharp
|
||||||
|
Result.Try(() => int.Parse(input))
|
||||||
|
.Map(x => x * 2)
|
||||||
|
.Where(v => v > 100, "Value too small")
|
||||||
|
.OnSuccess(v => Console.WriteLine(v))
|
||||||
|
.OnFailure((ex, msg) => logger.LogError(msg));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Dependency Injection (Hybrid Approach)
|
||||||
|
- **Primary**: Microsoft.Extensions.DependencyInjection for standard DI
|
||||||
|
- **Fallback**: Global service locator (Common.Global) for legacy/convenience
|
||||||
|
- Services registered in `DependencyInjection` folders within each module
|
||||||
|
- Client app startup (App.xaml.cs) orchestrates all service registration
|
||||||
|
|
||||||
|
#### 3. Configuration Management
|
||||||
|
- Multi-source configuration builder supporting JSON/XML/INI/TOML
|
||||||
|
- Hot-reload with change notifications
|
||||||
|
- Strong typing with DataAnnotations validation
|
||||||
|
- IOptions<T> pattern integration
|
||||||
|
- Environment variable overrides with optional prefix
|
||||||
|
|
||||||
|
#### 4. Repository Pattern with EF Core
|
||||||
|
- Generic `IRepository<TEntity>` abstraction
|
||||||
|
- Built-in entity types: `Entity`, `AuditableEntity`, `ISoftDeletable`
|
||||||
|
- Auto-audit tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
|
||||||
|
- Multi-database support: SQL Server, SQLite, PostgreSQL, MySQL, In-Memory
|
||||||
|
|
||||||
|
#### 5. MVVM for WPF
|
||||||
|
- `ViewModelBase` with INotifyPropertyChanged and IsBusy tracking
|
||||||
|
- `RelayCommand` and `AsyncCommand` implementations
|
||||||
|
- Services: `IDialogService`, `IFileDialogService`, `IDispatcherService`
|
||||||
|
- Converters: BoolToVisibility, InverseBooleanConverter, EnumToDescriptionConverter
|
||||||
|
- Global exception handling for UI/Task/AppDomain exceptions
|
||||||
|
|
||||||
|
#### 6. Middleware Pattern
|
||||||
|
- Custom HTTP server (NetServer) with ASP.NET Core-style pipeline
|
||||||
|
- Middleware: Authentication, CORS, Rate Limiting, Caching, Logging, Exception Handling
|
||||||
|
- Attribute-based routing with `RouteAttribute`
|
||||||
|
- Custom `IMiddleware` interface for extensibility
|
||||||
|
|
||||||
|
## Module Dependencies
|
||||||
|
|
||||||
|
**Core Layer** (no external dependencies):
|
||||||
|
- Common → (no dependencies)
|
||||||
|
|
||||||
|
**Configuration Layer**:
|
||||||
|
- Config → Microsoft.Extensions.Configuration.*, Tomlyn (TOML support)
|
||||||
|
|
||||||
|
**Data Layer**:
|
||||||
|
- Data → EF Core, Common
|
||||||
|
- Repository → Data
|
||||||
|
|
||||||
|
**Network Layer**:
|
||||||
|
- NetClient → Polly, Microsoft.Extensions.Http, Common
|
||||||
|
- NetServer → Common, Log
|
||||||
|
|
||||||
|
**WPF Layer**:
|
||||||
|
- Wpf → (no UI library dependency - pure MVVM)
|
||||||
|
- Wpf.HandyControl → Wpf, HandyControl 3.5.0 (optional UI library integration)
|
||||||
|
- Client → Wpf, Config, NetClient, NetServer, Log, Plc.Siemens, Ports, HandyControl
|
||||||
|
|
||||||
|
**Industrial IoT**:
|
||||||
|
- Ports → (serial port communication)
|
||||||
|
- Plc → (abstraction only)
|
||||||
|
- Plc.Siemens → Plc, S7.NET+ library
|
||||||
|
|
||||||
|
**Logging**:
|
||||||
|
- Log → Serilog, Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
## Common Development Workflows
|
||||||
|
|
||||||
|
### Quick Start for New Projects (Recommended)
|
||||||
|
1. Install NuGet packages (or reference projects)
|
||||||
|
2. Create `appsettings.json` in project root
|
||||||
|
3. Use Quick Start extensions in `App.xaml.cs`:
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
// Option 1: Minimal (Config + Log + Cache + WPF)
|
||||||
|
services.AddSharpEssentials();
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// Option 2: With Database
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
services.AddSharpEssentials(config);
|
||||||
|
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||||
|
services.AddSharpWpf();
|
||||||
|
|
||||||
|
// Register ViewModels
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
// Show main window...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. See `QUICKSTART.md` for complete examples
|
||||||
|
|
||||||
|
### Adding a new module/library
|
||||||
|
1. Create project under `src/Sharp/`
|
||||||
|
2. Target `net8.0` (or `net8.0-windows` for WPF)
|
||||||
|
3. Enable nullable reference types: `<Nullable>enable</Nullable>`
|
||||||
|
4. Create `DependencyInjection` folder for service registration extensions with `AddSharpXxx()` methods
|
||||||
|
5. Create comprehensive `README.md` with Quick Start section
|
||||||
|
6. Add test project under `test/` with MSTest
|
||||||
|
7. Add to Sharp.sln: `dotnet sln add <ProjectPath>`
|
||||||
|
|
||||||
|
### Working with Result<T>
|
||||||
|
- Use `Result.Try()` to wrap exception-prone code
|
||||||
|
- Chain operations with `Map()` for transformations
|
||||||
|
- Use `Bind()` for operations that return Result<T>
|
||||||
|
- Extract values with `GetOrDefault()`, `GetOrThrow()`, or `Match()`
|
||||||
|
- Always handle both success and failure cases
|
||||||
|
|
||||||
|
### Using the Global container
|
||||||
|
- Register singletons: `Global.RegisterSingleton<T>(instance)` or `Global.RegisterSingleton<T>(() => new T())`
|
||||||
|
- Register transients: `Global.RegisterTransient<T>(() => new T())`
|
||||||
|
- Subscribe to changes: `Global.Subscribe<T>(e => { /* handle change */ })`
|
||||||
|
- Retrieve services: `Global.Get<T>()`
|
||||||
|
- **Note**: Prefer standard DI (IServiceCollection) for new code; Global is for legacy/compatibility
|
||||||
|
|
||||||
|
### Configuration workflow (Quick Start)
|
||||||
|
1. Create `appsettings.json` in project root
|
||||||
|
2. Use default configuration:
|
||||||
|
```csharp
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
```
|
||||||
|
3. Configuration automatically loaded with:
|
||||||
|
- Hot-reload enabled
|
||||||
|
- Environment variables (prefix: `APP_`)
|
||||||
|
- Environment-specific files (appsettings.Development.json, etc.)
|
||||||
|
|
||||||
|
### Configuration workflow (Manual)
|
||||||
|
1. Add config files to project root (appsettings.json, etc.)
|
||||||
|
2. Create strongly-typed options class with DataAnnotations
|
||||||
|
3. Build ConfigManager: `ConfigBuilder.BuildFromFiles(...)`
|
||||||
|
4. Register with DI: `services.AddSingleton(cfg)` and `services.AddSingleton(cfg.Configuration)`
|
||||||
|
5. Use hot-reload callbacks: `cfg.OnReload(() => { /* refresh */ })`
|
||||||
|
|
||||||
|
### WPF Client startup flow (Modern Quick Start)
|
||||||
|
```csharp
|
||||||
|
protected override void OnStartup(StartupEventArgs e) {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||||
|
|
||||||
|
services.AddSharpEssentials(config);
|
||||||
|
services.AddSharpWpf();
|
||||||
|
services.AddViewModel<MainViewModel>();
|
||||||
|
services.AddTransient<MainWindow>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
provider.GetRequiredService<MainWindow>().Show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with PLC (Siemens)
|
||||||
|
- Interface: `IPlcService` in Plc project
|
||||||
|
- Implementation: Plc.Siemens using S7.NET+ library
|
||||||
|
- Supports: S7-1200, S7-1500, S7-300, S7-400
|
||||||
|
- Register with DI: `services.AddSiemensPlc(config.GetSection("Plc:Siemens"))`
|
||||||
|
- Methods: Read/Write memory areas, data blocks, Subscribe for monitoring
|
||||||
|
|
||||||
|
### Serial Port Communication
|
||||||
|
- Service: `ISerialPortService` in Ports module
|
||||||
|
- Discovery: `SerialPortDiscovery` for auto-detection
|
||||||
|
- Event-based data reception: `DataReceivedEventArgs`
|
||||||
|
- Configured with: baud rate, parity, stop bits, data bits
|
||||||
|
|
||||||
|
### Using Cache (When Implemented)
|
||||||
|
```csharp
|
||||||
|
// Dictionary data caching
|
||||||
|
var categories = await _cache.GetOrSetAsync(
|
||||||
|
"categories:all",
|
||||||
|
() => _repository.GetAllAsync(),
|
||||||
|
TimeSpan.FromMinutes(30)
|
||||||
|
);
|
||||||
|
|
||||||
|
// API response caching
|
||||||
|
var weather = await _cache.GetOrSetAsync(
|
||||||
|
$"weather:{city}",
|
||||||
|
() => _apiClient.GetWeatherAsync(city),
|
||||||
|
TimeSpan.FromMinutes(5)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using EventBus (When Implemented)
|
||||||
|
```csharp
|
||||||
|
// Publish
|
||||||
|
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
_eventBus.Subscribe<UserLoginEvent>(e => {
|
||||||
|
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Target Framework**: .NET 8 (net8.0 / net8.0-windows)
|
||||||
|
- **Nullable**: Enabled (`#nullable enable`)
|
||||||
|
- **Implicit Usings**: Enabled
|
||||||
|
- **Namespace Convention**: `Sharp.*` (Common will migrate to `Sharp.Common`)
|
||||||
|
- **Language**: All comments, docs, and strings in Chinese
|
||||||
|
- **Backwards Compatibility**: Maintain for minor versions; breaking changes only in major versions
|
||||||
|
- **Testing**: MSTest with parallel execution enabled
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **"引入即用" (Plug and Play)**: Every module should work with minimal configuration
|
||||||
|
2. **"约定优于配置" (Convention over Configuration)**: Sensible defaults for everything
|
||||||
|
3. **"固执己见" (Opinionated)**: Strong technology choices (EF Core, Serilog, HandyControl)
|
||||||
|
- If you can't use our stack, this framework is not for you
|
||||||
|
- Designed for team/personal consistency, not maximum flexibility
|
||||||
|
4. **Quick Start First**: Every module README must have a "Quick Start" section showing 1-line setup
|
||||||
|
5. **No Abstraction Overkill**: Don't create interfaces just for flexibility - commit to implementations
|
||||||
|
|
||||||
|
## Module README Template
|
||||||
|
|
||||||
|
Every module must have a README with:
|
||||||
|
1. **Quick Start** section (5-second setup)
|
||||||
|
2. **Why You Need This** section (problem/solution)
|
||||||
|
3. **Core API** section (main interfaces/classes)
|
||||||
|
4. **Typical Scenarios** section (3-5 real-world examples)
|
||||||
|
5. **Configuration** section (if applicable)
|
||||||
|
|
||||||
|
See `EventBus/README.md` and `Cache/README.md` for examples (even though not implemented yet).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU AGPLv3 - Network services that use this framework must open source modifications. Contact author for alternative commercial licensing.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>Sharp.Cache</AssemblyName>
|
<AssemblyName>Sharp.Cache</AssemblyName>
|
||||||
<RootNamespace>Cache</RootNamespace>
|
<RootNamespace>Sharp.Cache</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Cache;
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.Cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 缓存扩展方法
|
/// 缓存扩展方法
|
||||||
@@ -67,17 +69,16 @@ public static class CacheExtensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RemoveByPattern(this ICacheService cache, string pattern)
|
public static void RemoveByPattern(this ICacheService cache, string pattern)
|
||||||
{
|
{
|
||||||
// 简化实现:只支持前缀匹配(如 "user:*")
|
if (string.IsNullOrWhiteSpace(pattern))
|
||||||
if (!pattern.EndsWith("*"))
|
return;
|
||||||
|
|
||||||
|
if (!pattern.EndsWith("*", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
cache.Remove(pattern);
|
cache.Remove(pattern);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefix = pattern.TrimEnd('*');
|
var prefix = pattern.TrimEnd('*');
|
||||||
|
cache.RemoveByPrefix(prefix);
|
||||||
// 注意:此实现需要 MemoryCacheService 暴露所有键
|
|
||||||
// 简化版本:只能移除单个键
|
|
||||||
// 完整实现需要使用反射或其他方式访问内部键集合
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Cache.DependencyInjection;
|
namespace Sharp.Cache.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 缓存服务注册扩展
|
/// 缓存服务注册扩展
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
namespace Cache;
|
namespace Sharp.Cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 缓存服务接口
|
/// 缓存服务接口
|
||||||
@@ -37,6 +38,16 @@ public interface ICacheService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void Clear();
|
void Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按前缀批量移除
|
||||||
|
/// </summary>
|
||||||
|
void RemoveByPrefix(string prefix);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已缓存键集合(只读)
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyCollection<string> Keys { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 检查缓存是否存在
|
/// 检查缓存是否存在
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,11 +57,11 @@ public interface ICacheService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基于 MemoryCache 的缓存服务实现
|
/// 基于 MemoryCache 的缓存服务实现
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MemoryCacheService : ICacheService
|
public class MemoryCacheService : ICacheService
|
||||||
{
|
{
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear)
|
private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear)
|
||||||
private readonly object _lockObj = new();
|
private readonly object _lockObj = new();
|
||||||
|
|
||||||
public MemoryCacheService(IMemoryCache cache)
|
public MemoryCacheService(IMemoryCache cache)
|
||||||
{
|
{
|
||||||
@@ -123,20 +134,50 @@ public class MemoryCacheService : ICacheService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
|
||||||
lock (_lockObj)
|
|
||||||
{
|
{
|
||||||
foreach (var key in _keys.ToList())
|
lock (_lockObj)
|
||||||
{
|
{
|
||||||
_cache.Remove(key);
|
foreach (var key in _keys.ToList())
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
}
|
||||||
|
_keys.Clear();
|
||||||
}
|
}
|
||||||
_keys.Clear();
|
}
|
||||||
|
|
||||||
|
public void RemoveByPrefix(string prefix)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(prefix))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> targets;
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
targets = _keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in targets)
|
||||||
|
{
|
||||||
|
Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
return _keys.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(string key)
|
||||||
|
{
|
||||||
|
return _cache.TryGetValue(key, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Exists(string key)
|
|
||||||
{
|
|
||||||
return _cache.TryGetValue(key, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,8 @@ using Sharp.Wpf.Services;
|
|||||||
using Sharp.Wpf.Abstractions;
|
using Sharp.Wpf.Abstractions;
|
||||||
using Plc.Siemens.DependencyInjection;
|
using Plc.Siemens.DependencyInjection;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sharp.Process.DependencyInjection;
|
||||||
|
using Sharp.Python.DependencyInjection;
|
||||||
|
|
||||||
namespace Client
|
namespace Client
|
||||||
{
|
{
|
||||||
@@ -108,6 +110,10 @@ namespace Client
|
|||||||
options.MonitorInterval = 200;
|
options.MonitorInterval = 200;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process & Python
|
||||||
|
services.AddSharpProcess();
|
||||||
|
services.AddSharpPython();
|
||||||
|
|
||||||
// WPF app services
|
// WPF app services
|
||||||
services.AddSingleton<IDialogService, DialogService>();
|
services.AddSingleton<IDialogService, DialogService>();
|
||||||
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
||||||
@@ -118,10 +124,12 @@ namespace Client
|
|||||||
services.AddTransient<LoginViewModel>();
|
services.AddTransient<LoginViewModel>();
|
||||||
services.AddTransient<MainViewModel>();
|
services.AddTransient<MainViewModel>();
|
||||||
services.AddTransient<PlcViewModel>();
|
services.AddTransient<PlcViewModel>();
|
||||||
|
services.AddTransient<ProcessTestViewModel>();
|
||||||
|
|
||||||
services.AddTransient<LoginWindow>();
|
services.AddTransient<LoginWindow>();
|
||||||
services.AddTransient<MainWindow>();
|
services.AddTransient<MainWindow>();
|
||||||
services.AddTransient<PlcWindow>();
|
services.AddTransient<PlcWindow>();
|
||||||
|
services.AddTransient<ProcessTestWindow>();
|
||||||
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\Plc.Siemens\Plc.Siemens.csproj" />
|
<ProjectReference Include="..\Plc.Siemens\Plc.Siemens.csproj" />
|
||||||
<ProjectReference Include="..\Ports\Ports.csproj" />
|
<ProjectReference Include="..\Ports\Ports.csproj" />
|
||||||
|
<ProjectReference Include="..\Process\Process.csproj" />
|
||||||
|
<ProjectReference Include="..\Python\Python.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -49,6 +49,11 @@
|
|||||||
Padding="18,6"
|
Padding="18,6"
|
||||||
Margin="0,0,12,0"
|
Margin="0,0,12,0"
|
||||||
Command="{Binding ThrowCommand}" />
|
Command="{Binding ThrowCommand}" />
|
||||||
|
<Button Content="Process/Python 测试"
|
||||||
|
Padding="18,6"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Style="{StaticResource ButtonWarning}"
|
||||||
|
Command="{Binding OpenProcessTestCommand}" />
|
||||||
<Button Content="串口调试"
|
<Button Content="串口调试"
|
||||||
Padding="18,6"
|
Padding="18,6"
|
||||||
Margin="0,0,12,0"
|
Margin="0,0,12,0"
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<hc:GlowWindow x:Class="Client.ProcessTestWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Process / Python 测试工具" Height="700" Width="1000"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<hc:GlowWindow.Resources>
|
||||||
|
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
||||||
|
</hc:GlowWindow.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="300"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 主测试区域 -->
|
||||||
|
<TabControl Grid.Column="0" Margin="0,0,10,0">
|
||||||
|
<!-- Process 测试标签页 -->
|
||||||
|
<TabItem Header="Process 测试">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="15">
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<Border Background="{DynamicResource PrimaryBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||||
|
<TextBlock Text="{Binding ProcessStatus}" Foreground="White" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 命令输入 -->
|
||||||
|
<GroupBox Header="命令配置" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="命令:" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding Command}" Margin="10,0,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="参数:" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Arguments}" Margin="10,10,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="超时(秒):" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||||
|
<hc:NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding TimeoutSeconds}"
|
||||||
|
Minimum="1" Maximum="600" Margin="10,10,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 快捷按钮 -->
|
||||||
|
<GroupBox Header="快速测试" Margin="0,0,0,15">
|
||||||
|
<WrapPanel Margin="10" Orientation="Horizontal">
|
||||||
|
<Button Content="执行命令" Command="{Binding RunProcessCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||||
|
<Button Content="Shell 命令" Command="{Binding RunShellCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8"/>
|
||||||
|
<Button Content="测试 dotnet" Command="{Binding TestDotnetCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
<Button Content="测试 Git" Command="{Binding TestGitCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 输出 -->
|
||||||
|
<GroupBox Header="执行输出">
|
||||||
|
<TextBox Text="{Binding ProcessOutput, Mode=OneWay}"
|
||||||
|
Height="300"
|
||||||
|
IsReadOnly="True"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="10"/>
|
||||||
|
</GroupBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Python 测试标签页 -->
|
||||||
|
<TabItem Header="Python 测试">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="15">
|
||||||
|
<!-- Python 环境信息 -->
|
||||||
|
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="Python 环境" FontSize="16" FontWeight="Bold"/>
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<TextBlock Text="路径: " FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding PythonPath}" Margin="5,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5,0,0">
|
||||||
|
<TextBlock Text="版本: " FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding PythonVersion}" Margin="5,0,20,0"/>
|
||||||
|
<Button Content="重新检测" Command="{Binding DetectPythonCommand}" Padding="10,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<Border Background="{DynamicResource SuccessBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||||
|
<TextBlock Text="{Binding PythonStatus}" Foreground="White" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 代码执行 -->
|
||||||
|
<GroupBox Header="Python 代码执行" Margin="0,0,0,15">
|
||||||
|
<StackPanel Margin="10">
|
||||||
|
<TextBlock Text="Python 代码:" Margin="0,0,0,5"/>
|
||||||
|
<TextBox Text="{Binding PythonCode}"
|
||||||
|
Height="80"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
<WrapPanel>
|
||||||
|
<Button Content="执行代码" Command="{Binding RunPythonCodeCommand}"
|
||||||
|
Margin="0,0,10,0" Padding="15,8" Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
<Button Content="测试 JSON 输出" Command="{Binding TestPythonJsonCommand}"
|
||||||
|
Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 脚本执行 -->
|
||||||
|
<GroupBox Header="Python 脚本执行">
|
||||||
|
<StackPanel Margin="10">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Text="{Binding PythonScriptPath}"
|
||||||
|
hc:InfoElement.Placeholder="脚本文件路径"/>
|
||||||
|
<Button Grid.Column="1" Content="创建测试脚本"
|
||||||
|
Command="{Binding CreateTestScriptCommand}"
|
||||||
|
Margin="5,0,0,0" Padding="10,4"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="参数 (格式: key1=value1,key2=value2):" Margin="0,10,0,5"/>
|
||||||
|
<TextBox Text="{Binding PythonArgs}"
|
||||||
|
hc:InfoElement.Placeholder="例如: name=Sharp,count=3"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<Button Content="执行脚本" Command="{Binding RunPythonScriptCommand}"
|
||||||
|
Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- Python 输出 -->
|
||||||
|
<GroupBox Header="Python 输出" Margin="0,15,0,0">
|
||||||
|
<TextBox Text="{Binding PythonOutput, Mode=OneWay}"
|
||||||
|
Height="200"
|
||||||
|
IsReadOnly="True"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="10"/>
|
||||||
|
</GroupBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
<!-- 日志面板 -->
|
||||||
|
<Border Grid.Column="1" Background="{DynamicResource RegionBrush}"
|
||||||
|
Padding="15" CornerRadius="8">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="执行日志" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1" ItemsSource="{Binding LogMessages}"
|
||||||
|
FontFamily="Consolas" FontSize="10"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||||
|
<ListBox.ItemContainerStyle>
|
||||||
|
<Style TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="2"/>
|
||||||
|
</Style>
|
||||||
|
</ListBox.ItemContainerStyle>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<Button Grid.Row="2" Content="清空日志" Command="{Binding ClearLogCommand}"
|
||||||
|
Margin="0,10,0,0" HorizontalAlignment="Stretch"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</hc:GlowWindow>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Client.ViewModels;
|
||||||
|
|
||||||
|
namespace Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ProcessTestWindow.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class ProcessTestWindow
|
||||||
|
{
|
||||||
|
public ProcessTestWindow(ProcessTestViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,13 @@ public sealed class MainViewModel : ViewModelBase
|
|||||||
win.Show();
|
win.Show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
OpenProcessTestCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
var sp = Global.Get<IServiceProvider>();
|
||||||
|
var win = sp.GetRequiredService<ProcessTestWindow>();
|
||||||
|
win.Show();
|
||||||
|
});
|
||||||
|
|
||||||
// 1. 获取 PLC 服务
|
// 1. 获取 PLC 服务
|
||||||
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
|
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
|
||||||
|
|
||||||
@@ -109,6 +116,7 @@ public sealed class MainViewModel : ViewModelBase
|
|||||||
public ICommand ThrowCommand { get; }
|
public ICommand ThrowCommand { get; }
|
||||||
public ICommand OpenPlcToolCommand { get; }
|
public ICommand OpenPlcToolCommand { get; }
|
||||||
public ICommand OpenSerialPortToolCommand { get; }
|
public ICommand OpenSerialPortToolCommand { get; }
|
||||||
|
public ICommand OpenProcessTestCommand { get; }
|
||||||
public ICommand StartMachineCommand { get; }
|
public ICommand StartMachineCommand { get; }
|
||||||
public ICommand StopMachineCommand { get; }
|
public ICommand StopMachineCommand { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,510 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Sharp.Wpf.Mvvm;
|
||||||
|
using Sharp.Wpf.Commands;
|
||||||
|
using Sharp.Wpf.Abstractions;
|
||||||
|
using Sharp.Process;
|
||||||
|
using Sharp.Python;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Client.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process/Python 测试工具 ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessTestViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IProcessExecutor _processExecutor;
|
||||||
|
private readonly IPythonExecutor _pythonExecutor;
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
private readonly ILogger<ProcessTestViewModel> _logger;
|
||||||
|
|
||||||
|
// Process 测试
|
||||||
|
private string _command = "cmd";
|
||||||
|
private string _arguments = "/c echo Hello from Process!";
|
||||||
|
private int _timeoutSeconds = 30;
|
||||||
|
private string _processOutput = "输出将显示在这里...";
|
||||||
|
private string _processStatus = "就绪";
|
||||||
|
|
||||||
|
// Python 测试
|
||||||
|
private string _pythonPath = "(自动检测)";
|
||||||
|
private string _pythonVersion = "未检测";
|
||||||
|
private string _pythonCode = "print('Hello from Python!')";
|
||||||
|
private string _pythonScriptPath = "";
|
||||||
|
private string _pythonArgs = "";
|
||||||
|
private string _pythonOutput = "输出将显示在这里...";
|
||||||
|
private string _pythonStatus = "就绪";
|
||||||
|
|
||||||
|
// 日志
|
||||||
|
public ObservableCollection<string> LogMessages { get; } = new();
|
||||||
|
|
||||||
|
public ProcessTestViewModel(
|
||||||
|
IProcessExecutor processExecutor,
|
||||||
|
IPythonExecutor pythonExecutor,
|
||||||
|
IDispatcher dispatcher,
|
||||||
|
ILogger<ProcessTestViewModel> logger)
|
||||||
|
{
|
||||||
|
_processExecutor = processExecutor;
|
||||||
|
_pythonExecutor = pythonExecutor;
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Process 命令
|
||||||
|
RunProcessCommand = new AsyncCommand(RunProcessAsync);
|
||||||
|
RunShellCommand = new AsyncCommand(RunShellAsync);
|
||||||
|
TestDotnetCommand = new AsyncCommand(TestDotnetAsync);
|
||||||
|
TestGitCommand = new AsyncCommand(TestGitAsync);
|
||||||
|
|
||||||
|
// Python 命令
|
||||||
|
DetectPythonCommand = new AsyncCommand(DetectPythonAsync);
|
||||||
|
RunPythonCodeCommand = new AsyncCommand(RunPythonCodeAsync);
|
||||||
|
RunPythonScriptCommand = new AsyncCommand(RunPythonScriptAsync);
|
||||||
|
TestPythonJsonCommand = new AsyncCommand(TestPythonJsonAsync);
|
||||||
|
CreateTestScriptCommand = new RelayCommand(CreateTestScript);
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
ClearLogCommand = new RelayCommand(() => LogMessages.Clear());
|
||||||
|
|
||||||
|
// 自动检测 Python
|
||||||
|
_ = DetectPythonAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public string Command
|
||||||
|
{
|
||||||
|
get => _command;
|
||||||
|
set => SetProperty(ref _command, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Arguments
|
||||||
|
{
|
||||||
|
get => _arguments;
|
||||||
|
set => SetProperty(ref _arguments, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TimeoutSeconds
|
||||||
|
{
|
||||||
|
get => _timeoutSeconds;
|
||||||
|
set => SetProperty(ref _timeoutSeconds, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProcessOutput
|
||||||
|
{
|
||||||
|
get => _processOutput;
|
||||||
|
set => SetProperty(ref _processOutput, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProcessStatus
|
||||||
|
{
|
||||||
|
get => _processStatus;
|
||||||
|
set => SetProperty(ref _processStatus, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonPath
|
||||||
|
{
|
||||||
|
get => _pythonPath;
|
||||||
|
set => SetProperty(ref _pythonPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonVersion
|
||||||
|
{
|
||||||
|
get => _pythonVersion;
|
||||||
|
set => SetProperty(ref _pythonVersion, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonCode
|
||||||
|
{
|
||||||
|
get => _pythonCode;
|
||||||
|
set => SetProperty(ref _pythonCode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonScriptPath
|
||||||
|
{
|
||||||
|
get => _pythonScriptPath;
|
||||||
|
set => SetProperty(ref _pythonScriptPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonArgs
|
||||||
|
{
|
||||||
|
get => _pythonArgs;
|
||||||
|
set => SetProperty(ref _pythonArgs, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonOutput
|
||||||
|
{
|
||||||
|
get => _pythonOutput;
|
||||||
|
set => SetProperty(ref _pythonOutput, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonStatus
|
||||||
|
{
|
||||||
|
get => _pythonStatus;
|
||||||
|
set => SetProperty(ref _pythonStatus, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
public IAsyncCommand RunProcessCommand { get; }
|
||||||
|
public IAsyncCommand RunShellCommand { get; }
|
||||||
|
public IAsyncCommand TestDotnetCommand { get; }
|
||||||
|
public IAsyncCommand TestGitCommand { get; }
|
||||||
|
|
||||||
|
public IAsyncCommand DetectPythonCommand { get; }
|
||||||
|
public IAsyncCommand RunPythonCodeCommand { get; }
|
||||||
|
public IAsyncCommand RunPythonScriptCommand { get; }
|
||||||
|
public IAsyncCommand TestPythonJsonCommand { get; }
|
||||||
|
public ICommand CreateTestScriptCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ClearLogCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Process 测试
|
||||||
|
|
||||||
|
private async Task RunProcessAsync()
|
||||||
|
{
|
||||||
|
ProcessStatus = "执行中...";
|
||||||
|
ProcessOutput = "";
|
||||||
|
AddLog($"[Process] 执行命令: {Command} {Arguments}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = Command,
|
||||||
|
Arguments = Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||||
|
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ProcessOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(ProcessOutput));
|
||||||
|
AddLog($"[输出] {line}");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
OnErrorReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ProcessOutput += $"[错误] {line}\n";
|
||||||
|
OnPropertyChanged(nameof(ProcessOutput));
|
||||||
|
AddLog($"[错误] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _processExecutor.ExecuteAsync(options);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✓ 完成 (退出码: {result.Value.ExitCode}, 耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||||
|
ProcessOutput += $"\n=== 执行成功 ===\n退出码: {result.Value.ExitCode}\n耗时: {result.Value.Duration.TotalSeconds:F2}s";
|
||||||
|
AddLog($"[Process] 执行成功 (耗时: {result.Value.Duration.TotalMilliseconds}ms)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 失败: {result.ErrorMessage}";
|
||||||
|
ProcessOutput += $"\n=== 执行失败 ===\n{result.ErrorMessage}";
|
||||||
|
AddLog($"[Process] 执行失败: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
ProcessOutput = $"异常: {ex.Message}\n{ex.StackTrace}";
|
||||||
|
AddLog($"[Process] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunShellAsync()
|
||||||
|
{
|
||||||
|
ProcessStatus = "执行中...";
|
||||||
|
ProcessOutput = "";
|
||||||
|
|
||||||
|
var shellCmd = OperatingSystem.IsWindows()
|
||||||
|
? "dir | findstr .cs"
|
||||||
|
: "ls | grep .cs";
|
||||||
|
|
||||||
|
AddLog($"[Shell] 执行: {shellCmd}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await IProcessExecutor.RunShellAsync(shellCmd);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ProcessOutput = result.Value.StandardOutput;
|
||||||
|
ProcessStatus = $"✓ Shell 命令执行成功";
|
||||||
|
AddLog($"[Shell] 成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
ProcessStatus = $"✗ Shell 命令失败";
|
||||||
|
AddLog($"[Shell] 失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
ProcessOutput = ex.Message;
|
||||||
|
AddLog($"[Shell] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestDotnetAsync()
|
||||||
|
{
|
||||||
|
Command = "dotnet";
|
||||||
|
Arguments = "--version";
|
||||||
|
await RunProcessAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestGitAsync()
|
||||||
|
{
|
||||||
|
Command = "git";
|
||||||
|
Arguments = "--version";
|
||||||
|
await RunProcessAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Python 测试
|
||||||
|
|
||||||
|
private async Task DetectPythonAsync()
|
||||||
|
{
|
||||||
|
PythonStatus = "检测中...";
|
||||||
|
AddLog("[Python] 正在检测 Python 环境...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var env = PythonEnvironment.Discover();
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
PythonPath = env.PythonPath;
|
||||||
|
PythonVersion = env.Version ?? "未知版本";
|
||||||
|
PythonStatus = "✓ Python 环境已检测";
|
||||||
|
AddLog($"[Python] 找到 Python: {env.PythonPath}");
|
||||||
|
AddLog($"[Python] 版本: {env.Version}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonPath = "未找到";
|
||||||
|
PythonVersion = "未安装";
|
||||||
|
PythonStatus = "✗ 未找到 Python";
|
||||||
|
AddLog("[Python] 未找到 Python 环境,请安装 Python 或配置 PATH");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 检测失败: {ex.Message}";
|
||||||
|
AddLog($"[Python] 检测异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPythonCodeAsync()
|
||||||
|
{
|
||||||
|
PythonStatus = "执行中...";
|
||||||
|
PythonOutput = "";
|
||||||
|
AddLog($"[Python] 执行代码: {PythonCode}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _pythonExecutor.RunCodeAsync(PythonCode, new PythonOptions
|
||||||
|
{
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Python 输出] {line}");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
OnErrorReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += $"[错误] {line}\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Python 错误] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PythonOutput))
|
||||||
|
{
|
||||||
|
PythonOutput = result.Value.StandardOutput;
|
||||||
|
}
|
||||||
|
PythonStatus = $"✓ 执行成功 (耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||||
|
AddLog($"[Python] 执行成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
PythonStatus = $"✗ 执行失败";
|
||||||
|
AddLog($"[Python] 执行失败: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
PythonOutput = ex.Message;
|
||||||
|
AddLog($"[Python] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPythonScriptAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(PythonScriptPath) || !File.Exists(PythonScriptPath))
|
||||||
|
{
|
||||||
|
PythonStatus = "✗ 请选择有效的脚本文件";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PythonStatus = "执行中...";
|
||||||
|
PythonOutput = "";
|
||||||
|
AddLog($"[Python] 执行脚本: {PythonScriptPath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 解析参数(简单的 key=value 格式)
|
||||||
|
object? args = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(PythonArgs))
|
||||||
|
{
|
||||||
|
var argsDict = new Dictionary<string, string>();
|
||||||
|
var pairs = PythonArgs.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
var kv = pair.Split('=', 2);
|
||||||
|
if (kv.Length == 2)
|
||||||
|
{
|
||||||
|
argsDict[kv[0].Trim()] = kv[1].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = argsDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _pythonExecutor.RunScriptAsync(
|
||||||
|
PythonScriptPath,
|
||||||
|
args,
|
||||||
|
new PythonOptions
|
||||||
|
{
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Script] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PythonOutput))
|
||||||
|
{
|
||||||
|
PythonOutput = result.Value.StandardOutput;
|
||||||
|
}
|
||||||
|
PythonStatus = $"✓ 脚本执行成功";
|
||||||
|
AddLog($"[Python] 脚本执行成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
PythonStatus = $"✗ 脚本执行失败";
|
||||||
|
AddLog($"[Python] 脚本执行失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
PythonOutput = ex.Message;
|
||||||
|
AddLog($"[Python] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestPythonJsonAsync()
|
||||||
|
{
|
||||||
|
PythonCode = @"import json
|
||||||
|
result = {'status': 'ok', 'value': 42, 'message': '测试成功'}
|
||||||
|
print(json.dumps(result, ensure_ascii=False))";
|
||||||
|
await RunPythonCodeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTestScript()
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(Path.GetTempPath(), "sharp_test.py");
|
||||||
|
|
||||||
|
var scriptContent = @"import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--name', default='World')
|
||||||
|
parser.add_argument('--count', type=int, default=1)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 日志输出到 stderr
|
||||||
|
print(f'参数: name={args.name}, count={args.count}', file=sys.stderr)
|
||||||
|
|
||||||
|
# 处理
|
||||||
|
for i in range(args.count):
|
||||||
|
print(f'Hello, {args.name}! ({i+1})', file=sys.stderr)
|
||||||
|
|
||||||
|
# 结果输出到 stdout(JSON)
|
||||||
|
result = {
|
||||||
|
'message': f'Greeted {args.name} {args.count} times',
|
||||||
|
'success': True,
|
||||||
|
'timestamp': str(__import__('datetime').datetime.now())
|
||||||
|
}
|
||||||
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(tempPath, scriptContent);
|
||||||
|
PythonScriptPath = tempPath;
|
||||||
|
PythonArgs = "name=Sharp,count=3";
|
||||||
|
PythonStatus = $"✓ 测试脚本已创建: {tempPath}";
|
||||||
|
AddLog($"[Python] 测试脚本已创建: {tempPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 创建脚本失败: {ex.Message}";
|
||||||
|
AddLog($"[Python] 创建脚本失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void AddLog(string message)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
LogMessages.Add($"[{timestamp}] {message}");
|
||||||
|
|
||||||
|
// 保持日志数量在合理范围
|
||||||
|
while (LogMessages.Count > 200)
|
||||||
|
{
|
||||||
|
LogMessages.RemoveAt(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace EventBus
|
|
||||||
{
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus.DependencyInjection;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddEventBus(this IServiceCollection services, Action<EventBusOptions>? configure = null)
|
||||||
|
{
|
||||||
|
var options = new EventBusOptions();
|
||||||
|
configure?.Invoke(options);
|
||||||
|
services.AddSingleton(options);
|
||||||
|
services.AddSingleton<IEventBus, InMemoryEventBus>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>Sharp.EventBus</AssemblyName>
|
||||||
|
<RootNamespace>Sharp.EventBus</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
public class EventBusOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 事件队列容量,默认 1024;满时 Publish 会等待。
|
||||||
|
/// </summary>
|
||||||
|
public int ChannelCapacity { get; set; } = 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局错误回调。
|
||||||
|
/// </summary>
|
||||||
|
public Action<Exception, object>? OnError { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
internal sealed class EventSubscription : IDisposable
|
||||||
|
{
|
||||||
|
public Type EventType { get; }
|
||||||
|
public int Priority { get; }
|
||||||
|
public Func<object, Task> Handler { get; }
|
||||||
|
public Func<object, bool>? Filter { get; }
|
||||||
|
private readonly Action<EventSubscription> _onDispose;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public EventSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority, Action<EventSubscription> onDispose)
|
||||||
|
{
|
||||||
|
EventType = eventType;
|
||||||
|
Handler = handler;
|
||||||
|
Filter = filter;
|
||||||
|
Priority = priority;
|
||||||
|
_onDispose = onDispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(object evt)
|
||||||
|
{
|
||||||
|
if (evt.GetType() != EventType) return false;
|
||||||
|
return Filter == null || Filter(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_onDispose(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
public interface IEventBus : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 异步发布事件(入队后立即返回)。
|
||||||
|
/// </summary>
|
||||||
|
Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步包装,遇到队列满或取消会抛出。
|
||||||
|
/// </summary>
|
||||||
|
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(同步处理器)。
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅事件(异步处理器)。
|
||||||
|
/// </summary>
|
||||||
|
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace Sharp.EventBus;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 Channel 的进程内事件总线,线程安全,按优先级顺序分发。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InMemoryEventBus : IEventBus
|
||||||
|
{
|
||||||
|
private readonly Channel<object> _channel;
|
||||||
|
private readonly ConcurrentDictionary<Type, List<EventSubscription>> _subscriptions = new();
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
private readonly Task _worker;
|
||||||
|
private readonly EventBusOptions _options;
|
||||||
|
|
||||||
|
public InMemoryEventBus(EventBusOptions? options = null)
|
||||||
|
{
|
||||||
|
_options = options ?? new EventBusOptions();
|
||||||
|
_channel = Channel.CreateBounded<object>(new BoundedChannelOptions(_options.ChannelCapacity)
|
||||||
|
{
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
|
});
|
||||||
|
_worker = Task.Run(ProcessAsync, _cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish<TEvent>(TEvent @event) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_channel.Writer.WriteAsync(@event, _cts.Token).AsTask().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _channel.Writer.WriteAsync(@event, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
Task Wrapper(object evt)
|
||||||
|
{
|
||||||
|
handler((TEvent)evt);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
Task Wrapper(object evt) => handler((TEvent)evt);
|
||||||
|
|
||||||
|
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDisposable AddSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority)
|
||||||
|
{
|
||||||
|
var sub = new EventSubscription(eventType, handler, filter, priority, RemoveSubscription);
|
||||||
|
var list = _subscriptions.GetOrAdd(eventType, _ => new List<EventSubscription>());
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
list.Add(sub);
|
||||||
|
list.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||||
|
}
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSubscription(EventSubscription subscription)
|
||||||
|
{
|
||||||
|
if (_subscriptions.TryGetValue(subscription.EventType, out var list))
|
||||||
|
{
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
list.Remove(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await _channel.Reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (_channel.Reader.TryRead(out var evt))
|
||||||
|
{
|
||||||
|
await DispatchAsync(evt).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 正常关闭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DispatchAsync(object evt)
|
||||||
|
{
|
||||||
|
if (_subscriptions.TryGetValue(evt.GetType(), out var list))
|
||||||
|
{
|
||||||
|
List<EventSubscription> snapshot;
|
||||||
|
lock (list)
|
||||||
|
{
|
||||||
|
snapshot = list.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sub in snapshot)
|
||||||
|
{
|
||||||
|
if (!sub.CanHandle(evt)) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sub.Handler(evt).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_options.OnError?.Invoke(ex, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(InMemoryEventBus));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_channel.Writer.TryComplete();
|
||||||
|
_worker.Wait(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
_cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace NetClient
|
|
||||||
{
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Sharp.NetServer.Abstractions;
|
using Sharp.NetServer.Abstractions;
|
||||||
@@ -45,31 +46,48 @@ public class ResponseCacheMiddleware : IMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 捕获响应
|
// 捕获响应
|
||||||
var originalStream = context.Response.OutputStream;
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
|
|
||||||
// 临时替换输出流
|
|
||||||
var response = context.Response;
|
var response = context.Response;
|
||||||
var originalContentType = response.ContentType;
|
using var captured = new MemoryStream();
|
||||||
var originalStatusCode = response.StatusCode;
|
var originalStream = response.SwapOutputStream(captured);
|
||||||
|
|
||||||
// 执行下一个中间件
|
try
|
||||||
await next();
|
|
||||||
|
|
||||||
// 如果响应成功,缓存结果
|
|
||||||
if (response.StatusCode == 200)
|
|
||||||
{
|
{
|
||||||
// 注意:由于 HttpListenerResponse 的限制,我们无法直接拦截输出流
|
await next();
|
||||||
// 这里提供一个简化的实现,实际使用中可能需要在控制器层面配合
|
|
||||||
var cachedData = new CachedResponse
|
|
||||||
{
|
|
||||||
ContentType = response.ContentType ?? "application/json",
|
|
||||||
StatusCode = response.StatusCode,
|
|
||||||
Headers = new Dictionary<string, string>(),
|
|
||||||
// Body 需要在控制器中通过特殊处理来缓存
|
|
||||||
};
|
|
||||||
|
|
||||||
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
// 只处理捕获后的数据
|
||||||
|
var bodyBytes = captured.ToArray();
|
||||||
|
response.ContentLength = bodyBytes.Length;
|
||||||
|
|
||||||
|
// MISS 标记
|
||||||
|
response.AddHeader("X-Cache", "MISS");
|
||||||
|
|
||||||
|
// 写回真实输出
|
||||||
|
await response.FlushCapturedBodyAsync(captured, context.CancellationToken);
|
||||||
|
|
||||||
|
if (response.StatusCode == 200)
|
||||||
|
{
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
foreach (var key in response.Headers.AllKeys)
|
||||||
|
{
|
||||||
|
if (key is null) continue;
|
||||||
|
headers[key] = response.Headers[key]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedData = new CachedResponse
|
||||||
|
{
|
||||||
|
ContentType = response.ContentType ?? "application/json",
|
||||||
|
StatusCode = response.StatusCode,
|
||||||
|
Headers = headers,
|
||||||
|
Body = bodyBytes
|
||||||
|
};
|
||||||
|
|
||||||
|
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 还原输出流,避免影响后续逻辑
|
||||||
|
response.SwapOutputStream(originalStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +129,9 @@ public class ResponseCacheMiddleware : IMiddleware
|
|||||||
context.Response.AddHeader(header.Key, header.Value);
|
context.Response.AddHeader(header.Key, header.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedResponse.Body != null)
|
var body = cachedResponse.Body ?? Array.Empty<byte>();
|
||||||
{
|
context.Response.ContentLength = body.Length;
|
||||||
context.Response.ContentLength = cachedResponse.Body.Length;
|
await context.Response.OutputStream.WriteAsync(body);
|
||||||
await context.Response.OutputStream.WriteAsync(cachedResponse.Body);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Sharp.NetServer.Models;
|
namespace Sharp.NetServer.Models;
|
||||||
|
|
||||||
@@ -101,10 +102,12 @@ public class HttpRequestWrapper
|
|||||||
public class HttpResponseWrapper
|
public class HttpResponseWrapper
|
||||||
{
|
{
|
||||||
private readonly HttpListenerResponse _response;
|
private readonly HttpListenerResponse _response;
|
||||||
|
private Stream _outputStream;
|
||||||
|
|
||||||
public HttpResponseWrapper(HttpListenerResponse response)
|
public HttpResponseWrapper(HttpListenerResponse response)
|
||||||
{
|
{
|
||||||
_response = response;
|
_response = response;
|
||||||
|
_outputStream = response.OutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int StatusCode
|
public int StatusCode
|
||||||
@@ -125,7 +128,7 @@ public class HttpResponseWrapper
|
|||||||
set => _response.ContentLength64 = value;
|
set => _response.ContentLength64 = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream OutputStream => _response.OutputStream;
|
public Stream OutputStream => _outputStream;
|
||||||
public WebHeaderCollection Headers => _response.Headers;
|
public WebHeaderCollection Headers => _response.Headers;
|
||||||
public CookieCollection Cookies => _response.Cookies;
|
public CookieCollection Cookies => _response.Cookies;
|
||||||
|
|
||||||
@@ -134,6 +137,28 @@ public class HttpResponseWrapper
|
|||||||
_response.AddHeader(name, value);
|
_response.AddHeader(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换输出流(用于中间件包装),返回之前的流。
|
||||||
|
/// </summary>
|
||||||
|
public Stream SwapOutputStream(Stream stream)
|
||||||
|
{
|
||||||
|
var previous = _outputStream;
|
||||||
|
_outputStream = stream;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将捕获的内容写回到底层 HttpListener 输出流。
|
||||||
|
/// </summary>
|
||||||
|
public async Task FlushCapturedBodyAsync(Stream captured, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (captured.CanSeek)
|
||||||
|
{
|
||||||
|
captured.Position = 0;
|
||||||
|
}
|
||||||
|
await captured.CopyToAsync(_response.OutputStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public void Redirect(string url)
|
public void Redirect(string url)
|
||||||
{
|
{
|
||||||
_response.Redirect(url);
|
_response.Redirect(url);
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sharp.Process.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行服务注册扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class ProcessServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp.Process 服务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpProcess(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 注册进程执行器
|
||||||
|
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp.Process 服务(带配置)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configure">配置选项</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpProcess(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<ProcessGlobalOptions> configure)
|
||||||
|
{
|
||||||
|
// 配置全局选项
|
||||||
|
services.Configure(configure);
|
||||||
|
|
||||||
|
// 注册进程执行器
|
||||||
|
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行全局选项
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessGlobalOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 默认超时时长
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否默认启用自动日志记录
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoLogOutputByDefault { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Common.Results;
|
||||||
|
|
||||||
|
namespace Sharp.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行器接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IProcessExecutor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(异步)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
Task<Result<ProcessResult>> ExecuteAsync(
|
||||||
|
ProcessOptions options,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(同步)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
Result<ProcessResult> Execute(ProcessOptions options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速执行命令(静态便捷方法)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">命令</param>
|
||||||
|
/// <param name="arguments">参数</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
static Task<Result<ProcessResult>> RunAsync(
|
||||||
|
string command,
|
||||||
|
string arguments = "",
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var executor = new ProcessExecutor();
|
||||||
|
var options = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = command,
|
||||||
|
Arguments = string.IsNullOrEmpty(arguments)
|
||||||
|
? new List<string>()
|
||||||
|
: arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
|
||||||
|
};
|
||||||
|
return executor.ExecuteAsync(options, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速执行 Shell 命令
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="shellCommand">Shell 命令(支持管道、重定向等)</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
static Task<Result<ProcessResult>> RunShellAsync(
|
||||||
|
string shellCommand,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var executor = new ProcessExecutor();
|
||||||
|
var isWindows = OperatingSystem.IsWindows();
|
||||||
|
var options = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = isWindows ? "cmd.exe" : "/bin/bash",
|
||||||
|
Arguments = isWindows
|
||||||
|
? new List<string> { "/c", shellCommand }
|
||||||
|
: new List<string> { "-c", shellCommand },
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
return executor.ExecuteAsync(options, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sharp.Process</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Common.Results;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Sharp.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行器实现
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessExecutor : IProcessExecutor
|
||||||
|
{
|
||||||
|
private readonly ILogger<ProcessExecutor>? _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数(支持DI)
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutor(ILogger<ProcessExecutor>? logger = null)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(异步)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<ProcessResult>> ExecuteAsync(
|
||||||
|
ProcessOptions options,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Result.TryAsync(async () =>
|
||||||
|
{
|
||||||
|
ValidateOptions(options);
|
||||||
|
|
||||||
|
using var process = CreateProcess(options);
|
||||||
|
|
||||||
|
var outputBuilder = new StringBuilder();
|
||||||
|
var errorBuilder = new StringBuilder();
|
||||||
|
var outputLock = new object();
|
||||||
|
var errorLock = new object();
|
||||||
|
|
||||||
|
// 配置输出接收
|
||||||
|
process.OutputDataReceived += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (e.Data == null) return;
|
||||||
|
|
||||||
|
lock (outputLock)
|
||||||
|
{
|
||||||
|
outputBuilder.AppendLine(e.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发实时输出事件
|
||||||
|
options.OnOutputReceived?.Invoke(e.Data);
|
||||||
|
|
||||||
|
// 自动日志记录
|
||||||
|
if (options.AutoLogOutput && _logger != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[{Command}] {Output}", options.Command, e.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (e.Data == null) return;
|
||||||
|
|
||||||
|
lock (errorLock)
|
||||||
|
{
|
||||||
|
errorBuilder.AppendLine(e.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发实时错误事件
|
||||||
|
options.OnErrorReceived?.Invoke(e.Data);
|
||||||
|
|
||||||
|
// 自动日志记录
|
||||||
|
if (options.AutoLogOutput && _logger != null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Command}] {Error}", options.Command, e.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动进程
|
||||||
|
var startTime = DateTime.Now;
|
||||||
|
var started = process.Start();
|
||||||
|
|
||||||
|
if (!started)
|
||||||
|
throw new InvalidOperationException($"无法启动进程: {options.Command}");
|
||||||
|
|
||||||
|
var processId = process.Id;
|
||||||
|
|
||||||
|
// 设置进程优先级
|
||||||
|
if (options.Priority.HasValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.PriorityClass = options.Priority.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "无法设置进程优先级");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入标准输入
|
||||||
|
if (!string.IsNullOrEmpty(options.StandardInput))
|
||||||
|
{
|
||||||
|
await process.StandardInput.WriteAsync(options.StandardInput);
|
||||||
|
await process.StandardInput.FlushAsync();
|
||||||
|
process.StandardInput.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始异步读取输出
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
// 等待进程退出(带超时和取消支持)
|
||||||
|
var isTimeout = false;
|
||||||
|
var wasKilled = false;
|
||||||
|
|
||||||
|
using var timeoutCts = new CancellationTokenSource(options.Timeout);
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
|
cancellationToken, timeoutCts.Token);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await process.WaitForExitAsync(linkedCts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 检查是超时还是用户取消
|
||||||
|
if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
isTimeout = true;
|
||||||
|
_logger?.LogWarning("进程执行超时: {Command} (超时时长: {Timeout})",
|
||||||
|
options.Command, options.Timeout);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("进程执行被取消: {Command}", options.Command);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制终止进程
|
||||||
|
wasKilled = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!process.HasExited)
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
await process.WaitForExitAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex, "终止进程失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是用户取消,重新抛出异常
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitTime = DateTime.Now;
|
||||||
|
var duration = exitTime - startTime;
|
||||||
|
|
||||||
|
// 确保所有输出都被读取
|
||||||
|
await Task.Delay(100); // 给一点时间让输出事件完成
|
||||||
|
|
||||||
|
var result = new ProcessResult
|
||||||
|
{
|
||||||
|
ExitCode = process.ExitCode,
|
||||||
|
StandardOutput = outputBuilder.ToString().TrimEnd(),
|
||||||
|
StandardError = errorBuilder.ToString().TrimEnd(),
|
||||||
|
Duration = duration,
|
||||||
|
ProcessId = processId,
|
||||||
|
StartTime = startTime,
|
||||||
|
ExitTime = exitTime,
|
||||||
|
IsTimeout = isTimeout,
|
||||||
|
WasKilled = wasKilled,
|
||||||
|
CommandLine = options.BuildCommandLine()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录执行结果
|
||||||
|
if (_logger != null)
|
||||||
|
{
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("进程执行成功: {Command} (耗时: {Duration}ms, 退出码: {ExitCode})",
|
||||||
|
options.Command, duration.TotalMilliseconds, result.ExitCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("进程执行失败: {Command} (耗时: {Duration}ms, 退出码: {ExitCode}, 超时: {IsTimeout}, 被终止: {WasKilled})",
|
||||||
|
options.Command, duration.TotalMilliseconds, result.ExitCode, isTimeout, wasKilled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(同步)
|
||||||
|
/// </summary>
|
||||||
|
public Result<ProcessResult> Execute(ProcessOptions options)
|
||||||
|
{
|
||||||
|
return ExecuteAsync(options).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建进程对象
|
||||||
|
/// </summary>
|
||||||
|
private System.Diagnostics.Process CreateProcess(ProcessOptions options)
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = options.Command,
|
||||||
|
UseShellExecute = options.UseShellExecute,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardInput = !string.IsNullOrEmpty(options.StandardInput),
|
||||||
|
CreateNoWindow = true,
|
||||||
|
StandardOutputEncoding = options.OutputEncoding,
|
||||||
|
StandardErrorEncoding = options.ErrorEncoding
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加参数
|
||||||
|
foreach (var arg in options.Arguments)
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置工作目录
|
||||||
|
if (!string.IsNullOrEmpty(options.WorkingDirectory))
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(options.WorkingDirectory))
|
||||||
|
throw new DirectoryNotFoundException($"工作目录不存在: {options.WorkingDirectory}");
|
||||||
|
|
||||||
|
startInfo.WorkingDirectory = options.WorkingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加环境变量
|
||||||
|
foreach (var (key, value) in options.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
startInfo.Environment[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new System.Diagnostics.Process
|
||||||
|
{
|
||||||
|
StartInfo = startInfo,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证选项
|
||||||
|
/// </summary>
|
||||||
|
private void ValidateOptions(ProcessOptions options)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Command))
|
||||||
|
throw new ArgumentException("命令不能为空", nameof(options));
|
||||||
|
|
||||||
|
if (options.Timeout <= TimeSpan.Zero)
|
||||||
|
throw new ArgumentException("超时时长必须大于0", nameof(options));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Common.Results;
|
||||||
|
|
||||||
|
namespace Sharp.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行器的 Fluent API 封装
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessExecutorBuilder
|
||||||
|
{
|
||||||
|
private readonly IProcessExecutor _executor;
|
||||||
|
private readonly ProcessOptions _options = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder(IProcessExecutor? executor = null)
|
||||||
|
{
|
||||||
|
_executor = executor ?? new ProcessExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置命令和参数
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithCommand(string command, params string[] arguments)
|
||||||
|
{
|
||||||
|
_options.Command = command;
|
||||||
|
_options.Arguments = arguments.ToList();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加单个参数
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithArgument(string argument)
|
||||||
|
{
|
||||||
|
_options.Arguments.Add(argument);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置工作目录
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithWorkingDirectory(string workingDirectory)
|
||||||
|
{
|
||||||
|
_options.WorkingDirectory = workingDirectory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加环境变量
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithEnvironmentVariable(string key, string value)
|
||||||
|
{
|
||||||
|
_options.EnvironmentVariables[key] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量添加环境变量
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithEnvironmentVariables(Dictionary<string, string> variables)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in variables)
|
||||||
|
{
|
||||||
|
_options.EnvironmentVariables[key] = value;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置超时时长
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithTimeout(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
_options.Timeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置标准输入
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithStandardInput(string input)
|
||||||
|
{
|
||||||
|
_options.StandardInput = input;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置输出编码
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithOutputEncoding(Encoding encoding)
|
||||||
|
{
|
||||||
|
_options.OutputEncoding = encoding;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置错误输出编码
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithErrorEncoding(Encoding encoding)
|
||||||
|
{
|
||||||
|
_options.ErrorEncoding = encoding;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 Shell 执行
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithShellExecute(bool useShell = true)
|
||||||
|
{
|
||||||
|
_options.UseShellExecute = useShell;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置进程优先级
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithPriority(ProcessPriorityClass priority)
|
||||||
|
{
|
||||||
|
_options.Priority = priority;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置标准输出接收事件
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder OnOutputReceived(Action<string> handler)
|
||||||
|
{
|
||||||
|
_options.OnOutputReceived = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置标准错误接收事件
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder OnErrorReceived(Action<string> handler)
|
||||||
|
{
|
||||||
|
_options.OnErrorReceived = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用/禁用自动日志记录
|
||||||
|
/// </summary>
|
||||||
|
public ProcessExecutorBuilder WithLogging(bool autoLog = true)
|
||||||
|
{
|
||||||
|
_options.AutoLogOutput = autoLog;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(异步)
|
||||||
|
/// </summary>
|
||||||
|
public Task<Result<ProcessResult>> ExecuteAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _executor.ExecuteAsync(_options, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进程(同步)
|
||||||
|
/// </summary>
|
||||||
|
public Result<ProcessResult> Execute()
|
||||||
|
{
|
||||||
|
return _executor.Execute(_options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行器扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class ProcessExecutorExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建 Fluent API 构建器
|
||||||
|
/// </summary>
|
||||||
|
public static ProcessExecutorBuilder CreateBuilder(this IProcessExecutor executor)
|
||||||
|
{
|
||||||
|
return new ProcessExecutorBuilder(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速执行命令(扩展方法)
|
||||||
|
/// </summary>
|
||||||
|
public static Task<Result<ProcessResult>> RunAsync(
|
||||||
|
this IProcessExecutor executor,
|
||||||
|
string command,
|
||||||
|
params string[] arguments)
|
||||||
|
{
|
||||||
|
return executor.CreateBuilder()
|
||||||
|
.WithCommand(command, arguments)
|
||||||
|
.ExecuteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Sharp.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行选项
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 命令或可执行文件路径
|
||||||
|
/// </summary>
|
||||||
|
public string Command { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 命令参数列表
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Arguments { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作目录
|
||||||
|
/// </summary>
|
||||||
|
public string? WorkingDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 环境变量
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> EnvironmentVariables { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超时时长(默认2分钟)
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准输入内容
|
||||||
|
/// </summary>
|
||||||
|
public string? StandardInput { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 输出编码(默认 UTF-8)
|
||||||
|
/// </summary>
|
||||||
|
public Encoding OutputEncoding { get; set; } = Encoding.UTF8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误输出编码(默认 UTF-8)
|
||||||
|
/// </summary>
|
||||||
|
public Encoding ErrorEncoding { get; set; } = Encoding.UTF8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否使用 Shell 执行(默认 false,更安全)
|
||||||
|
/// </summary>
|
||||||
|
public bool UseShellExecute { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程优先级(可选)
|
||||||
|
/// </summary>
|
||||||
|
public ProcessPriorityClass? Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准输出接收事件
|
||||||
|
/// </summary>
|
||||||
|
public Action<string>? OnOutputReceived { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准错误接收事件
|
||||||
|
/// </summary>
|
||||||
|
public Action<string>? OnErrorReceived { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否自动记录日志到 ILogger
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoLogOutput { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建完整的命令行字符串(用于显示)
|
||||||
|
/// </summary>
|
||||||
|
internal string BuildCommandLine()
|
||||||
|
{
|
||||||
|
if (Arguments.Count == 0)
|
||||||
|
return Command;
|
||||||
|
|
||||||
|
var args = string.Join(" ", Arguments.Select(EscapeArgument));
|
||||||
|
return $"{Command} {args}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转义命令行参数(处理空格)
|
||||||
|
/// </summary>
|
||||||
|
private static string EscapeArgument(string arg)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(arg))
|
||||||
|
return "\"\"";
|
||||||
|
|
||||||
|
if (arg.Contains(' ') || arg.Contains('"'))
|
||||||
|
return $"\"{arg.Replace("\"", "\\\"")}\"";
|
||||||
|
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace Sharp.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程执行结果
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 退出码
|
||||||
|
/// </summary>
|
||||||
|
public int ExitCode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准输出
|
||||||
|
/// </summary>
|
||||||
|
public string StandardOutput { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准错误
|
||||||
|
/// </summary>
|
||||||
|
public string StandardError { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行时长
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Duration { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程ID
|
||||||
|
/// </summary>
|
||||||
|
public int ProcessId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime StartTime { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退出时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ExitTime { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否超时
|
||||||
|
/// </summary>
|
||||||
|
public bool IsTimeout { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否被强制终止
|
||||||
|
/// </summary>
|
||||||
|
public bool WasKilled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否成功执行(退出码为0)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess => ExitCode == 0 && !IsTimeout && !WasKilled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 命令行(用于调试)
|
||||||
|
/// </summary>
|
||||||
|
public string CommandLine { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Sharp.Process
|
||||||
|
|
||||||
|
通用进程执行抽象层,封装 `System.Diagnostics.Process` 的复杂性,提供简洁、强大的 API。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
- ✅ **同步/异步执行** - 支持 `Execute()` 和 `ExecuteAsync()`
|
||||||
|
- ✅ **实时输出流** - `OnOutputReceived` 和 `OnErrorReceived` 事件
|
||||||
|
- ✅ **超时控制** - 可配置超时时长,自动终止超时进程
|
||||||
|
- ✅ **取消支持** - 完整的 `CancellationToken` 集成
|
||||||
|
- ✅ **日志集成** - 自动对接 Serilog/ILogger
|
||||||
|
- ✅ **Result<T> 模式** - 统一的错误处理
|
||||||
|
- ✅ **Fluent API** - 链式调用,直观易用
|
||||||
|
- ✅ **Shell 模式** - 支持管道、重定向等 Shell 特性
|
||||||
|
- ✅ **编码处理** - 可配置输出编码,解决中文乱码
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add reference ../Process/Process.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 注册服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Program.cs 或 App.xaml.cs
|
||||||
|
services.AddSharpProcess();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用
|
||||||
|
|
||||||
|
#### 最简单的调用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 静态方法 - 零配置
|
||||||
|
var result = await IProcessExecutor.RunAsync("python", "script.py");
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine(result.Value.StandardOutput);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实时输出流
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var executor = new ProcessExecutor();
|
||||||
|
var options = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = "dotnet",
|
||||||
|
Arguments = new List<string> { "build" },
|
||||||
|
OnOutputReceived = line => Console.WriteLine($"[输出] {line}"),
|
||||||
|
OnErrorReceived = line => Console.WriteLine($"[错误] {line}"),
|
||||||
|
AutoLogOutput = true // 自动记录到 ILogger
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await executor.ExecuteAsync(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fluent API
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await new ProcessExecutor()
|
||||||
|
.CreateBuilder()
|
||||||
|
.WithCommand("python", "train.py")
|
||||||
|
.WithWorkingDirectory(@"C:\AI\models")
|
||||||
|
.WithTimeout(TimeSpan.FromMinutes(30))
|
||||||
|
.WithEnvironmentVariable("CUDA_VISIBLE_DEVICES", "0,1")
|
||||||
|
.OnOutputReceived(line => _logger.LogInfo(line))
|
||||||
|
.ExecuteAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Shell 命令
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Windows
|
||||||
|
var result = await IProcessExecutor.RunShellAsync("dir | findstr .txt");
|
||||||
|
|
||||||
|
// Linux/macOS
|
||||||
|
var result = await IProcessExecutor.RunShellAsync("ls | grep .log");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 典型场景
|
||||||
|
|
||||||
|
### 场景1: 调用外部工具(ffmpeg)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await new ProcessExecutor()
|
||||||
|
.CreateBuilder()
|
||||||
|
.WithCommand("ffmpeg")
|
||||||
|
.WithArgument("-i").WithArgument("input.mp4")
|
||||||
|
.WithArgument("output.avi")
|
||||||
|
.WithTimeout(TimeSpan.FromHours(1))
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
|
if (result.IsSuccess && result.Value.ExitCode == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("视频转换成功");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2: 长时间任务,实时显示进度
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var progressBar = new ProgressBar();
|
||||||
|
|
||||||
|
var result = await new ProcessExecutor()
|
||||||
|
.CreateBuilder()
|
||||||
|
.WithCommand("npm", "install")
|
||||||
|
.WithWorkingDirectory(@"C:\MyProject")
|
||||||
|
.OnOutputReceived(line => {
|
||||||
|
progressBar.Update(line);
|
||||||
|
_logger.LogInformation(line);
|
||||||
|
})
|
||||||
|
.ExecuteAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3: 超时控制和取消
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await executor.ExecuteAsync(options, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("任务已取消或超时");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4: WPF 中实时显示日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ViewModel
|
||||||
|
public async Task RunBuildAsync()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
|
||||||
|
var result = await _processExecutor.RunAsync("dotnet", "build")
|
||||||
|
.CreateBuilder()
|
||||||
|
.OnOutputReceived(line => {
|
||||||
|
_dispatcher.Invoke(() => {
|
||||||
|
LogOutput += $"{DateTime.Now:HH:mm:ss} {line}\n";
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ProcessResult 详解
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ProcessResult
|
||||||
|
{
|
||||||
|
int ExitCode; // 退出码(0 = 成功)
|
||||||
|
string StandardOutput; // 标准输出
|
||||||
|
string StandardError; // 标准错误
|
||||||
|
TimeSpan Duration; // 执行时长
|
||||||
|
int ProcessId; // 进程ID
|
||||||
|
DateTime StartTime; // 启动时间
|
||||||
|
DateTime ExitTime; // 退出时间
|
||||||
|
bool IsTimeout; // 是否超时
|
||||||
|
bool WasKilled; // 是否被强制终止
|
||||||
|
bool IsSuccess; // 是否成功(ExitCode == 0 && !IsTimeout && !WasKilled)
|
||||||
|
string CommandLine; // 完整命令行(用于调试)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Process": {
|
||||||
|
"DefaultTimeout": 120,
|
||||||
|
"AutoLogOutputByDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddSharpProcess(options => {
|
||||||
|
options.DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||||
|
options.AutoLogOutputByDefault = true;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **Shell 模式安全性** - 默认 `UseShellExecute = false`,更安全。只在需要 Shell 特性时启用。
|
||||||
|
2. **中文乱码** - 使用 `WithOutputEncoding(Encoding.GetEncoding("GBK"))` 处理 Windows 中文。
|
||||||
|
3. **长时间任务** - 调整 `Timeout`,避免被意外终止。
|
||||||
|
4. **进程清理** - 超时或取消时会自动终止进程树(`Kill(entireProcessTree: true)`)。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Common (Result<T>)
|
||||||
|
- Microsoft.Extensions.Logging.Abstractions
|
||||||
|
- Microsoft.Extensions.DependencyInjection.Abstractions
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU AGPLv3
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Sharp.Process.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sharp.Python.DependencyInjection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 服务注册扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class PythonServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp.Python 服务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpPython(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 确保 Process 服务已注册
|
||||||
|
services.AddSharpProcess();
|
||||||
|
|
||||||
|
// 注册 Python 执行器
|
||||||
|
services.AddSingleton<IPythonExecutor, PythonExecutor>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加 Sharp.Python 服务(带配置)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configure">配置选项</param>
|
||||||
|
/// <returns>服务集合</returns>
|
||||||
|
public static IServiceCollection AddSharpPython(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<PythonGlobalOptions> configure)
|
||||||
|
{
|
||||||
|
// 配置全局选项
|
||||||
|
services.Configure(configure);
|
||||||
|
|
||||||
|
// 确保 Process 服务已注册
|
||||||
|
services.AddSharpProcess();
|
||||||
|
|
||||||
|
// 注册 Python 执行器
|
||||||
|
services.AddSingleton<IPythonExecutor, PythonExecutor>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 全局选项
|
||||||
|
/// </summary>
|
||||||
|
public class PythonGlobalOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 默认 Python 解释器路径
|
||||||
|
/// </summary>
|
||||||
|
public string? DefaultPythonPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认虚拟环境路径
|
||||||
|
/// </summary>
|
||||||
|
public string? DefaultVirtualEnvPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认超时时长
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否默认启用自动日志记录
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoLogOutputByDefault { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Common.Results;
|
||||||
|
using Sharp.Process;
|
||||||
|
|
||||||
|
namespace Sharp.Python;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 执行器接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IPythonExecutor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 脚本文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scriptPath">脚本文件路径</param>
|
||||||
|
/// <param name="args">参数对象(将序列化为 JSON 传递)</param>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
Task<Result<ProcessResult>> RunScriptAsync(
|
||||||
|
string scriptPath,
|
||||||
|
object? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 脚本文件并解析 JSON 结果
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">结果类型</typeparam>
|
||||||
|
/// <param name="scriptPath">脚本文件路径</param>
|
||||||
|
/// <param name="args">参数对象(将序列化为 JSON 传递)</param>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>解析后的结果</returns>
|
||||||
|
Task<Result<T>> RunScriptAsync<T>(
|
||||||
|
string scriptPath,
|
||||||
|
object? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 代码片段
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">Python 代码</param>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
Task<Result<ProcessResult>> RunCodeAsync(
|
||||||
|
string code,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 模块
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleName">模块名</param>
|
||||||
|
/// <param name="args">参数</param>
|
||||||
|
/// <param name="options">执行选项</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>执行结果</returns>
|
||||||
|
Task<Result<ProcessResult>> RunModuleAsync(
|
||||||
|
string moduleName,
|
||||||
|
string[]? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Sharp.Python</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Process\Process.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Sharp.Python;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 环境信息
|
||||||
|
/// </summary>
|
||||||
|
public class PythonEnvironment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Python 解释器路径
|
||||||
|
/// </summary>
|
||||||
|
public string PythonPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 版本
|
||||||
|
/// </summary>
|
||||||
|
public string? Version { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 虚拟环境路径(如果有)
|
||||||
|
/// </summary>
|
||||||
|
public string? VirtualEnvPath { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否是虚拟环境
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVirtualEnv => !string.IsNullOrEmpty(VirtualEnvPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动发现系统中的 Python 解释器
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Python 环境信息,如果未找到则返回 null</returns>
|
||||||
|
public static PythonEnvironment? Discover()
|
||||||
|
{
|
||||||
|
// 尝试常见的 Python 命令
|
||||||
|
var pythonCommands = new[] { "python", "python3", "python.exe", "python3.exe" };
|
||||||
|
|
||||||
|
foreach (var command in pythonCommands)
|
||||||
|
{
|
||||||
|
var pythonPath = FindPythonInPath(command);
|
||||||
|
if (pythonPath != null)
|
||||||
|
{
|
||||||
|
return new PythonEnvironment
|
||||||
|
{
|
||||||
|
PythonPath = pythonPath,
|
||||||
|
Version = GetPythonVersion(pythonPath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows: 尝试从注册表查找
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
var registryPaths = FindPythonInRegistry();
|
||||||
|
if (registryPaths.Any())
|
||||||
|
{
|
||||||
|
var pythonPath = registryPaths.First();
|
||||||
|
return new PythonEnvironment
|
||||||
|
{
|
||||||
|
PythonPath = pythonPath,
|
||||||
|
Version = GetPythonVersion(pythonPath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在 PATH 环境变量中查找 Python
|
||||||
|
/// </summary>
|
||||||
|
private static string? FindPythonInPath(string command)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||||||
|
if (string.IsNullOrEmpty(pathEnv))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var paths = pathEnv.Split(Path.PathSeparator);
|
||||||
|
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(path, command);
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略无效路径
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Windows 注册表查找 Python(Windows only)
|
||||||
|
/// </summary>
|
||||||
|
private static List<string> FindPythonInRegistry()
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return results;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试查找常见的安装路径
|
||||||
|
var commonPaths = new[]
|
||||||
|
{
|
||||||
|
@"C:\Python39\python.exe",
|
||||||
|
@"C:\Python310\python.exe",
|
||||||
|
@"C:\Python311\python.exe",
|
||||||
|
@"C:\Python312\python.exe",
|
||||||
|
@"C:\Program Files\Python39\python.exe",
|
||||||
|
@"C:\Program Files\Python310\python.exe",
|
||||||
|
@"C:\Program Files\Python311\python.exe",
|
||||||
|
@"C:\Program Files\Python312\python.exe"
|
||||||
|
};
|
||||||
|
|
||||||
|
results.AddRange(commonPaths.Where(File.Exists));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Python 版本
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetPythonVersion(string pythonPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = pythonPath,
|
||||||
|
Arguments = "--version",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
process.WaitForExit(3000);
|
||||||
|
var output = process.StandardOutput.ReadToEnd();
|
||||||
|
var error = process.StandardError.ReadToEnd();
|
||||||
|
|
||||||
|
var versionText = string.IsNullOrEmpty(output) ? error : output;
|
||||||
|
return versionText.Trim();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从虚拟环境路径创建 Python 环境
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="venvPath">虚拟环境路径</param>
|
||||||
|
/// <returns>Python 环境信息</returns>
|
||||||
|
public static PythonEnvironment FromVirtualEnv(string venvPath)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(venvPath))
|
||||||
|
throw new DirectoryNotFoundException($"虚拟环境目录不存在: {venvPath}");
|
||||||
|
|
||||||
|
// 查找虚拟环境中的 Python 解释器
|
||||||
|
var pythonPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
? Path.Combine(venvPath, "Scripts", "python.exe")
|
||||||
|
: Path.Combine(venvPath, "bin", "python");
|
||||||
|
|
||||||
|
if (!File.Exists(pythonPath))
|
||||||
|
throw new FileNotFoundException($"虚拟环境中未找到 Python 解释器: {pythonPath}");
|
||||||
|
|
||||||
|
return new PythonEnvironment
|
||||||
|
{
|
||||||
|
PythonPath = pythonPath,
|
||||||
|
VirtualEnvPath = venvPath,
|
||||||
|
Version = GetPythonVersion(pythonPath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Common.Results;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Sharp.Process;
|
||||||
|
|
||||||
|
namespace Sharp.Python;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 执行器实现
|
||||||
|
/// </summary>
|
||||||
|
public class PythonExecutor : IPythonExecutor
|
||||||
|
{
|
||||||
|
private readonly IProcessExecutor _processExecutor;
|
||||||
|
private readonly ILogger<PythonExecutor>? _logger;
|
||||||
|
private PythonEnvironment? _cachedEnvironment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
public PythonExecutor(
|
||||||
|
IProcessExecutor processExecutor,
|
||||||
|
ILogger<PythonExecutor>? logger = null)
|
||||||
|
{
|
||||||
|
_processExecutor = processExecutor;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 脚本文件
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<ProcessResult>> RunScriptAsync(
|
||||||
|
string scriptPath,
|
||||||
|
object? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
return Result.Failure<ProcessResult>($"脚本文件不存在: {scriptPath}");
|
||||||
|
|
||||||
|
options ??= new PythonOptions();
|
||||||
|
|
||||||
|
var pythonEnv = GetPythonEnvironment(options);
|
||||||
|
if (pythonEnv == null)
|
||||||
|
return Result.Failure<ProcessResult>("未找到 Python 解释器,请确保已安装 Python 或配置 PythonPath");
|
||||||
|
|
||||||
|
var processOptions = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = pythonEnv.PythonPath,
|
||||||
|
Arguments = BuildScriptArguments(scriptPath, args),
|
||||||
|
WorkingDirectory = options.WorkingDirectory,
|
||||||
|
Timeout = options.Timeout,
|
||||||
|
AutoLogOutput = options.AutoLogOutput,
|
||||||
|
OnOutputReceived = options.OnOutputReceived,
|
||||||
|
OnErrorReceived = options.OnErrorReceived
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加环境变量
|
||||||
|
foreach (var (key, value) in options.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
processOptions.EnvironmentVariables[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用虚拟环境,添加 VIRTUAL_ENV 环境变量
|
||||||
|
if (pythonEnv.IsVirtualEnv)
|
||||||
|
{
|
||||||
|
processOptions.EnvironmentVariables["VIRTUAL_ENV"] = pythonEnv.VirtualEnvPath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogInformation("执行 Python 脚本: {Script} (Python: {PythonPath})",
|
||||||
|
scriptPath, pythonEnv.PythonPath);
|
||||||
|
|
||||||
|
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 脚本文件并解析 JSON 结果
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<T>> RunScriptAsync<T>(
|
||||||
|
string scriptPath,
|
||||||
|
object? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await RunScriptAsync(scriptPath, args, options, cancellationToken);
|
||||||
|
|
||||||
|
return result.Bind(processResult =>
|
||||||
|
{
|
||||||
|
if (!processResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return Result.Failure<T>(
|
||||||
|
$"Python 脚本执行失败 (退出码: {processResult.ExitCode}): {processResult.StandardError}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(processResult.StandardOutput))
|
||||||
|
{
|
||||||
|
return Result.Failure<T>("Python 脚本没有输出");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<T>(processResult.StandardOutput);
|
||||||
|
if (parsed == null)
|
||||||
|
return Result.Failure<T>("无法解析 Python 输出为 JSON");
|
||||||
|
|
||||||
|
return Result.Success(parsed);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
return Result.Failure<T>($"JSON 解析失败: {ex.Message}\n输出内容: {processResult.StandardOutput}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 代码片段
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<ProcessResult>> RunCodeAsync(
|
||||||
|
string code,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
options ??= new PythonOptions();
|
||||||
|
|
||||||
|
var pythonEnv = GetPythonEnvironment(options);
|
||||||
|
if (pythonEnv == null)
|
||||||
|
return Result.Failure<ProcessResult>("未找到 Python 解释器");
|
||||||
|
|
||||||
|
var processOptions = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = pythonEnv.PythonPath,
|
||||||
|
Arguments = new List<string> { "-c", code },
|
||||||
|
WorkingDirectory = options.WorkingDirectory,
|
||||||
|
Timeout = options.Timeout,
|
||||||
|
AutoLogOutput = options.AutoLogOutput,
|
||||||
|
OnOutputReceived = options.OnOutputReceived,
|
||||||
|
OnErrorReceived = options.OnErrorReceived
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加环境变量
|
||||||
|
foreach (var (key, value) in options.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
processOptions.EnvironmentVariables[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogInformation("执行 Python 代码片段 (Python: {PythonPath})", pythonEnv.PythonPath);
|
||||||
|
|
||||||
|
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行 Python 模块
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<ProcessResult>> RunModuleAsync(
|
||||||
|
string moduleName,
|
||||||
|
string[]? args = null,
|
||||||
|
PythonOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
options ??= new PythonOptions();
|
||||||
|
|
||||||
|
var pythonEnv = GetPythonEnvironment(options);
|
||||||
|
if (pythonEnv == null)
|
||||||
|
return Result.Failure<ProcessResult>("未找到 Python 解释器");
|
||||||
|
|
||||||
|
var arguments = new List<string> { "-m", moduleName };
|
||||||
|
if (args != null)
|
||||||
|
{
|
||||||
|
arguments.AddRange(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
var processOptions = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = pythonEnv.PythonPath,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = options.WorkingDirectory,
|
||||||
|
Timeout = options.Timeout,
|
||||||
|
AutoLogOutput = options.AutoLogOutput,
|
||||||
|
OnOutputReceived = options.OnOutputReceived,
|
||||||
|
OnErrorReceived = options.OnErrorReceived
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加环境变量
|
||||||
|
foreach (var (key, value) in options.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
processOptions.EnvironmentVariables[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogInformation("执行 Python 模块: {Module} (Python: {PythonPath})",
|
||||||
|
moduleName, pythonEnv.PythonPath);
|
||||||
|
|
||||||
|
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Python 环境
|
||||||
|
/// </summary>
|
||||||
|
private PythonEnvironment? GetPythonEnvironment(PythonOptions options)
|
||||||
|
{
|
||||||
|
// 优先使用虚拟环境
|
||||||
|
if (!string.IsNullOrEmpty(options.VirtualEnvPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PythonEnvironment.FromVirtualEnv(options.VirtualEnvPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "无法加载虚拟环境: {VenvPath}", options.VirtualEnvPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指定的 Python 路径
|
||||||
|
if (!string.IsNullOrEmpty(options.PythonPath))
|
||||||
|
{
|
||||||
|
if (File.Exists(options.PythonPath))
|
||||||
|
{
|
||||||
|
return new PythonEnvironment { PythonPath = options.PythonPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogWarning("指定的 Python 路径不存在: {PythonPath}", options.PythonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动发现(使用缓存)
|
||||||
|
_cachedEnvironment ??= PythonEnvironment.Discover();
|
||||||
|
|
||||||
|
if (_cachedEnvironment != null)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("使用 Python 解释器: {PythonPath} (版本: {Version})",
|
||||||
|
_cachedEnvironment.PythonPath, _cachedEnvironment.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _cachedEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建脚本参数列表
|
||||||
|
/// </summary>
|
||||||
|
private List<string> BuildScriptArguments(string scriptPath, object? args)
|
||||||
|
{
|
||||||
|
var arguments = new List<string> { scriptPath };
|
||||||
|
|
||||||
|
if (args == null)
|
||||||
|
return arguments;
|
||||||
|
|
||||||
|
// 如果是字典或匿名对象,转换为命令行参数
|
||||||
|
if (args is IDictionary<string, object> dict)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in dict)
|
||||||
|
{
|
||||||
|
arguments.Add($"--{key.Replace("_", "-")}");
|
||||||
|
arguments.Add(value?.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 使用反射提取属性
|
||||||
|
var properties = args.GetType().GetProperties();
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var name = prop.Name.Replace("_", "-").ToLowerInvariant();
|
||||||
|
var value = prop.GetValue(args);
|
||||||
|
|
||||||
|
arguments.Add($"--{name}");
|
||||||
|
arguments.Add(value?.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
namespace Sharp.Python;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python 执行选项
|
||||||
|
/// </summary>
|
||||||
|
public class PythonOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Python 解释器路径(可选,不设置则自动发现)
|
||||||
|
/// </summary>
|
||||||
|
public string? PythonPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 虚拟环境路径(可选)
|
||||||
|
/// </summary>
|
||||||
|
public string? VirtualEnvPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作目录(可选)
|
||||||
|
/// </summary>
|
||||||
|
public string? WorkingDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超时时长(默认5分钟,Python脚本通常比较耗时)
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 环境变量
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> EnvironmentVariables { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否自动记录日志
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoLogOutput { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准输出接收事件
|
||||||
|
/// </summary>
|
||||||
|
public Action<string>? OnOutputReceived { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准错误接收事件
|
||||||
|
/// </summary>
|
||||||
|
public Action<string>? OnErrorReceived { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# Sharp.Python
|
||||||
|
|
||||||
|
Python 脚本执行封装,基于 Sharp.Process 构建,专为 C# 调用 Python 优化。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
- ✅ **解释器自动发现** - 自动查找系统 Python(PATH、注册表)
|
||||||
|
- ✅ **虚拟环境支持** - 自动激活 venv/conda 环境
|
||||||
|
- ✅ **参数序列化** - C# 对象 → Python 命令行参数
|
||||||
|
- ✅ **JSON 结果解析** - Python 输出 → C# 强类型对象
|
||||||
|
- ✅ **三种执行模式** - 脚本文件 / 代码片段 / 模块
|
||||||
|
- ✅ **实时输出流** - 同步 Python 日志到 C#
|
||||||
|
- ✅ **Result<T> 模式** - 统一的错误处理
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add reference ../Python/Python.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 注册服务
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Program.cs 或 App.xaml.cs
|
||||||
|
services.AddSharpPython();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用
|
||||||
|
|
||||||
|
#### 最简单的调用
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var pythonExecutor = serviceProvider.GetRequiredService<IPythonExecutor>();
|
||||||
|
|
||||||
|
// 执行 Python 脚本
|
||||||
|
var result = await pythonExecutor.RunScriptAsync("train.py");
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine(result.Value.StandardOutput);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 传递参数(C# 对象 → Python)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// C# 代码
|
||||||
|
var result = await pythonExecutor.RunScriptAsync("process.py", new {
|
||||||
|
input_file = "data.csv",
|
||||||
|
output_dir = "results",
|
||||||
|
batch_size = 32
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等价于执行:
|
||||||
|
// python process.py --input-file data.csv --output-dir results --batch-size 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 脚本中接收参数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# process.py
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--input-file', required=True)
|
||||||
|
parser.add_argument('--output-dir', required=True)
|
||||||
|
parser.add_argument('--batch-size', type=int, default=16)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(f"处理文件: {args.input_file}")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON 结果解析(Python → C# 对象)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// C# 定义结果类型
|
||||||
|
public class PredictionResult
|
||||||
|
{
|
||||||
|
public double Confidence { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行脚本并解析结果
|
||||||
|
var result = await pythonExecutor.RunScriptAsync<PredictionResult>("predict.py", new {
|
||||||
|
model = "gpt",
|
||||||
|
temperature = 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
var prediction = result.Value;
|
||||||
|
Console.WriteLine($"预测: {prediction.Label} (置信度: {prediction.Confidence})");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 脚本输出 JSON:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# predict.py
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--model')
|
||||||
|
parser.add_argument('--temperature', type=float)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 执行预测...
|
||||||
|
result = {
|
||||||
|
"confidence": 0.95,
|
||||||
|
"label": "cat"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 输出 JSON(必须是标准输出的唯一内容)
|
||||||
|
print(json.dumps(result))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 典型场景
|
||||||
|
|
||||||
|
### 场景1: AI 模型推理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AIService
|
||||||
|
{
|
||||||
|
private readonly IPythonExecutor _python;
|
||||||
|
|
||||||
|
public async Task<string> ClassifyImageAsync(string imagePath)
|
||||||
|
{
|
||||||
|
var result = await _python.RunScriptAsync<ClassificationResult>(
|
||||||
|
@"C:\AI\models\classify.py",
|
||||||
|
new { image_path = imagePath, model = "resnet50" },
|
||||||
|
new PythonOptions
|
||||||
|
{
|
||||||
|
WorkingDirectory = @"C:\AI\models",
|
||||||
|
VirtualEnvPath = @"C:\AI\models\.venv",
|
||||||
|
Timeout = TimeSpan.FromMinutes(5)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.Match(
|
||||||
|
onSuccess: r => r.Label,
|
||||||
|
onFailure: (ex, msg) => $"分类失败: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2: 数据处理脚本
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task ProcessCsvAsync(string inputFile)
|
||||||
|
{
|
||||||
|
var result = await _python.RunScriptAsync("process_csv.py", new {
|
||||||
|
input = inputFile,
|
||||||
|
output = "processed.csv",
|
||||||
|
remove_duplicates = true,
|
||||||
|
normalize = true
|
||||||
|
}, new PythonOptions
|
||||||
|
{
|
||||||
|
OnOutputReceived = line => _logger.LogInformation("[Python] {Line}", line),
|
||||||
|
AutoLogOutput = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
MessageBox.Show("数据处理完成!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3: 执行 Python 代码片段
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 快速执行简单的 Python 代码
|
||||||
|
var result = await _python.RunCodeAsync("print('Hello from Python!')");
|
||||||
|
|
||||||
|
// 计算表达式
|
||||||
|
var calcResult = await _python.RunCodeAsync("print(2 ** 10)");
|
||||||
|
Console.WriteLine(calcResult.Value.StandardOutput); // 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4: 执行 Python 模块
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 执行 pip install
|
||||||
|
var result = await _python.RunModuleAsync("pip", new[] { "install", "numpy" });
|
||||||
|
|
||||||
|
// 执行 http.server
|
||||||
|
var result = await _python.RunModuleAsync("http.server", new[] { "8000" });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景5: WPF 中实时显示 Python 训练日志
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class TrainingViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IPythonExecutor _python;
|
||||||
|
private string _trainingLog = "";
|
||||||
|
|
||||||
|
public string TrainingLog
|
||||||
|
{
|
||||||
|
get => _trainingLog;
|
||||||
|
set => SetProperty(ref _trainingLog, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TrainModelAsync()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
TrainingLog = "";
|
||||||
|
|
||||||
|
var result = await _python.RunScriptAsync("train.py", new {
|
||||||
|
epochs = 100,
|
||||||
|
learning_rate = 0.001
|
||||||
|
}, new PythonOptions
|
||||||
|
{
|
||||||
|
VirtualEnvPath = @"C:\AI\.venv",
|
||||||
|
OnOutputReceived = line => {
|
||||||
|
_dispatcher.Invoke(() => {
|
||||||
|
TrainingLog += $"[{DateTime.Now:HH:mm:ss}] {line}\n";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
IsBusy = false;
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_dialogService.ShowMessage("训练完成!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python 环境管理
|
||||||
|
|
||||||
|
### 自动发现
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var env = PythonEnvironment.Discover();
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"找到 Python: {env.PythonPath}");
|
||||||
|
Console.WriteLine($"版本: {env.Version}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用虚拟环境
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||||
|
{
|
||||||
|
VirtualEnvPath = @"C:\MyProject\.venv" // 自动激活虚拟环境
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 指定 Python 路径
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||||
|
{
|
||||||
|
PythonPath = @"C:\Python311\python.exe"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Python": {
|
||||||
|
"DefaultPythonPath": "C:\\Python311\\python.exe",
|
||||||
|
"DefaultVirtualEnvPath": ".venv",
|
||||||
|
"DefaultTimeout": 300,
|
||||||
|
"AutoLogOutputByDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddSharpPython(options => {
|
||||||
|
options.DefaultPythonPath = @"C:\Python311\python.exe";
|
||||||
|
options.DefaultVirtualEnvPath = ".venv";
|
||||||
|
options.DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||||
|
options.AutoLogOutputByDefault = true;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
### 1. JSON 输出格式
|
||||||
|
|
||||||
|
Python 脚本输出 JSON 时,**必须确保 JSON 是标准输出的唯一内容**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 错误 - 包含额外输出
|
||||||
|
print("开始处理...")
|
||||||
|
result = {"status": "ok"}
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
# ✅ 正确 - 只输出 JSON
|
||||||
|
import sys
|
||||||
|
print("开始处理...", file=sys.stderr) # 日志输出到 stderr
|
||||||
|
result = {"status": "ok"}
|
||||||
|
print(json.dumps(result)) # JSON 输出到 stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 编码问题
|
||||||
|
|
||||||
|
Windows 中文环境可能需要设置编码:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||||
|
{
|
||||||
|
// 继承自 ProcessOptions
|
||||||
|
EnvironmentVariables = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 长时间运行任务
|
||||||
|
|
||||||
|
调整超时时长,避免被意外终止:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _python.RunScriptAsync("long_task.py", null, new PythonOptions
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromHours(2)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 依赖包检查
|
||||||
|
|
||||||
|
执行前确保 Python 环境已安装所需的包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在虚拟环境中安装依赖
|
||||||
|
cd C:\MyProject
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 对比其他方案
|
||||||
|
|
||||||
|
| 方案 | 优势 | 劣势 | 适用场景 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| **Sharp.Python** | 简单、轻量、跨平台 | 单向调用,无内存共享 | 脚本调用、数据处理、AI推理 |
|
||||||
|
| **Python.NET** | 双向互操作,内存共享 | 仅 Windows,环境配置复杂 | 深度集成,频繁交互 |
|
||||||
|
| **IronPython** | 完全嵌入 | Python 2.x,生态落后 | 已不推荐 |
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Sharp.Process
|
||||||
|
- Common (Result<T>)
|
||||||
|
- System.Text.Json
|
||||||
|
- Microsoft.Extensions.Logging.Abstractions
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU AGPLv3
|
||||||
@@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cache", "Cache\Cache.csproj
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.HandyControl", "Wpf.HandyControl\Wpf.HandyControl.csproj", "{F33723EE-5163-40F6-A264-D521A9A7142F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.HandyControl", "Wpf.HandyControl\Wpf.HandyControl.csproj", "{F33723EE-5163-40F6-A264-D521A9A7142F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Process", "Process\Process.csproj", "{5FF9393F-4881-4C35-A6C1-4D20F1117A60}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python", "Python\Python.csproj", "{99FD794A-E5A7-4A55-8193-F30541B3EEF5}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -335,6 +339,30 @@ Global
|
|||||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.Build.0 = Release|Any CPU
|
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.ActiveCfg = Release|Any CPU
|
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.Build.0 = Release|Any CPU
|
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in new issue
Block a user