using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; namespace JJServiceManager { /// 바인딩용 기본 클래스 public abstract class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool Set(ref T field, T value, [CallerMemberName] string name = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(name); return true; } protected void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } // ───────────────────────────────────────────────────────────── // [테스트용 중복 실행] --instance N 옵션 — 정식 릴리즈 시 이 클래스와 사용하는 곳을 삭제 // ───────────────────────────────────────────────────────────── public static class AppInstance { /// 1 = 기본 실행, 2 이상 = 테스트용 추가 실행 public static int Number { get; private set; } = 1; public static bool IsTest => Number > 1; /// 이름 뒤에 붙는 꼬리표 ("" 또는 "_instance2") public static string Suffix => IsTest ? "_instance" + Number : ""; /// 창 제목에 붙는 표시 public static string TitleTag => IsTest ? $" [테스트 인스턴스 {Number}]" : ""; public static void Parse(string[] args) { for (int i = 0; i < args.Length - 1; i++) { if (string.Equals(args[i], "--instance", StringComparison.OrdinalIgnoreCase) && int.TryParse(args[i + 1], out int n) && n >= 1 && n <= 9) { Number = n; } } } } /// 원격 통신 방식 public enum RemoteMode { Ssh = 0, // 내장 SSH (기본 2222) Port = 1 // 지정 포트 + TLS (기본 9988) } /// 관리 대상 PC public class RemoteTarget { public string Id { get; set; } = Guid.NewGuid().ToString("N"); public string Address { get; set; } = ""; /// 접속할 때 자동으로 채워짐 public string Hostname { get; set; } = ""; public RemoteMode Mode { get; set; } = RemoteMode.Port; public int Port { get; set; } = 9988; /// 접속 비밀번호 (Windows 사용자 계정으로 암호화, Base64) public string ProtectedPassword { get; set; } = ""; [JsonIgnore] public string Label => string.IsNullOrWhiteSpace(Hostname) ? Address : $"{Address} / {Hostname}"; public RemoteTarget Clone() => (RemoteTarget)MemberwiseClone(); } /// 로그창 띄우기 방식 public enum LogWindowMode { Individual = 0, // 개별 창 Tabs = 1, // 하나의 창에 탭 Split = 2 // 창을 좌우로 나누어 비교 } /// Esc 키 동작 public enum EscAction { Tray = 0, // 트레이로 (백그라운드) Exit = 1 // 완전 종료 } /// 등록된 서비스 한 건 public class ServiceEntry { public string ServiceName { get; set; } = ""; public string DisplayName { get; set; } = ""; /// 로그 파일 경로 또는 로그 폴더 경로 (없으면 빈 값) public string LogPath { get; set; } = ""; /// true면 LogPath는 폴더이며, 그 안의 가장 최근 수정 파일을 표시 public bool LogIsFolder { get; set; } /// 로그 인코딩: auto / utf-8 / cp949 public string LogEncoding { get; set; } = "auto"; } /// 프로그램 설정 (settings.json) public class AppSettings { public List Services { get; set; } = new List(); /// 컴퓨터 시작 시 기동 public bool RunAtStartup { get; set; } /// 단축키 사용 여부 public bool HotkeyEnabled { get; set; } = true; /// 단축키 보조키 (System.Windows.Input.ModifierKeys 값) public int HotkeyModifiers { get; set; } = 2 | 4; // Ctrl + Shift /// 단축키 키 (System.Windows.Input.Key 값) public int HotkeyKey { get; set; } = 44; // Key.A /// 서비스 시작 시 로그보기 = 작업 로그를 화면 하단에 표시할지 여부 public bool ShowOperationLog { get; set; } = true; /// 로그창 띄우기 방식 public LogWindowMode LogWindowMode { get; set; } = LogWindowMode.Individual; /// Esc 키 동작 public EscAction EscAction { get; set; } = EscAction.Tray; /// Windows 강제종료 / 재시작 버튼 표시 public bool ShowShutdownButtons { get; set; } = true; // ───────── 숫자 6자리 확인 ───────── /// 내 PC 서비스 Start/Stop 시 숫자 확인 public bool ConfirmLocalStartStop { get; set; } = false; /// 원격 서비스 Start/Stop 시 숫자 확인 public bool ConfirmRemoteStartStop { get; set; } = true; // ───────── 원격 접속 허용 (대상 역할) ───────── public bool RemoteServerEnabled { get; set; } public RemoteMode RemoteServerMode { get; set; } = RemoteMode.Port; public int RemoteServerTcpPort { get; set; } = 9988 + AppInstance.Number - 1; public int RemoteServerSshPort { get; set; } = 2222 + AppInstance.Number - 1; /// 접속 비밀번호 해시 (PBKDF2) public string RemotePasswordHash { get; set; } = ""; public string RemotePasswordSalt { get; set; } = ""; [JsonIgnore] public int RemoteServerPort => RemoteServerMode == RemoteMode.Ssh ? RemoteServerSshPort : RemoteServerTcpPort; // ───────── 다른 PC 관리 (관리 역할) ───────── public bool RemoteManageEnabled { get; set; } public List Targets { get; set; } = new List(); /// 초기화면에 열어둔 PC 탭 ("local" 또는 대상 Id) public List OpenHostTabs { get; set; } = new List(); /// 창 크기·위치 public double WindowLeft { get; set; } = double.NaN; public double WindowTop { get; set; } = double.NaN; public double WindowWidth { get; set; } = 1000; public double WindowHeight { get; set; } = 720; } /// 설정 파일 저장/불러오기 public static class SettingsStore { // [테스트용 중복 실행] 인스턴스마다 설정 폴더를 따로 씀 (정식 릴리즈 시 Suffix 제거) public static string DataFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "JJServiceManager" + AppInstance.Suffix); public static string SettingsPath => Path.Combine(DataFolder, "settings.json"); private static readonly JsonSerializerOptions Options = new JsonSerializerOptions { WriteIndented = true, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, Converters = { new JsonStringEnumConverter() } }; public static AppSettings Load() { try { if (File.Exists(SettingsPath)) { var json = File.ReadAllText(SettingsPath); var s = JsonSerializer.Deserialize(json, Options); if (s != null) { s.Services ??= new List(); s.Targets ??= new List(); s.OpenHostTabs ??= new List(); return s; } } } catch { // 설정 파일이 손상된 경우 기본값 사용 (원본 파일은 지우지 않음) } return new AppSettings(); } public static void Save(AppSettings settings) { try { Directory.CreateDirectory(DataFolder); var tmp = SettingsPath + ".tmp"; File.WriteAllText(tmp, JsonSerializer.Serialize(settings, Options)); File.Copy(tmp, SettingsPath, true); File.Delete(tmp); } catch { // 저장 실패는 무시 (다음 저장 때 다시 시도) } } } /// 작업 로그를 날짜별 파일에 추가 기록 (삭제 기능 없음) public static class OperationLogFile { private static readonly object Sync = new object(); public static string Folder => Path.Combine(SettingsStore.DataFolder, "logs"); public static void Append(DateTime time, string text) { try { lock (Sync) { Directory.CreateDirectory(Folder); var path = Path.Combine(Folder, time.ToString("yyyy-MM-dd") + ".log"); File.AppendAllText(path, $"[{time:yyyy-MM-dd HH:mm:ss}] {text}{Environment.NewLine}"); } } catch { // 기록 실패는 무시 } } } }