JJServiceManagerEnt/Windows/MainWindow.cs

701 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Security.Principal;
using System.ServiceProcess;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using Forms = System.Windows.Forms;
namespace JJServiceManager
{
/// <summary>초기화면</summary>
public sealed class MainWindow : Window
{
private static readonly TimeSpan OpTimeout = TimeSpan.FromSeconds(60);
private readonly AppSettings _settings;
private readonly ObservableCollection<ServiceRow> _rows = new ObservableCollection<ServiceRow>();
private readonly LogPanel _log;
private readonly LogWindowManager _logWindows;
private readonly DispatcherTimer _statusTimer;
private readonly ItemsControl _list;
private readonly CheckBox _chkAll;
private readonly TextBlock _txtCount, _txtEmpty, _txtLogHidden;
private readonly ContentControl _ccLog;
private readonly FrameworkElement _pnlShutdown;
private Forms.NotifyIcon _tray;
private HotkeyManager _hotkey;
private bool _exiting;
private bool _refreshing;
private bool _trayTipShown;
private bool _modalOpen;
public MainWindow(AppSettings settings)
{
_settings = settings;
Title = "JJServiceManager - 서비스 관리";
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#F5F7F8");
MinWidth = 760;
MinHeight = 520;
AppIcon.Apply(this);
RestoreWindowBounds(settings);
var root = XamlLoader.LoadElement("MainWindow.xaml");
Content = root;
_list = root.Find<ItemsControl>("icServices");
_chkAll = root.Find<CheckBox>("chkAll");
_txtCount = root.Find<TextBlock>("txtCount");
_txtEmpty = root.Find<TextBlock>("txtEmpty");
_txtLogHidden = root.Find<TextBlock>("txtLogHidden");
_ccLog = root.Find<ContentControl>("ccLog");
_pnlShutdown = root.Find<FrameworkElement>("pnlShutdown");
_log = new LogPanel { ShowTimestamp = true };
_ccLog.Content = _log;
_logWindows = new LogWindowManager(() => _settings, SaveSettings);
foreach (var e in settings.Services) _rows.Add(new ServiceRow(e));
_list.ItemsSource = _rows;
_rows.CollectionChanged += (s, e) => UpdateCounts();
// 상단 버튼
root.Find<Button>("btnAdd").Click += (s, e) => AddServices();
root.Find<Button>("btnDelete").Click += (s, e) => DeleteChecked();
root.Find<Button>("btnStartChecked").Click += async (s, e) => await RunChecked(true);
root.Find<Button>("btnStopChecked").Click += async (s, e) => await RunChecked(false);
root.Find<Button>("btnShutdown").Click += (s, e) => Shutdown(false);
root.Find<Button>("btnRestart").Click += (s, e) => Shutdown(true);
root.Find<Button>("btnSettings").Click += (s, e) => OpenSettings();
_chkAll.Click += (s, e) =>
{
bool v = _chkAll.IsChecked == true;
foreach (var r in _rows) r.IsChecked = v;
};
// 서비스 줄의 Start / Stop / Status / Log 버튼
_list.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowButton));
// 우클릭: 로그 경로 지정 메뉴
_list.AddHandler(UIElement.PreviewMouseRightButtonUpEvent, new MouseButtonEventHandler(OnRowRightClick), true);
PreviewKeyDown += OnPreviewKeyDown;
// 상태 자동 갱신 (2초)
_statusTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
_statusTimer.Tick += async (s, e) => await RefreshStatuses(false);
// 창 핸들을 미리 만들어 트레이 시작 상태에서도 단축키가 동작하게 함
new WindowInteropHelper(this).EnsureHandle();
_hotkey = new HotkeyManager(new WindowInteropHelper(this).Handle);
_hotkey.Pressed += ToggleVisibility;
CreateTray();
ApplySettings(startup: true);
UpdateCounts();
Log("프로그램 시작" + (IsAdmin() ? " (관리자 권한)" : ""), LogLevel.Notice);
if (!IsAdmin()) Log("관리자 권한이 아닙니다. 서비스 Start/Stop이 실패할 수 있습니다.", LogLevel.Warning);
if (_rows.Count == 0) Log("[추가] 버튼을 눌러 관리할 서비스를 등록하세요.");
_ = RefreshStatuses(true);
_statusTimer.Start();
// 컴퓨터 시작시 기동이 켜져 있으면 실행 파일 위치가 바뀌었을 경우를 대비해 다시 등록
if (_settings.RunAtStartup) Task.Run(() => StartupManager.Enable());
}
// ───────────────────────── 로그 ─────────────────────────
private void Log(string text, LogLevel level = LogLevel.Info)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new Action(() => Log(text, level)));
return;
}
OperationLogFile.Append(DateTime.Now, text);
if (_settings.ShowOperationLog) _log.Add(text, level);
}
// ───────────────────────── 서비스 목록 ─────────────────────────
private void UpdateCounts()
{
int running = _rows.Count(r => r.Status == ServiceControllerStatus.Running);
_txtCount.Text = _rows.Count == 0 ? "" : $"{_rows.Count}개 · 실행 중 {running}";
_txtEmpty.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private void SyncEntriesFromRows()
{
_settings.Services = _rows.Select(r => r.Entry).ToList();
SaveSettings();
_logWindows.ServicesChanged();
}
private void AddServices()
{
if (_modalOpen) return;
var picker = new ServicePickerWindow(_rows.Select(r => r.ServiceName)) { Owner = this };
bool? ok;
_modalOpen = true;
try { ok = picker.ShowDialog(); }
finally { _modalOpen = false; }
if (ok != true || picker.Result == null) return;
var chosen = picker.Result;
var chosenNames = new HashSet<string>(chosen.Select(c => c.ServiceName), StringComparer.OrdinalIgnoreCase);
// 체크 해제된 기존 항목은 목록에서 뺌
var removed = _rows.Where(r => !chosenNames.Contains(r.ServiceName)).ToList();
foreach (var r in removed)
{
_rows.Remove(r);
_logWindows.Close(r.ServiceName);
}
// 새로 체크된 항목 추가 (기존 순서 유지, 새 항목은 뒤에)
var existing = new HashSet<string>(_rows.Select(r => r.ServiceName), StringComparer.OrdinalIgnoreCase);
var added = new List<string>();
foreach (var c in chosen)
{
if (existing.Contains(c.ServiceName)) continue;
var row = new ServiceRow(new ServiceEntry { ServiceName = c.ServiceName, DisplayName = c.DisplayName });
_rows.Add(row);
added.Add(c.DisplayName);
}
if (added.Count > 0) Log($"서비스 추가: {string.Join(", ", added)}", LogLevel.Success);
if (removed.Count > 0) Log($"목록에서 제외: {string.Join(", ", removed.Select(r => r.DisplayName))}");
if (added.Count == 0 && removed.Count == 0) Log("변경 사항 없음");
SyncEntriesFromRows();
_ = RefreshStatuses(true);
}
private void DeleteChecked()
{
var targets = _rows.Where(r => r.IsChecked).ToList();
if (targets.Count == 0)
{
Dialogs.Info(this, "삭제할 서비스를 체크하세요.");
return;
}
var names = string.Join("\n", targets.Select(t => " · " + t.DisplayName));
if (!Dialogs.Confirm(this, $"아래 {targets.Count}개 서비스를 목록에서 삭제할까요?\n\n{names}\n\n(Windows 서비스 자체는 그대로 유지됩니다)", "삭제"))
return;
foreach (var t in targets)
{
_rows.Remove(t);
_logWindows.Close(t.ServiceName);
}
_chkAll.IsChecked = false;
Log($"목록에서 삭제: {string.Join(", ", targets.Select(t => t.DisplayName))}");
SyncEntriesFromRows();
}
// ───────────────────────── Start / Stop / Status / Log ─────────────────────────
private async void OnRowButton(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is not Button b || b.DataContext is not ServiceRow row) return;
switch (b.Tag as string)
{
case "start": await StartService(row); break;
case "stop": await StopService(row); break;
case "status": await ShowStatus(row); break;
case "log": OpenLog(row, b); break;
}
}
private async Task StartService(ServiceRow row)
{
if (row.IsBusy) return;
row.IsBusy = true;
Log($"[{row.DisplayName}] 시작 요청");
var sw = Stopwatch.StartNew();
try
{
await Task.Run(() => ServiceOps.Start(row.ServiceName, OpTimeout));
Log($"[{row.DisplayName}] 시작 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
}
catch (InfoException ex) { Log($"[{row.DisplayName}] {ex.Message}", LogLevel.Warning); }
catch (Exception ex) { Log($"[{row.DisplayName}] 시작 실패: {ServiceOps.Describe(ex)}", LogLevel.Error); }
finally
{
row.IsBusy = false;
await RefreshStatuses(true);
}
}
private async Task StopService(ServiceRow row)
{
if (row.IsBusy) return;
row.IsBusy = true;
Log($"[{row.DisplayName}] 중지 요청");
var sw = Stopwatch.StartNew();
try
{
await Task.Run(() => ServiceOps.Stop(row.ServiceName, OpTimeout));
Log($"[{row.DisplayName}] 중지 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
}
catch (InfoException ex) { Log($"[{row.DisplayName}] {ex.Message}", LogLevel.Warning); }
catch (Exception ex) { Log($"[{row.DisplayName}] 중지 실패: {ServiceOps.Describe(ex)}", LogLevel.Error); }
finally
{
row.IsBusy = false;
await RefreshStatuses(true);
}
}
private async Task RunChecked(bool start)
{
var targets = _rows.Where(r => r.IsChecked).ToList();
if (targets.Count == 0)
{
Dialogs.Info(this, $"{(start ? "시작" : "중지")}할 서비스를 체크하세요.");
return;
}
Log($"선택한 {targets.Count}개 서비스 {(start ? "시작" : "중지")}", LogLevel.Notice);
foreach (var r in targets)
{
if (start) await StartService(r);
else await StopService(r);
}
}
private async Task ShowStatus(ServiceRow row)
{
try
{
var lines = await Task.Run(() => ServiceOps.GetDetails(row.ServiceName));
Log($"[{row.DisplayName}] 상태 정보", LogLevel.Notice);
foreach (var l in lines) Log(" " + l);
}
catch (Exception ex)
{
Log($"[{row.DisplayName}] 상태 조회 실패: {ServiceOps.Describe(ex)}", LogLevel.Error);
}
await RefreshStatuses(true);
}
private void OpenLog(ServiceRow row, FrameworkElement anchor)
{
if (string.IsNullOrWhiteSpace(row.Entry.LogPath))
{
// 경로가 없으면 파일/폴더 중 선택
ShowLogMenu(row, anchor, includeView: false);
return;
}
_logWindows.Open(row.Entry);
}
private void OnRowRightClick(object sender, MouseButtonEventArgs e)
{
var fe = e.OriginalSource as FrameworkElement;
while (fe != null && fe.DataContext is not ServiceRow) fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
if (fe?.DataContext is ServiceRow row)
{
ShowLogMenu(row, e.OriginalSource as FrameworkElement ?? fe, includeView: true);
e.Handled = true;
}
}
private void ShowLogMenu(ServiceRow row, FrameworkElement anchor, bool includeView)
{
var menu = new ContextMenu { PlacementTarget = anchor, Placement = PlacementMode.MousePoint };
if (includeView && !string.IsNullOrWhiteSpace(row.Entry.LogPath))
{
var view = new MenuItem { Header = "로그 보기", FontWeight = FontWeights.Bold };
view.Click += (s, e) => _logWindows.Open(row.Entry);
menu.Items.Add(view);
menu.Items.Add(new Separator());
}
var file = new MenuItem { Header = "로그 파일 지정…" };
file.Click += (s, e) => PickLogFile(row);
menu.Items.Add(file);
var folder = new MenuItem { Header = "로그 폴더 지정… (가장 최근 파일 자동 표시)" };
folder.Click += (s, e) => PickLogFolder(row);
menu.Items.Add(folder);
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
{
menu.Items.Add(new Separator());
var cur = new MenuItem { Header = (row.Entry.LogIsFolder ? "현재 폴더: " : "현재 파일: ") + row.Entry.LogPath, IsEnabled = false };
menu.Items.Add(cur);
var clear = new MenuItem { Header = "로그 경로 해제" };
clear.Click += (s, e) =>
{
row.Entry.LogPath = "";
row.Entry.LogIsFolder = false;
row.LogPathChanged();
Log($"[{row.DisplayName}] 로그 경로 해제");
SyncEntriesFromRows();
_logWindows.EntryPathChanged(row.Entry);
};
menu.Items.Add(clear);
}
menu.IsOpen = true;
}
private void PickLogFile(ServiceRow row)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = $"{row.DisplayName} - 로그 파일 선택",
Filter = "로그 파일 (*.log;*.txt)|*.log;*.txt|모든 파일 (*.*)|*.*",
CheckFileExists = true
};
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
{
try
{
dlg.InitialDirectory = row.Entry.LogIsFolder
? row.Entry.LogPath
: System.IO.Path.GetDirectoryName(row.Entry.LogPath);
}
catch { }
}
if (dlg.ShowDialog(this) != true) return;
row.Entry.LogPath = dlg.FileName;
row.Entry.LogIsFolder = false;
row.LogPathChanged();
Log($"[{row.DisplayName}] 로그 파일 지정: {dlg.FileName}");
SyncEntriesFromRows();
_logWindows.EntryPathChanged(row.Entry);
_logWindows.Open(row.Entry);
}
private void PickLogFolder(ServiceRow row)
{
var dlg = new Microsoft.Win32.OpenFolderDialog
{
Title = $"{row.DisplayName} - 로그 폴더 선택 (가장 최근 파일을 자동으로 표시)"
};
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
{
try
{
dlg.InitialDirectory = row.Entry.LogIsFolder
? row.Entry.LogPath
: System.IO.Path.GetDirectoryName(row.Entry.LogPath);
}
catch { }
}
if (dlg.ShowDialog(this) != true) return;
row.Entry.LogPath = dlg.FolderName;
row.Entry.LogIsFolder = true;
row.LogPathChanged();
Log($"[{row.DisplayName}] 로그 폴더 지정: {dlg.FolderName}");
SyncEntriesFromRows();
_logWindows.EntryPathChanged(row.Entry);
_logWindows.Open(row.Entry);
}
// ───────────────────────── 상태 자동 갱신 ─────────────────────────
private async Task RefreshStatuses(bool force)
{
if (_refreshing && !force) return;
_refreshing = true;
try
{
var names = _rows.Select(r => r.ServiceName).ToList();
var result = await Task.Run(() => names.ToDictionary(n => n, n => ServiceOps.GetStatus(n), StringComparer.OrdinalIgnoreCase));
foreach (var r in _rows)
{
if (!result.TryGetValue(r.ServiceName, out var st)) continue;
if (r.StatusKnown && r.Status != st && !r.IsBusy)
{
Log($"[{r.DisplayName}] 상태 변경: {ServiceOps.StatusText(r.Status)} → {ServiceOps.StatusText(st)}",
st == ServiceControllerStatus.Running ? LogLevel.Success
: st == ServiceControllerStatus.Stopped ? LogLevel.Warning : LogLevel.Info);
}
r.Status = st;
}
UpdateCounts();
}
catch
{
// 다음 주기에 다시 시도
}
finally
{
_refreshing = false;
}
}
// ───────────────────────── Windows 종료 ─────────────────────────
private void Shutdown(bool restart)
{
var what = restart ? "재시작" : "강제 종료";
if (!Dialogs.Confirm(this, $"Windows를 지금 {what}합니다.\n저장하지 않은 작업은 모두 사라집니다.\n\n계속할까요?", "Windows " + what))
return;
Log($"Windows {what} 실행", LogLevel.Warning);
try
{
if (restart) PowerOps.ForceRestart(); else PowerOps.ForceShutdown();
}
catch (Exception ex)
{
Log($"Windows {what} 실패: {ex.Message}", LogLevel.Error);
}
}
// ───────────────────────── 설정 ─────────────────────────
private void OpenSettings()
{
ShowFromTray();
if (_modalOpen) return;
bool wasStartup = _settings.RunAtStartup;
var dlg = new SettingsWindow(_settings) { Owner = this };
// 설정 창에서 단축키를 누를 때 전역 단축키가 먼저 받지 않도록 잠시 해제
_hotkey.Unregister();
bool? dialogOk;
_modalOpen = true;
try { dialogOk = dlg.ShowDialog(); }
finally { _modalOpen = false; }
if (dialogOk != true)
{
ApplyHotkey(quiet: true);
return;
}
SaveSettings();
Log("설정 저장");
if (wasStartup != _settings.RunAtStartup || (_settings.RunAtStartup && !StartupManager.IsEnabled()))
{
var (ok, msg) = _settings.RunAtStartup ? StartupManager.Enable() : StartupManager.Disable();
if (ok) Log(_settings.RunAtStartup ? "컴퓨터 시작시 기동: 켬 (작업 스케줄러 등록)" : "컴퓨터 시작시 기동: 끔", LogLevel.Success);
else Log("컴퓨터 시작시 기동 설정 실패: " + msg, LogLevel.Error);
}
ApplySettings(startup: false);
}
private void ApplySettings(bool startup)
{
// Windows 종료 버튼 표시
_pnlShutdown.Visibility = _settings.ShowShutdownButtons ? Visibility.Visible : Visibility.Collapsed;
// 작업 로그 표시
_ccLog.Visibility = _settings.ShowOperationLog ? Visibility.Visible : Visibility.Collapsed;
_txtLogHidden.Visibility = _settings.ShowOperationLog ? Visibility.Collapsed : Visibility.Visible;
ApplyHotkey(quiet: startup);
}
private void ApplyHotkey(bool quiet)
{
var mods = (ModifierKeys)_settings.HotkeyModifiers;
var key = (Key)_settings.HotkeyKey;
if (_settings.HotkeyEnabled)
{
if (_hotkey.Register(mods, key))
{
if (!quiet) Log($"단축키 등록: {HotkeyManager.Format(mods, key)}");
}
else
{
var prev = _hotkey.Active;
Log($"단축키 {HotkeyManager.Format(mods, key)} 등록 실패 (다른 프로그램이 사용 중일 수 있습니다)"
+ (prev.HasValue ? $" → 이전 단축키 {HotkeyManager.Format(prev.Value.mods, prev.Value.key)} 유지" : ""),
LogLevel.Warning);
}
}
else
{
_hotkey.Unregister();
}
if (_tray != null)
_tray.Text = _settings.HotkeyEnabled
? Trim63($"JJServiceManager ({HotkeyManager.Format(mods, key)})")
: "JJServiceManager";
}
private static string Trim63(string s) => s.Length > 63 ? s.Substring(0, 63) : s;
private void SaveSettings()
{
if (WindowState == WindowState.Normal && IsLoaded)
{
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
_settings.WindowWidth = Width;
_settings.WindowHeight = Height;
}
SettingsStore.Save(_settings);
}
private void RestoreWindowBounds(AppSettings s)
{
Width = s.WindowWidth > 300 ? s.WindowWidth : 1000;
Height = s.WindowHeight > 300 ? s.WindowHeight : 720;
bool valid = !double.IsNaN(s.WindowLeft) && !double.IsNaN(s.WindowTop)
&& s.WindowLeft >= SystemParameters.VirtualScreenLeft - 50
&& s.WindowTop >= SystemParameters.VirtualScreenTop - 50
&& s.WindowLeft < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - 100
&& s.WindowTop < SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - 100;
if (valid)
{
WindowStartupLocation = WindowStartupLocation.Manual;
Left = s.WindowLeft;
Top = s.WindowTop;
}
else
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
}
// ───────────────────────── 트레이 / Esc / 종료 ─────────────────────────
private void CreateTray()
{
var menu = new Forms.ContextMenuStrip();
menu.Items.Add("열기", null, (s, e) => Dispatcher.Invoke(ShowFromTray));
menu.Items.Add("설정", null, (s, e) => { if (!_modalOpen) Dispatcher.Invoke(OpenSettings); else Dispatcher.Invoke(ShowFromTray); });
menu.Items.Add(new Forms.ToolStripSeparator());
menu.Items.Add("완전 종료", null, (s, e) => Dispatcher.Invoke(ExitApp));
_tray = new Forms.NotifyIcon
{
Icon = AppIcon.Icon,
Text = "JJServiceManager",
ContextMenuStrip = menu,
Visible = true
};
_tray.MouseClick += (s, e) =>
{
if (e.Button == Forms.MouseButtons.Left) Dispatcher.Invoke(ShowFromTray);
};
}
public void ShowFromTray()
{
if (!IsVisible) Show();
if (WindowState == WindowState.Minimized) WindowState = WindowState.Normal;
Activate();
Topmost = true; // 다른 창 뒤에 숨지 않도록 잠깐 맨 앞으로
Topmost = false;
Focus();
}
private void HideToTray()
{
SaveSettings();
Hide();
if (!_trayTipShown && _tray != null)
{
_trayTipShown = true;
var hk = _settings.HotkeyEnabled ? $" 단축키 {HotkeyManager.Format((ModifierKeys)_settings.HotkeyModifiers, (Key)_settings.HotkeyKey)}로 다시 열 수 있습니다." : "";
_tray.ShowBalloonTip(2500, "JJServiceManager", "트레이에서 계속 실행 중입니다." + hk, Forms.ToolTipIcon.Info);
}
}
private void ToggleVisibility()
{
if (IsVisible && IsActive && WindowState != WindowState.Minimized) HideToTray();
else ShowFromTray();
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Escape) return;
e.Handled = true;
if (_settings.EscAction == EscAction.Exit) ExitApp();
else HideToTray();
}
protected override void OnClosing(CancelEventArgs e)
{
if (!_exiting)
{
// 창의 X 버튼도 Esc 설정을 따름
if (_settings.EscAction == EscAction.Tray)
{
e.Cancel = true;
HideToTray();
return;
}
_exiting = true;
Cleanup();
}
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
Application.Current.Shutdown();
}
/// <summary>Windows 로그오프/종료 시 정리</summary>
public void OnSessionEnding()
{
if (_exiting) return;
_exiting = true;
OperationLogFile.Append(DateTime.Now, "Windows 세션 종료");
Cleanup();
}
public void ExitApp()
{
if (_exiting) return;
_exiting = true;
Cleanup();
Close();
Application.Current.Shutdown();
}
private void Cleanup()
{
OperationLogFile.Append(DateTime.Now, "프로그램 종료");
_statusTimer.Stop();
SaveSettings();
_logWindows.CloseAll();
_hotkey?.Dispose();
if (_tray != null)
{
_tray.Visible = false;
_tray.Dispose();
_tray = null;
}
}
private static bool IsAdmin()
{
try { return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); }
catch { return false; }
}
}
}