Migrate remaining legacy operator workflows
This commit is contained in:
@@ -15,7 +15,9 @@ public sealed partial class MainWindow
|
||||
private readonly SemaphoreSlim _manualOperatorDataGate = new(1, 1);
|
||||
private S5025TrustedManualFileStore? _manualNetSellStore;
|
||||
private ViTrustedManualFileStore? _viManualListStore;
|
||||
private LegacyManualOperatorDataImporter? _legacyManualOperatorDataImporter;
|
||||
private string? _manualOperatorDataInitializationError;
|
||||
private string? _manualOperatorDataInitializationCode;
|
||||
private int _manualOperatorWriteInFlight;
|
||||
private int _manualOperatorWriteQuarantined;
|
||||
private int _manualOperatorBrowserGeneration;
|
||||
@@ -30,6 +32,7 @@ public sealed partial class MainWindow
|
||||
OperatorDataDirectoryEnvironmentVariable);
|
||||
if (!string.IsNullOrWhiteSpace(configuredDirectory))
|
||||
{
|
||||
_manualOperatorDataInitializationCode = "UNSAFE_CONFIGURATION";
|
||||
_manualOperatorDataInitializationError =
|
||||
"외부 수동 데이터 경로는 TOCTOU 안전성을 보장할 수 없어 비활성화되었습니다. 앱 전용 로컬 운영 데이터 폴더를 사용하세요.";
|
||||
return;
|
||||
@@ -39,15 +42,37 @@ public sealed partial class MainWindow
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"OperatorData"));
|
||||
var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(directory);
|
||||
if (!inspection.IsReady)
|
||||
{
|
||||
(_manualOperatorDataInitializationCode,
|
||||
_manualOperatorDataInitializationError) = inspection.State switch
|
||||
{
|
||||
LegacyManualOperatorDataRuntimeState.InterruptedImport => (
|
||||
"IMPORT_INTERRUPTED",
|
||||
"이전 원본 수동 데이터 가져오기가 중단되었습니다. FSell/VI 파일은 열지 않았습니다. 다시 시작하기 전에 진행 표식, 완료 표식, 데이터 파일 4개를 확인하세요."),
|
||||
_ => (
|
||||
"IMPORTED_STATE_INVALID",
|
||||
"원본 수동 데이터 완료 표식과 파일 4개가 검증된 한 세트를 이루지 않습니다. FSell/VI 파일은 열지 않았습니다. 다시 시작하기 전에 앱 전용 운영 데이터 폴더를 확인하세요.")
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
_manualNetSellStore = new S5025TrustedManualFileStore(directory);
|
||||
_viManualListStore = new ViTrustedManualFileStore(directory);
|
||||
_legacyManualOperatorDataImporter = new LegacyManualOperatorDataImporter(
|
||||
directory,
|
||||
new FixedLegacyManualOperatorDataSource());
|
||||
}
|
||||
catch
|
||||
{
|
||||
_legacyManualOperatorDataImporter?.Dispose();
|
||||
_viManualListStore?.Dispose();
|
||||
_manualNetSellStore?.Dispose();
|
||||
_legacyManualOperatorDataImporter = null;
|
||||
_viManualListStore = null;
|
||||
_manualNetSellStore = null;
|
||||
_manualOperatorDataInitializationCode = "INITIALIZATION_FAILED";
|
||||
_manualOperatorDataInitializationError =
|
||||
"수동 송출 데이터 저장소를 초기화하지 못했습니다. 로컬 운영 데이터 설정을 확인하세요.";
|
||||
}
|
||||
@@ -75,6 +100,7 @@ public sealed partial class MainWindow
|
||||
{
|
||||
requestId,
|
||||
available = _manualNetSellStore is not null && _viManualListStore is not null,
|
||||
code = _manualOperatorDataInitializationCode ?? "READY",
|
||||
writeQuarantined = IsManualOperatorWriteQuarantined(),
|
||||
message = IsManualOperatorWriteQuarantined()
|
||||
? GetOperatorMutationQuarantineMessage(
|
||||
@@ -84,6 +110,120 @@ public sealed partial class MainWindow
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleLegacyManualOperatorImportAsync(JsonElement payload)
|
||||
{
|
||||
const string operation = "import-legacy-manual-data";
|
||||
if (payload.ValueKind != JsonValueKind.Object ||
|
||||
!HasOnlyProperties(payload, "requestId") ||
|
||||
!TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId))
|
||||
{
|
||||
PostManualOperatorError(
|
||||
SafeManualOperatorRequestId(payload),
|
||||
operation,
|
||||
"INVALID_REQUEST",
|
||||
"올바르지 않은 원본 수동 데이터 가져오기 요청입니다.",
|
||||
retryable: false,
|
||||
outcomeUnknown: false);
|
||||
return;
|
||||
}
|
||||
|
||||
var importer = _legacyManualOperatorDataImporter;
|
||||
if (importer is null || _manualNetSellStore is null || _viManualListStore is null)
|
||||
{
|
||||
PostManualOperatorUnavailable(requestId, operation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsManualOperatorWriteQuarantined())
|
||||
{
|
||||
PostManualOperatorQuarantine(requestId, operation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await TryEnterManualMutationGatesAsync(requestId, operation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
|
||||
Interlocked.Exchange(ref _manualOperatorWriteInFlight, 1);
|
||||
try
|
||||
{
|
||||
var result = await importer.ImportAsync(_lifetimeCancellation.Token);
|
||||
if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
|
||||
{
|
||||
QuarantineManualOperatorWrites(
|
||||
"원본 수동 데이터 가져오기 중 화면 상관관계가 사라졌습니다. 가져오기 marker와 4개 파일을 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
PostMessage("legacy-manual-operator-import-result", new
|
||||
{
|
||||
requestId,
|
||||
markerVersion = result.MarkerVersion,
|
||||
sourceSha256 = result.SourceSha256,
|
||||
importedAt = result.ImportedAt,
|
||||
netSellRowCount = result.NetSellRowCount,
|
||||
viItemCount = result.ViItemCount
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
|
||||
{
|
||||
// A pre-commit cancellation creates nothing. Once commit begins the
|
||||
// importer completes or rolls back without observing cancellation.
|
||||
}
|
||||
catch (LegacyManualOperatorImportException exception)
|
||||
{
|
||||
if (exception.OutcomeUnknown)
|
||||
{
|
||||
QuarantineManualOperatorWrites(
|
||||
"원본 수동 데이터 가져오기를 안전하게 되돌렸는지 확인할 수 없습니다. 4개 파일과 marker를 직접 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
|
||||
PostManualOperatorQuarantine(requestId, operation);
|
||||
return;
|
||||
}
|
||||
|
||||
var (code, message) = exception.Failure switch
|
||||
{
|
||||
LegacyManualOperatorImportFailure.SourceUnavailable => (
|
||||
"SOURCE_UNAVAILABLE",
|
||||
"원본 프로젝트의 FSell/VI 데이터 4개 파일을 찾을 수 없습니다."),
|
||||
LegacyManualOperatorImportFailure.SourceInvalid => (
|
||||
"SOURCE_INVALID",
|
||||
"원본 FSell/VI 데이터가 CP949 원본 형식 또는 크기 제한을 충족하지 않습니다."),
|
||||
LegacyManualOperatorImportFailure.AlreadyImported => (
|
||||
"ALREADY_IMPORTED",
|
||||
"이 앱 전용 폴더에는 이미 완료 marker가 있어 다시 가져오지 않습니다."),
|
||||
LegacyManualOperatorImportFailure.DestinationNotEmpty => (
|
||||
"DESTINATION_NOT_EMPTY",
|
||||
"앱 전용 폴더에 기존 FSell/VI 데이터가 있어 아무 파일도 덮어쓰지 않았습니다."),
|
||||
LegacyManualOperatorImportFailure.InterruptedImport => (
|
||||
"IMPORT_INTERRUPTED",
|
||||
"이전 가져오기의 진행 표식이 남아 있습니다. 완료 표식과 파일 4개를 확인하기 전에는 다시 시작하거나 반복하지 마세요."),
|
||||
_ => (
|
||||
"IMPORT_FAILED",
|
||||
"원본 수동 데이터 가져오기에 실패했고 생성된 파일은 되돌렸습니다. 자동으로 재시도하지 않습니다.")
|
||||
};
|
||||
PostManualOperatorError(
|
||||
requestId,
|
||||
operation,
|
||||
code,
|
||||
message,
|
||||
retryable: false,
|
||||
outcomeUnknown: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
QuarantineManualOperatorWrites(
|
||||
"원본 수동 데이터 가져오기 결과를 확인할 수 없습니다. 4개 파일과 marker를 직접 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
|
||||
PostManualOperatorQuarantine(requestId, operation);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _manualOperatorWriteInFlight, 0);
|
||||
ExitManualMutationGates();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleManualNetSellReadAsync(JsonElement payload)
|
||||
{
|
||||
if (!TryParseAudienceRequest(payload, out var requestId, out var audience))
|
||||
@@ -701,7 +841,7 @@ public sealed partial class MainWindow
|
||||
PostManualOperatorError(
|
||||
requestId,
|
||||
operation,
|
||||
"DATA_UNAVAILABLE",
|
||||
_manualOperatorDataInitializationCode ?? "DATA_UNAVAILABLE",
|
||||
_manualOperatorDataInitializationError ??
|
||||
"수동 송출 데이터 저장소를 사용할 수 없습니다.",
|
||||
retryable: false,
|
||||
@@ -750,21 +890,24 @@ public sealed partial class MainWindow
|
||||
InvalidateManualOperatorBrowserRequests();
|
||||
var netSellStore = Interlocked.Exchange(ref _manualNetSellStore, null);
|
||||
var viStore = Interlocked.Exchange(ref _viManualListStore, null);
|
||||
if (netSellStore is null && viStore is null)
|
||||
var importer = Interlocked.Exchange(ref _legacyManualOperatorDataImporter, null);
|
||||
if (netSellStore is null && viStore is null && importer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = DisposeManualOperatorStoresWhenIdleAsync(netSellStore, viStore);
|
||||
_ = DisposeManualOperatorStoresWhenIdleAsync(netSellStore, viStore, importer);
|
||||
}
|
||||
|
||||
private async Task DisposeManualOperatorStoresWhenIdleAsync(
|
||||
S5025TrustedManualFileStore? netSellStore,
|
||||
ViTrustedManualFileStore? viStore)
|
||||
ViTrustedManualFileStore? viStore,
|
||||
LegacyManualOperatorDataImporter? importer)
|
||||
{
|
||||
await _manualOperatorDataGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
importer?.Dispose();
|
||||
viStore?.Dispose();
|
||||
netSellStore?.Dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user