1532 lines
67 KiB
C#
1532 lines
67 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.Text.Json.Nodes;
|
|
using System.ServiceProcess;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using JJServiceManager.Remote;
|
|
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 FrameworkElement _root;
|
|
private readonly ObservableCollection<HostGroup> _groups = new ObservableCollection<HostGroup>();
|
|
private readonly HostGroup _local;
|
|
private readonly LogPanel _log;
|
|
private readonly LogWindowManager _logWindows;
|
|
private readonly DispatcherTimer _statusTimer;
|
|
private readonly RemoteServer _remoteServer;
|
|
private readonly PassiveHub _passiveHub;
|
|
private readonly RelayAgent _relayAgent;
|
|
private readonly RelayManager _relayManager;
|
|
private string _relayAgentKey = "", _relayManageKey = "";
|
|
private string _relayLoginNotice;
|
|
/// <summary>중계 기기 그룹의 표시용 대상 정보 (그룹 Id → 대상)</summary>
|
|
private readonly Dictionary<string, RemoteTarget> _relayTargets = new Dictionary<string, RemoteTarget>(StringComparer.OrdinalIgnoreCase);
|
|
/// <summary>시작할 때 열려 있던 중계 기기 탭 (로그인 뒤 기기 목록을 받으면 다시 엶)</summary>
|
|
private readonly HashSet<string> _pendingRelayTabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private RemoteIndicatorWindow _indicator;
|
|
private readonly Dictionary<string, RemoteViewerWindow> _viewers = new Dictionary<string, RemoteViewerWindow>(StringComparer.OrdinalIgnoreCase);
|
|
private string _passiveKey = "";
|
|
private string _passiveListenKey = "";
|
|
|
|
private readonly TabControl _tabs;
|
|
private readonly TabItem _tabAll;
|
|
private readonly ComboBox _cmbAddTab;
|
|
private readonly CheckBox _chkAll;
|
|
private readonly TextBlock _txtCount, _txtLogHidden;
|
|
private readonly ContentControl _ccLog;
|
|
private readonly FrameworkElement _pnlShutdown;
|
|
private readonly Dictionary<string, TabItem> _hostTabs = new Dictionary<string, TabItem>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>원격 접속 허용 쪽에 넘겨줄 등록 서비스 목록 (다른 스레드에서 읽음)</summary>
|
|
private volatile IReadOnlyList<ServiceEntry> _servicesSnapshot = Array.Empty<ServiceEntry>();
|
|
private string _serverKey = "";
|
|
private string _targetsKey = "";
|
|
|
|
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 - 서비스 관리" + AppInstance.TitleTag;
|
|
FontFamily = new FontFamily("Malgun Gothic");
|
|
FontSize = 13;
|
|
Background = Brushes2.Make("#F5F7F8");
|
|
MinWidth = 860;
|
|
MinHeight = 560;
|
|
AppIcon.Apply(this);
|
|
RestoreWindowBounds(settings);
|
|
|
|
_root = XamlLoader.LoadElement("MainWindow.xaml");
|
|
Content = _root;
|
|
|
|
_tabs = _root.Find<TabControl>("tabHosts");
|
|
_tabAll = _root.Find<TabItem>("tabAll");
|
|
_cmbAddTab = _root.Find<ComboBox>("cmbAddTab");
|
|
_chkAll = _root.Find<CheckBox>("chkAll");
|
|
_txtCount = _root.Find<TextBlock>("txtCount");
|
|
_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, LogChoices);
|
|
|
|
// 내 PC
|
|
_local = new HostGroup
|
|
{
|
|
Id = HostGroup.LocalId,
|
|
Title = $"내 PC ({Environment.MachineName})",
|
|
EmptyText = "[추가] 버튼을 눌러 관리할 서비스를 등록하세요."
|
|
};
|
|
foreach (var e in settings.Services) _local.Rows.Add(new ServiceRow(e, _local));
|
|
_local.Rows.CollectionChanged += (s, e) => UpdateCounts();
|
|
_groups.Add(_local);
|
|
UpdateSnapshot();
|
|
|
|
_root.Find<ItemsControl>("icGroups").ItemsSource = _groups;
|
|
_cmbAddTab.ItemsSource = _groups;
|
|
_cmbAddTab.SelectionChanged += OnAddTabSelected;
|
|
_tabs.SelectionChanged += (s, e) => { if (e.Source == _tabs) 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();
|
|
_root.Find<Button>("btnExit").Click += (s, e) => ExitApp();
|
|
_chkAll.Click += (s, e) =>
|
|
{
|
|
bool v = _chkAll.IsChecked == true;
|
|
foreach (var r in VisibleRows()) r.IsChecked = v;
|
|
};
|
|
|
|
// 서비스 줄의 Start / Stop / Status / Log 버튼 (모든 탭)
|
|
_tabs.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowButton));
|
|
// 우클릭: 로그 경로 지정 메뉴
|
|
_tabs.AddHandler(UIElement.PreviewMouseRightButtonUpEvent, new MouseButtonEventHandler(OnRowRightClick), true);
|
|
|
|
PreviewKeyDown += OnPreviewKeyDown;
|
|
|
|
// 처음 뜰 때 다른 창 뒤에 가려지지 않도록 맨 앞으로
|
|
ContentRendered += (s, e) =>
|
|
{
|
|
Activate();
|
|
Topmost = true;
|
|
Topmost = false;
|
|
};
|
|
|
|
// 내 PC 상태 자동 갱신 (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;
|
|
|
|
// 원격 접속 허용 (대상 역할)
|
|
_remoteServer = new RemoteServer(
|
|
() => _servicesSnapshot,
|
|
(m, l) => Log(m, l),
|
|
() => Dispatcher.BeginInvoke(new Action(() => _ = RefreshStatuses(true))),
|
|
() => new ScreenPolicy(_settings.RemoteAllowView, _settings.RemoteAllowView && _settings.RemoteAllowControl, _settings.RemoteRequireApproval),
|
|
(who, control) => Dispatcher.InvokeAsync(() => ApprovalWindow.AskAsync(who, control)).Task.Unwrap());
|
|
_remoteServer.ScreenViewersChanged += list => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_indicator == null)
|
|
{
|
|
_indicator = new RemoteIndicatorWindow();
|
|
_indicator.DisconnectRequested += () =>
|
|
{
|
|
Log("[원격 화면] 이 PC에서 연결 끊기", LogLevel.Warning);
|
|
_remoteServer.StopAllScreens();
|
|
};
|
|
}
|
|
_indicator.Update(list);
|
|
}));
|
|
|
|
// 관리 PC: Passive 대상 PC의 접속을 받는 곳
|
|
_passiveHub = new PassiveHub((m, l) => Log(m, l));
|
|
|
|
// 중계 서버: 대상 PC 역할 (이 PC를 서버에 등록해 두고 연결 요청을 받음)
|
|
_relayAgent = new RelayAgent(_remoteServer, (m, l) => Log(m, l));
|
|
_relayAgent.StateChanged += a => Dispatcher.BeginInvoke(new Action(UpdateLocalStatus));
|
|
_relayAgent.NameChanged += name => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_settings.RelayDeviceName != name) { _settings.RelayDeviceName = name; SaveSettings(); }
|
|
}));
|
|
_relayAgent.Revoked += why => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
_relayAgentKey = "revoked";
|
|
UpdateLocalStatus();
|
|
}));
|
|
|
|
// 중계 서버: 관리 PC 역할 (로그인 → 권한 있는 기기를 자동으로 표시)
|
|
_relayManager = new RelayManager((m, l) => Log(m, l));
|
|
_relayManager.DevicesChanged += m => Dispatcher.BeginInvoke(new Action(SyncRelayGroups));
|
|
_relayManager.Presence += (id, online) => Dispatcher.BeginInvoke(new Action(() => OnRelayPresence(id, online)));
|
|
_relayManager.StateChanged += m => Dispatcher.BeginInvoke(new Action(SyncRelayGroups));
|
|
_relayManager.LoginRequired += msg => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
if (_relayLoginNotice == msg) return;
|
|
_relayLoginNotice = msg;
|
|
Log(msg, LogLevel.Warning);
|
|
}));
|
|
|
|
CreateTray();
|
|
|
|
Log("프로그램 시작" + (IsAdmin() ? " (관리자 권한)" : "") + AppInstance.TitleTag, LogLevel.Notice);
|
|
if (!IsAdmin()) Log("관리자 권한이 아닙니다. 서비스 Start/Stop이 실패할 수 있습니다.", LogLevel.Warning);
|
|
if (_local.Rows.Count == 0) Log("[추가] 버튼을 눌러 관리할 서비스를 등록하세요.");
|
|
|
|
ApplySettings(startup: true);
|
|
RestoreHostTabs();
|
|
UpdateCounts();
|
|
|
|
_ = 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 IEnumerable<ServiceRow> AllRows() => _groups.SelectMany(g => g.Rows);
|
|
|
|
/// <summary>지금 보고 있는 탭의 서비스</summary>
|
|
private IEnumerable<ServiceRow> VisibleRows()
|
|
{
|
|
if (_tabs.SelectedItem is TabItem t && t != _tabAll && t.Tag is string id)
|
|
return _groups.FirstOrDefault(g => g.Id == id)?.Rows ?? Enumerable.Empty<ServiceRow>();
|
|
return AllRows();
|
|
}
|
|
|
|
private void UpdateCounts()
|
|
{
|
|
var rows = VisibleRows().ToList();
|
|
int running = rows.Count(r => r.Status == ServiceControllerStatus.Running);
|
|
_txtCount.Text = rows.Count == 0 ? "" : $"{rows.Count}개 · 실행 중 {running}";
|
|
}
|
|
|
|
private void UpdateSnapshot()
|
|
=> _servicesSnapshot = _local.Rows.Select(r => r.Entry).ToList();
|
|
|
|
private void SyncEntriesFromRows()
|
|
{
|
|
_settings.Services = _local.Rows.Select(r => r.Entry).ToList();
|
|
UpdateSnapshot();
|
|
SaveSettings();
|
|
_logWindows.ChoicesChanged();
|
|
}
|
|
|
|
private void OnAddTabSelected(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_cmbAddTab.SelectedItem is not HostGroup g) return;
|
|
OpenHostTab(g.Id, select: true);
|
|
// 다시 고를 수 있도록 선택 해제
|
|
Dispatcher.BeginInvoke(new Action(() => _cmbAddTab.SelectedItem = null));
|
|
}
|
|
|
|
/// <summary>PC 하나만 보여주는 탭 열기</summary>
|
|
private void OpenHostTab(string id, bool select)
|
|
{
|
|
var group = _groups.FirstOrDefault(g => g.Id == id);
|
|
if (group == null) return;
|
|
|
|
if (_hostTabs.TryGetValue(id, out var existing))
|
|
{
|
|
if (select) _tabs.SelectedItem = existing;
|
|
return;
|
|
}
|
|
|
|
var title = new TextBlock { DataContext = group, VerticalAlignment = VerticalAlignment.Center };
|
|
title.SetBinding(TextBlock.TextProperty, new Binding(nameof(HostGroup.Title)));
|
|
|
|
var list = new ItemsControl
|
|
{
|
|
ItemTemplate = (DataTemplate)_root.FindResource("GroupTemplate"),
|
|
ItemsSource = new[] { group },
|
|
Margin = new Thickness(4)
|
|
};
|
|
var tab = new TabItem
|
|
{
|
|
Tag = id,
|
|
Content = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Content = list }
|
|
};
|
|
tab.Header = LogWindowManager.TabHeader(title, () => CloseHostTab(id));
|
|
|
|
_hostTabs[id] = tab;
|
|
_tabs.Items.Add(tab);
|
|
if (select) _tabs.SelectedItem = tab;
|
|
SaveOpenTabs();
|
|
}
|
|
|
|
private void CloseHostTab(string id, bool save = true)
|
|
{
|
|
if (!_hostTabs.TryGetValue(id, out var tab)) return;
|
|
if (_tabs.SelectedItem == tab) _tabs.SelectedItem = _tabAll;
|
|
_tabs.Items.Remove(tab);
|
|
_hostTabs.Remove(id);
|
|
if (save) SaveOpenTabs();
|
|
}
|
|
|
|
private void SaveOpenTabs()
|
|
{
|
|
_settings.OpenHostTabs = _hostTabs.Keys.Concat(_pendingRelayTabs).Distinct().ToList();
|
|
SaveSettings();
|
|
}
|
|
|
|
private void RestoreHostTabs()
|
|
{
|
|
foreach (var id in _settings.OpenHostTabs.ToList())
|
|
{
|
|
if (id.StartsWith(RelayGroupPrefix, StringComparison.OrdinalIgnoreCase) && !_groups.Any(g => g.Id == id))
|
|
_pendingRelayTabs.Add(id); // 로그인 뒤 기기 목록을 받으면 엶
|
|
else
|
|
OpenHostTab(id, select: false);
|
|
}
|
|
// 없어진 PC 탭은 정리 (중계 기기 탭은 유지)
|
|
_settings.OpenHostTabs = _hostTabs.Keys.Concat(_pendingRelayTabs).Distinct().ToList();
|
|
}
|
|
|
|
// ───────────────────────── 내 PC 서비스 추가 / 삭제 ─────────────────────────
|
|
|
|
private void AddServices()
|
|
{
|
|
if (_modalOpen) return;
|
|
var picker = new ServicePickerWindow(_local.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 = _local.Rows.Where(r => !chosenNames.Contains(r.ServiceName)).ToList();
|
|
foreach (var r in removed)
|
|
{
|
|
_local.Rows.Remove(r);
|
|
_logWindows.Close("local|" + r.ServiceName);
|
|
}
|
|
|
|
// 새로 체크된 항목 추가 (기존 순서 유지, 새 항목은 뒤에)
|
|
var existing = new HashSet<string>(_local.Rows.Select(r => r.ServiceName), StringComparer.OrdinalIgnoreCase);
|
|
var added = new List<string>();
|
|
foreach (var c in chosen)
|
|
{
|
|
if (existing.Contains(c.ServiceName)) continue;
|
|
_local.Rows.Add(new ServiceRow(new ServiceEntry { ServiceName = c.ServiceName, DisplayName = c.DisplayName }, _local));
|
|
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 checkedRows = AllRows().Where(r => r.IsChecked).ToList();
|
|
var targets = checkedRows.Where(r => !r.IsRemote).ToList();
|
|
if (targets.Count == 0)
|
|
{
|
|
Dialogs.Info(this, checkedRows.Count > 0
|
|
? "원격 PC의 서비스는 여기서 삭제할 수 없습니다. (대상 PC에서 관리)\n삭제할 내 PC 서비스를 체크하세요."
|
|
: "삭제할 서비스를 체크하세요.");
|
|
return;
|
|
}
|
|
var names = string.Join("\n", targets.Select(t => " · " + t.DisplayName));
|
|
var note = checkedRows.Count > targets.Count ? "\n\n체크한 원격 서비스는 삭제 대상에서 제외됩니다." : "";
|
|
if (!Dialogs.Confirm(this, $"아래 {targets.Count}개 서비스를 목록에서 삭제할까요?\n\n{names}\n\n(Windows 서비스 자체는 그대로 유지됩니다){note}", "삭제"))
|
|
return;
|
|
|
|
foreach (var t in targets)
|
|
{
|
|
_local.Rows.Remove(t);
|
|
_logWindows.Close("local|" + t.ServiceName);
|
|
}
|
|
_chkAll.IsChecked = false;
|
|
Log($"목록에서 삭제: {string.Join(", ", targets.Select(t => t.DisplayName))}");
|
|
SyncEntriesFromRows();
|
|
}
|
|
|
|
// ───────────────────────── 숫자 6자리 확인 ─────────────────────────
|
|
|
|
private bool NeedsCode(IEnumerable<ServiceRow> rows)
|
|
=> rows.Any(r => r.IsRemote ? _settings.ConfirmRemoteStartStop : _settings.ConfirmLocalStartStop);
|
|
|
|
/// <summary>설정에 따라 숫자 확인창을 띄움. 실행해도 되면 true</summary>
|
|
private bool ConfirmStartStop(bool start, IList<ServiceRow> rows)
|
|
{
|
|
if (!NeedsCode(rows)) return true;
|
|
if (_modalOpen) return false;
|
|
|
|
var what = start ? "시작" : "중지";
|
|
var list = string.Join("\n", rows.Take(8).Select(r => " · " + (r.IsRemote ? $"[{r.HostPrefix}] " : "") + r.DisplayName));
|
|
if (rows.Count > 8) list += $"\n · 외 {rows.Count - 8}개";
|
|
|
|
bool ok;
|
|
_modalOpen = true;
|
|
try
|
|
{
|
|
ok = CodeConfirmWindow.Ask(this, "서비스 " + what, $"아래 서비스를 {what}합니다.\n\n{list}", "서비스 " + what);
|
|
}
|
|
finally { _modalOpen = false; }
|
|
if (!ok) Log($"서비스 {what} 취소", LogLevel.Info);
|
|
return ok;
|
|
}
|
|
|
|
// ───────────────────────── Start / Stop / Status / Log ─────────────────────────
|
|
|
|
private async void OnRowButton(object sender, RoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is Button hb && hb.DataContext is HostGroup hg && hb.Tag as string == "remoteDesktop")
|
|
{
|
|
e.Handled = true;
|
|
OpenRemoteDesktop(hg);
|
|
return;
|
|
}
|
|
if (e.OriginalSource is Button pb && pb.DataContext is HostGroup pg && pb.Tag as string == "relayPassword")
|
|
{
|
|
e.Handled = true;
|
|
AskRelayPassword(pg);
|
|
return;
|
|
}
|
|
if (e.OriginalSource is not Button b || b.DataContext is not ServiceRow row) return;
|
|
switch (b.Tag as string)
|
|
{
|
|
case "start":
|
|
if (ConfirmStartStop(true, new[] { row })) await StartService(row);
|
|
break;
|
|
case "stop":
|
|
if (ConfirmStartStop(false, new[] { row })) await StopService(row);
|
|
break;
|
|
case "status": await ShowStatus(row); break;
|
|
case "log": OpenLog(row, b); break;
|
|
}
|
|
}
|
|
|
|
private Task StartService(ServiceRow row) => row.IsRemote ? RemoteStartStop(row, true) : LocalStartStop(row, true);
|
|
private Task StopService(ServiceRow row) => row.IsRemote ? RemoteStartStop(row, false) : LocalStartStop(row, false);
|
|
|
|
private async Task LocalStartStop(ServiceRow row, bool start)
|
|
{
|
|
if (row.IsBusy) return;
|
|
var what = start ? "시작" : "중지";
|
|
row.IsBusy = true;
|
|
Log($"{row.LogName} {what} 요청");
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
if (start) ServiceOps.Start(row.ServiceName, OpTimeout);
|
|
else ServiceOps.Stop(row.ServiceName, OpTimeout);
|
|
});
|
|
Log($"{row.LogName} {what} 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
|
|
}
|
|
catch (InfoException ex) { Log($"{row.LogName} {ex.Message}", LogLevel.Warning); }
|
|
catch (Exception ex) { Log($"{row.LogName} {what} 실패: {ServiceOps.Describe(ex)}", LogLevel.Error); }
|
|
finally
|
|
{
|
|
row.IsBusy = false;
|
|
await RefreshStatuses(true);
|
|
}
|
|
}
|
|
|
|
private async Task RemoteStartStop(ServiceRow row, bool start)
|
|
{
|
|
if (row.IsBusy) return;
|
|
var client = row.Host.Client;
|
|
var what = start ? "시작" : "중지";
|
|
row.IsBusy = true;
|
|
Log($"{row.LogName} {what} 요청 (원격)");
|
|
try
|
|
{
|
|
var r = start ? await client.StartServiceAsync(row.ServiceName) : await client.StopServiceAsync(row.ServiceName);
|
|
double sec = 0;
|
|
try { sec = r["seconds"]?.GetValue<double>() ?? 0; } catch { }
|
|
Log($"{row.LogName} {what} 완료 ({sec:0.0}초)", LogLevel.Success);
|
|
}
|
|
catch (RemoteException ex) when (ex.IsInfo) { Log($"{row.LogName} {ex.Message}", LogLevel.Warning); }
|
|
catch (Exception ex) { Log($"{row.LogName} {what} 실패: {RemoteClient.Friendly(ex)}", LogLevel.Error); }
|
|
finally
|
|
{
|
|
row.IsBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task RunChecked(bool start)
|
|
{
|
|
var targets = AllRows().Where(r => r.IsChecked).ToList();
|
|
var what = start ? "시작" : "중지";
|
|
if (targets.Count == 0)
|
|
{
|
|
Dialogs.Info(this, $"{what}할 서비스를 체크하세요.");
|
|
return;
|
|
}
|
|
// 여러 개를 한꺼번에 할 때는 숫자를 한 번만 입력
|
|
if (!ConfirmStartStop(start, targets)) return;
|
|
|
|
Log($"선택한 {targets.Count}개 서비스 {what}", 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 = row.IsRemote
|
|
? await row.Host.Client.GetDetailsAsync(row.ServiceName)
|
|
: await Task.Run(() => ServiceOps.GetDetails(row.ServiceName));
|
|
Log($"{row.LogName} 상태 정보", LogLevel.Notice);
|
|
foreach (var l in lines) Log(" " + l);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = row.IsRemote ? RemoteClient.Friendly(ex) : ServiceOps.Describe(ex);
|
|
Log($"{row.LogName} 상태 조회 실패: {msg}", LogLevel.Error);
|
|
}
|
|
if (!row.IsRemote) await RefreshStatuses(true);
|
|
}
|
|
|
|
private void OpenLog(ServiceRow row, FrameworkElement anchor)
|
|
{
|
|
if (row.IsRemote)
|
|
{
|
|
if (!row.HasLog)
|
|
{
|
|
Dialogs.Info(this, $"{row.Host.Title} 에서 [{row.DisplayName}]의 로그 경로가 지정되지 않았습니다.\n대상 PC의 JJServiceManager에서 Log 버튼으로 경로를 지정하세요.");
|
|
return;
|
|
}
|
|
_logWindows.Open(RemoteLogTarget(row));
|
|
return;
|
|
}
|
|
|
|
if (!row.HasLog)
|
|
{
|
|
// 경로가 없으면 파일/폴더 중 선택
|
|
ShowLogMenu(row, anchor, includeView: false);
|
|
return;
|
|
}
|
|
_logWindows.Open(LogTarget.ForLocal(row.Entry));
|
|
}
|
|
|
|
private static LogTarget RemoteLogTarget(ServiceRow row)
|
|
{
|
|
var client = row.Host.Client;
|
|
var entry = row.Entry;
|
|
var svc = row.ServiceName;
|
|
return new LogTarget
|
|
{
|
|
Key = $"remote|{row.Host.Id}|{svc}",
|
|
Title = $"{row.HostPrefix} - {row.DisplayName}",
|
|
PathText = () => "대상 PC " + (entry.LogIsFolder ? "폴더: " : "파일: ") + entry.LogPath,
|
|
IsLocal = false,
|
|
CreateSource = (t, output) => new RemoteLogSource(client, svc, () => t.Encoding, output)
|
|
};
|
|
}
|
|
|
|
// ───────────────────────── 원격관리 (화면 보기 / 제어) ─────────────────────────
|
|
|
|
private void OpenRemoteDesktop(HostGroup g)
|
|
{
|
|
if (g.IsLocal || g.Client == null) return;
|
|
if (_viewers.TryGetValue(g.Id, out var existing))
|
|
{
|
|
if (existing.WindowState == WindowState.Minimized) existing.WindowState = WindowState.Normal;
|
|
existing.Activate();
|
|
return;
|
|
}
|
|
if (g.Client.State != RemoteState.Connected)
|
|
{
|
|
Dialogs.Info(this, $"{g.Title} 에 연결되어 있지 않습니다.\n({g.StatusText})");
|
|
return;
|
|
}
|
|
if (!g.Client.AllowView)
|
|
{
|
|
// 중계 서버 권한이 없어서인지, 대상 PC 설정이 꺼져 있어서인지 구분
|
|
if (g.Client.RelayPerms is { } rp && !rp.screenView)
|
|
Dialogs.Info(this, $"{g.Title} 의 [화면 보기] 권한이 중계 서버에서 주어지지 않았습니다.\n중계 서버 관리자에게 [서버 관리 → 권한]에서 '화면 보기'를 요청하세요.");
|
|
else
|
|
Dialogs.Info(this, $"{g.Title} 에서 화면 보기를 허용하지 않았습니다.\n대상 PC 설정 → 원격 접속 허용 → 원격관리에서 '화면 보기 허용'을 켜야 합니다.");
|
|
return;
|
|
}
|
|
|
|
// 원격관리는 항상 숫자 6자리 확인
|
|
if (_modalOpen) return;
|
|
bool ok;
|
|
_modalOpen = true;
|
|
try
|
|
{
|
|
ok = CodeConfirmWindow.Ask(this, "원격관리",
|
|
$"{g.Title} 의 화면을 {(g.Client.AllowControl ? "보고 키보드·마우스로 제어" : "봅")}니다.",
|
|
"원격관리 시작");
|
|
}
|
|
finally { _modalOpen = false; }
|
|
if (!ok)
|
|
{
|
|
Log($"[{g.Prefix}] 원격관리 취소");
|
|
return;
|
|
}
|
|
|
|
Log($"[{g.Prefix}] 원격관리 시작 ({(g.Client.AllowControl ? "보기 + 제어" : "보기")})", LogLevel.Notice);
|
|
var v = new RemoteViewerWindow(g.Id, g.Client, g.Title);
|
|
v.Closed += (s, e) =>
|
|
{
|
|
_viewers.Remove(g.Id);
|
|
Log($"[{g.Prefix}] 원격관리 끝");
|
|
};
|
|
_viewers[g.Id] = v;
|
|
v.Show();
|
|
}
|
|
|
|
/// <summary>좌우 분할 창에서 고를 수 있는 로그 (내 PC + 원격)</summary>
|
|
private List<LogTarget> LogChoices()
|
|
{
|
|
var list = new List<LogTarget>();
|
|
foreach (var r in _local.Rows.Where(r => r.HasLog)) list.Add(LogTarget.ForLocal(r.Entry));
|
|
foreach (var g in _groups.Where(g => !g.IsLocal))
|
|
foreach (var r in g.Rows.Where(r => r.HasLog)) list.Add(RemoteLogTarget(r));
|
|
return list;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (row.IsRemote)
|
|
{
|
|
// 원격 서비스: 로그 경로는 대상 PC에서 지정
|
|
if (row.HasLog)
|
|
{
|
|
var menu = new ContextMenu { PlacementTarget = fe, Placement = PlacementMode.MousePoint };
|
|
var view = new MenuItem { Header = "로그 보기", FontWeight = FontWeights.Bold };
|
|
view.Click += (s, a) => _logWindows.Open(RemoteLogTarget(row));
|
|
menu.Items.Add(view);
|
|
menu.Items.Add(new MenuItem { Header = "대상 PC 로그: " + row.Entry.LogPath, IsEnabled = false });
|
|
menu.IsOpen = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
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 && row.HasLog)
|
|
{
|
|
var view = new MenuItem { Header = "로그 보기", FontWeight = FontWeights.Bold };
|
|
view.Click += (s, e) => _logWindows.Open(LogTarget.ForLocal(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 (row.HasLog)
|
|
{
|
|
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.LogName} 로그 경로 해제");
|
|
SyncEntriesFromRows();
|
|
_logWindows.LocalPathChanged(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 (row.HasLog)
|
|
{
|
|
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.LogName} 로그 파일 지정: {dlg.FileName}");
|
|
SyncEntriesFromRows();
|
|
_logWindows.LocalPathChanged(row.Entry);
|
|
_logWindows.Open(LogTarget.ForLocal(row.Entry));
|
|
}
|
|
|
|
private void PickLogFolder(ServiceRow row)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFolderDialog
|
|
{
|
|
Title = $"{row.DisplayName} - 로그 폴더 선택 (가장 최근 파일을 자동으로 표시)"
|
|
};
|
|
if (row.HasLog)
|
|
{
|
|
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.LogName} 로그 폴더 지정: {dlg.FolderName}");
|
|
SyncEntriesFromRows();
|
|
_logWindows.LocalPathChanged(row.Entry);
|
|
_logWindows.Open(LogTarget.ForLocal(row.Entry));
|
|
}
|
|
|
|
// ───────────────────────── 내 PC 상태 자동 갱신 ─────────────────────────
|
|
|
|
private async Task RefreshStatuses(bool force)
|
|
{
|
|
if (_refreshing && !force) return;
|
|
_refreshing = true;
|
|
try
|
|
{
|
|
var names = _local.Rows.Select(r => r.ServiceName).ToList();
|
|
var result = await Task.Run(() => names.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(n => n, n => ServiceOps.GetStatus(n), StringComparer.OrdinalIgnoreCase));
|
|
|
|
foreach (var r in _local.Rows)
|
|
{
|
|
if (!result.TryGetValue(r.ServiceName, out var st)) continue;
|
|
if (r.StatusKnown && r.Status != st && !r.IsBusy) LogStatusChange(r, r.Status, st);
|
|
r.Status = st;
|
|
}
|
|
UpdateCounts();
|
|
}
|
|
catch
|
|
{
|
|
// 다음 주기에 다시 시도
|
|
}
|
|
finally
|
|
{
|
|
_refreshing = false;
|
|
}
|
|
}
|
|
|
|
private void LogStatusChange(ServiceRow r, ServiceControllerStatus? from, ServiceControllerStatus? to)
|
|
{
|
|
Log($"{r.LogName} 상태 변경: {ServiceOps.StatusText(from)} → {ServiceOps.StatusText(to)}",
|
|
to == ServiceControllerStatus.Running ? LogLevel.Success
|
|
: to == ServiceControllerStatus.Stopped ? LogLevel.Warning : LogLevel.Info);
|
|
}
|
|
|
|
// ───────────────────────── 원격: 다른 PC 관리 (관리 역할) ─────────────────────────
|
|
|
|
private static string TargetsKey(AppSettings s)
|
|
=> (s.RemoteManageEnabled ? "on;" : "off;")
|
|
+ string.Join(";", s.Targets.Select(t => $"{t.Id}|{t.Direction}|{t.AccessCode}|{t.Address}|{t.Mode}|{t.Port}|{t.ProtectedPassword}"));
|
|
|
|
/// <summary>관리 대상 PC 목록이 바뀌면 접속을 새로 만듦</summary>
|
|
private void ApplyRemoteManage()
|
|
{
|
|
var key = TargetsKey(_settings);
|
|
if (key == _targetsKey) return;
|
|
_targetsKey = key;
|
|
|
|
// 기존 원격 PC 정리
|
|
_passiveHub.Clear();
|
|
foreach (var v in _viewers.Values.ToList()) v.Close();
|
|
foreach (var g in _groups.Where(g => !g.IsLocal && !IsRelayGroup(g)).ToList())
|
|
RemoveRemoteGroup(g);
|
|
|
|
if (_settings.RemoteManageEnabled)
|
|
{
|
|
// 중계 기기 그룹보다 앞에 넣음
|
|
int index = 1;
|
|
foreach (var t in _settings.Targets)
|
|
{
|
|
var client = new RemoteClient(t);
|
|
var g = CreateRemoteGroup(t.Id, t.Label, client, index++);
|
|
client.PeerAddressDiscovered += (c, addr) => Dispatcher.BeginInvoke(new Action(() => OnPeerAddress(g, addr)));
|
|
if (t.Direction == ConnectDirection.Passive) _passiveHub.Register(client);
|
|
client.Start();
|
|
}
|
|
}
|
|
|
|
// 열어둔 PC 탭: 없어진 PC는 닫고, 남은 PC는 새 목록으로 다시 엶
|
|
var open = _hostTabs.Keys.ToList();
|
|
foreach (var id in open) CloseHostTab(id, save: false);
|
|
foreach (var id in open) OpenHostTab(id, select: false);
|
|
SaveOpenTabs();
|
|
|
|
_logWindows.ChoicesChanged();
|
|
UpdateCounts();
|
|
}
|
|
|
|
/// <summary>원격 PC 그룹 만들기 (Active / Passive / 중계 공통)</summary>
|
|
private HostGroup CreateRemoteGroup(string id, string title, RemoteClient client, int index = -1)
|
|
{
|
|
var g = new HostGroup
|
|
{
|
|
Id = id,
|
|
Client = client,
|
|
Title = title,
|
|
StatusText = "연결 중…",
|
|
StatusBrush = Brushes2.Pending,
|
|
EmptyText = "연결 중…"
|
|
};
|
|
client.StateChanged += c =>
|
|
{
|
|
// 상태는 이벤트 시점 값으로 넘김 (UI에서 나중에 읽으면 연속 변경이 모두 같은 값으로 보임)
|
|
var st = c.State;
|
|
var stMsg = c.StateMessage;
|
|
Dispatcher.BeginInvoke(new Action(() => OnRemoteState(g, st, stMsg)));
|
|
};
|
|
client.ServicesChanged += c => Dispatcher.BeginInvoke(new Action(() => OnRemoteServices(g, c)));
|
|
client.HostnameDiscovered += (c, host) => Dispatcher.BeginInvoke(new Action(() => OnHostname(g, c, host)));
|
|
if (index >= 0 && index <= _groups.Count) _groups.Insert(index, g);
|
|
else _groups.Add(g);
|
|
return g;
|
|
}
|
|
|
|
private void RemoveRemoteGroup(HostGroup g)
|
|
{
|
|
if (_viewers.TryGetValue(g.Id, out var v)) v.Close();
|
|
g.Client?.Dispose();
|
|
_logWindows.Close($"remote|{g.Id}|", prefix: true);
|
|
_groups.Remove(g);
|
|
}
|
|
|
|
// ───────────────────────── 중계 서버 ─────────────────────────
|
|
|
|
private const string RelayGroupPrefix = "relay-";
|
|
private static bool IsRelayGroup(HostGroup g) => g.Id.StartsWith(RelayGroupPrefix, StringComparison.OrdinalIgnoreCase);
|
|
|
|
/// <summary>대상 PC 역할: 이 PC를 중계 서버에 등록해 두고 온라인 유지</summary>
|
|
private void ApplyRelayAgent()
|
|
{
|
|
var s = _settings;
|
|
bool hasPw = !string.IsNullOrEmpty(s.RemotePasswordHash);
|
|
bool need = s.RemoteServerEnabled && hasPw && s.RelayAgentEnabled && !string.IsNullOrEmpty(s.RelayDeviceId);
|
|
var key = need ? $"{s.RelayAgentUrl}|{s.RelayDeviceId}|{s.RelayDeviceKeyProtected}" : "off";
|
|
if (key != _relayAgentKey)
|
|
{
|
|
_relayAgentKey = key;
|
|
if (need)
|
|
{
|
|
_relayAgent.Start(s.RelayAgentUrl, s.RelayDeviceId, RemoteSecurity.Unprotect(s.RelayDeviceKeyProtected),
|
|
s.RelayServerPublicKey, RelayActiveListen);
|
|
}
|
|
else
|
|
{
|
|
_relayAgent.Stop();
|
|
}
|
|
}
|
|
else if (need)
|
|
{
|
|
_relayAgent.SendUpdate(); // Active 수신 정보가 바뀌었을 수 있음
|
|
}
|
|
if (!need && !string.IsNullOrEmpty(s.RelayServerPublicKey)) _remoteServer.SetRelayTrust(s.RelayServerPublicKey, s.RelayDeviceId);
|
|
}
|
|
|
|
/// <summary>중계 서버에 알릴 Active 수신 정보 (직접 연결 후보)</summary>
|
|
private JsonObject RelayActiveListen()
|
|
{
|
|
if (!_remoteServer.IsRunning) return null;
|
|
return new JsonObject { ["mode"] = _remoteServer.Mode == RemoteMode.Ssh ? "Ssh" : "Port", ["port"] = _remoteServer.Port };
|
|
}
|
|
|
|
/// <summary>관리 역할: 중계 서버 로그인 유지</summary>
|
|
private void ApplyRelayManage()
|
|
{
|
|
var s = _settings;
|
|
bool need = s.RemoteManageEnabled && s.RelayManageEnabled
|
|
&& (!string.IsNullOrEmpty(s.RelayTokenProtected) || !string.IsNullOrEmpty(s.RelayPasswordProtected));
|
|
var key = need ? $"{s.RelayManageUrl}|{s.RelayUsername}|{s.RelayTokenProtected}|{s.RelayPasswordProtected}" : "off";
|
|
if (key == _relayManageKey) return;
|
|
_relayManageKey = key;
|
|
_relayLoginNotice = null;
|
|
if (need)
|
|
{
|
|
_relayManager.Start(s.RelayManageUrl, s.RelayUsername,
|
|
() => RemoteSecurity.Unprotect(_settings.RelayTokenProtected),
|
|
() => RemoteSecurity.Unprotect(_settings.RelayPasswordProtected),
|
|
token => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
_settings.RelayTokenProtected = RemoteSecurity.Protect(token);
|
|
_relayManageKey = $"{_settings.RelayManageUrl}|{_settings.RelayUsername}|{_settings.RelayTokenProtected}|{_settings.RelayPasswordProtected}";
|
|
SaveSettings();
|
|
})));
|
|
}
|
|
else
|
|
{
|
|
_relayManager.Stop();
|
|
}
|
|
SyncRelayGroups();
|
|
}
|
|
|
|
/// <summary>중계 서버의 기기 목록 → 초기화면 그룹 (권한 있는 기기만)</summary>
|
|
private readonly Dictionary<string, string> _relayPermSig = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
private static string PermSig(RelayDevice d) => $"{d.View}{d.Control}{d.ScreenView}{d.ScreenControl}";
|
|
|
|
private void SyncRelayGroups()
|
|
{
|
|
bool active = _settings.RemoteManageEnabled && _settings.RelayManageEnabled && _relayManageKey != "off" && _relayManageKey != "";
|
|
var devices = active ? _relayManager.Devices : Array.Empty<RelayDevice>();
|
|
var wanted = devices.Where(d => d.Enabled).ToDictionary(d => RelayGroupPrefix + d.Id, StringComparer.OrdinalIgnoreCase);
|
|
|
|
// 없어진 기기
|
|
foreach (var g in _groups.Where(IsRelayGroup).ToList())
|
|
{
|
|
if (wanted.ContainsKey(g.Id)) continue;
|
|
// 서버 연결이 잠깐 끊긴 경우(목록이 비어 있음)에는 그대로 둠
|
|
if (active && devices.Count == 0 && !_relayManager.IsLoggedIn) continue;
|
|
RemoveRemoteGroup(g);
|
|
_relayTargets.Remove(g.Id);
|
|
_relayPermSig.Remove(g.Id);
|
|
if (_hostTabs.ContainsKey(g.Id)) CloseHostTab(g.Id, save: false);
|
|
}
|
|
|
|
foreach (var kv in wanted)
|
|
{
|
|
var d = kv.Value;
|
|
var gid = kv.Key;
|
|
var existing = _groups.FirstOrDefault(x => x.Id == gid);
|
|
if (existing != null)
|
|
{
|
|
// 이름이 바뀌었으면 제목 갱신
|
|
if (_relayTargets.TryGetValue(gid, out var t0) && (t0.RelayName != d.Name))
|
|
{
|
|
t0.RelayName = d.Name;
|
|
existing.Title = t0.Label;
|
|
existing.TitleChanged();
|
|
}
|
|
// 권한이 바뀌었으면: 대상 PC가 옛 티켓 연결을 끊음 → 기다리지 않고 새 티켓으로 바로 다시 연결
|
|
bool permChanged = _relayPermSig.TryGetValue(gid, out var sig0) && sig0 != PermSig(d);
|
|
_relayPermSig[gid] = PermSig(d);
|
|
if (d.Online && (permChanged || existing.Client?.State != RemoteState.Connected)) existing.Client?.Nudge();
|
|
continue;
|
|
}
|
|
|
|
var t = new RemoteTarget
|
|
{
|
|
Id = gid,
|
|
Direction = ConnectDirection.Relay,
|
|
RelayDeviceId = d.Id,
|
|
RelayName = d.Name,
|
|
Hostname = d.Hostname
|
|
};
|
|
_relayTargets[gid] = t;
|
|
_relayPermSig[gid] = PermSig(d);
|
|
var deviceId = d.Id;
|
|
var client = new RemoteClient(t, () =>
|
|
_settings.RelayDevicePasswords.TryGetValue(deviceId, out var p) ? RemoteSecurity.Unprotect(p) : "", _relayManager);
|
|
var g = CreateRemoteGroup(gid, t.Label, client);
|
|
client.RelayModeChanged += c => Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
t.RelayMode = c.RelayMode;
|
|
g.Title = t.Label;
|
|
g.TitleChanged();
|
|
_logWindows.ChoicesChanged();
|
|
}));
|
|
client.Start();
|
|
if (_pendingRelayTabs.Remove(gid)) OpenHostTab(gid, select: false);
|
|
}
|
|
|
|
_logWindows.ChoicesChanged();
|
|
UpdateCounts();
|
|
}
|
|
|
|
private void OnRelayPresence(string deviceId, bool online)
|
|
{
|
|
var g = _groups.FirstOrDefault(x => x.Id == RelayGroupPrefix + deviceId);
|
|
if (g == null) return;
|
|
if (online) g.Client?.Nudge();
|
|
}
|
|
|
|
/// <summary>중계 기기의 대상 PC 접속 비밀번호 입력 (기기마다 저장)</summary>
|
|
private void AskRelayPassword(HostGroup g)
|
|
{
|
|
if (!_relayTargets.TryGetValue(g.Id, out var t) || _modalOpen) return;
|
|
string pw;
|
|
_modalOpen = true;
|
|
try
|
|
{
|
|
pw = TextPrompt.Ask(this, "대상 PC 접속 비밀번호",
|
|
$"{g.Title}\n\n대상 PC의 [원격 접속 허용] 접속 비밀번호를 입력하세요.\n(이 PC에 Windows 계정으로 암호화해 저장합니다)", "", password: true);
|
|
}
|
|
finally { _modalOpen = false; }
|
|
if (string.IsNullOrEmpty(pw)) return;
|
|
_settings.RelayDevicePasswords[t.RelayDeviceId] = RemoteSecurity.Protect(pw);
|
|
SaveSettings();
|
|
Log($"[{g.Prefix}] 접속 비밀번호 저장");
|
|
g.Client?.Nudge();
|
|
}
|
|
|
|
/// <summary>내 PC 제목 줄의 원격 접속 허용 상태</summary>
|
|
private void UpdateLocalStatus()
|
|
{
|
|
var s = _settings;
|
|
var parts = new List<string>();
|
|
bool passive = s.RemoteServerEnabled && !string.IsNullOrEmpty(s.RemotePasswordHash) && s.PassiveManagers.Count > 0;
|
|
if (_remoteServer.IsRunning) parts.Add($"Active {(s.RemoteServerMode == RemoteMode.Ssh ? "SSH" : "지정 포트")} {s.RemoteServerPort}");
|
|
if (passive) parts.Add($"Passive {s.PassiveManagers.Count}곳");
|
|
if (_relayAgentKey == "revoked") parts.Add("중계 서버: 다시 등록 필요");
|
|
else if (_relayAgentKey != "off" && _relayAgentKey != "") parts.Add(_relayAgent.IsOnline ? "중계 서버 온라인" : "중계 서버 연결 안 됨");
|
|
if (s.RemoteServerEnabled && s.RemoteAllowView) parts.Add(s.RemoteAllowControl ? "화면 보기·제어 허용" : "화면 보기 허용");
|
|
_local.StatusText = parts.Count > 0 ? "원격 접속 허용 중 · " + string.Join(" · ", parts) : "";
|
|
_local.StatusBrush = _relayAgentKey == "revoked" ? Brushes2.Stopped : Brushes2.Running;
|
|
}
|
|
|
|
/// <summary>Passive 대상이 있으면 수신 포트를 엶 (방화벽 포함)</summary>
|
|
private void ApplyPassiveListen(bool startup)
|
|
{
|
|
var s = _settings;
|
|
bool need = s.RemoteManageEnabled && s.Targets.Any(t => t.Direction == ConnectDirection.Passive);
|
|
var key = need ? $"{s.PassiveListenMode}|{s.PassiveListenPort}" : "off";
|
|
if (key == _passiveListenKey) return;
|
|
bool wasOn = _passiveListenKey != "" && _passiveListenKey != "off";
|
|
_passiveListenKey = key;
|
|
_passiveHub.Apply(need, s.PassiveListenMode, s.PassiveListenPort);
|
|
if (need || wasOn)
|
|
{
|
|
bool on = need && _passiveHub.IsRunning;
|
|
int port = s.PassiveListenPort;
|
|
Task.Run(() =>
|
|
{
|
|
var (ok, msg) = FirewallManager.Apply(FirewallManager.PassiveRule, on, port);
|
|
if (!startup || !ok) Log(msg, ok ? LogLevel.Info : LogLevel.Warning);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>Passive: 대상 PC가 접속해 온 IP 표시</summary>
|
|
private void OnPeerAddress(HostGroup g, string addr)
|
|
{
|
|
var t = _settings.Targets.FirstOrDefault(x => x.Id == g.Id);
|
|
if (t == null || string.IsNullOrEmpty(addr)) return;
|
|
if (t.LastAddress != addr)
|
|
{
|
|
t.LastAddress = addr;
|
|
SaveSettings();
|
|
}
|
|
g.Title = t.Label;
|
|
g.TitleChanged();
|
|
_logWindows.ChoicesChanged();
|
|
}
|
|
|
|
private void OnRemoteState(HostGroup g, RemoteState state, string stateMessage)
|
|
{
|
|
if (!_groups.Contains(g)) return;
|
|
switch (state)
|
|
{
|
|
case RemoteState.Connected:
|
|
g.StatusText = "연결됨";
|
|
g.StatusBrush = Brushes2.Running;
|
|
g.EmptyText = "대상 PC에 등록된 서비스가 없습니다.";
|
|
Log($"[{g.Prefix}] 연결됨", LogLevel.Success);
|
|
break;
|
|
case RemoteState.Connecting:
|
|
g.StatusText = "연결 중…";
|
|
g.StatusBrush = Brushes2.Pending;
|
|
break;
|
|
default:
|
|
bool wasConnected = g.Rows.Any(r => r.StatusKnown);
|
|
g.StatusText = (state == RemoteState.AuthFailed ? "인증 실패: " : "연결 안 됨: ") + stateMessage;
|
|
g.StatusBrush = Brushes2.Stopped;
|
|
g.EmptyText = g.StatusText;
|
|
foreach (var r in g.Rows) r.MarkUnknown();
|
|
// 같은 실패 메시지는 처음 한 번만 기록
|
|
if (wasConnected || g.Tag != stateMessage)
|
|
Log($"[{g.Prefix}] {g.StatusText}", state == RemoteState.AuthFailed ? LogLevel.Error : LogLevel.Warning);
|
|
g.Tag = stateMessage;
|
|
break;
|
|
}
|
|
if (state == RemoteState.Connected) g.Tag = null;
|
|
UpdateCounts();
|
|
}
|
|
|
|
private void OnRemoteServices(HostGroup g, RemoteClient c)
|
|
{
|
|
if (!_groups.Contains(g)) return;
|
|
var list = c.Services;
|
|
var names = new HashSet<string>(list.Select(s => s.ServiceName), StringComparer.OrdinalIgnoreCase);
|
|
bool changed = false;
|
|
|
|
// 대상 PC에서 빠진 서비스
|
|
foreach (var r in g.Rows.Where(r => !names.Contains(r.ServiceName)).ToList())
|
|
{
|
|
g.Rows.Remove(r);
|
|
_logWindows.Close($"remote|{g.Id}|{r.ServiceName}");
|
|
changed = true;
|
|
}
|
|
|
|
// 새로 생기거나 바뀐 서비스
|
|
int index = 0;
|
|
foreach (var info in list)
|
|
{
|
|
var row = g.Rows.FirstOrDefault(r => string.Equals(r.ServiceName, info.ServiceName, StringComparison.OrdinalIgnoreCase));
|
|
if (row == null)
|
|
{
|
|
row = new ServiceRow(new ServiceEntry { ServiceName = info.ServiceName, DisplayName = info.DisplayName }, g);
|
|
g.Rows.Insert(Math.Min(index, g.Rows.Count), row);
|
|
row.UpdateFromRemote(info);
|
|
changed = true;
|
|
}
|
|
else
|
|
{
|
|
var before = row.StatusKnown ? row.Status : null;
|
|
bool known = row.StatusKnown;
|
|
bool hadLog = row.HasLog;
|
|
row.UpdateFromRemote(info);
|
|
if (known && before != row.Status && !row.IsBusy) LogStatusChange(row, before, row.Status);
|
|
if (hadLog != row.HasLog) changed = true;
|
|
}
|
|
index++;
|
|
}
|
|
|
|
if (changed) _logWindows.ChoicesChanged();
|
|
UpdateCounts();
|
|
}
|
|
|
|
/// <summary>hostname 자동 등록</summary>
|
|
private void OnHostname(HostGroup g, RemoteClient c, string host)
|
|
{
|
|
if (_relayTargets.TryGetValue(g.Id, out var rt))
|
|
{
|
|
rt.Hostname = host;
|
|
g.Title = rt.Label;
|
|
g.TitleChanged();
|
|
_logWindows.ChoicesChanged();
|
|
return;
|
|
}
|
|
var t = _settings.Targets.FirstOrDefault(x => x.Id == g.Id);
|
|
if (t == null) return;
|
|
if (t.Hostname != host)
|
|
{
|
|
Log($"[{t.Address}] hostname 자동 등록: {host}");
|
|
t.Hostname = host;
|
|
SaveSettings();
|
|
}
|
|
g.Title = t.Label;
|
|
g.TitleChanged();
|
|
_logWindows.ChoicesChanged();
|
|
}
|
|
|
|
// ───────────────────────── 원격: 원격 접속 허용 (대상 역할) ─────────────────────────
|
|
|
|
private void ApplyRemoteServer(bool startup)
|
|
{
|
|
var s = _settings;
|
|
bool hasPw = !string.IsNullOrEmpty(s.RemotePasswordHash);
|
|
// 비밀번호는 Active / Passive 모두 사용하므로 항상 반영
|
|
_remoteServer.UpdatePassword(s.RemotePasswordHash, s.RemotePasswordSalt);
|
|
|
|
if (s.RemoteServerEnabled && !hasPw)
|
|
Log("원격 접속 허용: 접속 비밀번호가 없어 시작하지 않았습니다. (설정 → 원격 접속 허용)", LogLevel.Warning);
|
|
|
|
// ── Active 수신
|
|
bool active = s.RemoteServerEnabled && hasPw && s.RemoteServerActiveListen;
|
|
var key = active ? $"{s.RemoteServerMode}|{s.RemoteServerPort}" : "off";
|
|
if (key != _serverKey)
|
|
{
|
|
bool wasOn = _serverKey != "" && _serverKey != "off";
|
|
_serverKey = key;
|
|
if (active) _remoteServer.Start(s.RemoteServerMode, s.RemoteServerPort, s.RemotePasswordHash, s.RemotePasswordSalt);
|
|
else _remoteServer.Stop();
|
|
|
|
// 방화벽: 켤 때 포트 열기, 끌 때 규칙 삭제 (처음 실행 시 꺼져 있으면 건드리지 않음)
|
|
if (active || wasOn || !startup)
|
|
{
|
|
bool on = active && _remoteServer.IsRunning;
|
|
int port = s.RemoteServerPort;
|
|
Task.Run(() =>
|
|
{
|
|
var (ok, msg) = FirewallManager.Apply(FirewallManager.ActiveRule, on, port);
|
|
if (!startup || !ok) Log(msg, ok ? LogLevel.Info : LogLevel.Warning);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Passive 접속 (이 PC → 관리 PC)
|
|
bool passive = s.RemoteServerEnabled && hasPw && s.PassiveManagers.Count > 0;
|
|
var pkey = passive ? string.Join(";", s.PassiveManagers.Select(m => $"{m.Address}|{m.Mode}|{m.Port}|{m.AccessCode}")) : "off";
|
|
if (pkey != _passiveKey)
|
|
{
|
|
_passiveKey = pkey;
|
|
if (passive)
|
|
{
|
|
_remoteServer.StartPassive(s.PassiveManagers);
|
|
Log($"Passive 접속 시작: 관리 PC {s.PassiveManagers.Count}곳", LogLevel.Info);
|
|
}
|
|
else _remoteServer.StopPassive();
|
|
}
|
|
|
|
// ── 중계 서버 (이 PC를 등록해 두고 연결 요청 받기)
|
|
ApplyRelayAgent();
|
|
|
|
// ── 원격관리 허용을 끄면 보고 있던 연결은 다음 장면에서 자동으로 끊김
|
|
UpdateLocalStatus();
|
|
}
|
|
|
|
// ───────────────────────── Windows 종료 ─────────────────────────
|
|
|
|
private void Shutdown(bool restart)
|
|
{
|
|
var what = restart ? "재시작" : "강제 종료";
|
|
if (_modalOpen) return;
|
|
bool confirmed;
|
|
_modalOpen = true;
|
|
try
|
|
{
|
|
confirmed = CodeConfirmWindow.Ask(this, "Windows " + what,
|
|
$"Windows를 지금 {what}합니다.\n저장하지 않은 작업은 모두 사라집니다.",
|
|
restart ? "Windows 재시작" : "Windows 강제종료");
|
|
}
|
|
finally { _modalOpen = false; }
|
|
if (!confirmed)
|
|
{
|
|
Log($"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, _relayManager) { 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);
|
|
ApplyRemoteServer(startup);
|
|
ApplyRemoteManage();
|
|
ApplyRelayManage();
|
|
ApplyPassiveListen(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 = Trim63(_settings.HotkeyEnabled
|
|
? $"JJServiceManager{AppInstance.TitleTag} ({HotkeyManager.Format(mods, key)})"
|
|
: "JJServiceManager" + AppInstance.TitleTag);
|
|
}
|
|
|
|
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 = Trim63("JJServiceManager" + AppInstance.TitleTag),
|
|
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" + AppInstance.TitleTag, "트레이에서 계속 실행 중입니다." + 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();
|
|
foreach (var v in _viewers.Values.ToList()) v.Close();
|
|
foreach (var g in _groups) g.Client?.Dispose();
|
|
_relayManager.Dispose();
|
|
_relayAgent.Dispose();
|
|
_passiveHub.Dispose();
|
|
_remoteServer.Dispose();
|
|
_indicator?.Close();
|
|
_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; }
|
|
}
|
|
}
|
|
}
|