From 7397df62e414dae2fb7d308d015907dad997bca7 Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 3 Mar 2026 11:28:13 +0800 Subject: [PATCH] feat: improve error handling for file closing operations in DocumentService --- Controllers/FileController.cs | 14 +- Services/DocumentService.cs | 426 +++++++++++++++++++++++++++++++--- 2 files changed, 403 insertions(+), 37 deletions(-) diff --git a/Controllers/FileController.cs b/Controllers/FileController.cs index 84ac9de..6fd209f 100644 --- a/Controllers/FileController.cs +++ b/Controllers/FileController.cs @@ -213,12 +213,24 @@ namespace RevitHttpControl.Controllers } catch (InvalidOperationException ex) { + var errorCode = "NO_DOCUMENT_OPEN"; + if (ex.Message.Contains("文档切换未生效") || + ex.Message.Contains("无法切换到可关闭目标") || + ex.Message.IndexOf("active document", StringComparison.OrdinalIgnoreCase) >= 0) + { + errorCode = "ACTIVE_DOCUMENT_CANNOT_CLOSE"; + } + else if (ex.Message.Contains("目标文档未找到")) + { + errorCode = "TARGET_NOT_FOUND"; + } + var conflictResponse = new ApiResponse { Success = false, Code = 409, Message = ex.Message, - Data = new { error = "NO_DOCUMENT_OPEN" } + Data = new { error = errorCode } }; return Request.CreateResponse(HttpStatusCode.Conflict, conflictResponse); } diff --git a/Services/DocumentService.cs b/Services/DocumentService.cs index cf3e178..ad681d7 100644 --- a/Services/DocumentService.cs +++ b/Services/DocumentService.cs @@ -11,6 +11,10 @@ namespace RevitHttpControl.Services /// public static class DocumentService { + private const string ParkingFileName = "parking.rvt"; + private const string AutoParkingFileName = "parking.auto.rvt"; + private const string AutoParkingDirectoryName = "RevitHttpControl"; + /// /// 打开文档文件 /// @@ -92,58 +96,90 @@ namespace RevitHttpControl.Services { try { - var completed = false; - Exception capturedException = null; - CloseFileResponse response = null; + string targetTitle = null; + string targetPath = null; + string fileName = null; + string filePath = null; - App.Instance.EnqueueCommand(uiApp => + const int maxAttempts = 3; + Exception lastException = null; + for (var attempt = 1; attempt <= maxAttempts; attempt++) { try { - var activeDoc = uiApp.ActiveUIDocument?.Document; - if (activeDoc == null) + ExecuteRevitCommand(uiApp => { - throw new InvalidOperationException("没有打开的文档"); - } + var activeDoc = uiApp.ActiveUIDocument?.Document; + if (activeDoc == null) + { + throw new InvalidOperationException("没有打开的文档"); + } - var fileName = string.IsNullOrEmpty(activeDoc.PathName) - ? activeDoc.Title - : Path.GetFileName(activeDoc.PathName); - var filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : activeDoc.PathName; + if (string.IsNullOrWhiteSpace(targetTitle) && string.IsNullOrWhiteSpace(targetPath)) + { + targetTitle = activeDoc.Title; + targetPath = activeDoc.PathName; + fileName = string.IsNullOrEmpty(activeDoc.PathName) + ? activeDoc.Title + : Path.GetFileName(activeDoc.PathName); + filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : activeDoc.PathName; + } - activeDoc.Close(false); + var target = ResolveDocumentForClose(uiApp, targetTitle, targetPath); + if (target == null) + { + throw new InvalidOperationException("目标文档未找到或已关闭"); + } - response = new CloseFileResponse + if (!TrySwitchAwayFromDocument(uiApp, target)) + { + throw new InvalidOperationException("无法切换到可关闭目标(请先打开第二个文档,或确保默认模板可用以自动创建 parking 文档)"); + } + }, $"关闭文件操作超时(切换文档阶段,第{attempt}次)"); + + ExecuteRevitCommand(uiApp => { - Result = "文件关闭成功", - FileName = fileName, - FilePath = filePath - }; + var target = ResolveDocumentForClose(uiApp, targetTitle, targetPath); + if (target == null) + { + throw new InvalidOperationException("目标文档未找到或已关闭"); + } + + var activeDoc = uiApp.ActiveUIDocument?.Document; + if (IsSameDocument(activeDoc, targetTitle, targetPath)) + { + throw new InvalidOperationException("文档切换未生效,无法关闭当前激活文档"); + } + + target.Close(false); + TryFinalizeUiAfterClose(uiApp); + }, $"关闭文件操作超时(关闭文档阶段,第{attempt}次)"); + + lastException = null; + break; } catch (Exception ex) { - capturedException = ex; - } - finally - { - completed = true; - } - }); + lastException = ex; - // 等待命令执行完成(最多等待10秒) - var timeout = DateTime.Now.AddSeconds(10); - while (!completed && DateTime.Now < timeout) - { - System.Threading.Thread.Sleep(100); + if (attempt >= maxAttempts) + { + throw; + } + } } - if (!completed) - throw new TimeoutException("关闭文件操作超时"); + if (lastException != null) + { + throw lastException; + } - if (capturedException != null) - throw capturedException; - - return response; + return new CloseFileResponse + { + Result = "文件关闭成功", + FileName = fileName, + FilePath = filePath + }; } catch (TimeoutException) { @@ -159,6 +195,324 @@ namespace RevitHttpControl.Services } } + private static bool TrySwitchAwayFromDocument(UIApplication uiApp, Document sourceDoc) + { + var documents = uiApp.Application.Documents; + var sourceTitle = sourceDoc?.Title; + var sourcePath = sourceDoc?.PathName; + + foreach (Document doc in documents) + { + if (string.IsNullOrWhiteSpace(doc.PathName) || IsSameDocument(doc, sourceTitle, sourcePath)) + { + continue; + } + + try + { + uiApp.OpenAndActivateDocument(doc.PathName); + var activeAfterSwitch = uiApp.ActiveUIDocument?.Document; + if (!IsSameDocument(activeAfterSwitch, sourceTitle, sourcePath)) + { + return true; + } + } + catch + { + // 尝试下一个文档 + } + } + + var parkingPath = ResolveOrCreateParkingPath(uiApp, sourcePath); + if (string.IsNullOrWhiteSpace(parkingPath)) + { + return false; + } + + try + { + uiApp.OpenAndActivateDocument(parkingPath); + var activeAfterSwitch = uiApp.ActiveUIDocument?.Document; + return !IsSameDocument(activeAfterSwitch, sourceTitle, sourcePath); + } + catch + { + return false; + } + } + + private static string ResolveOrCreateParkingPath(UIApplication uiApp, string sourcePath) + { + var packagedParkingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ParkingFileName); + if (IsUsableParkingPath(packagedParkingPath, sourcePath)) + { + return packagedParkingPath; + } + + var autoParkingPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + AutoParkingDirectoryName, + AutoParkingFileName); + + if (IsSamePath(autoParkingPath, sourcePath)) + { + return null; + } + + if (File.Exists(autoParkingPath)) + { + return autoParkingPath; + } + + return TryCreateAutoParkingDocument(uiApp, autoParkingPath) ? autoParkingPath : null; + } + + private static bool IsUsableParkingPath(string parkingPath, string sourcePath) + { + if (string.IsNullOrWhiteSpace(parkingPath) || !File.Exists(parkingPath)) + { + return false; + } + + return !IsSamePath(parkingPath, sourcePath); + } + + private static bool IsSamePath(string pathA, string pathB) + { + if (string.IsNullOrWhiteSpace(pathA) || string.IsNullOrWhiteSpace(pathB)) + { + return false; + } + + try + { + return string.Equals( + Path.GetFullPath(pathA), + Path.GetFullPath(pathB), + StringComparison.OrdinalIgnoreCase); + } + catch + { + return string.Equals(pathA, pathB, StringComparison.OrdinalIgnoreCase); + } + } + + private static bool TryCreateAutoParkingDocument(UIApplication uiApp, string parkingPath) + { + Document parkingDoc = null; + try + { + var revitApp = uiApp.Application; + var defaultTemplate = ResolveProjectTemplatePath(revitApp); + + if (!string.IsNullOrWhiteSpace(defaultTemplate) && File.Exists(defaultTemplate)) + { + parkingDoc = revitApp.NewProjectDocument(defaultTemplate); + } + else + { + return false; + } + + if (parkingDoc == null) + { + return false; + } + + var directory = Path.GetDirectoryName(parkingPath); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var saveAsOptions = new SaveAsOptions + { + OverwriteExistingFile = true + }; + + parkingDoc.SaveAs(parkingPath, saveAsOptions); + return File.Exists(parkingPath); + } + catch + { + return false; + } + finally + { + if (parkingDoc != null) + { + try + { + parkingDoc.Close(false); + } + catch + { + // parking 文档关闭失败不阻塞主流程 + } + } + } + } + + private static string ResolveProjectTemplatePath(Autodesk.Revit.ApplicationServices.Application revitApp) + { + var defaultTemplate = revitApp.DefaultProjectTemplate; + if (!string.IsNullOrWhiteSpace(defaultTemplate) && File.Exists(defaultTemplate)) + { + return defaultTemplate; + } + + var version = revitApp.VersionNumber; + var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + var candidates = new[] + { + Path.Combine(programData, "Autodesk", $"RVT {version}", "Templates"), + Path.Combine(programData, "Autodesk", "RVT", version, "Templates") + }; + + foreach (var dir in candidates) + { + if (!Directory.Exists(dir)) + { + continue; + } + + try + { + var template = Directory.GetFiles(dir, "*.rte", SearchOption.AllDirectories); + if (template.Length > 0) + { + return template[0]; + } + } + catch + { + // ignore and try next candidate + } + } + + return null; + } + + private static void TryFinalizeUiAfterClose(UIApplication uiApp) + { + try + { + var activeDoc = uiApp.ActiveUIDocument?.Document; + var documents = uiApp.Application.Documents; + if (activeDoc == null || documents == null || documents.Size != 1) + { + return; + } + + if (!IsParkingDocument(activeDoc.PathName)) + { + return; + } + + var closeCmd = RevitCommandId.LookupPostableCommandId(PostableCommand.Close); + if (closeCmd != null && uiApp.CanPostCommand(closeCmd)) + { + uiApp.PostCommand(closeCmd); + } + } + catch + { + // UI收尾失败不影响主流程 + } + } + + private static bool IsParkingDocument(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + var packagedParkingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ParkingFileName); + var autoParkingPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + AutoParkingDirectoryName, + AutoParkingFileName); + + return IsSamePath(path, packagedParkingPath) || IsSamePath(path, autoParkingPath); + } + + private static Document ResolveDocumentForClose(UIApplication uiApp, string targetTitle, string targetPath) + { + foreach (Document doc in uiApp.Application.Documents) + { + if (IsSameDocument(doc, targetTitle, targetPath)) + { + return doc; + } + } + + return null; + } + + private static bool IsSameDocument(Document doc, string targetTitle, string targetPath) + { + if (doc == null) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(targetPath) && !string.IsNullOrWhiteSpace(doc.PathName)) + { + try + { + return string.Equals( + Path.GetFullPath(doc.PathName), + Path.GetFullPath(targetPath), + StringComparison.OrdinalIgnoreCase); + } + catch + { + return string.Equals(doc.PathName, targetPath, StringComparison.OrdinalIgnoreCase); + } + } + + return string.Equals(doc.Title, targetTitle, StringComparison.OrdinalIgnoreCase); + } + + private static void ExecuteRevitCommand(Action command, string timeoutMessage) + { + var completed = false; + Exception capturedException = null; + + App.Instance.EnqueueCommand(uiApp => + { + try + { + command(uiApp); + } + catch (Exception ex) + { + capturedException = ex; + } + finally + { + completed = true; + } + }); + + var timeout = DateTime.Now.AddSeconds(10); + while (!completed && DateTime.Now < timeout) + { + System.Threading.Thread.Sleep(100); + } + + if (!completed) + { + throw new TimeoutException(timeoutMessage); + } + + if (capturedException != null) + { + throw capturedException; + } + } + /// /// 异步打开文档文件 ///