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)
|
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>
|
var conflictResponse = new ApiResponse<object>
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
Code = 409,
|
Code = 409,
|
||||||
Message = ex.Message,
|
Message = ex.Message,
|
||||||
Data = new { error = "NO_DOCUMENT_OPEN" }
|
Data = new { error = errorCode }
|
||||||
};
|
};
|
||||||
return Request.CreateResponse(HttpStatusCode.Conflict, conflictResponse);
|
return Request.CreateResponse(HttpStatusCode.Conflict, conflictResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,10 @@ namespace RevitHttpControl.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DocumentService
|
public static class DocumentService
|
||||||
{
|
{
|
||||||
|
private const string ParkingFileName = "parking.rvt";
|
||||||
|
private const string AutoParkingFileName = "parking.auto.rvt";
|
||||||
|
private const string AutoParkingDirectoryName = "RevitHttpControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 打开文档文件
|
/// 打开文档文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -92,58 +96,90 @@ namespace RevitHttpControl.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var completed = false;
|
string targetTitle = null;
|
||||||
Exception capturedException = null;
|
string targetPath = null;
|
||||||
CloseFileResponse response = 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
|
try
|
||||||
{
|
{
|
||||||
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
ExecuteRevitCommand(uiApp =>
|
||||||
if (activeDoc == null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("没有打开的文档");
|
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
||||||
}
|
if (activeDoc == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("没有打开的文档");
|
||||||
|
}
|
||||||
|
|
||||||
var fileName = string.IsNullOrEmpty(activeDoc.PathName)
|
if (string.IsNullOrWhiteSpace(targetTitle) && string.IsNullOrWhiteSpace(targetPath))
|
||||||
? activeDoc.Title
|
{
|
||||||
: Path.GetFileName(activeDoc.PathName);
|
targetTitle = activeDoc.Title;
|
||||||
var filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : activeDoc.PathName;
|
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 = "文件关闭成功",
|
var target = ResolveDocumentForClose(uiApp, targetTitle, targetPath);
|
||||||
FileName = fileName,
|
if (target == null)
|
||||||
FilePath = filePath
|
{
|
||||||
};
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
capturedException = ex;
|
lastException = ex;
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
completed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待命令执行完成(最多等待10秒)
|
if (attempt >= maxAttempts)
|
||||||
var timeout = DateTime.Now.AddSeconds(10);
|
{
|
||||||
while (!completed && DateTime.Now < timeout)
|
throw;
|
||||||
{
|
}
|
||||||
System.Threading.Thread.Sleep(100);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!completed)
|
if (lastException != null)
|
||||||
throw new TimeoutException("关闭文件操作超时");
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
|
||||||
if (capturedException != null)
|
return new CloseFileResponse
|
||||||
throw capturedException;
|
{
|
||||||
|
Result = "文件关闭成功",
|
||||||
return response;
|
FileName = fileName,
|
||||||
|
FilePath = filePath
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (TimeoutException)
|
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>
|
||||||
/// 异步打开文档文件
|
/// 异步打开文档文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user