feat: improve error handling for file closing operations in DocumentService

This commit is contained in:
sladro 2026-03-03 11:28:13 +08:00
parent 80051f3759
commit 7397df62e4
2 changed files with 403 additions and 37 deletions

View File

@ -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<object>
{
Success = false,
Code = 409,
Message = ex.Message,
Data = new { error = "NO_DOCUMENT_OPEN" }
Data = new { error = errorCode }
};
return Request.CreateResponse(HttpStatusCode.Conflict, conflictResponse);
}

View File

@ -11,6 +11,10 @@ namespace RevitHttpControl.Services
/// </summary>
public static class DocumentService
{
private const string ParkingFileName = "parking.rvt";
private const string AutoParkingFileName = "parking.auto.rvt";
private const string AutoParkingDirectoryName = "RevitHttpControl";
/// <summary>
/// 打开文档文件
/// </summary>
@ -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<UIApplication> 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;
}
}
/// <summary>
/// 异步打开文档文件
/// </summary>