/
spruttechnology
/
cam-api-examples
Обзор
Документация
Войти
/
spruttechnology
/
cam-api-examples
Код
Запросы
3
Задачи
Пакеты
0
Релизы
1
Аналитика
v19/develop
Project/ExtensionUtilityExportInformationNet/viewer/main/Program.cs
162 строки
6 KB
renal
Добавление отображения информации в WEB - версии в расширении экспорта информации
20 июл 2026, 08:34
20 июл 2026, 08:34
c8b8e63
Код
Авторство
О чём код?
using System.Diagnostics; using ProjectInfoViewer; var options = CliOptions.Parse(args); // Single-instance: если вьюер уже работает — передаём ему новый json и открываем его URL. var existingUrl = await RunningInstance.TryActivateExistingAsync(options.JsonPath); if (existingUrl is not null) { Console.WriteLine($"Viewer is already running: {existingUrl} — reusing it."); if (options.OpenBrowser) OpenInDefaultBrowser(existingUrl); return; } var state = new ViewerState(options.JsonPath); var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory, WebRootPath = Path.Combine(AppContext.BaseDirectory, "wwwroot"), }); builder.Logging.SetMinimumLevel(LogLevel.Warning); builder.WebHost.UseUrls($"http://127.0.0.1:{options.Port}"); var app = builder.Build(); // Любой запрос (heartbeat страницы, статика, API) продлевает жизнь сервера. app.Use(async (context, next) => { state.Touch(); if (context.Request.Path.StartsWithSegments("/api")) context.Response.Headers.CacheControl = "no-store"; await next(); }); app.UseDefaultFiles(); app.UseStaticFiles(); app.MapGet("/api/instance", () => Results.Json(RunningInstance.Describe())); app.MapPost("/api/activate", (ActivateRequest request) => { if (string.IsNullOrWhiteSpace(request.JsonPath)) return Results.BadRequest(new { error = "jsonPath is required" }); state.JsonPath = Path.GetFullPath(request.JsonPath); Console.WriteLine($"Activated with new JSON: {state.JsonPath}"); return Results.Ok(); }); app.MapGet("/api/heartbeat", () => Results.Ok()); app.MapGet("/api/meta", () => { var jsonPath = state.JsonPath; var fileExists = File.Exists(jsonPath); return Results.Json(new { jsonPath, exists = fileExists, size = fileExists ? new FileInfo(jsonPath).Length : 0, modified = fileExists ? File.GetLastWriteTime(jsonPath) : (DateTime?)null, }); }); app.MapGet("/api/project", () => File.Exists(state.JsonPath) ? Results.File(state.JsonPath, "application/json; charset=utf-8") : Results.NotFound(new { error = $"JSON file not found: {state.JsonPath}" })); // Скриншоты проекта: экспортер кладёт их в папку screenshots рядом с json, // в json приходит относительный путь (поле File). app.MapGet("/api/screenshot", (string file) => ServeDataFile(DataRoot(), file, ImageContentType(file))); // Задел под будущие итерации: траектории операций и 3D-модели (.osd/.stl). app.MapGet("/api/toolpath", (string file) => ServeDataFile(DataRoot(), file, "application/json; charset=utf-8")); app.MapGet("/api/model/{name}", (string name) => ServeDataFile(Path.Combine(DataRoot(), "Output"), name, "application/octet-stream")); await app.StartAsync(); var url = app.Urls.First(); RunningInstance.WriteLockFile(url); StartIdleMonitor(app, state, options.IdleTimeoutSeconds); Console.WriteLine($"Project viewer: {url}"); Console.WriteLine($"JSON: {state.JsonPath}"); Console.WriteLine(options.IdleTimeoutSeconds > 0 ? $"Auto-exit after {options.IdleTimeoutSeconds}s without page activity (Ctrl+C to stop now)." : "Press Ctrl+C to stop."); if (options.OpenBrowser) OpenInDefaultBrowser(url); try { await app.WaitForShutdownAsync(); } finally { RunningInstance.DeleteLockFile(); } return; string DataRoot() => Path.GetDirectoryName(state.JsonPath) ?? AppContext.BaseDirectory; // Завершает сервер, когда страница перестала подавать признаки жизни (вкладка закрыта). static void StartIdleMonitor(WebApplication app, ViewerState state, int idleTimeoutSeconds) { if (idleTimeoutSeconds <= 0) return; var timeout = TimeSpan.FromSeconds(idleTimeoutSeconds); _ = Task.Run(async () => { var pollInterval = TimeSpan.FromSeconds(Math.Min(5, Math.Max(1, idleTimeoutSeconds / 4))); while (!app.Lifetime.ApplicationStopping.IsCancellationRequested) { await Task.Delay(pollInterval); if (DateTime.UtcNow - state.LastActivityUtc > timeout) { Console.WriteLine($"No activity for {idleTimeoutSeconds}s — shutting down."); app.Lifetime.StopApplication(); return; } } }); } static string ImageContentType(string fileName) => Path.GetExtension(fileName).ToLowerInvariant() switch { ".png" => "image/png", ".bmp" => "image/bmp", ".gif" => "image/gif", _ => "image/jpeg", }; // Отдаёт файл из каталога данных с защитой от path traversal. static IResult ServeDataFile(string allowedRoot, string relativePath, string contentType) { var fullRoot = Path.GetFullPath(allowedRoot); var fullPath = Path.GetFullPath(Path.Combine(fullRoot, relativePath)); if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase)) return Results.BadRequest(new { error = "Path outside of data root is not allowed" }); return File.Exists(fullPath) ? Results.File(fullPath, contentType) : Results.NotFound(new { error = $"File not found: {relativePath}" }); } static void OpenInDefaultBrowser(string url) { try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch (Exception e) { Console.WriteLine($"Failed to open browser: {e.Message}. Open {url} manually."); } }