fix: recover first-run live config conflicts
This commit is contained in:
@@ -100,6 +100,10 @@ Registry64 등록, 지속 K3D pin, `127.0.0.1:30001` Development Live 승인 및
|
||||
빌드를 한 번에 확인하므로 시간이 걸릴 수 있습니다. 이 과정은 DB 연결, K3D COM 활성화,
|
||||
Tornado2/PGM 연결이나 송출 명령을 실행하지 않습니다. 성공 뒤 누르는 다음 F5에서는
|
||||
`Cuts`와 `Res\MmoneyCoder.ini`를 복사하지 않고 선택한 원래 경로를 직접 사용합니다.
|
||||
기존 보호 Live 설정이 고정값과 다르면 자동으로 덮어쓰지 않고 정확한 원인과
|
||||
`기존 Live 설정 교체 후 다시 시도` 버튼을 표시합니다. 이 버튼은 안내된
|
||||
`127.0.0.1:30001`, 채널 미지정, 고정 allowlist/timeout만 명시적으로 교체하며 K3D pin이나
|
||||
기존 runtime binding은 변경하지 않습니다.
|
||||
|
||||
K3D는 다음 회사 표준 배치만 허용합니다.
|
||||
|
||||
|
||||
@@ -37,8 +37,11 @@
|
||||
|
||||
설정 중에는 DB 연결, K3D COM 활성화, Tornado2/PGM 연결 또는 송출 명령을 실행하지 않는다.
|
||||
설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 검증된 전체 앱이 시작된다. 기존 로컬
|
||||
설정과 충돌하거나 K3D pin이 달라 자동 설정이 중단되면 대상을 검토한 뒤 아래 수동 교체
|
||||
절차를 사용한다. 자세한 절차는
|
||||
Live 설정과 충돌하면 화면은 자동으로 덮어쓰지 않고 `127.0.0.1:30001`, 채널 미지정,
|
||||
고정 allowlist/timeout으로 교체된다는 안내와 별도
|
||||
`기존 Live 설정 교체 후 다시 시도` 버튼을 표시한다. 이 명시적 재시도는 K3D pin과 runtime
|
||||
binding을 바꾸지 않는다. runtime binding 충돌이나 K3D pin 차이로 중단되면 대상을 검토한
|
||||
뒤 아래 수동 교체 절차를 사용한다. 자세한 절차는
|
||||
[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를
|
||||
따른다.
|
||||
|
||||
|
||||
@@ -1494,7 +1494,8 @@ function Get-DevelopmentLivePreflight {
|
||||
# refused. The caller asked to configure Live, so failure is fail-closed.
|
||||
Remove-DevelopmentLiveAuthorizationFailClosed -Path $authorizationPath
|
||||
throw (
|
||||
'Existing Development Live files differ from the exact requested ' +
|
||||
'[MBN_SETUP_LIVE_CONFIG_REPLACEMENT_REQUIRED] Existing Development ' +
|
||||
'Live files differ from the exact requested ' +
|
||||
'endpoint/hash contract. Re-run with -ReplaceLiveConfig.')
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,12 @@
|
||||
Click="OnCloseClicked"
|
||||
Content="닫기"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="ReplaceLiveConfigButton"
|
||||
Click="OnReplaceLiveConfigClicked"
|
||||
Content="기존 Live 설정 교체 후 다시 시도"
|
||||
IsEnabled="False"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="InstallButton"
|
||||
Click="OnInstallClicked"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using Microsoft.UI.Windowing;
|
||||
@@ -14,10 +15,14 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
|
||||
private const string SetupScriptRelativePath =
|
||||
@"scripts\Initialize-ExistingDevelopmentPc.ps1";
|
||||
private const string LiveConfigReplacementRequiredMarker =
|
||||
"MBN_SETUP_LIVE_CONFIG_REPLACEMENT_REQUIRED";
|
||||
private const int MaximumCapturedOutputCharacters = 16 * 1024;
|
||||
|
||||
private string? _cutsDirectory;
|
||||
private string? _resourceDirectory;
|
||||
private bool _setupRunning;
|
||||
private bool _liveConfigReplacementAvailable;
|
||||
private readonly AppWindow _appWindow;
|
||||
|
||||
public FirstRunSetupWindow()
|
||||
@@ -36,6 +41,7 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
ClearLiveConfigReplacement();
|
||||
_cutsDirectory = null;
|
||||
CutsPathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
@@ -63,6 +69,7 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
ClearLiveConfigReplacement();
|
||||
_resourceDirectory = null;
|
||||
ResourcePathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
@@ -89,6 +96,30 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
ClearLiveConfigReplacement();
|
||||
await RunSetupFromUiAsync(replaceLiveConfig: false);
|
||||
}
|
||||
|
||||
private async void OnReplaceLiveConfigClicked(
|
||||
object sender,
|
||||
RoutedEventArgs e)
|
||||
{
|
||||
if (_setupRunning || !_liveConfigReplacementAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearLiveConfigReplacement();
|
||||
await RunSetupFromUiAsync(replaceLiveConfig: true);
|
||||
}
|
||||
|
||||
private async Task RunSetupFromUiAsync(bool replaceLiveConfig)
|
||||
{
|
||||
if (_setupRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryResolveRuntimeRoot(
|
||||
_cutsDirectory,
|
||||
_resourceDirectory,
|
||||
@@ -123,8 +154,10 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
repositoryRoot,
|
||||
runtimeRoot!,
|
||||
_cutsDirectory!,
|
||||
_resourceDirectory!);
|
||||
_resourceDirectory!,
|
||||
replaceLiveConfig);
|
||||
|
||||
ClearLiveConfigReplacement();
|
||||
SetupStatusBar.Title = "설정 완료";
|
||||
SetupStatusBar.Message =
|
||||
"Cuts/Res, DB, K3D 및 Live 설정을 검증했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
|
||||
@@ -132,10 +165,21 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
InstallButton.Visibility = Visibility.Collapsed;
|
||||
CloseButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (VerifiedSetupException exception)
|
||||
{
|
||||
_liveConfigReplacementAvailable =
|
||||
exception.LiveConfigReplacementAvailable;
|
||||
ReplaceLiveConfigButton.Visibility =
|
||||
_liveConfigReplacementAvailable
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
ShowError(exception.Message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearLiveConfigReplacement();
|
||||
ShowError(
|
||||
"개발 PC 자동 설정에 실패했습니다. Res\\MmoneyCoder.ini, C:\\K3DAsyncEngine x64 등록, Tornado2의 127.0.0.1:30001 설정을 확인해 주세요.");
|
||||
"개발 PC 자동 설정에 실패했습니다. 선택한 Res\\MmoneyCoder.ini, C:\\K3DAsyncEngine x64 등록과 Debug x64 빌드 환경을 확인해 주세요.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -317,7 +361,8 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
string repositoryRoot,
|
||||
string runtimeRoot,
|
||||
string cutsDirectory,
|
||||
string resourceDirectory)
|
||||
string resourceDirectory,
|
||||
bool replaceLiveConfig)
|
||||
{
|
||||
var powershellPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||
@@ -360,6 +405,10 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
AddArgument("-PlayoutPort");
|
||||
AddArgument("30001");
|
||||
AddArgument("-PinRegisteredK3D");
|
||||
if (replaceLiveConfig)
|
||||
{
|
||||
AddArgument("-ReplaceLiveConfig");
|
||||
}
|
||||
|
||||
using var process = new Process
|
||||
{
|
||||
@@ -371,14 +420,16 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
"The verified development-PC initializer could not be started.");
|
||||
}
|
||||
|
||||
var standardOutput = DrainOutputAsync(process.StandardOutput);
|
||||
var standardError = DrainOutputAsync(process.StandardError);
|
||||
var standardOutput = CaptureOutputTailAsync(process.StandardOutput);
|
||||
var standardError = CaptureOutputTailAsync(process.StandardError);
|
||||
await process.WaitForExitAsync();
|
||||
await Task.WhenAll(standardOutput, standardError);
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The verified development-PC initializer failed with exit code {process.ExitCode}.");
|
||||
throw CreateVerifiedSetupException(
|
||||
process.ExitCode,
|
||||
standardOutput.Result,
|
||||
standardError.Result);
|
||||
}
|
||||
|
||||
var loaded = new LegacyOperatorSettingsStore().Load();
|
||||
@@ -420,12 +471,63 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
void AddArgument(string value) => startInfo.ArgumentList.Add(value);
|
||||
}
|
||||
|
||||
private static async Task DrainOutputAsync(StreamReader reader)
|
||||
private static async Task<string> CaptureOutputTailAsync(StreamReader reader)
|
||||
{
|
||||
var captured = new StringBuilder(MaximumCapturedOutputCharacters);
|
||||
var buffer = new char[4096];
|
||||
while (await reader.ReadAsync(buffer.AsMemory()) > 0)
|
||||
int charactersRead;
|
||||
while ((charactersRead = await reader.ReadAsync(buffer.AsMemory())) > 0)
|
||||
{
|
||||
captured.Append(buffer, 0, charactersRead);
|
||||
var excess = captured.Length - MaximumCapturedOutputCharacters;
|
||||
if (excess > 0)
|
||||
{
|
||||
captured.Remove(0, excess);
|
||||
}
|
||||
}
|
||||
|
||||
return captured.ToString();
|
||||
}
|
||||
|
||||
private static VerifiedSetupException CreateVerifiedSetupException(
|
||||
int exitCode,
|
||||
string standardOutput,
|
||||
string standardError)
|
||||
{
|
||||
var diagnostic = string.Concat(
|
||||
standardError,
|
||||
Environment.NewLine,
|
||||
standardOutput);
|
||||
if (diagnostic.Contains(
|
||||
LiveConfigReplacementRequiredMarker,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return new VerifiedSetupException(
|
||||
"기존 보호 송출 설정이 현재 개발 PC 고정값과 다릅니다. 아래 버튼을 누르면 playout.local.json을 127.0.0.1:30001, 채널 미지정, 고정 허용 목록/시간 제한으로 교체하고 1회 승인을 새로 발급합니다. K3D pin은 변경하지 않습니다.",
|
||||
liveConfigReplacementAvailable: true);
|
||||
}
|
||||
|
||||
if (diagnostic.Contains(
|
||||
"Re-run with -ReplaceRuntimeBinding.",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return new VerifiedSetupException(
|
||||
"이 PC에 저장된 Cuts/Res 또는 빌드 경로가 방금 선택한 폴더와 다릅니다. 기존 로컬 경로 설정을 확인한 뒤 다시 진행해 주세요.",
|
||||
liveConfigReplacementAvailable: false);
|
||||
}
|
||||
|
||||
if (diagnostic.Contains(
|
||||
"Automatic pinning cannot approve a changed DLL.",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return new VerifiedSetupException(
|
||||
"등록된 K3D 파일의 해시가 이 PC의 기존 pin과 다릅니다. K3D 설치 파일을 독립적으로 확인하기 전에는 설정을 계속할 수 없습니다.",
|
||||
liveConfigReplacementAvailable: false);
|
||||
}
|
||||
|
||||
return new VerifiedSetupException(
|
||||
$"개발 PC 검증 도구가 종료 코드 {exitCode}로 중단되었습니다. 선택한 Res, K3D 등록과 Debug x64 빌드 출력을 확인해 주세요.",
|
||||
liveConfigReplacementAvailable: false);
|
||||
}
|
||||
|
||||
private static void EnsureOrdinaryFile(string path, string message)
|
||||
@@ -455,6 +557,8 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
_setupRunning = running;
|
||||
ChooseCutsButton.IsEnabled = !running;
|
||||
ChooseResourceButton.IsEnabled = !running;
|
||||
ReplaceLiveConfigButton.IsEnabled =
|
||||
!running && _liveConfigReplacementAvailable;
|
||||
InstallButton.IsEnabled = !running &&
|
||||
_cutsDirectory is not null &&
|
||||
_resourceDirectory is not null;
|
||||
@@ -463,6 +567,13 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ClearLiveConfigReplacement()
|
||||
{
|
||||
_liveConfigReplacementAvailable = false;
|
||||
ReplaceLiveConfigButton.IsEnabled = false;
|
||||
ReplaceLiveConfigButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
{
|
||||
SetupStatusBar.Title = "확인 필요";
|
||||
@@ -484,4 +595,18 @@ public sealed partial class FirstRunSetupWindow : Window
|
||||
return AppWindow.GetFromWindowId(windowId);
|
||||
}
|
||||
|
||||
private sealed class VerifiedSetupException : InvalidOperationException
|
||||
{
|
||||
public VerifiedSetupException(
|
||||
string message,
|
||||
bool liveConfigReplacementAvailable)
|
||||
: base(message)
|
||||
{
|
||||
LiveConfigReplacementAvailable =
|
||||
liveConfigReplacementAvailable;
|
||||
}
|
||||
|
||||
public bool LiveConfigReplacementAvailable { get; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
|
||||
Assert.Contains("Cuts 폴더 선택", buttons);
|
||||
Assert.Contains("Res 폴더 선택", buttons);
|
||||
Assert.Contains("기존 Live 설정 교체 후 다시 시도", buttons);
|
||||
Assert.DoesNotContain("INI 파일 선택", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("비밀번호", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("포트 입력", source, StringComparison.Ordinal);
|
||||
@@ -124,6 +125,10 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
var setup = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml.cs"));
|
||||
var initializer = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"scripts",
|
||||
"Initialize-ExistingDevelopmentPc.ps1"));
|
||||
|
||||
Assert.Contains(
|
||||
"await RunVerifiedSetupAsync(",
|
||||
@@ -178,13 +183,49 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"DrainOutputAsync(process.StandardOutput)",
|
||||
"if (replaceLiveConfig)",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"DrainOutputAsync(process.StandardError)",
|
||||
"AddArgument(\"-ReplaceLiveConfig\")",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"AddArgument(\"-ReplaceRuntimeBinding\")",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"AddArgument(\"-ReplaceK3DPin\")",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"CaptureOutputTailAsync(process.StandardOutput)",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"CaptureOutputTailAsync(process.StandardError)",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"MaximumCapturedOutputCharacters = 16 * 1024",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"RunSetupFromUiAsync(replaceLiveConfig: false)",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"RunSetupFromUiAsync(replaceLiveConfig: true)",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"MBN_SETUP_LIVE_CONFIG_REPLACEMENT_REQUIRED",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[MBN_SETUP_LIVE_CONFIG_REPLACEMENT_REQUIRED]",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"loaded.Settings.SceneDirectory",
|
||||
setup,
|
||||
|
||||
@@ -851,7 +851,7 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Existing Development Live files differ from the exact requested",
|
||||
"[MBN_SETUP_LIVE_CONFIG_REPLACEMENT_REQUIRED]",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user