diff --git a/dist/README.txt b/dist/README.txt index 45689cb..86810a6 100644 --- a/dist/README.txt +++ b/dist/README.txt @@ -1,5 +1,5 @@ UbisamBase 플랫폼 배포 폴더 (빌드할 때 자동 생성 - 직접 고치지 마세요) -갱신 시각 : 2026-09-21 22:26:56 +갱신 시각 : 2026-09-21 22:36:26 빌드 구성 : Release 원본 소스 : D:\업무\03_개발\Source\용접기\08_UbisamPlatform\ ---- diff --git a/dist/UbisamBase.Core.dll b/dist/UbisamBase.Core.dll index 3aa85c1..d2c32e7 100644 Binary files a/dist/UbisamBase.Core.dll and b/dist/UbisamBase.Core.dll differ diff --git a/dist/UbisamBase.Launcher.exe b/dist/UbisamBase.Launcher.exe index 31f1f29..27d8f67 100644 Binary files a/dist/UbisamBase.Launcher.exe and b/dist/UbisamBase.Launcher.exe differ diff --git a/dist/UbisamBase.Shell.exe b/dist/UbisamBase.Shell.exe index 4f2e26d..a4be496 100644 Binary files a/dist/UbisamBase.Shell.exe and b/dist/UbisamBase.Shell.exe differ diff --git a/dist/platform-version.json b/dist/platform-version.json index 71e6d80..d3ccb19 100644 --- a/dist/platform-version.json +++ b/dist/platform-version.json @@ -1,4 +1,4 @@ { -"version": "2026-09-21 22:26:56", +"version": "2026-09-21 22:36:26", "configuration": "Release" } diff --git a/doc/history/changelog.md b/doc/history/changelog.md index a5a0542..d53fa56 100644 --- a/doc/history/changelog.md +++ b/doc/history/changelog.md @@ -16,3 +16,5 @@ | 2026-09-21 | 업데이트 안내창을 플랫폼 디자인으로 교체(Launcher 자체 XAML, 시스템 테마 따름) | | 2026-09-21 | `ApiService` 추가 — 사내 API를 앱이 대신 호출(토큰은 설정 파일에만, 호출 간격 제한·타임아웃 포함) | | 2026-09-21 | 차트 종류별 디자인 정리 — 면적·계단선·누적/가로 막대·원형·도넛·산점 추가, 계열 팔레트(Chart.Series1~6)와 툴팁·범례 테마 적용 | +| 2026-09-21 | 업데이트 진행 창 추가 — 런처를 누르자마자 확인/받기 상태와 진행률을 보여주고, 다 받은 뒤 설치 여부를 묻는다 | +| 2026-09-21 | Shell.exe를 직접 실행하면 런처로 넘기도록 — 진입점을 런처 하나로 통일(업데이트 확인 누락 방지) | diff --git a/doc/platform-guide.md b/doc/platform-guide.md index a7268e2..2dfcff2 100644 --- a/doc/platform-guide.md +++ b/doc/platform-guide.md @@ -665,9 +665,17 @@ dist의 버전이 지금 것보다 새것인가? → 아니면 커밋만 자동 업데이트인가? → 예: 바로 받기 / 아니오: "업데이트할까요?" 물어보고 예일 때만 받기 ``` +- 확인·받기는 몇 초가 걸리므로 그동안 **진행 창**이 뜬다(확인 중 → 새 버전을 찾았습니다 → 받기 완료, + 진행률은 git이 흘리는 값을 그대로 읽어 표시한다). 받기가 **끝난 뒤에** 설치 여부를 묻는다 — + "예"를 누르고 또 기다리지 않게 하기 위함이다. - 어느 단계에서 막히든(설정 꺼짐 · 네트워크 없음 · git 미설치 · 저장소 접근 실패) **조용히 넘어가고 기존 버전으로 실행한다.** 업데이트 때문에 프로그램이 안 켜지는 일은 없어야 하기 때문이다. -- 물어봤을 때 "아니오"를 고르면 아무것도 기록하지 않는다 — 다음에 켤 때 다시 물어본다. +- 물어봤을 때 "나중에"를 고르면 아무것도 기록하지 않는다 — 다음에 켤 때 다시 물어본다. +- 확인 과정은 `D:\UbisamPlatform\update-log.txt`에 한 줄씩 남는다(실행할 때마다 덮어씀). + "왜 업데이트 창이 안 뜨지?" 싶을 때 이 파일을 먼저 본다. +- **프로그램의 진입점은 항상 런처다.** 앱 폴더에는 `UbisamBase.Shell.exe`도 같이 있지만 그것을 직접 + 실행하면 갱신 없이 옛 파일로 뜨게 되므로, Shell은 자기가 직접 실행된 것을 알아채면 조용히 런처를 + 띄우고 종료한다(`App.RedirectToLauncher`). 바로가기를 만들 때도 런처를 가리켜야 한다. - 받는 것은 `dist` 폴더뿐이다(`--depth 1 --filter=blob:none --sparse`). 소스 이력은 받지 않는다. - git 자격 증명 창이 뜨지 않게 막아 두었다(`GIT_TERMINAL_PROMPT=0`). 사내 비공개 저장소라면 그 PC에서 한 번은 `git` 자격 증명이 저장돼 있어야 한다. diff --git a/src/UbisamBase.Launcher/PlatformUpdater.cs b/src/UbisamBase.Launcher/PlatformUpdater.cs index 7b0dcb9..8cda912 100644 --- a/src/UbisamBase.Launcher/PlatformUpdater.cs +++ b/src/UbisamBase.Launcher/PlatformUpdater.cs @@ -4,7 +4,9 @@ using System.IO; using System.Net.NetworkInformation; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Windows; +using System.Windows.Threading; namespace UbisamBase.Launcher; @@ -13,10 +15,15 @@ namespace UbisamBase.Launcher; /// 새 버전이면 받아서 덮어쓴다. 순서는 이렇다. /// /// -/// 설정에서 켜져 있나? → 인터넷 연결됐나? → 저장소에 새 커밋이 있나? → 그 안의 업데이트 파일이 -/// 지금 것보다 새 버전인가? → 자동 업데이트면 바로 받기 / 아니면 물어보고 "예"일 때만 받기 +/// 설정에서 켜져 있나? → 인터넷 연결됐나? → (진행 창 표시) 저장소에 새 커밋이 있나? +/// → 업데이트 파일 받기(진행률 표시) → 지금 것보다 새 버전인가? +/// → 자동 업데이트면 바로 설치 / 아니면 물어보고 "예"일 때만 설치 /// /// +/// 받는 동안 를 띄운다 — 예전에는 아무것도 안 보여서 몇 초 동안 +/// 프로그램이 안 켜지는 것처럼 보였다. 설치 여부는 다 받은 뒤에 묻는다(그래야 "예"를 누른 +/// 다음에 또 기다리지 않는다). +/// /// 어느 단계에서 막히든(설정 꺼짐 · 네트워크 없음 · git 없음 · 저장소 접근 실패) 조용히 넘어가고 /// 기존 파일로 그대로 실행한다 — 업데이트 때문에 프로그램이 안 켜지는 일은 없어야 한다. /// @@ -96,80 +103,57 @@ internal static class PlatformUpdater return; // git이 깔려 있지 않은 PC — 업데이트 기능만 조용히 쉰다. } - // 2) 업데이트 할 게 있는가 — 원격 브랜치의 최신 커밋이 마지막에 설치한 커밋과 같으면 끝. - var branch = string.IsNullOrWhiteSpace(settings.Branch) ? "main" : settings.Branch.Trim(); - if (!TryRunGit($"ls-remote \"{settings.RepositoryUrl}\" \"refs/heads/{branch}\"", null, LsRemoteTimeoutMs, out var lsRemote)) + // 여기서부터 몇 초가 걸릴 수 있다 — 무엇을 하고 있는지 창으로 보여준다. + var progress = new ProgressWindow(); + progress.Show(); + + Download? download = null; + + // 받기는 배경 스레드에서 하고, 이 스레드는 DispatcherFrame으로 화면을 계속 그린다. + // (Application.Run을 쓸 수 없다 — Shell이 나중에 자기 Application을 만든다. Dispatcher를 + // 종료시키는 방식도 안 된다 — 같은 스레드에서 Shell이 계속 돌아야 하기 때문이다.) + var frame = new DispatcherFrame(); + var worker = new Thread(() => { - Note("원격 확인 실패(주소·권한·네트워크) — 건너뜀"); - return; // 주소가 틀렸거나 접근 권한이 없다 — 실행을 막지 않는다. + try + { + download = Prepare(platformDir, settings, progress); + } + catch (Exception ex) + { + Note("받는 중 예외 : " + ex.Message); + } + finally + { + progress.Dispatcher.BeginInvoke(new Action(() => frame.Continue = false)); + } + }) + { + IsBackground = true + }; + + worker.Start(); + Dispatcher.PushFrame(frame); + progress.Close(); + + if (download == null) + { + return; // 할 일이 없거나 중간에 막혔다 — 이유는 로그에 남아 있다. } - var remoteCommit = FirstToken(lsRemote); - Note($"원격 커밋:{Short(remoteCommit)} / 설치된 커밋:{Short(settings.InstalledCommit)}"); - - if (string.IsNullOrEmpty(remoteCommit) || remoteCommit == settings.InstalledCommit) - { - Note("새 커밋 없음 — 끝"); - return; - } - - var tempDir = Path.Combine(Path.GetTempPath(), "UbisamPlatformUpdate_" + Guid.NewGuid().ToString("N")); try { - var distName = string.IsNullOrWhiteSpace(settings.DistPath) ? "dist" : settings.DistPath.Trim().Replace('\\', '/'); - - // 업데이트 파일만 얕게 받는다 — 소스 전체 이력을 받지 않는다. - if (!TryRunGit( - $"clone --depth 1 --branch \"{branch}\" --filter=blob:none --sparse \"{settings.RepositoryUrl}\" \"{tempDir}\"", - null, CloneTimeoutMs, out _)) - { - Note("업데이트 파일을 내려받지 못함(clone 실패) — 건너뜀"); - return; - } - - if (!TryRunGit($"sparse-checkout set \"{distName}\"", tempDir, CloneTimeoutMs, out _)) - { - Note("업데이트 폴더를 꺼내지 못함(sparse-checkout 실패) — 건너뜀"); - return; - } - - var remoteDist = Path.Combine(tempDir, distName.Replace('/', '\\')); - if (!Directory.Exists(remoteDist)) - { - Note($"저장소에 {distName} 폴더가 없음 — 커밋만 기록"); - // 저장소에 아직 업데이트 파일이 없다 — 이 커밋은 확인했다고 기록만 하고 넘어간다. - settings.InstalledCommit = remoteCommit; - settings.Save(SettingsPath); - return; - } - - var remoteVersion = ReadVersion(Path.Combine(remoteDist, VersionFileName)); - var localVersion = ReadVersion(Path.Combine(platformDir, VersionFileName)); - - // 버전은 배포 시각("yyyy-MM-dd HH:mm:ss")이라 문자열 비교로 앞뒤가 가려진다. - // 원격이 더 새것일 때만 업데이트한다 — 소스만 바뀐 커밋이면 확인 기록만 남긴다. - Note($"원격 버전:{remoteVersion} / 지금 버전:{localVersion}"); - - if (string.IsNullOrEmpty(remoteVersion) || string.CompareOrdinal(remoteVersion, localVersion) <= 0) - { - Note("더 새 버전이 아님 — 커밋만 기록"); - settings.InstalledCommit = remoteCommit; - settings.Save(SettingsPath); - return; - } - - // 3) 자동 업데이트가 아니면 물어본다. - if (!settings.AutoUpdate && !UpdateDialog.Ask(localVersion, remoteVersion)) + if (!settings.AutoUpdate && !UpdateDialog.Ask(download.LocalVersion, download.RemoteVersion)) { Note("사용자가 \"나중에\"를 선택 — 다음 실행 때 다시 물어본다"); - // 기록을 남기지 않는다 — 다음에 켤 때 다시 물어본다. - return; + return; // 기록을 남기지 않는다 — 다음에 켤 때 다시 물어본다. } - var failed = CopyAll(remoteDist, platformDir); + var failed = CopyAll(download.DistDir, platformDir); Note($"업데이트 완료 — 바꾸지 못한 파일:{failed}개"); - settings.InstalledCommit = remoteCommit; - settings.InstalledVersion = remoteVersion; + + settings.InstalledCommit = download.Commit; + settings.InstalledVersion = download.RemoteVersion; settings.Save(SettingsPath); if (failed > 0) @@ -182,10 +166,130 @@ internal static class PlatformUpdater } finally { - TryDeleteDirectory(tempDir); + TryDeleteDirectory(download.TempDir); } } + /// 원격을 확인하고, 새 버전이면 업데이트 파일을 임시 폴더에 받아 온다. + /// 받을 것이 없으면 null. 화면 표시는 로 알린다. + private static Download? Prepare(string platformDir, Settings settings, ProgressWindow progress) + { + progress.SetStage("업데이트 확인 중", "저장소에서 최신 버전을 확인하고 있습니다."); + progress.SetPercent(6); + + var branch = string.IsNullOrWhiteSpace(settings.Branch) ? "main" : settings.Branch.Trim(); + if (!TryRunGit($"ls-remote \"{settings.RepositoryUrl}\" \"refs/heads/{branch}\"", null, LsRemoteTimeoutMs, out var lsRemote)) + { + Note("원격 확인 실패(주소·권한·네트워크) — 건너뜀"); + return null; + } + + var remoteCommit = FirstToken(lsRemote); + Note($"원격 커밋:{Short(remoteCommit)} / 설치된 커밋:{Short(settings.InstalledCommit)}"); + + if (string.IsNullOrEmpty(remoteCommit) || remoteCommit == settings.InstalledCommit) + { + Note("새 커밋 없음 — 끝"); + progress.SetStage("최신 상태입니다", "받을 새 버전이 없습니다."); + progress.SetPercent(100); + Thread.Sleep(400); // 창이 깜빡이고 사라지지 않도록 잠깐 보여준다. + return null; + } + + progress.SetStage("새 버전을 찾았습니다", "업데이트 파일을 불러오고 있습니다."); + progress.SetPercent(12); + + var tempDir = Path.Combine(Path.GetTempPath(), "UbisamPlatformUpdate_" + Guid.NewGuid().ToString("N")); + var distName = string.IsNullOrWhiteSpace(settings.DistPath) ? "dist" : settings.DistPath.Trim().Replace('\\', '/'); + var keepTemp = false; + + try + { + // 업데이트 파일만 얕게 받는다 — 소스 전체 이력을 받지 않는다. + // --progress를 줘야 진행률이 나온다(출력이 리디렉트되면 git은 기본적으로 조용하다). + if (!TryRunGit( + $"clone --progress --depth 1 --branch \"{branch}\" --filter=blob:none --sparse \"{settings.RepositoryUrl}\" \"{tempDir}\"", + null, CloneTimeoutMs, out _, p => progress.SetPercent(12 + (p * 0.58)))) + { + Note("업데이트 파일을 내려받지 못함(clone 실패) — 건너뜀"); + return null; + } + + progress.SetPercent(70); + + if (!TryRunGit($"sparse-checkout set \"{distName}\"", tempDir, CloneTimeoutMs, out _, p => progress.SetPercent(70 + (p * 0.22)))) + { + Note("업데이트 폴더를 꺼내지 못함(sparse-checkout 실패) — 건너뜀"); + return null; + } + + progress.SetPercent(94); + + var remoteDist = Path.Combine(tempDir, distName.Replace('/', '\\')); + if (!Directory.Exists(remoteDist)) + { + Note($"저장소에 {distName} 폴더가 없음 — 커밋만 기록"); + settings.InstalledCommit = remoteCommit; + settings.Save(SettingsPath); + return null; + } + + var remoteVersion = ReadVersion(Path.Combine(remoteDist, VersionFileName)); + var localVersion = ReadVersion(Path.Combine(platformDir, VersionFileName)); + Note($"원격 버전:{remoteVersion} / 지금 버전:{localVersion}"); + + // 버전은 배포 시각("yyyy-MM-dd HH:mm:ss")이라 문자열 비교로 앞뒤가 가려진다. + // 원격이 더 새것일 때만 설치한다 — 소스만 바뀐 커밋이면 확인 기록만 남긴다. + if (string.IsNullOrEmpty(remoteVersion) || string.CompareOrdinal(remoteVersion, localVersion) <= 0) + { + Note("더 새 버전이 아님 — 커밋만 기록"); + settings.InstalledCommit = remoteCommit; + settings.Save(SettingsPath); + progress.SetStage("최신 상태입니다", "받을 새 버전이 없습니다."); + progress.SetPercent(100); + Thread.Sleep(400); + return null; + } + + progress.SetStage("받기 완료", "설치할지 확인합니다."); + progress.SetPercent(100); + Thread.Sleep(350); // 100%가 눈에 보이도록 잠깐 둔다. + + keepTemp = true; // 설치까지 쓰므로 여기서 지우지 않는다. + return new Download(tempDir, remoteDist, remoteCommit, remoteVersion, localVersion); + } + finally + { + if (!keepTemp) + { + TryDeleteDirectory(tempDir); + } + } + } + + /// 받아둔 업데이트 파일 한 벌. + private sealed class Download + { + public Download(string tempDir, string distDir, string commit, string remoteVersion, string localVersion) + { + TempDir = tempDir; + DistDir = distDir; + Commit = commit; + RemoteVersion = remoteVersion; + LocalVersion = localVersion; + } + + public string TempDir { get; } + + public string DistDir { get; } + + public string Commit { get; } + + public string RemoteVersion { get; } + + public string LocalVersion { get; } + } + private static string Short(string commit) => string.IsNullOrEmpty(commit) ? "(없음)" : commit.Substring(0, Math.Min(7, commit.Length)); @@ -248,7 +352,12 @@ internal static class PlatformUpdater return failed; } - private static bool TryRunGit(string arguments, string? workingDirectory, int timeoutMs, out string output) + /// + /// git을 한 번 실행한다. 를 주면 진행률(0~100)을 알려준다 — + /// git은 진행률을 stderr에 줄바꿈 없이(\r) 흘리므로 줄 단위가 아니라 글자 단위로 읽는다. + /// stdout과 stderr를 각각 다른 스레드에서 읽는 이유는, 한쪽 버퍼가 차서 서로 기다리는 것을 막기 위함이다. + /// + private static bool TryRunGit(string arguments, string? workingDirectory, int timeoutMs, out string output, Action? onPercent = null) { output = string.Empty; @@ -276,8 +385,11 @@ internal static class PlatformUpdater return false; } - var stdout = process.StandardOutput.ReadToEnd(); - process.StandardError.ReadToEnd(); + var stdout = string.Empty; + var outReader = new Thread(() => stdout = process.StandardOutput.ReadToEnd()) { IsBackground = true }; + var errReader = new Thread(() => ReadProgress(process.StandardError, onPercent)) { IsBackground = true }; + outReader.Start(); + errReader.Start(); if (!process.WaitForExit(timeoutMs)) { @@ -293,6 +405,9 @@ internal static class PlatformUpdater return false; } + outReader.Join(2000); + errReader.Join(2000); + output = stdout; return process.ExitCode == 0; } @@ -302,6 +417,49 @@ internal static class PlatformUpdater } } + /// stderr를 글자 단위로 읽어 "…: 37%" 같은 진행률만 뽑아낸다. + private static void ReadProgress(StreamReader reader, Action? onPercent) + { + try + { + var line = new StringBuilder(); + + while (true) + { + var read = reader.Read(); + if (read < 0) + { + break; + } + + var ch = (char)read; + if (ch == '\r' || ch == '\n') + { + if (onPercent != null && line.Length > 0) + { + var match = Regex.Match(line.ToString(), @"(\d{1,3})%"); + if (match.Success && double.TryParse(match.Groups[1].Value, out var value)) + { + onPercent(value); + } + } + + line.Clear(); + continue; + } + + if (line.Length < 200) + { + line.Append(ch); + } + } + } + catch + { + // 읽기가 끊겨도 실행에는 지장이 없다. + } + } + private static void TryDeleteDirectory(string dir) { try diff --git a/src/UbisamBase.Launcher/ProgressWindow.xaml b/src/UbisamBase.Launcher/ProgressWindow.xaml new file mode 100644 index 0000000..8c8298e --- /dev/null +++ b/src/UbisamBase.Launcher/ProgressWindow.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UbisamBase.Launcher/ProgressWindow.xaml.cs b/src/UbisamBase.Launcher/ProgressWindow.xaml.cs new file mode 100644 index 0000000..e99e5b3 --- /dev/null +++ b/src/UbisamBase.Launcher/ProgressWindow.xaml.cs @@ -0,0 +1,112 @@ +using System; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using Microsoft.Win32; + +namespace UbisamBase.Launcher; + +/// +/// 업데이트를 확인하고 받는 동안 보여주는 창. 런처를 누르자마자 떠서 "지금 뭘 하고 있는지"를 알린다 +/// (예전에는 받는 동안 아무것도 안 보여서 프로그램이 안 켜지는 것처럼 보였다). +/// +/// 받기가 끝나면 이 창은 닫히고, 설치 여부는 UpdateDialog가 묻는다. +/// 색은 UpdateDialog와 같은 방식으로(윈도우 밝게/어둡게) 직접 넣는다 — Core를 참조할 수 없기 때문. +/// +public partial class ProgressWindow : Window +{ + private double percent; + + public ProgressWindow() + { + InitializeComponent(); + ApplyTheme(IsSystemLightTheme()); + SizeChanged += (_, _) => UpdateFillWidth(animate: false); + } + + /// 제목과 설명을 바꾼다. 어느 스레드에서 불러도 된다. + public void SetStage(string headline, string status) + => Dispatcher.Invoke(() => + { + HeadlineText.Text = headline; + StatusText.Text = status; + }); + + /// 0~100. 뒤로 가지 않도록 지금 값보다 큰 값만 반영한다 — git이 단계마다 0%부터 다시 세기 때문. + public void SetPercent(double value) + => Dispatcher.Invoke(() => + { + var clamped = Math.Max(0, Math.Min(100, value)); + if (clamped <= percent) + { + return; + } + + percent = clamped; + PercentText.Text = ((int)percent) + "%"; + UpdateFillWidth(animate: true); + }); + + private void UpdateFillWidth(bool animate) + { + var target = Math.Max(0, TrackBorder.ActualWidth * percent / 100.0); + + if (!animate) + { + FillBorder.BeginAnimation(WidthProperty, null); + FillBorder.Width = target; + return; + } + + // 숫자가 툭툭 튀지 않도록 짧게 이어 붙인다. + FillBorder.BeginAnimation(WidthProperty, new DoubleAnimation(target, TimeSpan.FromMilliseconds(220)) + { + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }, + FillBehavior = FillBehavior.HoldEnd + }); + } + + private void ApplyTheme(bool light) + { + var background = Brush(light ? "#FFFFFFFF" : "#FF2A2E34"); + var foreground = Brush(light ? "#FF1D1F20" : "#FFE8EAED"); + var border = Brush(light ? "#FFD6D6D9" : "#FF3A3F47"); + var muted = Brush(light ? "#FF98989B" : "#FF8A9099"); + var accent = Brush(light ? "#FF2196F3" : "#FF4DA6F5"); + var track = Brush(light ? "#FFEDEDEF" : "#FF23262B"); + var chip = Brush(light ? "#142196F3" : "#1F4DA6F5"); + + Background = background; + Foreground = foreground; + RootBorder.BorderBrush = border; + AccentStrip.Background = accent; + IconChip.Background = chip; + IconPath.Stroke = accent; + HeadlineText.Foreground = foreground; + StatusText.Foreground = muted; + PercentText.Foreground = muted; + TrackBorder.Background = track; + FillBorder.Background = accent; + } + + private static SolidColorBrush Brush(string hex) + => new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); + + private static bool IsSystemLightTheme() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + + return key?.GetValue("AppsUseLightTheme") is not int value || value != 0; + } + catch + { + return true; + } + } + + private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => DragMove(); +} diff --git a/src/UbisamBase.Shell/App.xaml.cs b/src/UbisamBase.Shell/App.xaml.cs index 11a9706..0d78d2f 100644 --- a/src/UbisamBase.Shell/App.xaml.cs +++ b/src/UbisamBase.Shell/App.xaml.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.IO; using System.Windows; using UbisamBase.Core.Bootstrap; @@ -10,10 +12,60 @@ namespace UbisamBase.Shell; /// public partial class App : Application { + private const string LauncherFileName = "UbisamBase.Launcher.exe"; + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); + if (RedirectToLauncher()) + { + return; + } + UbisamAppBuilder.Run(AppDomain.CurrentDomain.BaseDirectory); } + + /// + /// 이 exe를 직접 실행한 경우 런처로 넘긴다. + /// + /// 프로그램의 정식 진입점은 항상 런처다 — 런처만이 배포 폴더(D:\UbisamPlatform\bin)를 + /// 최신으로 맞추고 업데이트를 확인한다. 폴더에 exe가 둘 보여서 이걸 직접 누르는 일이 생기는데, + /// 그러면 갱신 없이 옛 파일로 떠서 "왜 업데이트가 안 되지?"가 된다. 그래서 조용히 런처로 넘긴다. + /// + /// 런처는 Shell을 같은 프로세스에서 로드하므로, 런처로 실행된 경우 이 프로세스의 exe + /// 이름은 런처다 — 그때는 아무 일도 하지 않는다. + /// + private bool RedirectToLauncher() + { + try + { + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var currentExe = Process.GetCurrentProcess().MainModule?.FileName; + + if (string.Equals(Path.GetFileName(currentExe), LauncherFileName, StringComparison.OrdinalIgnoreCase)) + { + return false; // 런처가 띄운 정상 경로 + } + + var launcherPath = Path.Combine(baseDir, LauncherFileName); + if (!File.Exists(launcherPath)) + { + return false; // 런처 없이 배포된 환경 — 그대로 실행한다 + } + + Process.Start(new ProcessStartInfo(launcherPath) + { + UseShellExecute = true, + WorkingDirectory = baseDir + }); + + Shutdown(); + return true; + } + catch + { + return false; // 넘기지 못하면 그냥 이대로 실행한다 — 프로그램이 안 켜지는 것보다 낫다. + } + } }