Merhaba arkadaşlar, bu yazıda sizlere Windows Service Projeini Debug etme ve service içerisinden harici exe çalıştırma yönteminden bahsedeceğim.
Windows Service nedir?
Windows Service, geri planda işletim sistemi üzerinde çalışır, "services" komutu ile çalışan servisleri görebiliriz
Windows Service, .net ortamında geliştirilebilen bir proje türüdür.
Windows Service'in Install Edilmesi (Sisteme Tanıtılması)
Projemizi "release" modda build ettikten sonra aşağıdaki folder yapısına benzer bir görünüm elde ederiz
Administrator olarak CommandPromptu açarız ilgili klasöre cd komutu ile gideriz
sc create Easy2PatchService binpath="C:\Projects\Arge\ArkSoft\Tumark\TumarkClientService\bin\Release\TumarkClientService.exe" komutu ile servisi sisteme kurmuş oluruz (servisi services modül listesinden focuslanıp manuel olarak elle başlatmamız gerekmektedir, default olarak manuel kurulur)
Not: sc komutunun alternatifi "installutil.exe" komutudur. İkisi de aynı kapıya çıkar. Aralarında çok hafif farklar vardır. Sc komutu daha çok tercih ediliyor.
Bu işlemler için .bat dosyası kullanıyorum, cs.bat (createService.bat) bu komutu otomatik olarak çalıştırmaktadır
Çalışan servisi komut satırından durdurmak için
sc stop Easy2PatchService komutunu gireriz
Durdurulmuş servisi komut satırından kaldırmak için (servisin önce durdulmuş olması gerekmektedir)
sc delete Easy2PatchService komutunu gireriz
Stop ve Delete işlemini ard arda yapması için ds.bat (deleteService.bat) hazırladım, bu şekilde hızlıca kullanabiliyorum
Geliştirilen servis projelerinde release folder altında cs.bat ve ds.bat komutlarını hazırda bulundurmak iyidir, zaman tasarrufu sağlar
Windows Service Hakkında Bilinmesi Gerekenler
Bir service projesi, bünyesinde birden fazla service barındırabilir. Bu nedenle yeni oluşturulan bir service projesinde array şeklinde çalışmasının sebebi budur (birden fazla servisi yönetmek ve iş akışlarını parçalama amaçlı kullanmak üzere)
Servis projeleri, geri planda çalışır. Release modda olmak zorundadır.
Servisi runtime'da çalıştırabilmek için sisteme kurulması ve start edilmesi gerekmektedir.
Servis uygulamalarında, Winform UI elementleri kullanılamamaktadır (servis uygulamalarının UI ile bağlantı kurabilmesi mümkün olmamaktadır)
Normal şartlarda servis, dışarıdan bir harici uygulamayı çalıştıramamaktadır (Dışarıdan harici bir uygulamayı çalıştırmak için kullanılan Process.Run komutu, service accountunun scopu ile ilgili bir durumdur. Her durumda dışarıdan bir uygulamayı çalıştırma işlemi istersek kernel seviyesinde çalıştırabilmek mümkün olduğu görülmüştür. Process.Run komutu yeterli olmamaktadır.
Normal şartlarda, service projesi debug edilememektedir. Bu genel yaşanan bir sıkıntıdır. Ancak, reflection yöntemlerini kullanarak debug modda çalıştırmak mümkün olmaktadır. Bunun nasıl yapılabildiğini göstereceğim.
Helper Classlarının Hazırlanması
Windows service projemizde evladiyelik kullanabileceğimiz bazı helper classlarını önceden hazırlamış olmamız gerekmektedir, bunlardan temel olanları
Singleton classı : Classlarımıza singleton patternini uygulamak için kullanırız.
FileHelper classı: Bu projemizde dosyaya log yazdırmak için kullanırız.
ServiceHelper classı: Servisimizi debug modda çalıştırabilmek (kod iz sürmek ve akış takibi yapmak için) kullanırız
ProcessStart classı: Harici uygulamayı çağırabilmek için (kernel seviyesinde) kullanırız
Singleton Classı
using System;
namespace LockScreenServiceArge.Helpers
{
///
<summary>
/// Singleton – singleton patternini uygular -helalmis
///
</summary>
///
<typeparam
name=”T“></typeparam>
public
class
Singleton<T> where T : class, new()
{
#region Singleton members
private
static T instance;
public
static T Instance
{
get
{
if (instance == null)
instance = Activator.CreateInstance<T>(); // new T();
return instance;
}
}
#endregion
}
}
FileHelper Classı
using System;
using System.IO;
namespace LockScreenServiceArge.Helpers
{
///
<summary>
/// FileHelper – dosya ile ilgili helper -helalmis
///
</summary>
public
class
FileHelper : Singleton<FileHelper>
{
// ToLog – gönderilen bilgiyi dosyaya yazar
public
void ToLog(string message)
{
string path = $”{AppDomain.CurrentDomain.BaseDirectory}\\Logs”;
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
string filepath = $”{path}\\ServiceLog_{DateTime.Now.Date.ToShortDateString().Replace(‘/’, ‘_’)}.txt”;
if (!File.Exists(filepath))
{
using (StreamWriter sw = File.CreateText(filepath))
{
sw.WriteLine(message);
}
}
else
{
using (StreamWriter sw = File.AppendText(filepath))
{
sw.WriteLine(message);
}
}
}
}
}
ServiceHelper Classı
using System;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;
namespace LockScreenServiceArge.Helpers
{
///
<summary>
/// ServiceHelper – service ile ilgili helper -helalmis
///
</summary>
public
class
ServiceHelper : Singleton<ServiceHelper>
{
// RunService
public
void RunService(ServiceBase[] arr, bool showConsole = false)
{
// debug modda çalışıyor isen
if (Environment.UserInteractive)
{
if (showConsole)
{
Console.WriteLine(“UserInteractive is on”);
Console.WriteLine();
}
// reflection kullanarak mevcut servisin OnStart methodunu hazırlıyoruz
MethodInfo onStart = typeof(ServiceBase).GetMethod(“OnStart”, BindingFlags.Instance | BindingFlags.NonPublic);
foreach (ServiceBase service in arr)
{
if (showConsole)
{
Console.WriteLine(“Starting {0}…”, service.ServiceName);
}
// onstart methodunu çağırıyoruz (debuga düşürüyoruz)
onStart.Invoke(service, new
object[] { new
string[] { } });
if (showConsole)
{
Console.WriteLine(“Started”);
}
}
if (showConsole)
{
Console.WriteLine();
Console.WriteLine(“Press any key to exit”);
}
// bir tuşa basılıncaya kadar bekle
Console.ReadKey();
if (showConsole)
{
Console.WriteLine();
}
// onStop methodunu hazırla
MethodInfo onStop = typeof(ServiceBase).GetMethod(“OnStop”, BindingFlags.Instance | BindingFlags.NonPublic);
foreach (ServiceBase service in arr)
{
if (showConsole)
{
Console.Write(“Stopping {0}…”, service.ServiceName);
}
// onStop methodunu çağır
onStop.Invoke(service, null);
if (showConsole)
{
Console.WriteLine(“Stopped”);
}
}
if (showConsole)
{
Console.WriteLine(“All services stopped”);
}
Thread.Sleep(1000);
}
else
{
// release modda çalışıyor isen
ServiceBase.Run(arr);
}
}
}
}
ProcessHandler Classı
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
namespace LockScreenServiceArge
{
// Ref: https://stackoverflow.com/questions/19776716/c-sharp-windows-service-creates-process-but-doesnt-executes-it
[SuppressUnmanagedCodeSecurity]
public
class
ProcessHandler
{
public
const
int GENERIC_ALL_ACCESS = 0x10000000;
public
const
int STARTF_USESHOWWINDOW = 0x00000001;
public
const
int SE_PRIVILEGE_ENABLED = 0x00000002;
public
const
string SE_INCREASE_QUOTA_NAME = “SeIncreaseQuotaPrivilege”;
internal
const
string SE_TCB_NAME = “SeTcbPrivilege”;
enum
CreateProcessFlags
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00000800,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
enum
TOKEN_INFORMATION_CLASS
{
TokenUser = 1,
TokenGroups,
TokenPrivileges,
TokenOwner,
TokenPrimaryGroup,
TokenDefaultDacl,
TokenSource,
TokenType,
TokenImpersonationLevel,
TokenStatistics,
TokenRestrictedSids,
TokenSessionId,
TokenGroupsAndPrivileges,
TokenSessionReference,
TokenSandBoxInert,
TokenAuditPolicy,
TokenOrigin,
TokenElevationType,
TokenLinkedToken,
TokenElevation,
TokenHasRestrictions,
TokenAccessInformation,
TokenVirtualizationAllowed,
TokenVirtualizationEnabled,
TokenIntegrityLevel,
TokenUIAccess,
TokenMandatoryPolicy,
TokenLogonSid,
MaxTokenInfoClass
}
public
enum
SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
public
enum
TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation
}
#region struct
[StructLayout(LayoutKind.Sequential)]
public
struct
STARTUPINFO
{
public Int32 cb;
public
string lpReserved;
public
string lpDesktop;
public
string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public
struct
PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public Int32 dwProcessID;
public Int32 dwThreadID;
}
[StructLayout(LayoutKind.Sequential)]
public
struct
SECURITY_ATTRIBUTES
{
public Int32 Length;
public IntPtr lpSecurityDescriptor;
public
bool bInheritHandle;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public
struct
TokPriv1Luid
{
public
int Count;
public
long Luid;
public
int Attr;
}
#endregion
#region Win32 API
[DllImport(“advapi32.dll”, SetLastError = true)]
internal
static
extern
bool LookupPrivilegeValue(string host, string name, ref
long pluid);
[DllImport(“advapi32.dll”, ExactSpelling = true, SetLastError = true)]
internal
static
extern
bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
[DllImport(“advapi32.dll”, EntryPoint = “ImpersonateLoggedOnUser”, SetLastError = true,
CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public
static
extern IntPtr ImpersonateLoggedOnUser(IntPtr hToken);
[
DllImport(“kernel32.dll”,
EntryPoint = “CloseHandle”, SetLastError = true,
CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
]
public
static
extern
bool CloseHandle(IntPtr handle);
[
DllImport(“advapi32.dll”,
EntryPoint = “CreateProcessAsUser”, SetLastError = true,
CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
]
public
static
extern
bool
CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine,
ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes,
bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment,
string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
ref PROCESS_INFORMATION lpProcessInformation);
[
DllImport(“advapi32.dll”,
EntryPoint = “DuplicateTokenEx”)
]
public
static
extern
bool
DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess,
ref SECURITY_ATTRIBUTES lpThreadAttributes,
Int32 ImpersonationLevel, Int32 dwTokenType,
ref IntPtr phNewToken);
[DllImport(“advapi32.dll”, SetLastError = true)]
static
extern
bool RevertToSelf();
[DllImport(“Kernel32.dll”, SetLastError = true)]
public
static
extern IntPtr WTSGetActiveConsoleSessionId();
[DllImport(“advapi32.dll”)]
public
static
extern IntPtr SetTokenInformation(IntPtr TokenHandle, IntPtr TokenInformationClass, IntPtr TokenInformation, IntPtr TokenInformationLength);
[DllImport(“wtsapi32.dll”, SetLastError = true)]
public
static
extern
bool WTSQueryUserToken(uint sessionId, out IntPtr Token);
#endregion
private
static
int GetCurrentUserSessionID()
{
uint dwSessionId = (uint)WTSGetActiveConsoleSessionId();
Process[] processes = Process.GetProcessesByName(“winlogon”);
foreach (Process p in processes)
{
if ((uint)p.SessionId == dwSessionId)
{
return p.SessionId;
}
}
return -1;
}
public
static Process CreateProcessAsUser(string filePath, string args)
{
var dupedToken = IntPtr.Zero;
var pi = new PROCESS_INFORMATION();
var sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);
try
{
var token = WindowsIdentity.GetCurrent().Token;
var si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = “”;
si.dwFlags = STARTF_USESHOWWINDOW;
var dir = Path.GetDirectoryName(filePath);
var fileName = Path.GetFileName(filePath);
if (!DuplicateTokenEx(
token,
GENERIC_ALL_ACCESS,
ref sa,
(int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
(int)TOKEN_TYPE.TokenPrimary,
ref dupedToken
))
{
throw
new Win32Exception(Marshal.GetLastWin32Error());
}
uint curSessionid = (uint)GetCurrentUserSessionID();
if (!WTSQueryUserToken(curSessionid, out dupedToken))
{
throw
new Win32Exception(Marshal.GetLastWin32Error());
}
WindowsIdentity.Impersonate(WindowsIdentity.GetCurrent().Token);
if (!CreateProcessAsUser(
dupedToken,
filePath,
string.Format(“\”{0}\” {1}”, fileName.Replace(“\””, “\”\””), args),
ref sa,
ref sa,
false,
(int)CreateProcessFlags.CREATE_NEW_CONSOLE,
IntPtr.Zero,
dir,
ref si,
ref pi
))
{
throw
new Win32Exception(Marshal.GetLastWin32Error());
}
return Process.GetProcessById(pi.dwProcessID);
}
finally
{
if (pi.hProcess != IntPtr.Zero)
CloseHandle(pi.hProcess);
if (pi.hThread != IntPtr.Zero)
CloseHandle(pi.hThread);
if (dupedToken != IntPtr.Zero)
CloseHandle(dupedToken);
}
}
}
}
Output Ayarının Setlenmesi
Service projesinin outputtype tipi ConsoleApplication seçilir
Program.cs
using LockScreenServiceArge.Helpers;
using System.ServiceProcess;
namespace LockScreenServiceArge
{
static
class
Program
{
///
<summary>
/// The main entry point for the application.
///
</summary>
static
void Main()
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new Service1()
};
// helper classımızı çağırarak debug modda koda düşebiliyor olacağız
ServiceHelper.Instance.RunService(ServicesToRun);
}
}
}
Debug modda run (F5) yaptığımızda servisimizin OnStart başlangıç metoduna düştüğünü görebiliriz, F10-F11 tuş kombinasyonları ile adım adım kod iz sürülebilir
Böylece, mevcut methodların debug için ayrı, release için ayrı kopyalarını çıkartmak zorunda kalmıyoruz
Release modda çalıştırıp, eventViewer loglarına girdilerin yapılıp hangi değer neymiş gibi bakmamıza gerek kalmayacak.
Kod daha stabil hale gelmiş olacak, debugda çalışan bir akışın releaseda da çalışıyor olmasını bekliyor olacağız
Service1.cs
using LockScreenServiceArge.Helpers;
using Microsoft.Win32;
using System;
using System.ServiceProcess;
namespace LockScreenServiceArge
{
public
partial
class
Service1 : ServiceBase
{
public
Service1()
{
// session change eventinin yakalanması için gerekli
this.CanHandleSessionChangeEvent = true;
InitializeComponent();
}
protected
override
void OnStart(string[] args)
{
FileHelper.Instance.ToLog($”{DateTime.Now} service started”);
}
protected
override
void OnSessionChange(SessionChangeDescription changeDescription)
{
// Lock, Unlock gibi işlemlerin yakalanması için
FileHelper.Instance.ToLog($”{DateTime.Now} OnSessionChange”);
try
{
switch (changeDescription.Reason)
{
case SessionChangeReason.ConsoleConnect:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.ConsoleDisconnect:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.RemoteConnect:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.RemoteDisconnect:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.SessionLogon:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.SessionLogoff:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.SessionLock:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.SessionUnlock:
{
// unlock yapıldığında aşağıdaki dosyayı çalıştır
string file = $@”C:\Users\husamettin.elalmis\source\repos\FindByIdArge\FullScreenArge\bin\Debug\FullScreenArge.exe”;
ProcessHandler.CreateProcessAsUser($@”{file}“, “”);
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
case SessionChangeReason.SessionRemoteControl:
{
FileHelper.Instance.ToLog($”{DateTime.Now}
{changeDescription.Reason.ToString()}“);
break;
}
default:
break;
}
}
catch (Exception ex)
{
FileHelper.Instance.ToLog($”Exception: {DateTime.Now}
{ex.Message}“);
}
}
private
void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
// powermod değiştiğinde
Console.WriteLine(e.Mode.ToString());
}
protected
override
void OnStop()
{
// service durdurulduğunda
}
}
}
Sonuç
Burada bahsi geçen kodların tamamı örnek proje yapılarak uygulanmış ve hem debug hem release modda çalıştığı görülmüştür
Bu projedeki arge şunu hedeflemiştir,
Service uygulaması kod seviyesinde Debug edilebilsin
MFA simulasyonu yapabilsin (service dışarıdaki bir harici uygulamayı çalıştırabilsin)
Çalıştırılan harici uygulamadaki keyboard eventslar argelenebilsin (tuş takımı kilitlemeleri vb.)
Lock ve Unlock durumlarını algılasın, Unlock yapıldığında MFA formu ekrana gelsin vb.
Windows Service projelerinde burada bahsi geçen temel bilgileri ve helper classlarını standard halde kullanabiliriz (Easy2Patch projesinde bu şekilde kullandım, sessionLimit ve diğer servislerde de aynı yapı kullanılabilir)