]> sourceware.org Git - newlib-cygwin.git/blob - winsup/cygserver/cygserver.cc
Cygwin: cygserver: build with -Wimplicit-fallthrough=4 -Werror
[newlib-cygwin.git] / winsup / cygserver / cygserver.cc
1 /* cygserver.cc
2
3 Written by Egor Duda <deo@logos-m.ru>
4
5 This file is part of Cygwin.
6
7 This software is a copyrighted work licensed under the terms of the
8 Cygwin license. Please consult the file "CYGWIN_LICENSE" for
9 details. */
10
11 #ifdef __OUTSIDE_CYGWIN__
12 #include "woutsup.h"
13
14 #include <sys/types.h>
15
16 #include <assert.h>
17 #include <errno.h>
18 #include <ctype.h>
19 #include <getopt.h>
20 #include <signal.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <unistd.h>
25
26 #include "cygwin_version.h"
27
28 #include "cygserver.h"
29 #include "process.h"
30 #include "transport.h"
31
32 #include "cygserver_ipc.h"
33 #include "cygserver_msg.h"
34 #include "cygserver_sem.h"
35
36 #define DEF_CONFIG_FILE "" SYSCONFDIR "/cygserver.conf"
37
38 #define SERVER_VERSION "1.20"
39
40 GENERIC_MAPPING access_mapping;
41
42 static bool
43 setup_privileges ()
44 {
45 BOOL rc, ret_val;
46 HANDLE hToken = NULL;
47 TOKEN_PRIVILEGES sPrivileges;
48
49 rc = OpenProcessToken (GetCurrentProcess () , TOKEN_ALL_ACCESS , &hToken) ;
50 if (!rc)
51 {
52 debug ("error opening process token (err %u)", GetLastError ());
53 return false;
54 }
55 rc = LookupPrivilegeValue (NULL, SE_DEBUG_NAME, &sPrivileges.Privileges[0].Luid);
56 if (!rc)
57 {
58 debug ("error getting privilege luid (err %u)", GetLastError ());
59 ret_val = false;
60 goto out;
61 }
62 sPrivileges.PrivilegeCount = 1 ;
63 sPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED ;
64 rc = AdjustTokenPrivileges (hToken, FALSE, &sPrivileges, 0, NULL, NULL) ;
65 if (!rc)
66 {
67 debug ("error adjusting privilege level. (err %u)", GetLastError ());
68 ret_val = false;
69 goto out;
70 }
71
72 access_mapping.GenericRead = FILE_READ_DATA;
73 access_mapping.GenericWrite = FILE_WRITE_DATA;
74 access_mapping.GenericExecute = 0;
75 access_mapping.GenericAll = FILE_READ_DATA | FILE_WRITE_DATA;
76
77 ret_val = true;
78
79 out:
80 CloseHandle (hToken);
81 return ret_val;
82 }
83
84 int
85 check_and_dup_handle (HANDLE from_process, HANDLE to_process,
86 HANDLE from_process_token,
87 DWORD access,
88 HANDLE from_handle,
89 HANDLE *to_handle_ptr, BOOL bInheritHandle = FALSE)
90 {
91 HANDLE local_handle = NULL;
92 int ret_val = EACCES;
93 char sd_buf [1024];
94 PSECURITY_DESCRIPTOR sd = (PSECURITY_DESCRIPTOR) &sd_buf;
95 DWORD bytes_needed;
96 PRIVILEGE_SET ps;
97 DWORD ps_len = sizeof (ps);
98 BOOL status;
99
100 if (from_process != GetCurrentProcess ())
101 {
102 if (!DuplicateHandle (from_process, from_handle,
103 GetCurrentProcess (), &local_handle,
104 0, bInheritHandle,
105 DUPLICATE_SAME_ACCESS))
106 {
107 log (LOG_ERR, "error getting handle(%p) to server (err %u)",
108 from_handle, GetLastError ());
109 goto out;
110 }
111 } else
112 local_handle = from_handle;
113
114 if (!GetKernelObjectSecurity (local_handle,
115 (OWNER_SECURITY_INFORMATION
116 | GROUP_SECURITY_INFORMATION
117 | DACL_SECURITY_INFORMATION),
118 sd, sizeof (sd_buf), &bytes_needed))
119 {
120 log (LOG_ERR, "error getting handle SD (err %u)", GetLastError ());
121 goto out;
122 }
123
124 MapGenericMask (&access, &access_mapping);
125
126 if (!AccessCheck (sd, from_process_token, access, &access_mapping,
127 &ps, &ps_len, &access, &status))
128 {
129 log (LOG_ERR, "error checking access rights (err %u)", GetLastError ());
130 goto out;
131 }
132
133 if (!status)
134 {
135 log (LOG_ERR, "access to object denied");
136 goto out;
137 }
138
139 if (!DuplicateHandle (from_process, from_handle,
140 to_process, to_handle_ptr,
141 access, bInheritHandle, 0))
142 {
143 log (LOG_ERR, "error getting handle to client (err %u)", GetLastError ());
144 goto out;
145 }
146
147 debug ("Duplicated %p to %p", from_handle, *to_handle_ptr);
148
149 ret_val = 0;
150
151 out:
152 if (local_handle && from_process != GetCurrentProcess ())
153 CloseHandle (local_handle);
154
155 return (ret_val);
156 }
157
158 /*
159 * client_request_attach_tty::serve ()
160 */
161
162 void
163 client_request_attach_tty::serve (transport_layer_base *const conn,
164 process_cache *)
165 {
166 assert (conn);
167
168 assert (!error_code ());
169
170 if (msglen () != sizeof (req))
171 {
172 log (LOG_ERR, "bad request body length: expecting %lu bytes, got %lu",
173 sizeof (req), msglen ());
174 error_code (EINVAL);
175 msglen (0);
176 return;
177 }
178
179 msglen (0); // Until we fill in some fields.
180
181 debug ("pid %d:(%p,%p) -> pid %d", req.master_pid, req.from_master,
182 req.to_master, req.pid);
183
184 debug ("opening process %d", req.master_pid);
185
186 const HANDLE from_process_handle =
187 OpenProcess (PROCESS_DUP_HANDLE, FALSE, req.master_pid);
188
189 if (!from_process_handle)
190 {
191 log (LOG_ERR, "error opening `from' process (err %u)", GetLastError ());
192 error_code (EACCES);
193 return;
194 }
195
196 debug ("opening process %d", req.pid);
197
198 const HANDLE to_process_handle =
199 OpenProcess (PROCESS_DUP_HANDLE, FALSE, req.pid);
200
201 if (!to_process_handle)
202 {
203 log (LOG_ERR, "error opening `to' process (err %u)", GetLastError ());
204 CloseHandle (from_process_handle);
205 error_code (EACCES);
206 return;
207 }
208
209 debug ("Impersonating client");
210 if (!conn->impersonate_client ())
211 {
212 CloseHandle (from_process_handle);
213 CloseHandle (to_process_handle);
214 error_code (EACCES);
215 return;
216 }
217
218 HANDLE token_handle = NULL;
219
220 debug ("about to open thread token");
221 const DWORD rc = OpenThreadToken (GetCurrentThread (),
222 TOKEN_QUERY,
223 TRUE,
224 &token_handle);
225
226 debug ("opened thread token, rc=%u", rc);
227 if (!conn->revert_to_self ())
228 {
229 CloseHandle (from_process_handle);
230 CloseHandle (to_process_handle);
231 error_code (EACCES);
232 return;
233 }
234
235 if (!rc)
236 {
237 log (LOG_ERR, "error opening thread token (err %u)", GetLastError ());
238 CloseHandle (from_process_handle);
239 CloseHandle (to_process_handle);
240 error_code (EACCES);
241 return;
242 }
243
244 // From this point on, a reply body is returned to the client.
245
246 const HANDLE from_master = req.from_master;
247 const HANDLE to_master = req.to_master;
248
249 req.from_master = NULL;
250 req.to_master = NULL;
251
252 msglen (sizeof (req));
253
254 if (from_master)
255 if (check_and_dup_handle (from_process_handle, to_process_handle,
256 token_handle,
257 GENERIC_READ,
258 from_master,
259 &req.from_master, TRUE) != 0)
260 {
261 log (LOG_ERR, "error duplicating from_master handle (err %u)",
262 GetLastError ());
263 error_code (EACCES);
264 }
265
266 if (to_master)
267 if (check_and_dup_handle (from_process_handle, to_process_handle,
268 token_handle,
269 GENERIC_WRITE,
270 to_master,
271 &req.to_master, TRUE) != 0)
272 {
273 log (LOG_ERR, "error duplicating to_master handle (err %u)",
274 GetLastError ());
275 error_code (EACCES);
276 }
277
278 CloseHandle (from_process_handle);
279 CloseHandle (to_process_handle);
280 CloseHandle (token_handle);
281
282 debug ("%u(%p, %p) -> %u(%p,%p)", req.master_pid, from_master, to_master,
283 req.pid, req.from_master, req.to_master);
284
285 return;
286 }
287
288 void
289 client_request_get_version::serve (transport_layer_base *, process_cache *)
290 {
291 assert (!error_code ());
292
293 if (msglen ())
294 log (LOG_ERR, "unexpected request body ignored: %lu bytes", msglen ());
295
296 msglen (sizeof (version));
297
298 version.major = CYGWIN_SERVER_VERSION_MAJOR;
299 version.api = CYGWIN_SERVER_VERSION_API;
300 version.minor = CYGWIN_SERVER_VERSION_MINOR;
301 version.patch = CYGWIN_SERVER_VERSION_PATCH;
302 }
303
304 class server_request : public queue_request
305 {
306 public:
307 server_request (transport_layer_base *const conn, process_cache *const cache)
308 : _conn (conn), _cache (cache)
309 {}
310
311 virtual ~server_request ()
312 {
313 delete _conn;
314 }
315
316 virtual void process ()
317 {
318 client_request::handle_request (_conn, _cache);
319 }
320
321 private:
322 transport_layer_base *const _conn;
323 process_cache *const _cache;
324 };
325
326 class server_submission_loop : public queue_submission_loop
327 {
328 public:
329 server_submission_loop (threaded_queue *const queue,
330 transport_layer_base *const transport,
331 process_cache *const cache)
332 : queue_submission_loop (queue, false),
333 _transport (transport),
334 _cache (cache)
335 {
336 assert (_transport);
337 assert (_cache);
338 }
339
340 private:
341 transport_layer_base *const _transport;
342 process_cache *const _cache;
343
344 virtual void request_loop ();
345 };
346
347 /* FIXME: this is a little ugly. What we really want is to wait on
348 * two objects: one for the pipe/socket, and one for being told to
349 * shutdown. Otherwise this will stay a problem (we won't actually
350 * shutdown until the request _AFTER_ the shutdown request. And
351 * sending ourselves a request is ugly
352 */
353 void
354 server_submission_loop::request_loop ()
355 {
356 /* I'd like the accepting thread's priority to be above any "normal"
357 * thread in the system to avoid overflowing the listen queue (for
358 * sockets; similar issues exist for named pipes); but, for example,
359 * a normal priority thread in a foregrounded process is boosted to
360 * THREAD_PRIORITY_HIGHEST (AFAICT). Thus try to set the current
361 * thread's priority to a level one above that. This fails on
362 * win9x/ME so assume any failure in that call is due to that and
363 * simply call again at one priority level lower.
364 * FIXME: This looks weird and is an issue on NT, too. Per MSDN,
365 * THREAD_PRIORITY_HIGHEST + 1 is only a valid priority level if
366 * the priority class is set to REALTIME_PRIORITY_CLASS.
367 */
368 if (!SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_HIGHEST + 1))
369 if (!SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_HIGHEST))
370 debug ("failed to raise accept thread priority (err %u)",
371 GetLastError ());
372
373 while (_running)
374 {
375 bool recoverable = false;
376 transport_layer_base *const conn = _transport->accept (&recoverable);
377 if (!conn && !recoverable)
378 {
379 log (LOG_ERR, "fatal error on IPC transport: closing down");
380 return;
381 }
382 // EINTR probably implies a shutdown request; so back off for a
383 // moment to let the main thread take control, otherwise the
384 // server spins here receiving EINTR repeatedly since the signal
385 // handler in the main thread doesn't get a chance to be called.
386 if (!conn && errno == EINTR)
387 {
388 if (!SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_NORMAL))
389 debug ("failed to reset thread priority (err %u)",
390 GetLastError ());
391
392 Sleep (0);
393 if (!SetThreadPriority (GetCurrentThread (),
394 THREAD_PRIORITY_HIGHEST + 1))
395 if (!SetThreadPriority (GetCurrentThread (),
396 THREAD_PRIORITY_HIGHEST))
397 debug ("failed to raise thread priority (err %u)",
398 GetLastError ());
399 }
400 if (conn)
401 _queue->add (new server_request (conn, _cache));
402 }
403 }
404
405 client_request_shutdown::client_request_shutdown ()
406 : client_request (CYGSERVER_REQUEST_SHUTDOWN)
407 {
408 }
409
410 void
411 client_request_shutdown::serve (transport_layer_base *, process_cache *)
412 {
413 assert (!error_code ());
414
415 if (msglen ())
416 log (LOG_ERR, "unexpected request body ignored: %lu bytes", msglen ());
417
418 /* FIXME: link upwards, and then this becomes a trivial method call to
419 * only shutdown _this queue_
420 */
421
422 kill (getpid (), SIGINT);
423
424 msglen (0);
425 }
426
427 static sig_atomic_t shutdown_server = false;
428
429 static void
430 handle_signal (const int signum)
431 {
432 /* any signal makes us die :} */
433
434 shutdown_server = true;
435 }
436
437 /*
438 * print_usage ()
439 */
440
441 static void
442 print_usage (const char *const pgm)
443 {
444 log (LOG_NOTICE, "Usage: %s [OPTIONS]\n"
445 "\n"
446 "Cygwin background service daemon\n"
447 "\n"
448 "Configuration option:\n"
449 "\n"
450 " -f, --config-file <file> Use <file> as config file. Default is\n"
451 " " DEF_CONFIG_FILE "\n"
452 "\n"
453 "Performance options:\n"
454 "\n"
455 " -c, --cleanup-threads <num> Number of cleanup threads to use.\n"
456 " -p, --process-cache <num> Size of process cache.\n"
457 " -r, --request-threads <num> Number of request threads to use.\n"
458 "\n"
459 "Logging options:\n"
460 "\n"
461 " -d, --debug Log debug messages to stderr.\n"
462 " -e, --stderr Log to stderr (default if stderr is a tty).\n"
463 " -E, --no-stderr Don't log to stderr (see -y, -Y options).\n"
464 " -l, --log-level <level> Verbosity of logging (1..7). Default: 6\n"
465 " -y, --syslog Log to syslog (default if stderr is no tty).\n"
466 " -Y, --no-syslog Don't log to syslog (See -e, -E options).\n"
467 "\n"
468 "Support options:\n"
469 "\n"
470 " -m, --no-sharedmem Don't start XSI Shared Memory support.\n"
471 " -q, --no-msgqueues Don't start XSI Message Queue support.\n"
472 " -s, --no-semaphores Don't start XSI Semaphore support.\n"
473 "\n"
474 "Miscellaneous:\n"
475 "\n"
476 " -S, --shutdown Shutdown the daemon.\n"
477 " -h, --help Output usage information and exit.\n"
478 " -V, --version Output version information and exit.\n"
479 , pgm);
480 }
481
482 /*
483 * print_version ()
484 */
485
486 static void
487 print_version ()
488 {
489 log (LOG_INFO,
490 "cygserver (cygwin) %d.%d.%d\n"
491 "Cygwin background service daemon\n"
492 "Copyright (C) 2001 - %s Cygwin Authors\n"
493 "This is free software; see the source for copying conditions. There is NO\n"
494 "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
495 CYGWIN_VERSION_DLL_MAJOR / 1000,
496 CYGWIN_VERSION_DLL_MAJOR % 1000,
497 CYGWIN_VERSION_DLL_MINOR,
498 strrchr (__DATE__, ' ') + 1);
499 }
500
501 /*
502 * main ()
503 */
504
505 int
506 main (const int argc, char *argv[])
507 {
508 const struct option longopts[] = {
509 {"cleanup-threads", required_argument, NULL, 'c'},
510 {"debug", no_argument, NULL, 'd'},
511 {"stderr", no_argument, NULL, 'e'},
512 {"no-stderr", no_argument, NULL, 'E'},
513 {"config-file", required_argument, NULL, 'f'},
514 {"help", no_argument, NULL, 'h'},
515 {"log-level", required_argument, NULL, 'l'},
516 {"no-sharedmem", no_argument, NULL, 'm'},
517 {"process-cache", required_argument, NULL, 'p'},
518 {"no-msgqueues", no_argument, NULL, 'q'},
519 {"request-threads", required_argument, NULL, 'r'},
520 {"no-semaphores", no_argument, NULL, 's'},
521 {"shutdown", no_argument, NULL, 'S'},
522 {"version", no_argument, NULL, 'V'},
523 {"syslog", no_argument, NULL, 'y'},
524 {"no-syslog", no_argument, NULL, 'Y'},
525 {0, no_argument, NULL, 0}
526 };
527
528 const char opts[] = "c:deEf:hl:mp:qr:sSVyY";
529
530 int32_t cleanup_threads = 0;
531 int32_t request_threads = 0;
532 int32_t process_cache_size = 0;
533 bool shutdown = false;
534 const char *config_file = DEF_CONFIG_FILE;
535 bool force_config_file = false;
536 tun_bool_t option_log_stderr = TUN_UNDEF;
537 tun_bool_t option_log_syslog = TUN_UNDEF;
538
539 char *c = NULL;
540
541 /* Check if we have a terminal. If so, default to stderr logging,
542 otherwise default to syslog logging. This must be done early
543 to allow default logging already in option processing state. */
544 openlog ("cygserver", LOG_PID, LOG_KERN);
545 if (isatty (2))
546 log_stderr = TUN_TRUE;
547 else
548 log_syslog = TUN_TRUE;
549
550 int opt;
551
552 securityinit ();
553
554 opterr = 0;
555 while ((opt = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
556 switch (opt)
557 {
558 case 'c':
559 c = NULL;
560 cleanup_threads = strtol (optarg, &c, 10);
561 if (cleanup_threads <= 0 || cleanup_threads > 32 || (c && *c))
562 panic ("Number of cleanup threads must be between 1 and 32");
563 break;
564
565 case 'd':
566 log_debug = TUN_TRUE;
567 break;
568
569 case 'e':
570 option_log_stderr = TUN_TRUE;
571 break;
572
573 case 'E':
574 option_log_stderr = TUN_FALSE;
575 break;
576
577 case 'f':
578 config_file = optarg;
579 force_config_file = true;
580 break;
581
582 case 'h':
583 print_usage (getprogname ());
584 return 0;
585
586 case 'l':
587 c = NULL;
588 log_level = strtoul (optarg, &c, 10);
589 if (!log_level || log_level > 7 || (c && *c))
590 panic ("Log level must be between 1 and 7");
591 break;
592
593 case 'm':
594 support_sharedmem = TUN_FALSE;
595 break;
596
597 case 'p':
598 c = NULL;
599 process_cache_size = strtol (optarg, &c, 10);
600 if (process_cache_size <= 0 || process_cache_size > 310 || (c && *c))
601 panic ("Size of process cache must be between 1 and 310");
602 break;
603
604 case 'q':
605 support_msgqueues = TUN_FALSE;
606 break;
607
608 case 'r':
609 c = NULL;
610 request_threads = strtol (optarg, &c, 10);
611 if (request_threads <= 0 || request_threads > 310 || (c && *c))
612 panic ("Number of request threads must be between 1 and 310");
613 break;
614
615 case 's':
616 support_semaphores = TUN_FALSE;
617 break;
618
619 case 'S':
620 shutdown = true;
621 break;
622
623 case 'V':
624 print_version ();
625 return 0;
626
627 case 'y':
628 option_log_syslog = TUN_TRUE;
629 break;
630
631 case 'Y':
632 option_log_syslog = TUN_FALSE;
633 break;
634
635 case '?':
636 panic ("unknown option -- %c\n"
637 "Try `%s --help' for more information.", optopt, getprogname ());
638 }
639
640 if (optind != argc)
641 panic ("Too many arguments");
642
643 if (shutdown)
644 {
645 /* Setting `cygserver_running' stops the request code making a
646 * version request, which is not much to the point.
647 */
648 cygserver_running = CYGSERVER_OK;
649
650 client_request_shutdown req;
651
652 if (req.make_request () == -1 || req.error_code ())
653 panic("Shutdown request failed: %s", strerror (req.error_code ()));
654
655 // FIXME: It would be nice to wait here for the daemon to exit.
656
657 return 0;
658 }
659
660 SIGHANDLE (SIGHUP);
661 SIGHANDLE (SIGINT);
662 SIGHANDLE (SIGTERM);
663
664 tunable_param_init (config_file, force_config_file);
665
666 loginit (option_log_stderr, option_log_syslog);
667
668 log (LOG_INFO, "daemon starting up");
669
670 if (!cleanup_threads)
671 TUNABLE_INT_FETCH ("kern.srv.cleanup_threads", &cleanup_threads);
672 if (!cleanup_threads)
673 cleanup_threads = 2;
674
675 if (!request_threads)
676 TUNABLE_INT_FETCH ("kern.srv.request_threads", &request_threads);
677 if (!request_threads)
678 request_threads = 10;
679
680 if (!process_cache_size)
681 TUNABLE_INT_FETCH ("kern.srv.process_cache_size", &process_cache_size);
682 if (!process_cache_size)
683 process_cache_size = 62;
684
685 if (support_sharedmem == TUN_UNDEF)
686 TUNABLE_BOOL_FETCH ("kern.srv.sharedmem", &support_sharedmem);
687 if (support_sharedmem == TUN_UNDEF)
688 support_sharedmem = TUN_TRUE;
689
690 if (support_msgqueues == TUN_UNDEF)
691 TUNABLE_BOOL_FETCH ("kern.srv.msgqueues", &support_msgqueues);
692 if (support_msgqueues == TUN_UNDEF)
693 support_msgqueues = TUN_TRUE;
694
695 if (support_semaphores == TUN_UNDEF)
696 TUNABLE_BOOL_FETCH ("kern.srv.semaphores", &support_semaphores);
697 if (support_semaphores == TUN_UNDEF)
698 support_semaphores = TUN_TRUE;
699
700 if (!setup_privileges ())
701 panic ("Setting process privileges failed.");
702
703 ipcinit ();
704
705 /*XXXXX*/
706 threaded_queue request_queue (request_threads);
707
708 transport_layer_base *const transport = create_server_transport ();
709 assert (transport);
710
711 if (transport->listen () == -1)
712 return 1;
713
714 process_cache cache (process_cache_size, cleanup_threads);
715
716 server_submission_loop submission_loop (&request_queue, transport, &cache);
717
718 request_queue.add_submission_loop (&submission_loop);
719
720 cache.start ();
721
722 request_queue.start ();
723
724 log (LOG_NOTICE, "Initialization complete. Waiting for requests.");
725
726 /* TODO: wait on multiple objects - the thread handle for each
727 * request loop + all the process handles. This should be done by
728 * querying the request_queue and the process cache for all their
729 * handles, and then waiting for (say) 30 seconds. after that we
730 * recreate the list of handles to wait on, and wait again. the
731 * point of all this abstraction is that we can trivially server
732 * both sockets and pipes simply by making a new transport, and then
733 * calling request_queue.process_requests (transport2);
734 */
735 /* WaitForMultipleObjects abort && request_queue && process_queue && signal
736 -- if signal event then retrigger it
737 */
738 while (!shutdown_server && request_queue.running () && cache.running ())
739 {
740 pause ();
741 if (ipcunload ())
742 {
743 shutdown_server = false;
744 log (LOG_WARNING, "Shutdown request received but ignored. "
745 "Dependent processes still running.");
746 }
747 }
748
749 log (LOG_INFO, "Shutdown request received - new requests will be denied");
750 request_queue.stop ();
751 log (LOG_INFO, "All pending requests processed");
752 delete transport;
753 log (LOG_INFO, "No longer accepting requests - cygwin will operate in daemonless mode");
754 cache.stop ();
755 log (LOG_INFO, "All outstanding process-cache activities completed");
756 log (LOG_NOTICE, "Shutdown finished.");
757
758 return 0;
759 }
760 #endif /* __OUTSIDE_CYGWIN__ */
This page took 0.069693 seconds and 5 git commands to generate.