בשבוע שעבר כתבתי דוגמת קוד לשימוש ב ServiceBase על מנת לממש Windows Service תחת .Net.
ServiceBase ממש ועוטף את כל מה שנדרש מנת לממש Service תחת .Net אבל לא כל הפונקציונליות של ה Service Control Manager או בקיצור SCM נחשפת ואנחנו צריכים לקרוא ל SCM API באופן ישיר דרך PINVOKE עבור חלק משירותים. אחד מהשירותים הוא היכולת של Service לעדכן את הSCM לגבי מצבו ב Startup/Shutdown כגון כמה זמן על ה SCM להמתין עד שהService יסיים את הפעולה.
בMSDN קיימת דוגמת קוד אשר מראה כיצד לקרוא לפונקציה SetServiceStatus על מנת לעדכן את ה SCM במצב ה Service
הדוגמה מגדירה Struct בשם public struct SERVICE_STATUS אשר מכיל את כל השדות הנדרשים ו public enum State אשר מכיל את הערכים אשר ה SCM מצפה לקבל.
CODE
1 [StructLayout(LayoutKind.Sequential)]
2 public struct SERVICE_STATUS
3 {
4 public int serviceType;
5 public int currentState;
6 public int controlsAccepted;
7 public int win32ExitCode;
8 public int serviceSpecificExitCode;
9 public int checkPoint;
10 public int waitHint;
11 }
12
13 public enum State
14 {
15 SERVICE_STOPPED = 0x00000001,
16 SERVICE_START_PENDING = 0x00000002,
17 SERVICE_STOP_PENDING = 0x00000003,
18 SERVICE_RUNNING = 0x00000004,
19 SERVICE_CONTINUE_PENDING = 0x00000005,
20 SERVICE_PAUSE_PENDING = 0x00000006,
21 SERVICE_PAUSED = 0x00000007,
22 }
[DllImport("advapi32.dll", SetLastError = true)]
public static extern int SetServiceStatus(IntPtr hServiceStatus, SERVICE_STATUS lpServiceStatus);
protected override void OnStart(string[] args)
{
IntPtr handle = this.ServiceHandle;
try
{
myServiceStatus.currentState = (int) State.SERVICE_START_PENDING;
SetServiceStatus(handle, ref myServiceStatus);
// Do our Service Init staff here
...
...
}
catch (Exception ex)
{
ServiceEventLog.WriteEntry(String.Format("Failed to start Time Logger Service, Exception is {0} on {1} at {2}",
ex.ToString(),
ex.TargetSite,
ex.StackTrace), EventLogEntryType.Error);
myServiceStatus.currentState = (int) State.SERVICE_STOPPED;
SetServiceStatus(handle, ref myServiceStatus);
throw;
}
}
כאשר ננסה להפעיל את ה Service לאחר ההתקנה נקבל את ההודעה הבאה ב EventLog
Service cannot be started. System.AccessViolationException:
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
at ServiceSample.SimpleService.SetServiceStatus(IntPtr hServiceStatus, SERVICE_STATUS lpServiceStatus)
at ServiceSample.SimpleService.OnStart(String[] args)
at System.ServiceProcess.ServiceBase.ServiceQueuedMainCallback(Object state)"
BOOL WINAPI SetServiceStatus(
__in SERVICE_STATUS_HANDLE hServiceStatus,
__in LPSERVICE_STATUS lpServiceStatus
);
עד כאן טוב ויפה, זה נראה זהה להגדרה שלי ב DLLImport ועדיין אני מקבל את התעופה.
אז לאחר כמה דקות על Google נמצאה הבעיה. ההגדרה של ה DLLImport מוטעת ההגדרה הנכונה היא
[DllImport("advapi32.dll", SetLastError = true)]
public static extern int SetServiceStatus(IntPtr hServiceStatus, ref SERVICE_STATUS lpServiceStatus);
כאשר אנחנו קוראים לSetServiceStatus מקוד Native אין שום בעיה מאחר ועובר מצביע לעותק Struct והפונקציה הנקראת לא יכולה לשנות את ה Struct המקורי אבל אף אחד לא באמת בודק מה היא עושה איתו, מתברר שהיא כן כותבת לשם וכאשר אנחנו רצים בסביבת Managed של .Net הבעיה מתחילה ואנחנו מקבלים את ה Exception.
לאחר זה ישבתי עם WinDBG על מנת לנסות להבין את הבעייה וראיתי את ה Machine Code של הפונקציה ומצאתי את הנקודה שכותבת ל Struct. זאת כנראה הייתה הדרך בה הייתי נוקט אם לא הייתי מוצא שום דבר חיפוש אחר הבעייה.
לסיכומו של עניין, מה שכנראה כבר ידענו מזמן, גם בתיעוד של MSDN יש טעויות.
אני יודע שעבר הרבה זמן מהפוסט האחרון אבל (זמן התירוצים) ....
- אני ענת (אישתי) ודרור (בן השמונה חודשים) בילינו שבוע חופשה מדהימה ברומא ...
- שבוע אחרי זה אני נסעתי לנסיעת עבודה ללא גישה לרשת (יש עדיין מקומות כאלה בעולם) וכמובן ללא זמן חופשי לכתוב כלום.
- חזרתי מאז אני מאוד עסוק בעבודה מול לקוח מאוד תובעני ...
אבל מה על הפרק בקרוב :
- טעויות בקוד דמו של Microsoft (לא יכול להיות)
- המשך הסדרה על החיים ללא משרד פיזי