feat: improve error handling for file closing operations in DocumentService
This commit is contained in:
parent
80051f3759
commit
7397df62e4
@ -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);
|
||||
}
|
||||
|
||||
@ -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,13 +96,18 @@ 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
|
||||
{
|
||||
ExecuteRevitCommand(uiApp =>
|
||||
{
|
||||
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
||||
if (activeDoc == null)
|
||||
@ -106,45 +115,72 @@ namespace RevitHttpControl.Services
|
||||
throw new InvalidOperationException("没有打开的文档");
|
||||
}
|
||||
|
||||
var fileName = string.IsNullOrEmpty(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);
|
||||
var filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : 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 =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
lastException = ex;
|
||||
|
||||
if (attempt >= maxAttempts)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastException != null)
|
||||
{
|
||||
throw lastException;
|
||||
}
|
||||
|
||||
return new CloseFileResponse
|
||||
{
|
||||
Result = "文件关闭成功",
|
||||
FileName = fileName,
|
||||
FilePath = filePath
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
capturedException = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
completed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 等待命令执行完成(最多等待10秒)
|
||||
var timeout = DateTime.Now.AddSeconds(10);
|
||||
while (!completed && DateTime.Now < timeout)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
if (!completed)
|
||||
throw new TimeoutException("关闭文件操作超时");
|
||||
|
||||
if (capturedException != null)
|
||||
throw capturedException;
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
throw;
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user