post זה הוא השלישי והאחרון בסדרה אודות תמיכה ב WindowsService בסביבת פיתוח
קדמו לו:
הפעם נראה, כיצד נוכל שאותה אפליקציה, תוכל:
-
להתקין את ה Service ב Service Control Managment
-
להסיר את ההתקנה
-
להריץ את ה Service
-
לעצור אותו
-
ובהמשך, ניתן גם פתרון ל Service שרץ תחת סביבה אינטראקטיבית, ולא יכול להשתמש ביכולת שהצגנו ב post הראשון.
כדי לבצע את המשימה, נקלוט את שורת הפקודה, ובהתאם לקלט, נבצע את אחת מן הפקודות הנ"ל.
בנוסף, על האפליקציה לדעת מה שם ניתן לה בSCM (קרי: Service Control Managment) , ולשם כך נשתמש בקונפיגורציה.
אז איך מתקינים?
1: public static void Install(string serviceName)
2: {
3: IDictionary savedState = new Hashtable();
4:
5: using (var installer =
6: new AssemblyInstaller(typeof(TService).Assembly,
7: new string[] { "/logFile=WindowsServiceRunner-" + typeof(TService).Name + "-.log" }) { UseNewContext = true })
8: {
9: CatchExceptions(() =>
10: {
11: installer.Install(savedState);
12: installer.Commit(savedState);
13: });
14: }
15: Start(serviceName);
16: }
17:
המתודה Install, מתקינה את ה Service לתוך ה SCM, את ה log עם התוצאות היא מפיקה לתוך קובץ בספריית ההתקנה, כאשר בשם הקובץ, משובץ שם המחלקה.
בסופו של התהליך נקראת המתודה Start שמריצה את ה Service. (ניתן בהחלט, לנתק בין שתי הפעולות)
הפעולות שקיימת סבירות שיזרקו חריגה, עטופות במתודה CatchExceptions המוגדרת כך:
1: static void CatchExceptions(Action action)
2: {
3: try
4: {
5: action();
6: }
7: catch (Exception ex)
8: {
9: Console.WriteLine(ex.ToString());
10: Console.ReadLine();
11: }
12: }
13:
שאר הפעולות (הסרת ההתקנה, עצירה והרצה) הם פשוטות יותר, וניתן לעיין בהן בקוד המקור המצורף.
כדי לקרוא לפעולות אלו, נשתמש בארגומנטים המתקבלים בשורת הפקודה
(המתודה CompareInput מחלצת ובודקת האם הארגומנטים מתאימים לקלט. כמובן די באחת משתי האפשרויות המוצגות להלן)
1: public static void Run(string[] args, string serviceName)
2: {
3: if (Environment.UserInteractive)
4: {
5: if (args.Length > 0)
6: {
7: if (CompareInput(args[0], "i", "/i"))
8: {
9: ServiceControlManager<TService>.Install(serviceName);
10: }
11: else if (CompareInput(args[0], "u", "/u"))
12: {
13: ServiceControlManager<TService>.Uninstall();
14: }
15: else if (CompareInput(args[0], "r", "/r"))
16: {
17: ServiceControlManager<TService>.Start(serviceName);
18: }
19: else if (CompareInput(args[0], "s", "/s"))
20: {
21: ServiceControlManager<TService>.Stop(serviceName);
22: }
23: else if (CompareInput(args[0], "?", "/?"))
24: {
25: ShowHelp();
26: }
27: else
28: {
29: RunServiceInConsole(args);
30: }
31: }
32: else
33: {
34: RunServiceInConsole(args);
35: }
36: }
37: else
38: {
39: RunService();
40: }
41: }
כדי להתקין את האפליקציה מתוכנת התקנה, נוסיף Commit Action שיקבל כארגומנט i. (ניתן לראות דוגמא בקוד המקור).
וכך נראית ה Main בקוד הדוגמא שלנו:
1: static void Main(string [] args)
2: {
3: WindowsServiceRunner<MyService>.Run(args, Settings.Default.ServiceName);
4: }
אחרי שהצגנו את הפתרון הנ"ל, נוכל לתת פתרון גם ל Service שהינו אינטארקטיבי, ובמקרה זה Environment.UserInteractive מחזיר תמיד true.
לשם כך הוספנו בקוד המקור את המחלקה InteractiveWindowsServiceRunner המטפלת במקרה זה.
כאן הטיפול הוא ע"י העברת ארגומנט d בסביבת הפיתוח:
1: if (CompareInput(args[0], "d", "/d"))
2: {
3: ServiceControlManager<TService>.Install(serviceName);
4: }
להורדת הקוד לכל הסדרה
post זה הוא המשך של קודמו: תמיכה ב WindowsService בזמן פיתוח.
בזמן שננצל את היכולת שהוצגה בpost הקודם, והיא עבודה אינטראקטיבית מליאה בזמן פיתוח, נוכל להציג מידע שחשוב לנו, בחלון ה Console.
יתכן שמידע tracing זה, יהיה חשוב לנו גם בזמן שנריץ את הקוד כ WindowsService ולא רק בזמן ההרצה כ Console Application.
איך נדאג לכך?
ראשית, בזמן עבודה כ Service נצטרך להפרד מידידנו האהוב ה Console. אין שום דרך להציג אותו, הלא בחרנו ב service שאינו אינטראקטיבי.
במקום חלון ה console נוכל להסתפק בקובץ.
אם כך, במה זה שונה מ log? זה לא שונה, פרט להתנהגות אחת: זה אותו המידע שאנו מציגים בחלון ה Console בסביבת הפיתוח.
לשם כך, נשתמש ב Trace (מתוך System.Diagnostics) ולא ב Console.Write.
נדאג שהודעות ה Trace יוצגו הן בחלון ה Console והן בקובץ יעודי, בהתאם לסביבת הריצה.
בדוגמא הבאה, נציג את הקונפיגורציה של שני המצבים כאחד (אם כי בד"כ דרוש לנו רק אחת משתי האפשרויות):
1: <system.diagnostics>
2: <trace autoflush="true" indentsize="4">
3: <listeners>
4: <add name="fileListener" type="System.Diagnostics.TextWriterTraceListener"
5: initializeData="c:\logs\tracing.log" />
6: <add name="consoleListener" type="System.Diagnostics.ConsoleTraceListener"/>
7: </listeners>
8: </trace>
9: </system.diagnostics>
TextWriterTraceListener – כשמו, רושם לתוך הקובץ המוגדר ב initializeData
ConsoleTraceListener – רושם את ההודעות לחלון ה Console.
כדי לתת ממשק זהה ונוח כמו של Console.Write הוספתי class שמבצע זאת. בנוסף, הוא גם מוסיף חתימה של זמן לכל הודעת Trace.
קטע דוגמא:
1: public static class Tracing
2: {
3: public static void WriteLine(string format, params object[] args)
4: {
5: TraceTimestamp();
6: Trace.WriteLine(string.Format(format, args));
7: }
8:
9: //More methods here
10:
11: private static void TraceTimestamp()
12: {
13: Trace.Write(DateTime.Now.ToString("dd/MM/yyy hh:mm:ss.fff => "));
14: }
15: }
16:
להורדת הקוד לכל הסדרה
ה post הבא בנושא: Self Installing WindowsService -שרות שיודע להתקין את עצמו
בעבודה עם WindowsService קיימת אי נוחות בסביבת הפיתוח.
- אי אפשר סתם כך להריץ אותו ב VisualStudio (לא ניתן ללחוץ F5 ולהריץ).
- יש צורך להוסיף שורה שטוענת את ה Debugger , זו שורה שנוספת לקוד על כל המשתמע מכך, ויש גם לדאוג שלא תופיע בסביבת Realese.
- לא ניתן להדפיס הודעות ל Console
סדרת פוסטים זו , נועדה כדי לפתור בעיות אלו.
הרצת ה Service כ Console בזמן debug.
כדי להריץ את ה Service בלחיצת F5 (או סתם כך משורת הפקודה) , יש ליצור הבחנה בין הסביבה בה רצה האפליקציה כ Service לבין הסביבה בה רצה האפליקציה שלא כ Service.
הפתרון הפשוט הוא, לבצע אבחנה האם למשתמש יש אינטראקציה עם שלחן העבודה. למי אין? ל WindowsService (אלא אם כן הוגדר אחרת – ולזה ניתן פתרון אחר).
בעת עליית המערכת נבדוק:
if (Environment.UserInteractive())
בדיקה זו תספק כמעט את כל הפתרון, וכך נוכל לבדל בין שתי סביבות ההרצה.
if (Environment.UserInteractive)
{
RunServiceInConsole();
}
else
{
RunAsWindowsService();
}
המתודה RunAsWindowsService תספק את המימוש הרגיל
private static void RunService()
{
ServiceBase.Run(new[] { new MyService() });
}
המתודה RunServiceInConsole תקרא למתודות של ה Service באמצעות Reflection:
private static void RunServiceInConsole(string[] args)
MyService service = new MyService();
Console.WriteLine("Press <ENTER> to terminate service.");
המתודה service.Start אינה קיימת, אלא זו הרחבה:
public static class WindowsServiceHelper
{
private static MethodInfo MethodInfo { get; set; }
public static void Start(
this ServiceBase service, string[] args)
{
if (MethodInfo == null)
{
MethodInfo = service.GetType()
.GetMethod("OnStart",
BindingFlags.Instance | BindingFlags.NonPublic);
Debug.Assert(MethodInfo != null
, "Can't get method OnStart");
}
MethodInfo.Invoke(service, new[] { args })
}
}
בשיטה המתוארת כאן, נוכל להריץ את ה Service שלנו משורת הפקודה, והוא יריץ אותו כאפליקצית Console רגילה.
במידה ובחרנו שלService שלנו תהיה אינטראקציה עם ה desktop הפתרון המוצע כאן לא יועיל. למקרה זה יהיה פתרון ב post נוסף.
נקודה נוספת שחשוב להדגיש היא, שיש לשנות את סוג הפרויקט ל Console Application
כמובן שעדיף לשפר את הפתרון, ולהשתמש לשם כך ב generics:
public static class WindowsServiceRunner<TService>
where TService : ServiceBase, new()
{
public static void Run(string[] args, string serviceName)
{
if (Environment.UserInteractive)
{
RunServiceInConsole(args);
}
else
{
RunService();
}
}
private static void RunService()
{
ServiceBase.Run(new[] { new TService() });
}
private static void RunServiceInConsole(string[] args)
{
TService service = new TService();
service.Start(args);
Console.WriteLine("Press <ENTER> to terminate service.");
Console.ReadLine();
service.Stop();
}
}
להורדת הקוד לכל הסדרה
הפוסטים הבאים: