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)
}
/// 접속 방향
public enum ConnectDirection
{
Active = 0, // 관리 PC → 대상 PC 로 접속 (대상 PC가 포트를 엶)
Passive = 1 // 대상 PC → 관리 PC 로 접속 (관리 PC가 포트를 엶)
}
/// 관리 대상 PC
public class RemoteTarget
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
/// Active: 관리 PC가 접속 / Passive: 대상 PC가 접속해 옴
public ConnectDirection Direction { get; set; } = ConnectDirection.Active;
/// Passive일 때 대상 PC가 알려주는 접속 코드 (예: AB12-CD34)
public string AccessCode { get; set; } = "";
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; } = "";
/// Passive로 접속해 온 대상 PC의 IP (표시용, 자동)
public string LastAddress { get; set; } = "";
[JsonIgnore]
public string Label
{
get
{
if (Direction == ConnectDirection.Passive)
{
// 아직 한 번도 접속하지 않았으면 코드로 표시
if (string.IsNullOrWhiteSpace(LastAddress)) return "Passive " + AccessCode;
var p = string.IsNullOrWhiteSpace(Hostname) ? LastAddress : $"{LastAddress} / {Hostname}";
return p + " (Passive)";
}
return string.IsNullOrWhiteSpace(Hostname) ? Address : $"{Address} / {Hostname}";
}
}
public RemoteTarget Clone() => (RemoteTarget)MemberwiseClone();
/// 새 접속 코드 (헷갈리는 글자 0/O, 1/I 제외)
public static string NewAccessCode()
{
const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 8; i++)
{
if (i == 4) sb.Append('-');
sb.Append(chars[System.Security.Cryptography.RandomNumberGenerator.GetInt32(chars.Length)]);
}
return sb.ToString();
}
}
/// 대상 PC 쪽: Passive로 접속할 관리 PC
public class PassiveManager
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string Address { get; set; } = "";
public RemoteMode Mode { get; set; } = RemoteMode.Port;
public int Port { get; set; } = 9990;
public string AccessCode { get; set; } = "";
[JsonIgnore]
public string Label => $"{Address} ({(Mode == RemoteMode.Ssh ? "SSH" : "지정 포트")} {Port}) · 코드 {AccessCode}";
public PassiveManager Clone() => (PassiveManager)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;
/// Active 수신: 이 PC가 포트를 열고 관리 PC의 접속을 받음
public bool RemoteServerActiveListen { get; set; } = true;
/// Passive 접속: 이 PC가 관리 PC로 접속
public List PassiveManagers { get; set; } = new List();
// ───────── 원격관리 (화면 보기 / 제어) ─────────
/// 원격에서 이 PC 화면 보기 허용
public bool RemoteAllowView { get; set; }
/// 원격에서 이 PC 키보드·마우스 제어 허용 (화면 보기가 켜져 있어야 함)
public bool RemoteAllowControl { get; set; }
/// 원격 화면 연결 시 이 PC에서 승인해야 함
public bool RemoteRequireApproval { get; set; } = true;
// ───────── 다른 PC 관리 (관리 역할) ─────────
public bool RemoteManageEnabled { get; set; }
public List Targets { get; set; } = new List();
/// Passive 대상 PC의 접속을 기다리는 방식/포트
public RemoteMode PassiveListenMode { get; set; } = RemoteMode.Port;
public int PassiveListenTcpPort { get; set; } = 9990 + AppInstance.Number - 1;
public int PassiveListenSshPort { get; set; } = 2224 + AppInstance.Number - 1;
[JsonIgnore]
public int PassiveListenPort => PassiveListenMode == RemoteMode.Ssh ? PassiveListenSshPort : PassiveListenTcpPort;
/// 초기화면에 열어둔 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.PassiveManagers ??= 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
{
// 기록 실패는 무시
}
}
}
}