272 lines
11 KiB
C#
272 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.ServiceProcess;
|
|
using System.Text;
|
|
using Microsoft.Win32;
|
|
|
|
namespace JJServiceManager
|
|
{
|
|
/// <summary>Windows 서비스 조회/제어</summary>
|
|
public static class ServiceOps
|
|
{
|
|
public static string StatusText(ServiceControllerStatus? s) => s switch
|
|
{
|
|
ServiceControllerStatus.Running => "실행 중",
|
|
ServiceControllerStatus.Stopped => "중지됨",
|
|
ServiceControllerStatus.StartPending => "시작 중",
|
|
ServiceControllerStatus.StopPending => "중지 중",
|
|
ServiceControllerStatus.Paused => "일시 중지됨",
|
|
ServiceControllerStatus.PausePending => "일시 중지 중",
|
|
ServiceControllerStatus.ContinuePending => "계속 중",
|
|
null => "서비스 없음",
|
|
_ => s.ToString()
|
|
};
|
|
|
|
public static string StartTypeText(ServiceStartMode? m, bool delayed = false) => m switch
|
|
{
|
|
ServiceStartMode.Automatic => delayed ? "자동(지연된 시작)" : "자동",
|
|
ServiceStartMode.Manual => "수동",
|
|
ServiceStartMode.Disabled => "사용 안 함",
|
|
ServiceStartMode.Boot => "부팅",
|
|
ServiceStartMode.System => "시스템",
|
|
null => "",
|
|
_ => m.ToString()
|
|
};
|
|
|
|
/// <summary>상태 조회. 서비스가 없으면 null</summary>
|
|
public static ServiceControllerStatus? GetStatus(string serviceName)
|
|
{
|
|
try
|
|
{
|
|
using var sc = new ServiceController(serviceName);
|
|
return sc.Status;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>서비스 시작 후 실행 상태가 될 때까지 대기</summary>
|
|
public static void Start(string serviceName, TimeSpan timeout)
|
|
{
|
|
using var sc = new ServiceController(serviceName);
|
|
sc.Refresh();
|
|
if (sc.Status == ServiceControllerStatus.Running) throw new InfoException("이미 실행 중입니다.");
|
|
if (sc.Status == ServiceControllerStatus.Paused) sc.Continue();
|
|
else if (sc.Status != ServiceControllerStatus.StartPending) sc.Start();
|
|
sc.WaitForStatus(ServiceControllerStatus.Running, timeout);
|
|
}
|
|
|
|
/// <summary>서비스 중지 후 중지 상태가 될 때까지 대기</summary>
|
|
public static void Stop(string serviceName, TimeSpan timeout)
|
|
{
|
|
using var sc = new ServiceController(serviceName);
|
|
sc.Refresh();
|
|
if (sc.Status == ServiceControllerStatus.Stopped) throw new InfoException("이미 중지되어 있습니다.");
|
|
if (!sc.CanStop && sc.Status == ServiceControllerStatus.Running)
|
|
throw new InvalidOperationException("이 서비스는 중지를 허용하지 않습니다.");
|
|
if (sc.Status != ServiceControllerStatus.StopPending) sc.Stop();
|
|
sc.WaitForStatus(ServiceControllerStatus.Stopped, timeout);
|
|
}
|
|
|
|
/// <summary>예외를 사람이 읽기 쉬운 메시지로</summary>
|
|
public static string Describe(Exception ex)
|
|
{
|
|
if (ex is System.ServiceProcess.TimeoutException) return "제한 시간 안에 상태가 바뀌지 않았습니다.";
|
|
var inner = ex;
|
|
while (inner.InnerException != null) inner = inner.InnerException;
|
|
if (inner is Win32Exception w && !ReferenceEquals(inner, ex))
|
|
return $"{ex.Message} ({w.Message})";
|
|
return ex.Message;
|
|
}
|
|
|
|
/// <summary>Status 버튼용 상세 정보</summary>
|
|
public static List<string> GetDetails(string serviceName)
|
|
{
|
|
var lines = new List<string>();
|
|
using var sc = new ServiceController(serviceName);
|
|
sc.Refresh();
|
|
|
|
var reg = ReadRegistry(serviceName);
|
|
lines.Add($"표시 이름 : {sc.DisplayName}");
|
|
lines.Add($"서비스명 : {sc.ServiceName}");
|
|
lines.Add($"상태 : {StatusText(sc.Status)}");
|
|
|
|
ServiceStartMode? mode = null;
|
|
try { mode = sc.StartType; } catch { }
|
|
lines.Add($"시작 유형 : {StartTypeText(mode, reg.DelayedStart)}");
|
|
|
|
int pid = TryGetProcessId(sc);
|
|
if (pid > 0)
|
|
{
|
|
string pname = "";
|
|
try
|
|
{
|
|
using var p = Process.GetProcessById(pid);
|
|
pname = p.ProcessName;
|
|
var started = p.StartTime;
|
|
lines.Add($"프로세스 : {pname} (PID {pid}), 시작 {started:yyyy-MM-dd HH:mm:ss}");
|
|
}
|
|
catch
|
|
{
|
|
lines.Add($"프로세스 : PID {pid}");
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(reg.ImagePath)) lines.Add($"실행 파일 : {reg.ImagePath}");
|
|
if (!string.IsNullOrEmpty(reg.Account)) lines.Add($"로그온 계정 : {reg.Account}");
|
|
|
|
try
|
|
{
|
|
var deps = sc.ServicesDependedOn;
|
|
if (deps.Length > 0)
|
|
{
|
|
var names = new List<string>();
|
|
foreach (var d in deps) { names.Add(d.ServiceName); d.Dispose(); }
|
|
lines.Add($"필요한 서비스 : {string.Join(", ", names)}");
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
if (!string.IsNullOrEmpty(reg.Description)) lines.Add($"설명 : {reg.Description}");
|
|
return lines;
|
|
}
|
|
|
|
// ───────────────────────── 레지스트리 ─────────────────────────
|
|
|
|
public class RegInfo
|
|
{
|
|
public string Description = "";
|
|
public string ImagePath = "";
|
|
public string Account = "";
|
|
public bool DelayedStart;
|
|
}
|
|
|
|
public static RegInfo ReadRegistry(string serviceName)
|
|
{
|
|
var info = new RegInfo();
|
|
try
|
|
{
|
|
using var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\" + serviceName);
|
|
if (key == null) return info;
|
|
info.Description = ResolveIndirect(key.GetValue("Description") as string);
|
|
info.ImagePath = key.GetValue("ImagePath") as string ?? "";
|
|
info.Account = key.GetValue("ObjectName") as string ?? "";
|
|
info.DelayedStart = (key.GetValue("DelayedAutostart") is int d) && d == 1;
|
|
}
|
|
catch { }
|
|
return info;
|
|
}
|
|
|
|
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved);
|
|
|
|
/// <summary>"@%SystemRoot%\...dll,-123" 같은 간접 문자열을 실제 문구로 변환</summary>
|
|
private static string ResolveIndirect(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value)) return "";
|
|
if (!value.StartsWith("@")) return value;
|
|
try
|
|
{
|
|
var sb = new StringBuilder(2048);
|
|
if (SHLoadIndirectString(value, sb, sb.Capacity, IntPtr.Zero) == 0) return sb.ToString();
|
|
}
|
|
catch { }
|
|
return "";
|
|
}
|
|
|
|
// ───────────────────────── PID 조회 ─────────────────────────
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct SERVICE_STATUS_PROCESS
|
|
{
|
|
public int dwServiceType;
|
|
public int dwCurrentState;
|
|
public int dwControlsAccepted;
|
|
public int dwWin32ExitCode;
|
|
public int dwServiceSpecificExitCode;
|
|
public int dwCheckPoint;
|
|
public int dwWaitHint;
|
|
public int dwProcessId;
|
|
public int dwServiceFlags;
|
|
}
|
|
|
|
[DllImport("advapi32.dll", SetLastError = true)]
|
|
private static extern bool QueryServiceStatusEx(SafeHandle hService, int infoLevel, ref SERVICE_STATUS_PROCESS buffer, int bufSize, out int bytesNeeded);
|
|
|
|
private static int TryGetProcessId(ServiceController sc)
|
|
{
|
|
try
|
|
{
|
|
var ssp = new SERVICE_STATUS_PROCESS();
|
|
if (QueryServiceStatusEx(sc.ServiceHandle, 0, ref ssp, Marshal.SizeOf<SERVICE_STATUS_PROCESS>(), out _))
|
|
return ssp.dwProcessId;
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>오류가 아닌 안내성 메시지 (이미 실행 중 등)</summary>
|
|
public class InfoException : Exception
|
|
{
|
|
public InfoException(string message) : base(message) { }
|
|
}
|
|
|
|
/// <summary>서비스 선택 화면의 한 줄</summary>
|
|
public class ServiceInfo : ObservableObject
|
|
{
|
|
private bool _isSelected;
|
|
|
|
public string ServiceName { get; init; } = "";
|
|
public string DisplayName { get; init; } = "";
|
|
public string Description { get; init; } = "";
|
|
public string StatusText { get; init; } = "";
|
|
public string StartTypeText { get; init; } = "";
|
|
public bool IsRegistered { get; init; }
|
|
|
|
public bool IsSelected { get => _isSelected; set => Set(ref _isSelected, value); }
|
|
|
|
public string Tooltip => $"서비스명: {ServiceName}" + (IsRegistered ? " (이미 등록됨)" : "");
|
|
|
|
public static List<ServiceInfo> LoadAll(ISet<string> registered)
|
|
{
|
|
var result = new List<ServiceInfo>();
|
|
ServiceController[] all;
|
|
try { all = ServiceController.GetServices(); }
|
|
catch { return result; }
|
|
|
|
foreach (var sc in all)
|
|
{
|
|
try
|
|
{
|
|
var reg = ServiceOps.ReadRegistry(sc.ServiceName);
|
|
ServiceStartMode? mode = null;
|
|
try { mode = sc.StartType; } catch { }
|
|
ServiceControllerStatus? st = null;
|
|
try { st = sc.Status; } catch { }
|
|
|
|
bool isReg = registered.Contains(sc.ServiceName);
|
|
result.Add(new ServiceInfo
|
|
{
|
|
ServiceName = sc.ServiceName,
|
|
DisplayName = sc.DisplayName,
|
|
Description = reg.Description,
|
|
StatusText = ServiceOps.StatusText(st),
|
|
StartTypeText = ServiceOps.StartTypeText(mode, reg.DelayedStart),
|
|
IsRegistered = isReg,
|
|
IsSelected = isReg
|
|
});
|
|
}
|
|
catch { }
|
|
finally { sc.Dispose(); }
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|