JJServiceManagerEnt/Core/Models.cs

260 lines
10 KiB
C#

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
{
/// <summary>바인딩용 기본 클래스</summary>
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (EqualityComparer<T>.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
{
/// <summary>1 = 기본 실행, 2 이상 = 테스트용 추가 실행</summary>
public static int Number { get; private set; } = 1;
public static bool IsTest => Number > 1;
/// <summary>이름 뒤에 붙는 꼬리표 ("" 또는 "_instance2")</summary>
public static string Suffix => IsTest ? "_instance" + Number : "";
/// <summary>창 제목에 붙는 표시</summary>
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;
}
}
}
}
/// <summary>원격 통신 방식</summary>
public enum RemoteMode
{
Ssh = 0, // 내장 SSH (기본 2222)
Port = 1 // 지정 포트 + TLS (기본 9988)
}
/// <summary>관리 대상 PC</summary>
public class RemoteTarget
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string Address { get; set; } = "";
/// <summary>접속할 때 자동으로 채워짐</summary>
public string Hostname { get; set; } = "";
public RemoteMode Mode { get; set; } = RemoteMode.Port;
public int Port { get; set; } = 9988;
/// <summary>접속 비밀번호 (Windows 사용자 계정으로 암호화, Base64)</summary>
public string ProtectedPassword { get; set; } = "";
[JsonIgnore]
public string Label => string.IsNullOrWhiteSpace(Hostname) ? Address : $"{Address} / {Hostname}";
public RemoteTarget Clone() => (RemoteTarget)MemberwiseClone();
}
/// <summary>로그창 띄우기 방식</summary>
public enum LogWindowMode
{
Individual = 0, // 개별 창
Tabs = 1, // 하나의 창에 탭
Split = 2 // 창을 좌우로 나누어 비교
}
/// <summary>Esc 키 동작</summary>
public enum EscAction
{
Tray = 0, // 트레이로 (백그라운드)
Exit = 1 // 완전 종료
}
/// <summary>등록된 서비스 한 건</summary>
public class ServiceEntry
{
public string ServiceName { get; set; } = "";
public string DisplayName { get; set; } = "";
/// <summary>로그 파일 경로 또는 로그 폴더 경로 (없으면 빈 값)</summary>
public string LogPath { get; set; } = "";
/// <summary>true면 LogPath는 폴더이며, 그 안의 가장 최근 수정 파일을 표시</summary>
public bool LogIsFolder { get; set; }
/// <summary>로그 인코딩: auto / utf-8 / cp949</summary>
public string LogEncoding { get; set; } = "auto";
}
/// <summary>프로그램 설정 (settings.json)</summary>
public class AppSettings
{
public List<ServiceEntry> Services { get; set; } = new List<ServiceEntry>();
/// <summary>컴퓨터 시작 시 기동</summary>
public bool RunAtStartup { get; set; }
/// <summary>단축키 사용 여부</summary>
public bool HotkeyEnabled { get; set; } = true;
/// <summary>단축키 보조키 (System.Windows.Input.ModifierKeys 값)</summary>
public int HotkeyModifiers { get; set; } = 2 | 4; // Ctrl + Shift
/// <summary>단축키 키 (System.Windows.Input.Key 값)</summary>
public int HotkeyKey { get; set; } = 44; // Key.A
/// <summary>서비스 시작 시 로그보기 = 작업 로그를 화면 하단에 표시할지 여부</summary>
public bool ShowOperationLog { get; set; } = true;
/// <summary>로그창 띄우기 방식</summary>
public LogWindowMode LogWindowMode { get; set; } = LogWindowMode.Individual;
/// <summary>Esc 키 동작</summary>
public EscAction EscAction { get; set; } = EscAction.Tray;
/// <summary>Windows 강제종료 / 재시작 버튼 표시</summary>
public bool ShowShutdownButtons { get; set; } = true;
// ───────── 숫자 6자리 확인 ─────────
/// <summary>내 PC 서비스 Start/Stop 시 숫자 확인</summary>
public bool ConfirmLocalStartStop { get; set; } = false;
/// <summary>원격 서비스 Start/Stop 시 숫자 확인</summary>
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;
/// <summary>접속 비밀번호 해시 (PBKDF2)</summary>
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<RemoteTarget> Targets { get; set; } = new List<RemoteTarget>();
/// <summary>초기화면에 열어둔 PC 탭 ("local" 또는 대상 Id)</summary>
public List<string> OpenHostTabs { get; set; } = new List<string>();
/// <summary>창 크기·위치</summary>
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;
}
/// <summary>설정 파일 저장/불러오기</summary>
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<AppSettings>(json, Options);
if (s != null)
{
s.Services ??= new List<ServiceEntry>();
s.Targets ??= new List<RemoteTarget>();
s.OpenHostTabs ??= new List<string>();
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
{
// 저장 실패는 무시 (다음 저장 때 다시 시도)
}
}
}
/// <summary>작업 로그를 날짜별 파일에 추가 기록 (삭제 기능 없음)</summary>
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
{
// 기록 실패는 무시
}
}
}
}