EventMessageFile should be unquoted.
[nssm.git] / registry.cpp
1 #include "nssm.h"\r
2 \r
3 extern const TCHAR *exit_action_strings[];\r
4 \r
5 static int service_registry_path(const TCHAR *service_name, bool parameters, const TCHAR *sub, TCHAR *buffer, unsigned long buflen) {\r
6   int ret;\r
7 \r
8   if (parameters) {\r
9     if (sub) ret = _sntprintf_s(buffer, buflen, _TRUNCATE, NSSM_REGISTRY _T("\\") NSSM_REG_PARAMETERS _T("\\%s"), service_name, sub);\r
10     else ret = _sntprintf_s(buffer, buflen, _TRUNCATE, NSSM_REGISTRY _T("\\") NSSM_REG_PARAMETERS, service_name);\r
11   }\r
12   else ret = _sntprintf_s(buffer, buflen, _TRUNCATE, NSSM_REGISTRY, service_name);\r
13 \r
14   return ret;\r
15 }\r
16 \r
17 static HKEY open_registry_key(const TCHAR *registry, REGSAM sam, bool must_exist) {\r
18   HKEY key;\r
19 \r
20   if (sam & KEY_SET_VALUE) {\r
21     if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, registry, 0, 0, REG_OPTION_NON_VOLATILE, sam, 0, &key, 0) != ERROR_SUCCESS) {\r
22       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENKEY_FAILED, registry, error_string(GetLastError()), 0);\r
23       return 0;\r
24     }\r
25   }\r
26   else {\r
27     long error = RegOpenKeyEx(HKEY_LOCAL_MACHINE, registry, 0, sam, &key);\r
28     if (error != ERROR_SUCCESS) {\r
29       if (error == ERROR_FILE_NOT_FOUND && ! must_exist) return 0;\r
30       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENKEY_FAILED, registry, error_string(GetLastError()), 0);\r
31       return 0;\r
32     }\r
33   }\r
34 \r
35   return key;\r
36 }\r
37 \r
38 int create_messages() {\r
39   HKEY key;\r
40 \r
41   TCHAR registry[KEY_LENGTH];\r
42   if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, _T("SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\%s"), NSSM) < 0) {\r
43     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("eventlog registry"), _T("create_messages()"), 0);\r
44     return 1;\r
45   }\r
46 \r
47   if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, registry, 0, 0, REG_OPTION_NON_VOLATILE, KEY_WRITE, 0, &key, 0) != ERROR_SUCCESS) {\r
48     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENKEY_FAILED, registry, error_string(GetLastError()), 0);\r
49     return 2;\r
50   }\r
51 \r
52   /* Get path of this program */\r
53   const TCHAR *path = nssm_unquoted_imagepath();\r
54 \r
55   /* Try to register the module but don't worry so much on failure */\r
56   RegSetValueEx(key, _T("EventMessageFile"), 0, REG_SZ, (const unsigned char *) path, (unsigned long) (_tcslen(path) +  1) * sizeof(TCHAR));\r
57   unsigned long types = EVENTLOG_INFORMATION_TYPE | EVENTLOG_WARNING_TYPE | EVENTLOG_ERROR_TYPE;\r
58   RegSetValueEx(key, _T("TypesSupported"), 0, REG_DWORD, (const unsigned char *) &types, sizeof(types));\r
59 \r
60   return 0;\r
61 }\r
62 \r
63 int create_parameters(nssm_service_t *service, bool editing) {\r
64   /* Try to open the registry */\r
65   HKEY key = open_registry(service->name, KEY_WRITE);\r
66   if (! key) return 1;\r
67 \r
68   /* Remember parameters in case we need to delete them. */\r
69   TCHAR registry[KEY_LENGTH];\r
70   int ret = service_registry_path(service->name, true, 0, registry, _countof(registry));\r
71 \r
72   /* Try to create the parameters */\r
73   if (set_expand_string(key, NSSM_REG_EXE, service->exe)) {\r
74     if (ret > 0) RegDeleteKey(HKEY_LOCAL_MACHINE, registry);\r
75     RegCloseKey(key);\r
76     return 2;\r
77   }\r
78   if (set_expand_string(key, NSSM_REG_FLAGS, service->flags)) {\r
79     if (ret > 0) RegDeleteKey(HKEY_LOCAL_MACHINE, registry);\r
80     RegCloseKey(key);\r
81     return 3;\r
82   }\r
83   if (set_expand_string(key, NSSM_REG_DIR, service->dir)) {\r
84     if (ret > 0) RegDeleteKey(HKEY_LOCAL_MACHINE, registry);\r
85     RegCloseKey(key);\r
86     return 4;\r
87   }\r
88 \r
89   /* Other non-default parameters. May fail. */\r
90   if (service->priority != NORMAL_PRIORITY_CLASS) set_number(key, NSSM_REG_PRIORITY, service->priority);\r
91   else if (editing) RegDeleteValue(key, NSSM_REG_PRIORITY);\r
92   if (service->affinity) {\r
93     TCHAR *string;\r
94     if (! affinity_mask_to_string(service->affinity, &string)) {\r
95       if (RegSetValueEx(key, NSSM_REG_AFFINITY, 0, REG_SZ, (const unsigned char *) string, (unsigned long) (_tcslen(string) + 1) * sizeof(TCHAR)) != ERROR_SUCCESS) {\r
96         log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, NSSM_REG_AFFINITY, error_string(GetLastError()), 0);\r
97         HeapFree(GetProcessHeap(), 0, string);\r
98         return 5;\r
99       }\r
100     }\r
101     if (string) HeapFree(GetProcessHeap(), 0, string);\r
102   }\r
103   else if (editing) RegDeleteValue(key, NSSM_REG_AFFINITY);\r
104   unsigned long stop_method_skip = ~service->stop_method;\r
105   if (stop_method_skip) set_number(key, NSSM_REG_STOP_METHOD_SKIP, stop_method_skip);\r
106   else if (editing) RegDeleteValue(key, NSSM_REG_STOP_METHOD_SKIP);\r
107   if (service->default_exit_action < NSSM_NUM_EXIT_ACTIONS) create_exit_action(service->name, exit_action_strings[service->default_exit_action], editing);\r
108   if (service->restart_delay) set_number(key, NSSM_REG_RESTART_DELAY, service->restart_delay);\r
109   else if (editing) RegDeleteValue(key, NSSM_REG_RESTART_DELAY);\r
110   if (service->throttle_delay != NSSM_RESET_THROTTLE_RESTART) set_number(key, NSSM_REG_THROTTLE, service->throttle_delay);\r
111   else if (editing) RegDeleteValue(key, NSSM_REG_THROTTLE);\r
112   if (service->kill_console_delay != NSSM_KILL_CONSOLE_GRACE_PERIOD) set_number(key, NSSM_REG_KILL_CONSOLE_GRACE_PERIOD, service->kill_console_delay);\r
113   else if (editing) RegDeleteValue(key, NSSM_REG_KILL_CONSOLE_GRACE_PERIOD);\r
114   if (service->kill_window_delay != NSSM_KILL_WINDOW_GRACE_PERIOD) set_number(key, NSSM_REG_KILL_WINDOW_GRACE_PERIOD, service->kill_window_delay);\r
115   else if (editing) RegDeleteValue(key, NSSM_REG_KILL_WINDOW_GRACE_PERIOD);\r
116   if (service->kill_threads_delay != NSSM_KILL_THREADS_GRACE_PERIOD) set_number(key, NSSM_REG_KILL_THREADS_GRACE_PERIOD, service->kill_threads_delay);\r
117   else if (editing) RegDeleteValue(key, NSSM_REG_KILL_THREADS_GRACE_PERIOD);\r
118   if (! service->kill_process_tree) set_number(key, NSSM_REG_KILL_PROCESS_TREE, 0);\r
119   else if (editing) RegDeleteValue(key, NSSM_REG_KILL_PROCESS_TREE);\r
120   if (service->stdin_path[0] || editing) {\r
121     if (service->stdin_path[0]) set_expand_string(key, NSSM_REG_STDIN, service->stdin_path);\r
122     else if (editing) RegDeleteValue(key, NSSM_REG_STDIN);\r
123     if (service->stdin_sharing != NSSM_STDIN_SHARING) set_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_SHARING, service->stdin_sharing);\r
124     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_SHARING);\r
125     if (service->stdin_disposition != NSSM_STDIN_DISPOSITION) set_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_DISPOSITION, service->stdin_disposition);\r
126     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_DISPOSITION);\r
127     if (service->stdin_flags != NSSM_STDIN_FLAGS) set_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_FLAGS, service->stdin_flags);\r
128     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDIN, NSSM_REG_STDIO_FLAGS);\r
129   }\r
130   if (service->stdout_path[0] || editing) {\r
131     if (service->stdout_path[0]) set_expand_string(key, NSSM_REG_STDOUT, service->stdout_path);\r
132     else if (editing) RegDeleteValue(key, NSSM_REG_STDOUT);\r
133     if (service->stdout_sharing != NSSM_STDOUT_SHARING) set_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_SHARING, service->stdout_sharing);\r
134     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_SHARING);\r
135     if (service->stdout_disposition != NSSM_STDOUT_DISPOSITION) set_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_DISPOSITION, service->stdout_disposition);\r
136     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_DISPOSITION);\r
137     if (service->stdout_flags != NSSM_STDOUT_FLAGS) set_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_FLAGS, service->stdout_flags);\r
138     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_FLAGS);\r
139     if (service->stdout_copy_and_truncate) set_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_COPY_AND_TRUNCATE, 1);\r
140     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDOUT, NSSM_REG_STDIO_COPY_AND_TRUNCATE);\r
141   }\r
142   if (service->stderr_path[0] || editing) {\r
143     if (service->stderr_path[0]) set_expand_string(key, NSSM_REG_STDERR, service->stderr_path);\r
144     else if (editing) RegDeleteValue(key, NSSM_REG_STDERR);\r
145     if (service->stderr_sharing != NSSM_STDERR_SHARING) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_SHARING, service->stderr_sharing);\r
146     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_SHARING);\r
147     if (service->stderr_disposition != NSSM_STDERR_DISPOSITION) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_DISPOSITION, service->stderr_disposition);\r
148     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_DISPOSITION);\r
149     if (service->stderr_flags != NSSM_STDERR_FLAGS) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_FLAGS, service->stderr_flags);\r
150     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_FLAGS);\r
151     if (service->stderr_copy_and_truncate) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_COPY_AND_TRUNCATE, 1);\r
152     else if (editing) delete_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_COPY_AND_TRUNCATE);\r
153   }\r
154   if (service->rotate_files) set_number(key, NSSM_REG_ROTATE, 1);\r
155   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE);\r
156   if (service->rotate_stdout_online) set_number(key, NSSM_REG_ROTATE_ONLINE, 1);\r
157   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE_ONLINE);\r
158   if (service->rotate_seconds) set_number(key, NSSM_REG_ROTATE_SECONDS, service->rotate_seconds);\r
159   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE_SECONDS);\r
160   if (service->rotate_bytes_low) set_number(key, NSSM_REG_ROTATE_BYTES_LOW, service->rotate_bytes_low);\r
161   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE_BYTES_LOW);\r
162   if (service->rotate_bytes_high) set_number(key, NSSM_REG_ROTATE_BYTES_HIGH, service->rotate_bytes_high);\r
163   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE_BYTES_HIGH);\r
164   if (service->rotate_delay != NSSM_ROTATE_DELAY) set_number(key, NSSM_REG_ROTATE_DELAY, service->rotate_delay);\r
165   else if (editing) RegDeleteValue(key, NSSM_REG_ROTATE_DELAY);\r
166   if (service->no_console) set_number(key, NSSM_REG_NO_CONSOLE, 1);\r
167   else if (editing) RegDeleteValue(key, NSSM_REG_NO_CONSOLE);\r
168 \r
169   /* Environment */\r
170   if (service->env) {\r
171     if (RegSetValueEx(key, NSSM_REG_ENV, 0, REG_MULTI_SZ, (const unsigned char *) service->env, (unsigned long) service->envlen * sizeof(TCHAR)) != ERROR_SUCCESS) {\r
172       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, NSSM_REG_ENV, error_string(GetLastError()), 0);\r
173     }\r
174   }\r
175   else if (editing) RegDeleteValue(key, NSSM_REG_ENV);\r
176   if (service->env_extra) {\r
177     if (RegSetValueEx(key, NSSM_REG_ENV_EXTRA, 0, REG_MULTI_SZ, (const unsigned char *) service->env_extra, (unsigned long) service->env_extralen * sizeof(TCHAR)) != ERROR_SUCCESS) {\r
178       log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, NSSM_REG_ENV_EXTRA, error_string(GetLastError()), 0);\r
179     }\r
180   }\r
181   else if (editing) RegDeleteValue(key, NSSM_REG_ENV_EXTRA);\r
182 \r
183   /* Close registry. */\r
184   RegCloseKey(key);\r
185 \r
186   return 0;\r
187 }\r
188 \r
189 int create_exit_action(TCHAR *service_name, const TCHAR *action_string, bool editing) {\r
190   /* Get registry */\r
191   TCHAR registry[KEY_LENGTH];\r
192   if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, NSSM_REGISTRY _T("\\%s"), service_name, NSSM_REG_EXIT) < 0) {\r
193     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("NSSM_REG_EXIT"), _T("create_exit_action()"), 0);\r
194     return 1;\r
195   }\r
196 \r
197   /* Try to open the registry */\r
198   HKEY key;\r
199   unsigned long disposition;\r
200   if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, registry, 0, 0, REG_OPTION_NON_VOLATILE, KEY_WRITE, 0, &key, &disposition) != ERROR_SUCCESS) {\r
201     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OPENKEY_FAILED, registry, error_string(GetLastError()), 0);\r
202     return 2;\r
203   }\r
204 \r
205   /* Do nothing if the key already existed */\r
206   if (disposition == REG_OPENED_EXISTING_KEY && ! editing) {\r
207     RegCloseKey(key);\r
208     return 0;\r
209   }\r
210 \r
211   /* Create the default value */\r
212   if (RegSetValueEx(key, 0, 0, REG_SZ, (const unsigned char *) action_string, (unsigned long) (_tcslen(action_string) + 1) * sizeof(TCHAR)) != ERROR_SUCCESS) {\r
213     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, NSSM_REG_EXIT, error_string(GetLastError()), 0);\r
214     RegCloseKey(key);\r
215     return 3;\r
216   }\r
217 \r
218   /* Close registry */\r
219   RegCloseKey(key);\r
220 \r
221   return 0;\r
222 }\r
223 \r
224 int get_environment(TCHAR *service_name, HKEY key, TCHAR *value, TCHAR **env, unsigned long *envlen) {\r
225   unsigned long type = REG_MULTI_SZ;\r
226 \r
227   /* Dummy test to find buffer size */\r
228   unsigned long ret = RegQueryValueEx(key, value, 0, &type, NULL, envlen);\r
229   if (ret != ERROR_SUCCESS) {\r
230     *env = 0;\r
231     *envlen = 0;\r
232     /* The service probably doesn't have any environment configured */\r
233     if (ret == ERROR_FILE_NOT_FOUND) return 0;\r
234     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(GetLastError()), 0);\r
235     return 1;\r
236   }\r
237 \r
238   if (type != REG_MULTI_SZ) {\r
239     *env = 0;\r
240     *envlen = 0;\r
241     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_INVALID_ENVIRONMENT_STRING_TYPE, value, service_name, 0);\r
242     return 2;\r
243   }\r
244 \r
245   /* Probably not possible */\r
246   if (! *envlen) return 0;\r
247 \r
248   /* Previously initialised? */\r
249   if (*env) HeapFree(GetProcessHeap(), 0, *env);\r
250 \r
251   *env = (TCHAR *) HeapAlloc(GetProcessHeap(), 0, *envlen);\r
252   if (! *env) {\r
253     *envlen = 0;\r
254     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, value, _T("get_environment()"), 0);\r
255     return 3;\r
256   }\r
257 \r
258   /* Actually get the strings */\r
259   ret = RegQueryValueEx(key, value, 0, &type, (unsigned char *) *env, envlen);\r
260   if (ret != ERROR_SUCCESS) {\r
261     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(GetLastError()), 0);\r
262     HeapFree(GetProcessHeap(), 0, *env);\r
263     *env = 0;\r
264     *envlen = 0;\r
265     return 4;\r
266   }\r
267 \r
268   return 0;\r
269 }\r
270 \r
271 \r
272 int get_string(HKEY key, TCHAR *value, TCHAR *data, unsigned long datalen, bool expand, bool sanitise, bool must_exist) {\r
273   TCHAR *buffer = (TCHAR *) HeapAlloc(GetProcessHeap(), 0, datalen);\r
274   if (! buffer) {\r
275     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, value, _T("get_string()"), 0);\r
276     return 1;\r
277   }\r
278 \r
279   ZeroMemory(data, datalen);\r
280 \r
281   unsigned long type = REG_EXPAND_SZ;\r
282   unsigned long buflen = datalen;\r
283 \r
284   unsigned long ret = RegQueryValueEx(key, value, 0, &type, (unsigned char *) buffer, &buflen);\r
285   if (ret != ERROR_SUCCESS) {\r
286     unsigned long error = GetLastError();\r
287     HeapFree(GetProcessHeap(), 0, buffer);\r
288 \r
289     if (ret == ERROR_FILE_NOT_FOUND) {\r
290       if (! must_exist) return 0;\r
291     }\r
292 \r
293     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(error), 0);\r
294     return 2;\r
295   }\r
296 \r
297   /* Paths aren't allowed to contain quotes. */\r
298   if (sanitise) PathUnquoteSpaces(buffer);\r
299 \r
300   /* Do we want to expand the string? */\r
301   if (! expand) {\r
302     if (type == REG_EXPAND_SZ) type = REG_SZ;\r
303   }\r
304 \r
305   /* Technically we shouldn't expand environment strings from REG_SZ values */\r
306   if (type != REG_EXPAND_SZ) {\r
307     memmove(data, buffer, buflen);\r
308     HeapFree(GetProcessHeap(), 0, buffer);\r
309     return 0;\r
310   }\r
311 \r
312   ret = ExpandEnvironmentStrings((TCHAR *) buffer, data, datalen);\r
313   if (! ret || ret > datalen) {\r
314     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_EXPANDENVIRONMENTSTRINGS_FAILED, buffer, error_string(GetLastError()), 0);\r
315     HeapFree(GetProcessHeap(), 0, buffer);\r
316     return 3;\r
317   }\r
318 \r
319   HeapFree(GetProcessHeap(), 0, buffer);\r
320   return 0;\r
321 }\r
322 \r
323 int get_string(HKEY key, TCHAR *value, TCHAR *data, unsigned long datalen, bool sanitise) {\r
324   return get_string(key, value, data, datalen, false, sanitise, true);\r
325 }\r
326 \r
327 int expand_parameter(HKEY key, TCHAR *value, TCHAR *data, unsigned long datalen, bool sanitise, bool must_exist) {\r
328   return get_string(key, value, data, datalen, true, sanitise, must_exist);\r
329 }\r
330 \r
331 int expand_parameter(HKEY key, TCHAR *value, TCHAR *data, unsigned long datalen, bool sanitise) {\r
332   return expand_parameter(key, value, data, datalen, sanitise, true);\r
333 }\r
334 \r
335 /*\r
336   Sets a string in the registry.\r
337   Returns: 0 if it was set.\r
338            1 on error.\r
339 */\r
340 int set_string(HKEY key, TCHAR *value, TCHAR *string, bool expand) {\r
341   unsigned long type = expand ? REG_EXPAND_SZ : REG_SZ;\r
342   if (RegSetValueEx(key, value, 0, type, (const unsigned char *) string, (unsigned long) (_tcslen(string) + 1) * sizeof(TCHAR)) == ERROR_SUCCESS) return 0;\r
343   log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, value, error_string(GetLastError()), 0);\r
344   return 1;\r
345 }\r
346 \r
347 int set_string(HKEY key, TCHAR *value, TCHAR *string) {\r
348   return set_string(key, value, string, false);\r
349   return 1;\r
350 }\r
351 \r
352 int set_expand_string(HKEY key, TCHAR *value, TCHAR *string) {\r
353   return set_string(key, value, string, true);\r
354   return 1;\r
355 }\r
356 \r
357 /*\r
358   Set an unsigned long in the registry.\r
359   Returns: 0 if it was set.\r
360            1 on error.\r
361 */\r
362 int set_number(HKEY key, TCHAR *value, unsigned long number) {\r
363   if (RegSetValueEx(key, value, 0, REG_DWORD, (const unsigned char *) &number, sizeof(number)) == ERROR_SUCCESS) return 0;\r
364   log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_SETVALUE_FAILED, value, error_string(GetLastError()), 0);\r
365   return 1;\r
366 }\r
367 \r
368 /*\r
369   Query an unsigned long from the registry.\r
370   Returns:  1 if a number was retrieved.\r
371             0 if none was found and must_exist is false.\r
372            -1 if none was found and must_exist is true.\r
373            -2 otherwise.\r
374 */\r
375 int get_number(HKEY key, TCHAR *value, unsigned long *number, bool must_exist) {\r
376   unsigned long type = REG_DWORD;\r
377   unsigned long number_len = sizeof(unsigned long);\r
378 \r
379   int ret = RegQueryValueEx(key, value, 0, &type, (unsigned char *) number, &number_len);\r
380   if (ret == ERROR_SUCCESS) return 1;\r
381 \r
382   if (ret == ERROR_FILE_NOT_FOUND) {\r
383     if (! must_exist) return 0;\r
384   }\r
385 \r
386   log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(GetLastError()), 0);\r
387   if (ret == ERROR_FILE_NOT_FOUND) return -1;\r
388 \r
389   return -2;\r
390 }\r
391 \r
392 int get_number(HKEY key, TCHAR *value, unsigned long *number) {\r
393   return get_number(key, value, number, true);\r
394 }\r
395 \r
396 /* Replace NULL with CRLF. Leave NULL NULL as the end marker. */\r
397 int format_double_null(TCHAR *dn, unsigned long dnlen, TCHAR **formatted, unsigned long *newlen) {\r
398   unsigned long i, j;\r
399   *newlen = dnlen;\r
400 \r
401   if (! *newlen) {\r
402     *formatted = 0;\r
403     return 0;\r
404   }\r
405 \r
406   for (i = 0; i < dnlen; i++) if (! dn[i] && dn[i + 1]) ++*newlen;\r
407 \r
408   *formatted = (TCHAR *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, *newlen * sizeof(TCHAR));\r
409   if (! *formatted) {\r
410     *newlen = 0;\r
411     return 1;\r
412   }\r
413 \r
414   for (i = 0, j = 0; i < dnlen; i++) {\r
415     (*formatted)[j] = dn[i];\r
416     if (! dn[i]) {\r
417       if (dn[i + 1]) {\r
418         (*formatted)[j] = _T('\r');\r
419         (*formatted)[++j] = _T('\n');\r
420       }\r
421     }\r
422     j++;\r
423   }\r
424 \r
425   return 0;\r
426 }\r
427 \r
428 /* Strip CR and replace LF with NULL. */\r
429 int unformat_double_null(TCHAR *dn, unsigned long dnlen, TCHAR **unformatted, unsigned long *newlen) {\r
430   unsigned long i, j;\r
431   *newlen = 0;\r
432 \r
433   if (! dnlen) {\r
434     *unformatted = 0;\r
435     return 0;\r
436   }\r
437 \r
438   for (i = 0; i < dnlen; i++) if (dn[i] != _T('\r')) ++*newlen;\r
439 \r
440   /* Skip blank lines. */\r
441   for (i = 0; i < dnlen; i++) {\r
442     if (dn[i] == _T('\r') && dn[i + 1] == _T('\n')) {\r
443       /* This is the last CRLF. */\r
444       if (i >= dnlen - 2) break;\r
445 \r
446       /*\r
447         Strip at the start of the block or if the next characters are\r
448         CRLF too.\r
449       */\r
450       if (! i || (dn[i + 2] == _T('\r') && dn[i + 3] == _T('\n'))) {\r
451         for (j = i + 2; j < dnlen; j++) dn[j - 2] = dn[j];\r
452         dn[dnlen--] = _T('\0');\r
453         dn[dnlen--] = _T('\0');\r
454         i--;\r
455         --*newlen;\r
456       }\r
457     }\r
458   }\r
459 \r
460   /* Must end with two NULLs. */\r
461   *newlen += 2;\r
462 \r
463   *unformatted = (TCHAR *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, *newlen * sizeof(TCHAR));\r
464   if (! *unformatted) return 1;\r
465 \r
466   for (i = 0, j = 0; i < dnlen; i++) {\r
467     if (dn[i] == _T('\r')) continue;\r
468     if (dn[i] == _T('\n')) (*unformatted)[j] = _T('\0');\r
469     else (*unformatted)[j] = dn[i];\r
470     j++;\r
471   }\r
472 \r
473   return 0;\r
474 }\r
475 \r
476 void override_milliseconds(TCHAR *service_name, HKEY key, TCHAR *value, unsigned long *buffer, unsigned long default_value, unsigned long event) {\r
477   unsigned long type = REG_DWORD;\r
478   unsigned long buflen = sizeof(unsigned long);\r
479   bool ok = false;\r
480   unsigned long ret = RegQueryValueEx(key, value, 0, &type, (unsigned char *) buffer, &buflen);\r
481   if (ret != ERROR_SUCCESS) {\r
482     if (ret != ERROR_FILE_NOT_FOUND) {\r
483       if (type != REG_DWORD) {\r
484         TCHAR milliseconds[16];\r
485         _sntprintf_s(milliseconds, _countof(milliseconds), _TRUNCATE, _T("%lu"), default_value);\r
486         log_event(EVENTLOG_WARNING_TYPE, event, service_name, value, milliseconds, 0);\r
487       }\r
488       else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, value, error_string(GetLastError()), 0);\r
489     }\r
490   }\r
491   else ok = true;\r
492 \r
493   if (! ok) *buffer = default_value;\r
494 }\r
495 \r
496 HKEY open_service_registry(const TCHAR *service_name, REGSAM sam, bool must_exist) {\r
497   /* Get registry */\r
498   TCHAR registry[KEY_LENGTH];\r
499   if (service_registry_path(service_name, false, 0, registry, _countof(registry)) < 0) {\r
500     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, NSSM_REGISTRY, _T("open_service_registry()"), 0);\r
501     return 0;\r
502   }\r
503 \r
504   return open_registry_key(registry, sam, must_exist);\r
505 }\r
506 \r
507 HKEY open_registry(const TCHAR *service_name, const TCHAR *sub, REGSAM sam, bool must_exist) {\r
508   /* Get registry */\r
509   TCHAR registry[KEY_LENGTH];\r
510   if (service_registry_path(service_name, true, sub, registry, _countof(registry)) < 0) {\r
511     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, NSSM_REGISTRY, _T("open_registry()"), 0);\r
512     return 0;\r
513   }\r
514 \r
515   return open_registry_key(registry, sam, must_exist);\r
516 }\r
517 \r
518 HKEY open_registry(const TCHAR *service_name, const TCHAR *sub, REGSAM sam) {\r
519   return open_registry(service_name, sub, sam, true);\r
520 }\r
521 \r
522 HKEY open_registry(const TCHAR *service_name, REGSAM sam) {\r
523   return open_registry(service_name, 0, sam, true);\r
524 }\r
525 \r
526 int get_io_parameters(nssm_service_t *service, HKEY key) {\r
527   /* stdin */\r
528   if (get_createfile_parameters(key, NSSM_REG_STDIN, service->stdin_path, &service->stdin_sharing, NSSM_STDIN_SHARING, &service->stdin_disposition, NSSM_STDIN_DISPOSITION, &service->stdin_flags, NSSM_STDIN_FLAGS, 0)) {\r
529     service->stdin_sharing = service->stdin_disposition = service->stdin_flags = 0;\r
530     ZeroMemory(service->stdin_path, _countof(service->stdin_path) * sizeof(TCHAR));\r
531     return 1;\r
532   }\r
533 \r
534   /* stdout */\r
535   if (get_createfile_parameters(key, NSSM_REG_STDOUT, service->stdout_path, &service->stdout_sharing, NSSM_STDOUT_SHARING, &service->stdout_disposition, NSSM_STDOUT_DISPOSITION, &service->stdout_flags, NSSM_STDOUT_FLAGS, &service->stdout_copy_and_truncate)) {\r
536     service->stdout_sharing = service->stdout_disposition = service->stdout_flags = 0;\r
537     ZeroMemory(service->stdout_path, _countof(service->stdout_path) * sizeof(TCHAR));\r
538     return 2;\r
539   }\r
540 \r
541   /* stderr */\r
542   if (get_createfile_parameters(key, NSSM_REG_STDERR, service->stderr_path, &service->stderr_sharing, NSSM_STDERR_SHARING, &service->stderr_disposition, NSSM_STDERR_DISPOSITION, &service->stderr_flags, NSSM_STDERR_FLAGS, &service->stderr_copy_and_truncate)) {\r
543     service->stderr_sharing = service->stderr_disposition = service->stderr_flags = 0;\r
544     ZeroMemory(service->stderr_path, _countof(service->stderr_path) * sizeof(TCHAR));\r
545     return 3;\r
546   }\r
547 \r
548   return 0;\r
549 }\r
550 \r
551 int get_parameters(nssm_service_t *service, STARTUPINFO *si) {\r
552   unsigned long ret;\r
553 \r
554   /* Try to open the registry */\r
555   HKEY key = open_registry(service->name, KEY_READ);\r
556   if (! key) return 1;\r
557 \r
558   /* Don't expand parameters when retrieving for the GUI. */\r
559   bool expand = si ? true : false;\r
560 \r
561   /* Try to get environment variables - may fail */\r
562   get_environment(service->name, key, NSSM_REG_ENV, &service->env, &service->envlen);\r
563   /* Environment variables to add to existing rather than replace - may fail. */\r
564   get_environment(service->name, key, NSSM_REG_ENV_EXTRA, &service->env_extra, &service->env_extralen);\r
565 \r
566   /* Set environment if we are starting the service. */\r
567   if (si) set_service_environment(service);\r
568 \r
569   /* Try to get executable file - MUST succeed */\r
570   if (get_string(key, NSSM_REG_EXE, service->exe, sizeof(service->exe), expand, false, true)) {\r
571     RegCloseKey(key);\r
572     return 3;\r
573   }\r
574 \r
575   /* Try to get flags - may fail and we don't care */\r
576   if (get_string(key, NSSM_REG_FLAGS, service->flags, sizeof(service->flags), expand, false, true)) {\r
577     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_NO_FLAGS, NSSM_REG_FLAGS, service->name, service->exe, 0);\r
578     ZeroMemory(service->flags, sizeof(service->flags));\r
579   }\r
580 \r
581   /* Try to get startup directory - may fail and we fall back to a default */\r
582   if (get_string(key, NSSM_REG_DIR, service->dir, sizeof(service->dir), expand, true, true) || ! service->dir[0]) {\r
583     _sntprintf_s(service->dir, _countof(service->dir), _TRUNCATE, _T("%s"), service->exe);\r
584     strip_basename(service->dir);\r
585     if (service->dir[0] == _T('\0')) {\r
586       /* Help! */\r
587       ret = GetWindowsDirectory(service->dir, sizeof(service->dir));\r
588       if (! ret || ret > sizeof(service->dir)) {\r
589         log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_NO_DIR_AND_NO_FALLBACK, NSSM_REG_DIR, service->name, 0);\r
590         RegCloseKey(key);\r
591         return 4;\r
592       }\r
593     }\r
594     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_NO_DIR, NSSM_REG_DIR, service->name, service->dir, 0);\r
595   }\r
596 \r
597   /* Try to get processor affinity - may fail. */\r
598   TCHAR buffer[512];\r
599   if (get_string(key, NSSM_REG_AFFINITY, buffer, sizeof(buffer), false, false, false) || ! buffer[0]) service->affinity = 0LL;\r
600   else if (affinity_string_to_mask(buffer, &service->affinity)) {\r
601     log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_BOGUS_AFFINITY_MASK, service->name, buffer);\r
602     service->affinity = 0LL;\r
603   }\r
604   else {\r
605     DWORD_PTR affinity, system_affinity;\r
606 \r
607     if (GetProcessAffinityMask(GetCurrentProcess(), &affinity, &system_affinity)) {\r
608       _int64 effective_affinity = service->affinity & system_affinity;\r
609       if (effective_affinity != service->affinity) {\r
610         TCHAR *system = 0;\r
611         if (! affinity_mask_to_string(system_affinity, &system)) {\r
612           TCHAR *effective = 0;\r
613           if (! affinity_mask_to_string(effective_affinity, &effective)) {\r
614             log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_EFFECTIVE_AFFINITY_MASK, service->name, buffer, system, effective, 0);\r
615           }\r
616           HeapFree(GetProcessHeap(), 0, effective);\r
617         }\r
618         HeapFree(GetProcessHeap(), 0, system);\r
619       }\r
620     }\r
621   }\r
622 \r
623   /* Try to get priority - may fail. */\r
624   unsigned long priority;\r
625   if (get_number(key, NSSM_REG_PRIORITY, &priority, false) == 1) {\r
626     if (priority == (priority & priority_mask())) service->priority = priority;\r
627     else log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_BOGUS_PRIORITY, service->name, NSSM_REG_PRIORITY, 0);\r
628   }\r
629 \r
630   /* Try to get file rotation settings - may fail. */\r
631   unsigned long rotate_files;\r
632   if (get_number(key, NSSM_REG_ROTATE, &rotate_files, false) == 1) {\r
633     if (rotate_files) service->rotate_files = true;\r
634     else service->rotate_files = false;\r
635   }\r
636   else service->rotate_files = false;\r
637   if (get_number(key, NSSM_REG_ROTATE_ONLINE, &rotate_files, false) == 1) {\r
638     if (rotate_files) service->rotate_stdout_online = service->rotate_stderr_online = true;\r
639     else service->rotate_stdout_online = service->rotate_stderr_online = false;\r
640   }\r
641   else service->rotate_stdout_online = service->rotate_stderr_online = false;\r
642   if (get_number(key, NSSM_REG_ROTATE_SECONDS, &service->rotate_seconds, false) != 1) service->rotate_seconds = 0;\r
643   if (get_number(key, NSSM_REG_ROTATE_BYTES_LOW, &service->rotate_bytes_low, false) != 1) service->rotate_bytes_low = 0;\r
644   if (get_number(key, NSSM_REG_ROTATE_BYTES_HIGH, &service->rotate_bytes_high, false) != 1) service->rotate_bytes_high = 0;\r
645   override_milliseconds(service->name, key, NSSM_REG_ROTATE_DELAY, &service->rotate_delay, NSSM_ROTATE_DELAY, NSSM_EVENT_BOGUS_THROTTLE);\r
646 \r
647   /* Try to get force new console setting - may fail. */\r
648   if (get_number(key, NSSM_REG_NO_CONSOLE, &service->no_console, false) != 1) service->no_console = 0;\r
649 \r
650   /* Change to startup directory in case stdout/stderr are relative paths. */\r
651   TCHAR cwd[PATH_LENGTH];\r
652   GetCurrentDirectory(_countof(cwd), cwd);\r
653   SetCurrentDirectory(service->dir);\r
654 \r
655   /* Try to get stdout and stderr */\r
656   if (get_io_parameters(service, key)) {\r
657     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_OUTPUT_HANDLES_FAILED, service->name, 0);\r
658     RegCloseKey(key);\r
659     SetCurrentDirectory(cwd);\r
660     return 5;\r
661   }\r
662 \r
663   /* Change back in case the startup directory needs to be deleted. */\r
664   SetCurrentDirectory(cwd);\r
665 \r
666   /* Try to get mandatory restart delay */\r
667   override_milliseconds(service->name, key, NSSM_REG_RESTART_DELAY, &service->restart_delay, 0, NSSM_EVENT_BOGUS_RESTART_DELAY);\r
668 \r
669   /* Try to get throttle restart delay */\r
670   override_milliseconds(service->name, key, NSSM_REG_THROTTLE, &service->throttle_delay, NSSM_RESET_THROTTLE_RESTART, NSSM_EVENT_BOGUS_THROTTLE);\r
671 \r
672   /* Try to get service stop flags. */\r
673   unsigned long type = REG_DWORD;\r
674   unsigned long stop_method_skip;\r
675   unsigned long buflen = sizeof(stop_method_skip);\r
676   bool stop_ok = false;\r
677   ret = RegQueryValueEx(key, NSSM_REG_STOP_METHOD_SKIP, 0, &type, (unsigned char *) &stop_method_skip, &buflen);\r
678   if (ret != ERROR_SUCCESS) {\r
679     if (ret != ERROR_FILE_NOT_FOUND) {\r
680       if (type != REG_DWORD) {\r
681         log_event(EVENTLOG_WARNING_TYPE, NSSM_EVENT_BOGUS_STOP_METHOD_SKIP, service->name, NSSM_REG_STOP_METHOD_SKIP, NSSM, 0);\r
682       }\r
683       else log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_QUERYVALUE_FAILED, NSSM_REG_STOP_METHOD_SKIP, error_string(GetLastError()), 0);\r
684     }\r
685   }\r
686   else stop_ok = true;\r
687 \r
688   /* Try all methods except those requested to be skipped. */\r
689   service->stop_method = ~0;\r
690   if (stop_ok) service->stop_method &= ~stop_method_skip;\r
691 \r
692   /* Try to get kill delays - may fail. */\r
693   override_milliseconds(service->name, key, NSSM_REG_KILL_CONSOLE_GRACE_PERIOD, &service->kill_console_delay, NSSM_KILL_CONSOLE_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_CONSOLE_GRACE_PERIOD);\r
694   override_milliseconds(service->name, key, NSSM_REG_KILL_WINDOW_GRACE_PERIOD, &service->kill_window_delay, NSSM_KILL_WINDOW_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_WINDOW_GRACE_PERIOD);\r
695   override_milliseconds(service->name, key, NSSM_REG_KILL_THREADS_GRACE_PERIOD, &service->kill_threads_delay, NSSM_KILL_THREADS_GRACE_PERIOD, NSSM_EVENT_BOGUS_KILL_THREADS_GRACE_PERIOD);\r
696 \r
697   /* Try to get process tree settings - may fail. */\r
698   unsigned long kill_process_tree;\r
699   if (get_number(key, NSSM_REG_KILL_PROCESS_TREE, &kill_process_tree, false) == 1) {\r
700     if (kill_process_tree) service->kill_process_tree = true;\r
701     else service->kill_process_tree = false;\r
702   }\r
703   else service->kill_process_tree = true;\r
704 \r
705   /* Try to get default exit action. */\r
706   bool default_action;\r
707   service->default_exit_action = NSSM_EXIT_RESTART;\r
708   TCHAR action_string[ACTION_LEN];\r
709   if (! get_exit_action(service->name, 0, action_string, &default_action)) {\r
710     for (int i = 0; exit_action_strings[i]; i++) {\r
711       if (! _tcsnicmp((const TCHAR *) action_string, exit_action_strings[i], ACTION_LEN)) {\r
712         service->default_exit_action = i;\r
713         break;\r
714       }\r
715     }\r
716   }\r
717 \r
718   /* Close registry */\r
719   RegCloseKey(key);\r
720 \r
721   return 0;\r
722 }\r
723 \r
724 /*\r
725   Sets the string for the exit action corresponding to the exit code.\r
726 \r
727   ret is a pointer to an unsigned long containing the exit code.\r
728   If ret is NULL, we retrieve the default exit action unconditionally.\r
729 \r
730   action is a buffer which receives the string.\r
731 \r
732   default_action is a pointer to a bool which is set to false if there\r
733   was an explicit string for the given exit code, or true if we are\r
734   returning the default action.\r
735 \r
736   Returns: 0 on success.\r
737            1 on error.\r
738 */\r
739 int get_exit_action(const TCHAR *service_name, unsigned long *ret, TCHAR *action, bool *default_action) {\r
740   /* Are we returning the default action or a status-specific one? */\r
741   *default_action = ! ret;\r
742 \r
743   /* Try to open the registry */\r
744   HKEY key = open_registry(service_name, NSSM_REG_EXIT, KEY_READ);\r
745   if (! key) return 1;\r
746 \r
747   unsigned long type = REG_SZ;\r
748   unsigned long action_len = ACTION_LEN;\r
749 \r
750   TCHAR code[16];\r
751   if (! ret) code[0] = _T('\0');\r
752   else if (_sntprintf_s(code, _countof(code), _TRUNCATE, _T("%lu"), *ret) < 0) {\r
753     RegCloseKey(key);\r
754     return get_exit_action(service_name, 0, action, default_action);\r
755   }\r
756   if (RegQueryValueEx(key, code, 0, &type, (unsigned char *) action, &action_len) != ERROR_SUCCESS) {\r
757     RegCloseKey(key);\r
758     /* Try again with * as the key if an exit code was defined */\r
759     if (ret) return get_exit_action(service_name, 0, action, default_action);\r
760     return 0;\r
761   }\r
762 \r
763   /* Close registry */\r
764   RegCloseKey(key);\r
765 \r
766   return 0;\r
767 }\r
768 \r
769 int set_hook(const TCHAR *service_name, const TCHAR *hook_event, const TCHAR *hook_action, TCHAR *cmd) {\r
770   /* Try to open the registry */\r
771   TCHAR registry[KEY_LENGTH];\r
772   if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, _T("%s\\%s"), NSSM_REG_HOOK, hook_event) < 0) {\r
773     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook registry"), _T("set_hook()"), 0);\r
774     return 1;\r
775   }\r
776 \r
777   HKEY key;\r
778   long error;\r
779 \r
780   /* Don't create keys needlessly. */\r
781   if (! _tcslen(cmd)) {\r
782     key = open_registry(service_name, registry, KEY_READ, false);\r
783     if (! key) return 0;\r
784     error = RegQueryValueEx(key, hook_action, 0, 0, 0, 0);\r
785     RegCloseKey(key);\r
786     if (error == ERROR_FILE_NOT_FOUND) return 0;\r
787   }\r
788 \r
789   key = open_registry(service_name, registry, KEY_WRITE);\r
790   if (! key) return 1;\r
791 \r
792   int ret = 1;\r
793   if (_tcslen(cmd)) ret = set_string(key, (TCHAR *) hook_action, cmd, true);\r
794   else {\r
795     error = RegDeleteValue(key, hook_action);\r
796     if (error == ERROR_SUCCESS || error == ERROR_FILE_NOT_FOUND) ret = 0;\r
797   }\r
798 \r
799   /* Close registry */\r
800   RegCloseKey(key);\r
801 \r
802   return ret;\r
803 }\r
804 \r
805 int get_hook(const TCHAR *service_name, const TCHAR *hook_event, const TCHAR *hook_action, TCHAR *buffer, unsigned long buflen) {\r
806   /* Try to open the registry */\r
807   TCHAR registry[KEY_LENGTH];\r
808   if (_sntprintf_s(registry, _countof(registry), _TRUNCATE, _T("%s\\%s"), NSSM_REG_HOOK, hook_event) < 0) {\r
809     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_OUT_OF_MEMORY, _T("hook registry"), _T("get_hook()"), 0);\r
810     return 1;\r
811   }\r
812   HKEY key = open_registry(service_name, registry, KEY_READ, false);\r
813   if (! key) return 1;\r
814 \r
815   int ret = expand_parameter(key, (TCHAR *) hook_action, buffer, buflen, true, false);\r
816 \r
817   /* Close registry */\r
818   RegCloseKey(key);\r
819 \r
820   return ret;\r
821 }\r