697 lines
31 KiB
C#
697 lines
31 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace JJServiceManager.Remote
|
|
{
|
|
/// <summary>원격관리(화면) 허용 설정</summary>
|
|
public readonly record struct ScreenPolicy(bool AllowView, bool AllowControl, bool RequireApproval);
|
|
|
|
/// <summary>지금 이 PC 화면을 보고 있는 관리 PC</summary>
|
|
public readonly record struct ScreenViewer(int Id, string Who, bool Control);
|
|
|
|
/// <summary>
|
|
/// 원격 접속 허용 (대상 역할).
|
|
/// - Active: 포트를 열고 관리 PC의 접속을 받음
|
|
/// - Passive: 관리 PC로 접속해 감 (접속 코드)
|
|
/// 연결된 뒤에는 똑같이, 이 PC에 "등록된 서비스"에 한해서만
|
|
/// 목록 / 상태 / Start / Stop / 상세 / 로그 보기를 처리하고,
|
|
/// 허용된 경우 화면 보기 / 키보드·마우스 제어를 처리함.
|
|
/// </summary>
|
|
public sealed class RemoteServer : IDisposable
|
|
{
|
|
public const string Version = "1.2";
|
|
public const string SshUser = "jjsm";
|
|
public const string PassiveSshUser = "jjsm-passive";
|
|
|
|
private static readonly TimeSpan OpTimeout = TimeSpan.FromSeconds(60);
|
|
|
|
private readonly Func<IReadOnlyList<ServiceEntry>> _services;
|
|
private readonly Action<string, LogLevel> _log;
|
|
private readonly Action _changed;
|
|
private readonly Func<ScreenPolicy> _policy;
|
|
private readonly Func<string, bool, Task<bool>> _approve;
|
|
private readonly object _sync = new object();
|
|
|
|
private RemoteListener _listener;
|
|
private string _hash = "", _salt = "";
|
|
private readonly Queue<DateTime> _failures = new Queue<DateTime>();
|
|
|
|
// Passive
|
|
private CancellationTokenSource _passiveCts;
|
|
|
|
// 화면 보기 중인 세션
|
|
private readonly ConcurrentDictionary<int, ScreenSession> _screens = new ConcurrentDictionary<int, ScreenSession>();
|
|
private int _nextScreenId;
|
|
|
|
public bool IsRunning => _listener != null;
|
|
public RemoteMode Mode { get; private set; }
|
|
public int Port { get; private set; }
|
|
|
|
/// <summary>화면을 보는 관리 PC 목록이 바뀜 (백그라운드 스레드)</summary>
|
|
public event Action<IReadOnlyList<ScreenViewer>> ScreenViewersChanged;
|
|
|
|
/// <param name="services">등록된 서비스 목록 (스레드 안전한 복사본을 돌려줘야 함)</param>
|
|
/// <param name="log">작업 로그 기록</param>
|
|
/// <param name="changed">원격에서 Start/Stop 했을 때 (상태 갱신용)</param>
|
|
/// <param name="policy">원격관리 허용 설정</param>
|
|
/// <param name="approve">화면 연결 승인 요청 (누가, 제어 여부) → 허용 여부</param>
|
|
public RemoteServer(Func<IReadOnlyList<ServiceEntry>> services, Action<string, LogLevel> log, Action changed,
|
|
Func<ScreenPolicy> policy, Func<string, bool, Task<bool>> approve)
|
|
{
|
|
_services = services;
|
|
_log = log;
|
|
_changed = changed;
|
|
_policy = policy;
|
|
_approve = approve;
|
|
}
|
|
|
|
/// <summary>비밀번호만 바뀐 경우 (재시작 없이)</summary>
|
|
public void UpdatePassword(string hash, string salt)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
_hash = hash ?? "";
|
|
_salt = salt ?? "";
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── Active: 접속 받기 ─────────────────────────
|
|
|
|
public void Start(RemoteMode mode, int port, string hash, string salt)
|
|
{
|
|
Stop();
|
|
UpdatePassword(hash, salt);
|
|
Mode = mode;
|
|
Port = port;
|
|
try
|
|
{
|
|
_listener = RemoteListener.Start(mode, port, CheckSshPasswordAsync,
|
|
(conn, peer, ct) => ServeAsync(conn, peer, requireHelloPassword: mode == RemoteMode.Port, ct));
|
|
_listener.Failed += msg =>
|
|
{
|
|
_log($"원격 접속 허용(SSH) 중단: {msg}", LogLevel.Error);
|
|
_listener?.Dispose();
|
|
_listener = null;
|
|
};
|
|
_log($"원격 접속 허용 시작 (Active 수신): {(mode == RemoteMode.Ssh ? "SSH" : "지정 포트(TLS)")} {port}", LogLevel.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_listener = null;
|
|
var msg = ex is System.Net.Sockets.SocketException se && se.SocketErrorCode == System.Net.Sockets.SocketError.AddressAlreadyInUse
|
|
? $"포트 {port}를 다른 프로그램이 사용 중입니다."
|
|
: ex.Message;
|
|
_log($"원격 접속 허용 시작 실패 (포트 {port}): {msg}", LogLevel.Error);
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (_listener == null) return;
|
|
_listener.Dispose();
|
|
_listener = null;
|
|
_log("원격 접속 허용 (Active 수신) 중지", LogLevel.Info);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
StopPassive();
|
|
StopAllScreens();
|
|
}
|
|
|
|
// ───────────────────────── Passive: 관리 PC로 접속 ─────────────────────────
|
|
|
|
/// <summary>Passive 접속 목록 적용 (빈 목록이면 중지)</summary>
|
|
public void StartPassive(IReadOnlyList<PassiveManager> managers)
|
|
{
|
|
StopPassive();
|
|
if (managers == null || managers.Count == 0) return;
|
|
_passiveCts = new CancellationTokenSource();
|
|
foreach (var m in managers) _ = Task.Run(() => PassiveLoopAsync(m.Clone(), _passiveCts.Token));
|
|
}
|
|
|
|
public void StopPassive()
|
|
{
|
|
try { _passiveCts?.Cancel(); } catch { }
|
|
_passiveCts = null;
|
|
}
|
|
|
|
private async Task PassiveLoopAsync(PassiveManager m, CancellationToken ct)
|
|
{
|
|
string where = $"{m.Address}:{m.Port}";
|
|
string lastProblem = null;
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
var retry = TimeSpan.FromSeconds(5);
|
|
try
|
|
{
|
|
JsonLineConnection conn;
|
|
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct))
|
|
{
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(10));
|
|
conn = await RemoteDialer.DialAsync(m.Mode, m.Address, m.Port, PassiveSshUser, m.AccessCode, timeout.Token).ConfigureAwait(false);
|
|
await conn.SendAsync(new JsonObject
|
|
{
|
|
["op"] = "announce",
|
|
["code"] = m.AccessCode,
|
|
["hostname"] = Environment.MachineName,
|
|
["version"] = Version
|
|
}, timeout.Token).ConfigureAwait(false);
|
|
}
|
|
_log($"Passive 연결됨: 관리 PC {where}", LogLevel.Success);
|
|
lastProblem = null;
|
|
await ServeAsync(conn, $"{m.Address}(Passive)", requireHelloPassword: true, ct).ConfigureAwait(false);
|
|
if (!ct.IsCancellationRequested) _log($"Passive 연결 끊김: 관리 PC {where}", LogLevel.Warning);
|
|
}
|
|
catch (Exception ex) when (!ct.IsCancellationRequested)
|
|
{
|
|
if (ex is RemoteAuthException) retry = TimeSpan.FromSeconds(30);
|
|
var msg = RemoteClient.Friendly(ex);
|
|
if (msg != lastProblem)
|
|
{
|
|
_log($"Passive 접속 실패 (관리 PC {where}): {msg}", LogLevel.Warning);
|
|
lastProblem = msg;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
break;
|
|
}
|
|
|
|
try { await Task.Delay(retry, ct).ConfigureAwait(false); }
|
|
catch { break; }
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── 인증 ─────────────────────────
|
|
|
|
private async Task<bool> CheckSshPasswordAsync(string user, string password)
|
|
{
|
|
if (user != SshUser) return false;
|
|
if (IsThrottled()) return false;
|
|
if (CheckPassword(password)) return true;
|
|
RecordFailure();
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
_log("원격 접속 거부 (SSH): 비밀번호가 맞지 않습니다.", LogLevel.Warning);
|
|
return false;
|
|
}
|
|
|
|
private bool CheckPassword(string password)
|
|
{
|
|
string h, s;
|
|
lock (_sync) { h = _hash; s = _salt; }
|
|
return RemoteSecurity.Verify(password, h, s);
|
|
}
|
|
|
|
/// <summary>5분 동안 10번 넘게 틀리면 잠시 거부</summary>
|
|
private bool IsThrottled()
|
|
{
|
|
lock (_failures)
|
|
{
|
|
while (_failures.Count > 0 && DateTime.UtcNow - _failures.Peek() > TimeSpan.FromMinutes(5)) _failures.Dequeue();
|
|
return _failures.Count >= 10;
|
|
}
|
|
}
|
|
|
|
private void RecordFailure()
|
|
{
|
|
lock (_failures) _failures.Enqueue(DateTime.UtcNow);
|
|
}
|
|
|
|
// ───────────────────────── 요청 처리 ─────────────────────────
|
|
|
|
private async Task ServeAsync(JsonLineConnection conn, string peer, bool requireHelloPassword, CancellationToken ct)
|
|
{
|
|
bool authed = !requireHelloPassword;
|
|
string who = peer;
|
|
var streams = new Dictionary<int, LogTailer>();
|
|
int nextStream = 1;
|
|
ScreenSession screen = null;
|
|
|
|
try
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
var msg = await conn.ReadAsync(ct).ConfigureAwait(false);
|
|
if (msg == null) break;
|
|
|
|
var op = Json.Str(msg, "op");
|
|
int id = Json.Int(msg, "id");
|
|
|
|
if (op == "hello")
|
|
{
|
|
var client = Json.Str(msg, "client");
|
|
who = string.IsNullOrEmpty(client) ? peer : $"{client}({peer})";
|
|
if (!authed)
|
|
{
|
|
if (IsThrottled())
|
|
{
|
|
await Reply(conn, id, error: "비밀번호를 여러 번 틀려 잠시 접속이 막혔습니다. 몇 분 뒤 다시 시도하세요.", extra: new JsonObject { ["authFailed"] = true });
|
|
break;
|
|
}
|
|
if (!CheckPassword(Json.Str(msg, "password")))
|
|
{
|
|
RecordFailure();
|
|
await Task.Delay(1000, ct).ConfigureAwait(false);
|
|
_log($"원격 접속 거부: {who} 비밀번호가 맞지 않습니다.", LogLevel.Warning);
|
|
await Reply(conn, id, error: "접속 비밀번호가 맞지 않습니다.", extra: new JsonObject { ["authFailed"] = true });
|
|
break;
|
|
}
|
|
authed = true;
|
|
}
|
|
_log($"원격 접속: {who}", LogLevel.Notice);
|
|
var pol = _policy();
|
|
await Reply(conn, id, result: new JsonObject
|
|
{
|
|
["hostname"] = Environment.MachineName,
|
|
["version"] = Version,
|
|
["instance"] = AppInstance.Number,
|
|
["allowView"] = pol.AllowView,
|
|
["allowControl"] = pol.AllowView && pol.AllowControl
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (!authed)
|
|
{
|
|
await Reply(conn, id, error: "먼저 인증해야 합니다.");
|
|
break;
|
|
}
|
|
|
|
switch (op)
|
|
{
|
|
case "ping":
|
|
await Reply(conn, id, result: new JsonObject());
|
|
break;
|
|
|
|
case "list":
|
|
_ = Task.Run(() => SafeReply(conn, id, () => ListServices()));
|
|
break;
|
|
|
|
case "start":
|
|
case "stop":
|
|
_ = Task.Run(() => SafeReply(conn, id, () => StartStop(op == "start", Json.Str(msg, "service"), who)));
|
|
break;
|
|
|
|
case "details":
|
|
_ = Task.Run(() => SafeReply(conn, id, () =>
|
|
{
|
|
var e = Registered(Json.Str(msg, "service"));
|
|
var arr = new JsonArray();
|
|
foreach (var l in ServiceOps.GetDetails(e.ServiceName)) arr.Add(l);
|
|
return new JsonObject { ["lines"] = arr };
|
|
}));
|
|
break;
|
|
|
|
case "logOpen":
|
|
{
|
|
try
|
|
{
|
|
var e = Registered(Json.Str(msg, "service"));
|
|
if (string.IsNullOrWhiteSpace(e.LogPath))
|
|
throw new RemoteException("대상 PC에서 이 서비스의 로그 경로가 지정되지 않았습니다.");
|
|
var enc = Json.Str(msg, "encoding");
|
|
var copy = new ServiceEntry
|
|
{
|
|
ServiceName = e.ServiceName,
|
|
DisplayName = e.DisplayName,
|
|
LogPath = e.LogPath,
|
|
LogIsFolder = e.LogIsFolder,
|
|
LogEncoding = string.IsNullOrEmpty(enc) ? e.LogEncoding : enc
|
|
};
|
|
int sid = nextStream++;
|
|
var tailer = new LogTailer(copy, (lines, level) => SendLog(conn, sid, lines, level))
|
|
{
|
|
SkipInitial = Json.Bool(msg, "newOnly")
|
|
};
|
|
tailer.FileChanged += f => Send(conn, new JsonObject { ["event"] = "logfile", ["stream"] = sid, ["path"] = f });
|
|
lock (streams) streams[sid] = tailer;
|
|
await Reply(conn, id, result: new JsonObject
|
|
{
|
|
["stream"] = sid,
|
|
["path"] = e.LogPath,
|
|
["isFolder"] = e.LogIsFolder
|
|
});
|
|
tailer.Start();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await Reply(conn, id, error: ex.Message);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "logClose":
|
|
{
|
|
int sid = Json.Int(msg, "stream");
|
|
lock (streams)
|
|
{
|
|
if (streams.Remove(sid, out var t)) t.Dispose();
|
|
}
|
|
await Reply(conn, id, result: new JsonObject());
|
|
break;
|
|
}
|
|
|
|
// ───── 원격관리: 화면 ─────
|
|
|
|
case "screenInfo":
|
|
{
|
|
var pol = _policy();
|
|
var arr = new JsonArray();
|
|
foreach (var m in ScreenCapture.Monitors())
|
|
arr.Add(new JsonObject { ["index"] = m.Index, ["width"] = m.Width, ["height"] = m.Height, ["primary"] = m.Primary });
|
|
await Reply(conn, id, result: new JsonObject
|
|
{
|
|
["allowView"] = pol.AllowView,
|
|
["allowControl"] = pol.AllowView && pol.AllowControl,
|
|
["monitors"] = arr
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "screenStart":
|
|
{
|
|
var whoNow = who;
|
|
int monitor = Json.Int(msg, "monitor", 1);
|
|
// 승인 창을 기다리는 동안 다른 요청은 계속 처리되도록 따로 실행
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var pol = _policy();
|
|
if (!pol.AllowView) throw new RemoteException("대상 PC에서 화면 보기를 허용하지 않았습니다.");
|
|
bool control = pol.AllowControl;
|
|
if (pol.RequireApproval)
|
|
{
|
|
_log($"[원격 화면] {whoNow} 연결 승인 요청", LogLevel.Notice);
|
|
bool ok = await _approve(whoNow, control).ConfigureAwait(false);
|
|
if (!ok)
|
|
{
|
|
_log($"[원격 화면] {whoNow} 연결 거부", LogLevel.Warning);
|
|
throw new RemoteException("대상 PC에서 연결을 거부했습니다.");
|
|
}
|
|
}
|
|
var old = Interlocked.Exchange(ref screen, null);
|
|
old?.Stop();
|
|
var s = new ScreenSession(Interlocked.Increment(ref _nextScreenId), whoNow, control, monitor);
|
|
screen = s;
|
|
_screens[s.Id] = s;
|
|
_log($"[원격 화면] {whoNow} {(control ? "보기 + 제어" : "보기")} 시작", LogLevel.Notice);
|
|
NotifyViewers();
|
|
await Reply(conn, id, result: new JsonObject { ["sid"] = s.Id, ["control"] = control });
|
|
_ = Task.Run(() => ScreenLoopAsync(conn, s, ct));
|
|
}
|
|
catch (RemoteException ex) { await Reply(conn, id, error: ex.Message); }
|
|
catch (Exception ex) { await Reply(conn, id, error: ex.Message); }
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "screenMonitor":
|
|
{
|
|
var s = screen;
|
|
if (s != null) s.Monitor = Json.Int(msg, "monitor", 1);
|
|
await Reply(conn, id, result: new JsonObject());
|
|
break;
|
|
}
|
|
|
|
case "screenStop":
|
|
{
|
|
var s = Interlocked.Exchange(ref screen, null);
|
|
s?.Stop();
|
|
await Reply(conn, id, result: new JsonObject());
|
|
break;
|
|
}
|
|
|
|
case "input":
|
|
{
|
|
// 응답 없이 바로 실행 (빠르게)
|
|
var s = screen;
|
|
if (s == null || s.Stopped || !s.Control) break;
|
|
var pol = _policy();
|
|
if (!pol.AllowView || !pol.AllowControl) break;
|
|
try
|
|
{
|
|
if (Json.Str(msg, "kind") == "key")
|
|
{
|
|
InputInjector.Key(Json.Int(msg, "vk"), Json.Bool(msg, "down"));
|
|
}
|
|
else
|
|
{
|
|
double nx = 0, ny = 0;
|
|
try { nx = msg["x"]?.GetValue<double>() ?? 0; ny = msg["y"]?.GetValue<double>() ?? 0; } catch { }
|
|
InputInjector.Mouse(ScreenCapture.Area(s.Monitor), nx, ny,
|
|
Json.Str(msg, "action"), Json.Str(msg, "button"), Json.Int(msg, "delta"));
|
|
}
|
|
}
|
|
catch { }
|
|
break;
|
|
}
|
|
|
|
default:
|
|
await Reply(conn, id, error: "알 수 없는 요청: " + op);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 연결 끊김
|
|
}
|
|
finally
|
|
{
|
|
lock (streams)
|
|
{
|
|
foreach (var t in streams.Values) t.Dispose();
|
|
streams.Clear();
|
|
}
|
|
var s = Interlocked.Exchange(ref screen, null);
|
|
s?.Stop();
|
|
conn.Dispose();
|
|
if (authed && who != peer) _log($"원격 연결 종료: {who}", LogLevel.Info);
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── 원격관리: 화면 보내기 ─────────────────────────
|
|
|
|
private sealed class ScreenSession
|
|
{
|
|
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
|
public int Id { get; }
|
|
public string Who { get; }
|
|
public bool Control { get; }
|
|
public volatile int Monitor;
|
|
public bool Stopped => _cts.IsCancellationRequested;
|
|
public CancellationToken Token => _cts.Token;
|
|
|
|
public ScreenSession(int id, string who, bool control, int monitor)
|
|
{
|
|
Id = id;
|
|
Who = who;
|
|
Control = control;
|
|
Monitor = monitor;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
try { _cts.Cancel(); } catch { }
|
|
}
|
|
}
|
|
|
|
private async Task ScreenLoopAsync(JsonLineConnection conn, ScreenSession s, CancellationToken ct)
|
|
{
|
|
const int FrameMs = 120; // 최대 약 8장/초
|
|
ulong lastHash = 0;
|
|
var lastSent = DateTime.MinValue;
|
|
int lastMonitor = -1;
|
|
string endReason = "종료";
|
|
try
|
|
{
|
|
while (!s.Stopped && !ct.IsCancellationRequested && !conn.IsDisposed)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
var pol = _policy();
|
|
if (!pol.AllowView)
|
|
{
|
|
endReason = "대상 PC에서 화면 보기 허용을 껐습니다.";
|
|
break;
|
|
}
|
|
|
|
int monitor = s.Monitor;
|
|
var area = ScreenCapture.Area(monitor);
|
|
int maxWidth = monitor == 0 ? 2560 : 1920;
|
|
byte[] jpeg = null;
|
|
int w = 0, h = 0;
|
|
ulong hash = 0;
|
|
try { jpeg = ScreenCapture.CaptureJpeg(area, maxWidth, 60L, out w, out h, out hash); }
|
|
catch { /* 잠금 화면 등 캡처할 수 없는 순간 */ }
|
|
|
|
bool changed = hash != lastHash || monitor != lastMonitor;
|
|
if (jpeg != null && (changed || DateTime.UtcNow - lastSent > TimeSpan.FromSeconds(2)))
|
|
{
|
|
await conn.SendAsync(new JsonObject
|
|
{
|
|
["event"] = "frame",
|
|
["sid"] = s.Id,
|
|
["monitor"] = monitor,
|
|
["w"] = w,
|
|
["h"] = h,
|
|
["data"] = Convert.ToBase64String(jpeg)
|
|
}, s.Token).ConfigureAwait(false);
|
|
lastHash = hash;
|
|
lastMonitor = monitor;
|
|
lastSent = DateTime.UtcNow;
|
|
}
|
|
|
|
int wait = FrameMs - (int)sw.ElapsedMilliseconds;
|
|
if (wait > 0) await Task.Delay(wait, s.Token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 연결 끊김 또는 중지
|
|
}
|
|
finally
|
|
{
|
|
s.Stop();
|
|
_screens.TryRemove(s.Id, out _);
|
|
if (!conn.IsDisposed)
|
|
{
|
|
try { await conn.SendAsync(new JsonObject { ["event"] = "screenEnd", ["sid"] = s.Id, ["reason"] = endReason }).ConfigureAwait(false); }
|
|
catch { }
|
|
}
|
|
_log($"[원격 화면] {s.Who} 연결 끝", LogLevel.Info);
|
|
NotifyViewers();
|
|
}
|
|
}
|
|
|
|
/// <summary>이 PC에서 [연결 끊기]를 눌렀을 때 모든 화면 연결 종료</summary>
|
|
public void StopAllScreens()
|
|
{
|
|
foreach (var s in _screens.Values) s.Stop();
|
|
}
|
|
|
|
private void NotifyViewers()
|
|
{
|
|
var list = _screens.Values.Where(s => !s.Stopped).Select(s => new ScreenViewer(s.Id, s.Who, s.Control)).ToList();
|
|
ScreenViewersChanged?.Invoke(list);
|
|
}
|
|
|
|
// ───────────────────────── 서비스 ─────────────────────────
|
|
|
|
private ServiceEntry Registered(string serviceName)
|
|
{
|
|
var e = _services().FirstOrDefault(x => string.Equals(x.ServiceName, serviceName, StringComparison.OrdinalIgnoreCase));
|
|
if (e == null) throw new RemoteException("대상 PC에 등록되지 않은 서비스입니다: " + serviceName);
|
|
return e;
|
|
}
|
|
|
|
private JsonObject ListServices()
|
|
{
|
|
var arr = new JsonArray();
|
|
foreach (var e in _services())
|
|
{
|
|
var st = ServiceOps.GetStatus(e.ServiceName);
|
|
arr.Add(new JsonObject
|
|
{
|
|
["name"] = e.ServiceName,
|
|
["display"] = e.DisplayName,
|
|
["status"] = st.HasValue ? (int)st.Value : null,
|
|
["logPath"] = e.LogPath ?? "",
|
|
["logIsFolder"] = e.LogIsFolder
|
|
});
|
|
}
|
|
var pol = _policy();
|
|
return new JsonObject
|
|
{
|
|
["services"] = arr,
|
|
["allowView"] = pol.AllowView,
|
|
["allowControl"] = pol.AllowView && pol.AllowControl
|
|
};
|
|
}
|
|
|
|
private JsonObject StartStop(bool start, string serviceName, string who)
|
|
{
|
|
var e = Registered(serviceName);
|
|
var name = string.IsNullOrEmpty(e.DisplayName) ? e.ServiceName : e.DisplayName;
|
|
var what = start ? "시작" : "중지";
|
|
_log($"[원격 {who}] [{name}] {what} 요청", LogLevel.Notice);
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
if (start) ServiceOps.Start(e.ServiceName, OpTimeout);
|
|
else ServiceOps.Stop(e.ServiceName, OpTimeout);
|
|
_log($"[원격 {who}] [{name}] {what} 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
|
|
return new JsonObject { ["seconds"] = Math.Round(sw.Elapsed.TotalSeconds, 1) };
|
|
}
|
|
catch (InfoException ex)
|
|
{
|
|
throw new RemoteException(ex.Message, isInfo: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var msg = ServiceOps.Describe(ex);
|
|
_log($"[원격 {who}] [{name}] {what} 실패: {msg}", LogLevel.Error);
|
|
throw new RemoteException(msg);
|
|
}
|
|
finally
|
|
{
|
|
_changed?.Invoke();
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── 보내기 ─────────────────────────
|
|
|
|
private static async Task SafeReply(JsonLineConnection conn, int id, Func<JsonObject> work)
|
|
{
|
|
try
|
|
{
|
|
await Reply(conn, id, result: work());
|
|
}
|
|
catch (RemoteException ex)
|
|
{
|
|
await Reply(conn, id, error: ex.Message, extra: ex.IsInfo ? new JsonObject { ["info"] = true } : null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await Reply(conn, id, error: ex.Message);
|
|
}
|
|
}
|
|
|
|
private static async Task Reply(JsonLineConnection conn, int id, JsonObject result = null, string error = null, JsonObject extra = null)
|
|
{
|
|
var msg = new JsonObject { ["id"] = id, ["ok"] = error == null };
|
|
if (error == null) msg["result"] = result ?? new JsonObject();
|
|
else msg["error"] = error;
|
|
if (extra != null)
|
|
foreach (var kv in extra.ToList())
|
|
{
|
|
extra.Remove(kv.Key);
|
|
msg[kv.Key] = kv.Value;
|
|
}
|
|
try { await conn.SendAsync(msg).ConfigureAwait(false); } catch { }
|
|
}
|
|
|
|
private static void SendLog(JsonLineConnection conn, int sid, IList<string> lines, LogLevel level)
|
|
{
|
|
var arr = new JsonArray();
|
|
foreach (var l in lines) arr.Add(l);
|
|
Send(conn, new JsonObject { ["event"] = "log", ["stream"] = sid, ["level"] = level.ToString(), ["lines"] = arr });
|
|
}
|
|
|
|
/// <summary>로그 이벤트는 순서가 중요하므로 보낼 때까지 기다림 (로그 타이머 스레드에서 호출)</summary>
|
|
private static void Send(JsonLineConnection conn, JsonObject msg)
|
|
{
|
|
if (conn.IsDisposed) return;
|
|
try { conn.SendAsync(msg).GetAwaiter().GetResult(); } catch { }
|
|
}
|
|
}
|
|
}
|