change - 添加 HoistServer、HikRoBotServer启动项,由 UI 控制启动停止
parent
b54bad1fbb
commit
d065a2b9ba
@ -0,0 +1,104 @@
|
||||
using System.Reflection;
|
||||
using Com.Ctrip.Framework.Apollo;
|
||||
using Sln.Wcs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NeoSmart.Caching.Sqlite;
|
||||
using Newtonsoft.Json;
|
||||
using Sln.Wcs.HikRoBotAdapter.Domain.Dto.GbTaskSubmit;
|
||||
using Sln.Wcs.HikRoBotAdapter.Service;
|
||||
using Sln.Wcs.HikRoBotDispatcher;
|
||||
using Sln.Wcs.Model.Domain;
|
||||
using Sln.Wcs.Repository;
|
||||
using Sln.Wcs.Repository.service;
|
||||
using Sln.Wcs.Serilog;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var basePath = AppContext.BaseDirectory;
|
||||
|
||||
// ---- 配置 ----
|
||||
var localConfig = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
|
||||
var apolloConfigSection = localConfig.GetSection("apollo");
|
||||
builder.Services.AddSingleton<IConfiguration>(localConfig);
|
||||
|
||||
var configProvider = new UpdateableConfigProvider();
|
||||
configProvider.Set("PLC参数", "");
|
||||
|
||||
var apolloConfig = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddApollo(apolloConfigSection)
|
||||
.AddDefault()
|
||||
.Add(configProvider)
|
||||
.Build();
|
||||
|
||||
builder.Services.Remove(new ServiceDescriptor(typeof(IConfiguration), localConfig));
|
||||
builder.Services.AddSingleton<IConfiguration>(apolloConfig);
|
||||
|
||||
// ---- DI 扫描 ----
|
||||
var assemblies = new[]
|
||||
{
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotSdk.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotAdapter.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotDispatcher.dll")),
|
||||
};
|
||||
|
||||
builder.Services.Scan(scan => scan.FromAssemblies(assemblies)
|
||||
.AddClasses().AsImplementedInterfaces().AsSelf().WithTransientLifetime());
|
||||
|
||||
builder.Services.AddSingleton(typeof(SerilogHelper));
|
||||
builder.Services.AddSqlSugarSetup();
|
||||
builder.Services.AddFusionCache()
|
||||
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
|
||||
.WithDistributedCache(new SqliteCache(new SqliteCacheOptions { CachePath = apolloConfig["cachePath"]! }));
|
||||
|
||||
// ---- Swagger ----
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "HikRobot Engine API", Version = "v1" }));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// ---- 启动初始化 ----
|
||||
var sp = app.Services;
|
||||
sp.UseSerilogExtensions();
|
||||
var log = sp.GetRequiredService<SerilogHelper>();
|
||||
log.Info($"HikRoBotServer 启动, 日志:{apolloConfig["logPath"]}");
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "HikRobot Engine v1"); c.RoutePrefix = "swagger"; });
|
||||
|
||||
// ---- HikRoBotDispatchHub.ReciveTask ----
|
||||
var hub = sp.GetRequiredService<HikRoBotDispatchHub>();
|
||||
var api = app.MapGroup("/api");
|
||||
|
||||
// Hub 方法: ReciveTask
|
||||
api.MapPost("/task/receive", (ReceiveTaskRequest req) =>
|
||||
{
|
||||
var detail = new LiveTaskDetail
|
||||
{
|
||||
taskCode = req.TaskCode, deviceType = 1, taskStatus = 1,
|
||||
startPoint = req.StartPoint, endPoint = req.EndPoint
|
||||
};
|
||||
hub.ReciveTask(detail);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" }));
|
||||
|
||||
log.Info("HikRoBotServer 就绪: http://localhost:5200/swagger");
|
||||
app.Run();
|
||||
|
||||
record ReceiveTaskRequest(string TaskCode, string StartPoint, string EndPoint);
|
||||
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sln.Wcs.HikRoBotDispatcher\Sln.Wcs.HikRoBotDispatcher.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Cache\Sln.Wcs.Cache.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Common\Sln.Wcs.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Com.Ctrip.Framework.Apollo" Version="2.11.0" />
|
||||
<PackageReference Include="Com.Ctrip.Framework.Apollo.Configuration" Version="2.11.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Scrutor" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Sln.Wcs;
|
||||
|
||||
internal class UpdateableConfigProvider : ConfigurationProvider, IConfigurationSource
|
||||
{
|
||||
public new void Set(string key, string value)
|
||||
{
|
||||
base.Set(key, value);
|
||||
OnReload();
|
||||
}
|
||||
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
{
|
||||
"exclude": [
|
||||
"**/bin",
|
||||
"**/bower_components",
|
||||
"**/jspm_packages",
|
||||
"**/node_modules",
|
||||
"**/obj",
|
||||
"**/platforms"
|
||||
],
|
||||
"apollo": {
|
||||
"AppId": "SlnWcs",
|
||||
"Env": "DEV",
|
||||
"MetaServer": "http://119.45.202.115:4320", //配置网址 4310
|
||||
"ConfigServer": [
|
||||
"http://119.45.202.115:4320"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
using System.Reflection;
|
||||
using Com.Ctrip.Framework.Apollo;
|
||||
using Sln.Wcs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NeoSmart.Caching.Sqlite;
|
||||
using Newtonsoft.Json;
|
||||
using Sln.Wcs.HoistDispatcher;
|
||||
using Sln.Wcs.Model.Domain;
|
||||
using Sln.Wcs.Plc;
|
||||
using Sln.Wcs.Repository;
|
||||
using Sln.Wcs.Repository.service;
|
||||
using Sln.Wcs.Serilog;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var basePath = AppContext.BaseDirectory;
|
||||
|
||||
// ---- 配置 ----
|
||||
var localConfig = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
|
||||
var apolloConfigSection = localConfig.GetSection("apollo");
|
||||
builder.Services.AddSingleton<IConfiguration>(localConfig);
|
||||
|
||||
var configProvider = new UpdateableConfigProvider();
|
||||
configProvider.Set("PLC参数", "");
|
||||
|
||||
var apolloConfig = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddApollo(apolloConfigSection)
|
||||
.AddDefault()
|
||||
.Add(configProvider)
|
||||
.Build();
|
||||
|
||||
builder.Services.Remove(new ServiceDescriptor(typeof(IConfiguration), localConfig));
|
||||
builder.Services.AddSingleton<IConfiguration>(apolloConfig);
|
||||
|
||||
// ---- DI 扫描 ----
|
||||
var assemblies = new[]
|
||||
{
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Plc.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistSdk.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistAdapter.dll")),
|
||||
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistDispatcher.dll")),
|
||||
};
|
||||
|
||||
builder.Services.Scan(scan => scan.FromAssemblies(assemblies)
|
||||
.AddClasses().AsImplementedInterfaces().AsSelf().WithTransientLifetime());
|
||||
|
||||
builder.Services.AddSingleton(typeof(SerilogHelper));
|
||||
builder.Services.AddSqlSugarSetup();
|
||||
builder.Services.AddPlcSetup();
|
||||
builder.Services.AddFusionCache()
|
||||
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
|
||||
.WithDistributedCache(new SqliteCache(new SqliteCacheOptions { CachePath = apolloConfig["cachePath"]! }));
|
||||
|
||||
// ---- Swagger ----
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Hoist Engine API", Version = "v1" }));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// ---- 启动初始化 ----
|
||||
var sp = app.Services;
|
||||
sp.UseSerilogExtensions();
|
||||
var log = sp.GetRequiredService<SerilogHelper>();
|
||||
log.Info($"HoistServer 启动, 日志:{apolloConfig["logPath"]}");
|
||||
|
||||
var deviceInfoService = sp.GetRequiredService<IBaseDeviceInfoService>();
|
||||
var list = deviceInfoService.GetDeviceInfos(x => x.isFlag == 1).ToList();
|
||||
configProvider.Set("PLC参数", JsonConvert.SerializeObject(list));
|
||||
log.Info($"PLC参数已加载, 共{list.Count}台设备");
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Hoist Engine v1"); c.RoutePrefix = "swagger"; });
|
||||
|
||||
// ---- HoistDispatchHub 四个方法 ----
|
||||
var hub = sp.GetRequiredService<HoistDispatchHub>();
|
||||
var api = app.MapGroup("/api");
|
||||
|
||||
// 1. ReceivePallet
|
||||
api.MapPost("/hoist/receive-pallet", (ReceivePalletRequest req, IBaseDeviceInfoService devSvc) =>
|
||||
{
|
||||
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
|
||||
.FirstOrDefault(d => d.hostCode == req.HostCode);
|
||||
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
|
||||
|
||||
var detail = new LiveTaskDetail
|
||||
{
|
||||
taskCode = req.TaskCode, palletBarcode = req.PalletBarcode,
|
||||
startPoint = req.StartPoint, endPoint = req.EndPoint,
|
||||
deviceType = 2, taskStatus = 1
|
||||
};
|
||||
hub.ReceivePallet(detail, device);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
// 2. TaskRun
|
||||
api.MapPost("/hoist/task-run", (TaskRunRequest req, IBaseDeviceInfoService devSvc) =>
|
||||
{
|
||||
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
|
||||
.FirstOrDefault(d => d.hostCode == req.HostCode);
|
||||
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
|
||||
device.deviceSerialNo = req.SerialNo;
|
||||
hub.TaskRun(device);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
// 3. TaskDispatch
|
||||
api.MapPost("/task/dispatch", (TaskDispatchRequest req, IBaseDeviceInfoService devSvc) =>
|
||||
{
|
||||
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
|
||||
.FirstOrDefault(d => d.hostCode == req.HostCode);
|
||||
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
|
||||
|
||||
var detail = new LiveTaskDetail
|
||||
{
|
||||
taskCode = req.TaskCode, startPoint = req.StartPoint, endPoint = req.EndPoint,
|
||||
deviceType = 2, taskStatus = 1
|
||||
};
|
||||
hub.TaskDispatch(device, detail);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
// 4. GetFreeHoistAsync
|
||||
api.MapGet("/hoist/free", async (string hostCode) =>
|
||||
{
|
||||
var d = await hub.GetFreeHoistAsync(hostCode);
|
||||
return Results.Ok(new { found = d != null, d?.deviceCode, d?.deviceName });
|
||||
});
|
||||
|
||||
api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" }));
|
||||
|
||||
log.Info("HoistServer 就绪: http://localhost:5100/swagger");
|
||||
app.Run();
|
||||
|
||||
record ReceivePalletRequest(string HostCode, int SerialNo, string TaskCode, string PalletBarcode, string StartPoint, string EndPoint);
|
||||
record TaskRunRequest(string HostCode, int SerialNo);
|
||||
record TaskDispatchRequest(string HostCode, int SerialNo, string TaskCode, string StartPoint, string EndPoint);
|
||||
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sln.Wcs.HoistDispatcher\Sln.Wcs.HoistDispatcher.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Cache\Sln.Wcs.Cache.csproj" />
|
||||
<ProjectReference Include="..\Sln.Wcs.Common\Sln.Wcs.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Com.Ctrip.Framework.Apollo" Version="2.11.0" />
|
||||
<PackageReference Include="Com.Ctrip.Framework.Apollo.Configuration" Version="2.11.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Scrutor" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Sln.Wcs;
|
||||
|
||||
internal class UpdateableConfigProvider : ConfigurationProvider, IConfigurationSource
|
||||
{
|
||||
public new void Set(string key, string value)
|
||||
{
|
||||
base.Set(key, value);
|
||||
OnReload();
|
||||
}
|
||||
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
{
|
||||
"exclude": [
|
||||
"**/bin",
|
||||
"**/bower_components",
|
||||
"**/jspm_packages",
|
||||
"**/node_modules",
|
||||
"**/obj",
|
||||
"**/platforms"
|
||||
],
|
||||
"apollo": {
|
||||
"AppId": "SlnWcs",
|
||||
"Env": "DEV",
|
||||
"MetaServer": "http://119.45.202.115:4320", //配置网址 4310
|
||||
"ConfigServer": [
|
||||
"http://119.45.202.115:4320"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sln.Wcs.UI.Services;
|
||||
|
||||
public interface IAgvEngineProcessService
|
||||
{
|
||||
bool IsRunning { get; }
|
||||
string StatusText { get; }
|
||||
string? LastError { get; }
|
||||
event Action<string>? OutputReceived;
|
||||
event Action? StateChanged;
|
||||
Task StartAsync();
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class AgvEngineProcessService : IAgvEngineProcessService
|
||||
{
|
||||
private Process? _process;
|
||||
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(3) };
|
||||
private const string BaseUrl = "http://localhost:5200";
|
||||
private readonly StringBuilder _outputBuffer = new();
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public string StatusText => IsRunning ? "运行中" : "已停止";
|
||||
public string? LastError { get; private set; }
|
||||
public event Action<string>? OutputReceived;
|
||||
public event Action? StateChanged;
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
LastError = null;
|
||||
_outputBuffer.Clear();
|
||||
|
||||
var dllPath = Path.Combine(AppContext.BaseDirectory, "Sln.Wcs.HikRoBotServer.dll");
|
||||
if (!File.Exists(dllPath))
|
||||
throw new InvalidOperationException($"找不到 {dllPath}");
|
||||
|
||||
_process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"\"{dllPath}\" --urls {BaseUrl}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
EnableRaisingEvents = true,
|
||||
};
|
||||
|
||||
_process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
|
||||
};
|
||||
_process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
|
||||
};
|
||||
|
||||
_process.Exited += (_, _) =>
|
||||
{
|
||||
IsRunning = false;
|
||||
StateChanged?.Invoke();
|
||||
_process?.Dispose();
|
||||
_process = null;
|
||||
};
|
||||
|
||||
_process.Start();
|
||||
_process.BeginOutputReadLine();
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (_process.HasExited)
|
||||
{
|
||||
LastError = _outputBuffer.ToString();
|
||||
_process.Dispose();
|
||||
_process = null;
|
||||
throw new InvalidOperationException($"进程异常退出:\n{LastError}");
|
||||
}
|
||||
if (await HealthCheckAsync())
|
||||
{
|
||||
IsRunning = true;
|
||||
StateChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LastError = _outputBuffer.ToString();
|
||||
Stop();
|
||||
throw new InvalidOperationException($"启动超时 (20s):\n{LastError}");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_process == null) return;
|
||||
IsRunning = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Kill();
|
||||
_process.WaitForExit(3000);
|
||||
}
|
||||
_process.Dispose();
|
||||
}
|
||||
catch { }
|
||||
finally { _process = null; }
|
||||
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await _http.GetAsync($"{BaseUrl}/api/health");
|
||||
return res.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sln.Wcs.UI.Services;
|
||||
|
||||
public interface IEngineProcessService
|
||||
{
|
||||
bool IsRunning { get; }
|
||||
string StatusText { get; }
|
||||
string? LastError { get; }
|
||||
event Action<string>? OutputReceived;
|
||||
event Action? StateChanged;
|
||||
Task StartAsync();
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class EngineProcessService : IEngineProcessService
|
||||
{
|
||||
private Process? _process;
|
||||
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(3) };
|
||||
private const string BaseUrl = "http://localhost:5100";
|
||||
private readonly StringBuilder _outputBuffer = new();
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public string StatusText => IsRunning ? "运行中" : "已停止";
|
||||
public string? LastError { get; private set; }
|
||||
public event Action<string>? OutputReceived;
|
||||
public event Action? StateChanged;
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
LastError = null;
|
||||
_outputBuffer.Clear();
|
||||
|
||||
var dllPath = Path.Combine(AppContext.BaseDirectory, "Sln.Wcs.HoistServer.dll");
|
||||
if (!File.Exists(dllPath))
|
||||
throw new InvalidOperationException($"找不到 {dllPath}");
|
||||
|
||||
_process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"\"{dllPath}\" --urls {BaseUrl}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
},
|
||||
EnableRaisingEvents = true,
|
||||
};
|
||||
|
||||
_process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
|
||||
};
|
||||
_process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
|
||||
};
|
||||
|
||||
_process.Exited += (_, _) =>
|
||||
{
|
||||
IsRunning = false;
|
||||
StateChanged?.Invoke();
|
||||
_process?.Dispose();
|
||||
_process = null;
|
||||
};
|
||||
|
||||
_process.Start();
|
||||
_process.BeginOutputReadLine();
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
// 等待健康检查,最多 20 秒
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (_process.HasExited)
|
||||
{
|
||||
LastError = _outputBuffer.ToString();
|
||||
_process.Dispose();
|
||||
_process = null;
|
||||
throw new InvalidOperationException($"进程异常退出:\n{LastError}");
|
||||
}
|
||||
if (await HealthCheckAsync())
|
||||
{
|
||||
IsRunning = true;
|
||||
StateChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LastError = _outputBuffer.ToString();
|
||||
Stop();
|
||||
throw new InvalidOperationException($"启动超时 (20s):\n{LastError}");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_process == null) return;
|
||||
IsRunning = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Kill();
|
||||
_process.WaitForExit(3000);
|
||||
}
|
||||
_process.Dispose();
|
||||
}
|
||||
catch { }
|
||||
finally { _process = null; }
|
||||
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private async Task<bool> HealthCheckAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await _http.GetAsync($"{BaseUrl}/api/health");
|
||||
return res.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue