Line 0
Link Here
|
|
|
1 |
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ |
2 |
/* |
3 |
* Copyright (C) 2011 Red Hat, Inc. |
4 |
* |
5 |
* This library is free software; you can redistribute it and/or |
6 |
* modify it under the terms of the GNU Lesser General Public |
7 |
* License as published by the Free Software Foundation; either |
8 |
* version 2 of the License, or (at your option) any later version. |
9 |
* |
10 |
* This library is distributed in the hope that it will be useful, |
11 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 |
* Lesser General Public License for more details. |
14 |
* |
15 |
* You should have received a copy of the GNU Lesser General |
16 |
* Public License along with this library; if not, write to the |
17 |
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
18 |
* Boston, MA 02110-1301, USA. |
19 |
* |
20 |
* Author: Matthias Clasen |
21 |
*/ |
22 |
|
23 |
#include "config.h" |
24 |
#include <errno.h> |
25 |
#include <pwd.h> |
26 |
#include <grp.h> |
27 |
#include <string.h> |
28 |
#include <sys/stat.h> |
29 |
#include <glib/gstdio.h> |
30 |
#include <gio/gio.h> |
31 |
#ifdef SESSION_TRACKING_SYSTEMD |
32 |
#include <systemd/sd-login.h> |
33 |
#endif |
34 |
#include <stdlib.h> |
35 |
|
36 |
#include "nm-session-utils.h" |
37 |
#include "nm-session-monitor.h" |
38 |
#include "nm-logging.h" |
39 |
|
40 |
#define CKDB_PATH "/var/run/ConsoleKit/database" |
41 |
|
42 |
/********************************************************************/ |
43 |
|
44 |
#ifdef SESSION_TRACKING_SYSTEMD |
45 |
typedef struct { |
46 |
GSource source; |
47 |
GPollFD pollfd; |
48 |
sd_login_monitor *monitor; |
49 |
} SdSource; |
50 |
|
51 |
static gboolean |
52 |
sd_source_prepare (GSource *source, gint *timeout) |
53 |
{ |
54 |
*timeout = -1; |
55 |
return FALSE; |
56 |
} |
57 |
|
58 |
static gboolean |
59 |
sd_source_check (GSource *source) |
60 |
{ |
61 |
SdSource *sd_source = (SdSource *) source; |
62 |
|
63 |
return sd_source->pollfd.revents != 0; |
64 |
} |
65 |
|
66 |
static gboolean |
67 |
sd_source_dispatch (GSource *source, |
68 |
GSourceFunc callback, |
69 |
gpointer user_data) |
70 |
|
71 |
{ |
72 |
SdSource *sd_source = (SdSource *)source; |
73 |
gboolean ret; |
74 |
|
75 |
g_warn_if_fail (callback != NULL); |
76 |
ret = (*callback) (user_data); |
77 |
sd_login_monitor_flush (sd_source->monitor); |
78 |
return ret; |
79 |
} |
80 |
|
81 |
static void |
82 |
sd_source_finalize (GSource *source) |
83 |
{ |
84 |
SdSource *sd_source = (SdSource*) source; |
85 |
|
86 |
sd_login_monitor_unref (sd_source->monitor); |
87 |
} |
88 |
|
89 |
static GSourceFuncs sd_source_funcs = { |
90 |
sd_source_prepare, |
91 |
sd_source_check, |
92 |
sd_source_dispatch, |
93 |
sd_source_finalize |
94 |
}; |
95 |
|
96 |
static GSource * |
97 |
sd_source_new (void) |
98 |
{ |
99 |
GSource *source; |
100 |
SdSource *sd_source; |
101 |
int ret; |
102 |
|
103 |
source = g_source_new (&sd_source_funcs, sizeof (SdSource)); |
104 |
sd_source = (SdSource *)source; |
105 |
|
106 |
ret = sd_login_monitor_new (NULL, &sd_source->monitor); |
107 |
if (ret < 0) |
108 |
g_printerr ("Error getting login monitor: %d", ret); |
109 |
else { |
110 |
sd_source->pollfd.fd = sd_login_monitor_get_fd (sd_source->monitor); |
111 |
sd_source->pollfd.events = G_IO_IN; |
112 |
g_source_add_poll (source, &sd_source->pollfd); |
113 |
} |
114 |
|
115 |
return source; |
116 |
} |
117 |
#endif /* SESSION_TRACKING_SYSTEMD */ |
118 |
|
119 |
struct _NMSessionMonitor { |
120 |
GObject parent_instance; |
121 |
|
122 |
GKeyFile *database; |
123 |
GFileMonitor *database_monitor; |
124 |
time_t database_mtime; |
125 |
GHashTable *sessions_by_uid; |
126 |
GHashTable *sessions_by_user; |
127 |
|
128 |
GSource *sd_source; |
129 |
}; |
130 |
|
131 |
struct _NMSessionMonitorClass { |
132 |
GObjectClass parent_class; |
133 |
|
134 |
void (*changed) (NMSessionMonitor *monitor); |
135 |
}; |
136 |
|
137 |
|
138 |
enum { |
139 |
CHANGED_SIGNAL, |
140 |
LAST_SIGNAL, |
141 |
}; |
142 |
static guint signals[LAST_SIGNAL] = {0}; |
143 |
|
144 |
G_DEFINE_TYPE (NMSessionMonitor, nm_session_monitor, G_TYPE_OBJECT); |
145 |
|
146 |
/* ---------------------------------------------------------------------------------------------------- */ |
147 |
|
148 |
typedef struct { |
149 |
char *user; |
150 |
uid_t uid; |
151 |
gboolean local; |
152 |
gboolean active; |
153 |
} Session; |
154 |
|
155 |
static void |
156 |
session_free (Session *s) |
157 |
{ |
158 |
g_free (s->user); |
159 |
memset (s, 0, sizeof (Session)); |
160 |
g_free (s); |
161 |
} |
162 |
|
163 |
static gboolean |
164 |
check_key (GKeyFile *keyfile, const char *group, const char *key, GError **error) |
165 |
{ |
166 |
if (g_key_file_has_key (keyfile, group, key, error)) |
167 |
return TRUE; |
168 |
|
169 |
if (!error) { |
170 |
g_set_error (error, |
171 |
NM_SESSION_MONITOR_ERROR, |
172 |
NM_SESSION_MONITOR_ERROR_MALFORMED_DATABASE, |
173 |
"ConsoleKit database " CKDB_PATH " group '%s' had no '%s' key", |
174 |
group, key); |
175 |
} |
176 |
return FALSE; |
177 |
} |
178 |
|
179 |
static Session * |
180 |
session_new (GKeyFile *keyfile, const char *group, GError **error) |
181 |
{ |
182 |
GError *local = NULL; |
183 |
Session *s; |
184 |
const char *uname = NULL; |
185 |
|
186 |
s = g_new0 (Session, 1); |
187 |
g_assert (s); |
188 |
|
189 |
s->uid = G_MAXUINT; /* paranoia */ |
190 |
if (!check_key (keyfile, group, "uid", &local)) |
191 |
goto error; |
192 |
s->uid = (uid_t) g_key_file_get_integer (keyfile, group, "uid", &local); |
193 |
if (local) |
194 |
goto error; |
195 |
|
196 |
if (!check_key (keyfile, group, "is_active", &local)) |
197 |
goto error; |
198 |
s->active = g_key_file_get_boolean (keyfile, group, "is_active", &local); |
199 |
if (local) |
200 |
goto error; |
201 |
|
202 |
if (!check_key (keyfile, group, "is_local", &local)) |
203 |
goto error; |
204 |
s->local = g_key_file_get_boolean (keyfile, group, "is_local", &local); |
205 |
if (local) |
206 |
goto error; |
207 |
|
208 |
if (!nm_session_uid_to_user (s->uid, &uname, error)) |
209 |
return FALSE; |
210 |
s->user = g_strdup (uname); |
211 |
|
212 |
return s; |
213 |
|
214 |
error: |
215 |
session_free (s); |
216 |
g_propagate_error (error, local); |
217 |
return NULL; |
218 |
} |
219 |
|
220 |
static void |
221 |
session_merge (Session *src, Session *dest) |
222 |
{ |
223 |
g_return_if_fail (src != NULL); |
224 |
g_return_if_fail (dest != NULL); |
225 |
|
226 |
g_warn_if_fail (g_strcmp0 (src->user, dest->user) == 0); |
227 |
g_warn_if_fail (src->uid == dest->uid); |
228 |
|
229 |
dest->local = (dest->local || src->local); |
230 |
dest->active = (dest->active || src->active); |
231 |
} |
232 |
|
233 |
static void |
234 |
free_database (NMSessionMonitor *self) |
235 |
{ |
236 |
if (self->database != NULL) { |
237 |
g_key_file_free (self->database); |
238 |
self->database = NULL; |
239 |
} |
240 |
|
241 |
g_hash_table_remove_all (self->sessions_by_uid); |
242 |
g_hash_table_remove_all (self->sessions_by_user); |
243 |
} |
244 |
|
245 |
static gboolean |
246 |
reload_database (NMSessionMonitor *self, GError **error) |
247 |
{ |
248 |
struct stat statbuf; |
249 |
char **groups = NULL; |
250 |
gsize len = 0, i; |
251 |
Session *s; |
252 |
|
253 |
free_database (self); |
254 |
|
255 |
errno = 0; |
256 |
if (stat (CKDB_PATH, &statbuf) != 0) { |
257 |
g_set_error (error, |
258 |
NM_SESSION_MONITOR_ERROR, |
259 |
errno == ENOENT ? NM_SESSION_MONITOR_ERROR_NO_DATABASE : NM_SESSION_MONITOR_ERROR_IO_ERROR, |
260 |
"Error statting file " CKDB_PATH ": %s", |
261 |
strerror (errno)); |
262 |
goto error; |
263 |
} |
264 |
self->database_mtime = statbuf.st_mtime; |
265 |
|
266 |
self->database = g_key_file_new (); |
267 |
if (!g_key_file_load_from_file (self->database, CKDB_PATH, G_KEY_FILE_NONE, error)) |
268 |
goto error; |
269 |
|
270 |
groups = g_key_file_get_groups (self->database, &len); |
271 |
if (!groups) { |
272 |
g_set_error_literal (error, |
273 |
NM_SESSION_MONITOR_ERROR, |
274 |
NM_SESSION_MONITOR_ERROR_IO_ERROR, |
275 |
"Could not load groups from " CKDB_PATH ""); |
276 |
goto error; |
277 |
} |
278 |
|
279 |
for (i = 0; i < len; i++) { |
280 |
Session *found; |
281 |
|
282 |
if (!g_str_has_prefix (groups[i], "Session ")) |
283 |
continue; |
284 |
|
285 |
s = session_new (self->database, groups[i], error); |
286 |
if (!s) |
287 |
goto error; |
288 |
|
289 |
found = g_hash_table_lookup (self->sessions_by_user, (gpointer) s->user); |
290 |
if (found) { |
291 |
session_merge (s, found); |
292 |
session_free (s); |
293 |
} else { |
294 |
/* Entirely new user */ |
295 |
g_hash_table_insert (self->sessions_by_user, (gpointer) s->user, s); |
296 |
g_hash_table_insert (self->sessions_by_uid, GUINT_TO_POINTER (s->uid), s); |
297 |
} |
298 |
} |
299 |
|
300 |
g_strfreev (groups); |
301 |
return TRUE; |
302 |
|
303 |
error: |
304 |
if (groups) |
305 |
g_strfreev (groups); |
306 |
free_database (self); |
307 |
return FALSE; |
308 |
} |
309 |
|
310 |
static gboolean |
311 |
ensure_database (NMSessionMonitor *self, GError **error) |
312 |
{ |
313 |
gboolean ret = FALSE; |
314 |
|
315 |
if (self->database != NULL) { |
316 |
struct stat statbuf; |
317 |
|
318 |
errno = 0; |
319 |
if (stat (CKDB_PATH, &statbuf) != 0) { |
320 |
g_set_error (error, |
321 |
NM_SESSION_MONITOR_ERROR, |
322 |
errno == ENOENT ? NM_SESSION_MONITOR_ERROR_NO_DATABASE : NM_SESSION_MONITOR_ERROR_IO_ERROR, |
323 |
"Error statting file " CKDB_PATH " to check timestamp: %s", |
324 |
strerror (errno)); |
325 |
goto out; |
326 |
} |
327 |
|
328 |
if (statbuf.st_mtime == self->database_mtime) { |
329 |
ret = TRUE; |
330 |
goto out; |
331 |
} |
332 |
} |
333 |
|
334 |
ret = reload_database (self, error); |
335 |
|
336 |
out: |
337 |
return ret; |
338 |
} |
339 |
|
340 |
static void |
341 |
on_file_monitor_changed (GFileMonitor * file_monitor, |
342 |
GFile * file, |
343 |
GFile * other_file, |
344 |
GFileMonitorEvent event_type, |
345 |
gpointer user_data) |
346 |
{ |
347 |
NMSessionMonitor *self = NM_SESSION_MONITOR (user_data); |
348 |
|
349 |
/* throw away cache */ |
350 |
free_database (self); |
351 |
|
352 |
g_signal_emit (self, signals[CHANGED_SIGNAL], 0); |
353 |
} |
354 |
|
355 |
#ifdef SESSION_TRACKING_SYSTEMD |
356 |
static gboolean |
357 |
sessions_changed (gpointer user_data) |
358 |
{ |
359 |
NMSessionMonitor *monitor = NM_SESSION_MONITOR (user_data); |
360 |
|
361 |
g_signal_emit (monitor, signals[CHANGED_SIGNAL], 0); |
362 |
return TRUE; |
363 |
} |
364 |
#endif /* SESSION_TRACKING_SYSTEMD */ |
365 |
|
366 |
|
367 |
static void |
368 |
nm_session_monitor_init (NMSessionMonitor *monitor) |
369 |
{ |
370 |
GError *error; |
371 |
GFile *file; |
372 |
|
373 |
monitor->sd_source = NULL; |
374 |
monitor->database = NULL; |
375 |
monitor->database_monitor = NULL; |
376 |
monitor->sessions_by_uid = NULL; |
377 |
monitor->sessions_by_user = NULL; |
378 |
|
379 |
#ifdef SESSION_TRACKING_SYSTEMD |
380 |
if (LOGIND_RUNNING()) |
381 |
{ |
382 |
monitor->sd_source = sd_source_new (); |
383 |
g_source_set_callback (monitor->sd_source, sessions_changed, monitor, NULL); |
384 |
g_source_attach (monitor->sd_source, NULL); |
385 |
return; |
386 |
} |
387 |
/* fall through */ |
388 |
#endif /* SESSION_TRACKING_SYSTEMD */ |
389 |
|
390 |
error = NULL; |
391 |
|
392 |
/* Sessions-by-user is responsible for destroying the Session objects */ |
393 |
monitor->sessions_by_user = g_hash_table_new_full (g_str_hash, g_str_equal, |
394 |
NULL, (GDestroyNotify) session_free); |
395 |
monitor->sessions_by_uid = g_hash_table_new (g_direct_hash, g_direct_equal); |
396 |
|
397 |
|
398 |
error = NULL; |
399 |
if (!ensure_database (monitor, &error)) { |
400 |
/* Ignore the first error if the CK database isn't found yet */ |
401 |
if (g_error_matches (error, |
402 |
NM_SESSION_MONITOR_ERROR, |
403 |
NM_SESSION_MONITOR_ERROR_NO_DATABASE) == FALSE) { |
404 |
nm_log_err (LOGD_CORE, "Error loading " CKDB_PATH ": %s", error->message); |
405 |
} |
406 |
g_error_free (error); |
407 |
} |
408 |
|
409 |
error = NULL; |
410 |
file = g_file_new_for_path (CKDB_PATH); |
411 |
monitor->database_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error); |
412 |
g_object_unref (file); |
413 |
if (monitor->database_monitor == NULL) { |
414 |
nm_log_err (LOGD_CORE, "Error monitoring " CKDB_PATH ": %s", error->message); |
415 |
g_error_free (error); |
416 |
} else { |
417 |
g_signal_connect (monitor->database_monitor, |
418 |
"changed", |
419 |
G_CALLBACK (on_file_monitor_changed), |
420 |
monitor); |
421 |
} |
422 |
} |
423 |
|
424 |
static void |
425 |
nm_session_monitor_finalize (GObject *object) |
426 |
{ |
427 |
NMSessionMonitor *monitor = NM_SESSION_MONITOR (object); |
428 |
|
429 |
if (monitor->sd_source != NULL) { |
430 |
g_source_destroy (monitor->sd_source); |
431 |
g_source_unref (monitor->sd_source); |
432 |
} |
433 |
|
434 |
if (monitor->database_monitor != NULL) |
435 |
g_object_unref (monitor->database_monitor); |
436 |
|
437 |
free_database (monitor); |
438 |
|
439 |
if (G_OBJECT_CLASS (nm_session_monitor_parent_class)->finalize != NULL) |
440 |
G_OBJECT_CLASS (nm_session_monitor_parent_class)->finalize (object); |
441 |
} |
442 |
|
443 |
static void |
444 |
nm_session_monitor_class_init (NMSessionMonitorClass *klass) |
445 |
{ |
446 |
GObjectClass *gobject_class; |
447 |
|
448 |
gobject_class = G_OBJECT_CLASS (klass); |
449 |
gobject_class->finalize = nm_session_monitor_finalize; |
450 |
|
451 |
/** |
452 |
* NMSessionMonitor::changed: |
453 |
* @monitor: A #NMSessionMonitor |
454 |
* |
455 |
* Emitted when something changes. |
456 |
*/ |
457 |
signals[CHANGED_SIGNAL] = g_signal_new (NM_SESSION_MONITOR_CHANGED, |
458 |
NM_TYPE_SESSION_MONITOR, |
459 |
G_SIGNAL_RUN_LAST, |
460 |
G_STRUCT_OFFSET (NMSessionMonitorClass, changed), |
461 |
NULL, /* accumulator */ |
462 |
NULL, /* accumulator data */ |
463 |
g_cclosure_marshal_VOID__VOID, |
464 |
G_TYPE_NONE, |
465 |
0); |
466 |
} |
467 |
|
468 |
NMSessionMonitor * |
469 |
nm_session_monitor_get (void) |
470 |
{ |
471 |
static NMSessionMonitor *singleton = NULL; |
472 |
|
473 |
if (singleton) |
474 |
return g_object_ref (singleton); |
475 |
|
476 |
singleton = NM_SESSION_MONITOR (g_object_new (NM_TYPE_SESSION_MONITOR, NULL)); |
477 |
g_assert (singleton); |
478 |
return singleton; |
479 |
} |
480 |
|
481 |
gboolean |
482 |
nm_session_monitor_user_has_session (NMSessionMonitor *monitor, |
483 |
const char *username, |
484 |
uid_t *out_uid, |
485 |
GError **error) |
486 |
{ |
487 |
#ifdef SESSION_TRACKING_SYSTEMD |
488 |
if (LOGIND_RUNNING()) |
489 |
{ |
490 |
uid_t uid; |
491 |
|
492 |
if (!nm_session_user_to_uid (username, &uid, error)) |
493 |
return FALSE; |
494 |
|
495 |
if (out_uid) |
496 |
*out_uid = uid; |
497 |
|
498 |
return nm_session_monitor_uid_has_session (monitor, uid, NULL, error); |
499 |
} |
500 |
/* fall through */ |
501 |
#endif /* SESSION_TRACKING_SYSTEMD */ |
502 |
|
503 |
Session *s; |
504 |
|
505 |
if (!ensure_database (monitor, error)) |
506 |
return FALSE; |
507 |
|
508 |
s = g_hash_table_lookup (monitor->sessions_by_user, (gpointer) username); |
509 |
if (!s) { |
510 |
g_set_error (error, |
511 |
NM_SESSION_MONITOR_ERROR, |
512 |
NM_SESSION_MONITOR_ERROR_UNKNOWN_USER, |
513 |
"No session found for user '%s'", |
514 |
username); |
515 |
return FALSE; |
516 |
} |
517 |
|
518 |
if (out_uid) |
519 |
*out_uid = s->uid; |
520 |
return TRUE; |
521 |
} |
522 |
|
523 |
gboolean |
524 |
nm_session_monitor_user_active (NMSessionMonitor *monitor, |
525 |
const char *username, |
526 |
GError **error) |
527 |
{ |
528 |
#ifdef SESSION_TRACKING_SYSTEMD |
529 |
if (LOGIND_RUNNING()) |
530 |
{ |
531 |
uid_t uid; |
532 |
|
533 |
if (!nm_session_user_to_uid (username, &uid, error)) |
534 |
return FALSE; |
535 |
|
536 |
return nm_session_monitor_uid_active (monitor, uid, error); |
537 |
} |
538 |
/* fall through */ |
539 |
#endif |
540 |
|
541 |
Session *s; |
542 |
|
543 |
if (!ensure_database (monitor, error)) |
544 |
return FALSE; |
545 |
|
546 |
s = g_hash_table_lookup (monitor->sessions_by_user, (gpointer) username); |
547 |
if (!s) { |
548 |
g_set_error (error, |
549 |
NM_SESSION_MONITOR_ERROR, |
550 |
NM_SESSION_MONITOR_ERROR_UNKNOWN_USER, |
551 |
"No session found for user '%s'", |
552 |
username); |
553 |
return FALSE; |
554 |
} |
555 |
|
556 |
return s->active; |
557 |
} |
558 |
|
559 |
gboolean |
560 |
nm_session_monitor_uid_has_session (NMSessionMonitor *monitor, |
561 |
uid_t uid, |
562 |
const char **out_user, |
563 |
GError **error) |
564 |
{ |
565 |
#ifdef SESSION_TRACKING_SYSTEMD |
566 |
if (LOGIND_RUNNING()) |
567 |
{ |
568 |
int ret; |
569 |
|
570 |
if (!nm_session_uid_to_user (uid, out_user, error)) |
571 |
return FALSE; |
572 |
|
573 |
ret = sd_uid_get_sessions (uid, FALSE, NULL) > 0; |
574 |
if (ret < 0) { |
575 |
nm_log_warn (LOGD_CORE, "Failed to get systemd sessions for uid %d: %d", |
576 |
uid, ret); |
577 |
return FALSE; |
578 |
} |
579 |
return ret > 0 ? TRUE : FALSE; |
580 |
} |
581 |
/* fall through */ |
582 |
#endif |
583 |
|
584 |
Session *s; |
585 |
|
586 |
if (!ensure_database (monitor, error)) |
587 |
return FALSE; |
588 |
|
589 |
s = g_hash_table_lookup (monitor->sessions_by_uid, GUINT_TO_POINTER (uid)); |
590 |
if (!s) { |
591 |
g_set_error (error, |
592 |
NM_SESSION_MONITOR_ERROR, |
593 |
NM_SESSION_MONITOR_ERROR_UNKNOWN_USER, |
594 |
"No session found for uid %d", |
595 |
uid); |
596 |
return FALSE; |
597 |
} |
598 |
|
599 |
if (out_user) |
600 |
*out_user = s->user; |
601 |
return TRUE; |
602 |
} |
603 |
|
604 |
gboolean |
605 |
nm_session_monitor_uid_active (NMSessionMonitor *monitor, |
606 |
uid_t uid, |
607 |
GError **error) |
608 |
{ |
609 |
#ifdef SESSION_TRACKING_SYSTEMD |
610 |
if (LOGIND_RUNNING()) |
611 |
{ |
612 |
int ret; |
613 |
|
614 |
ret = sd_uid_get_sessions (uid, TRUE, NULL) > 0; |
615 |
if (ret < 0) { |
616 |
nm_log_warn (LOGD_CORE, "Failed to get active systemd sessions for uid %d: %d", |
617 |
uid, ret); |
618 |
return FALSE; |
619 |
} |
620 |
return ret > 0 ? TRUE : FALSE; |
621 |
} |
622 |
/* fall through */ |
623 |
#endif |
624 |
|
625 |
Session *s; |
626 |
|
627 |
if (!ensure_database (monitor, error)) |
628 |
return FALSE; |
629 |
|
630 |
s = g_hash_table_lookup (monitor->sessions_by_uid, GUINT_TO_POINTER (uid)); |
631 |
if (!s) { |
632 |
g_set_error (error, |
633 |
NM_SESSION_MONITOR_ERROR, |
634 |
NM_SESSION_MONITOR_ERROR_UNKNOWN_USER, |
635 |
"No session found for uid '%d'", |
636 |
uid); |
637 |
return FALSE; |
638 |
} |
639 |
|
640 |
return s->active; |
641 |
} |