gdm-common.c 27 KB
Newer Older
1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
2
 *
3
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
4 5 6 7 8 9 10 11
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 14 15 16
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
17 18
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
19 20 21 22 23 24 25 26
 */

#include "config.h"

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <locale.h>
27
#include <fcntl.h>
28
#include <sys/wait.h>
29
#include <grp.h>
30
#include <pwd.h>
31

32 33
#include <glib.h>
#include <glib/gi18n.h>
34
#include <glib/gstdio.h>
35
#include <gio/gio.h>
36

37 38
#include "gdm-common.h"

39 40 41 42 43 44 45 46
#include <systemd/sd-login.h>

#define GDM_DBUS_NAME                            "org.gnome.DisplayManager"
#define GDM_DBUS_LOCAL_DISPLAY_FACTORY_PATH      "/org/gnome/DisplayManager/LocalDisplayFactory"
#define GDM_DBUS_LOCAL_DISPLAY_FACTORY_INTERFACE "org.gnome.DisplayManager.LocalDisplayFactory"

G_DEFINE_QUARK (gdm-common-error, gdm_common_error);

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
gboolean
gdm_clear_close_on_exec_flag (int fd)
{
        int flags;

        if (fd < 0) {
                return FALSE;
        }

        flags = fcntl (fd, F_GETFD, 0);

        if (flags < 0) {
                return FALSE;
        }

        if ((flags & FD_CLOEXEC) != 0) {
                int status;

                status = fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC);

                return status != -1;
        }

        return TRUE;
}

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
gboolean
gdm_get_pwent_for_name (const char     *name,
                        struct passwd **pwentp)
{
        struct passwd *pwent;

        do {
                errno = 0;
                pwent = getpwnam (name);
        } while (pwent == NULL && errno == EINTR);

        if (pwentp != NULL) {
                *pwentp = pwent;
        }

        return (pwent != NULL);
}

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
static gboolean
gdm_get_grent_for_gid (gint           gid,
                       struct group **grentp)
{
        struct group *grent;

        do {
                errno = 0;
                grent = getgrgid (gid);
        } while (grent == NULL && errno == EINTR);

        if (grentp != NULL) {
                *grentp = grent;
        }

        return (grent != NULL);
}

109
int
110 111
gdm_wait_on_and_disown_pid (int pid,
                            int timeout)
112 113
{
        int status;
114 115 116
        int ret;
        int num_tries;
        int flags;
Ray Strode's avatar
Ray Strode committed
117
        gboolean already_reaped;
118 119 120 121 122 123 124 125

        if (timeout > 0) {
                flags = WNOHANG;
                num_tries = 10 * timeout;
        } else {
                flags = 0;
                num_tries = 0;
        }
126 127
 wait_again:
        errno = 0;
Ray Strode's avatar
Ray Strode committed
128
        already_reaped = FALSE;
129 130
        ret = waitpid (pid, &status, flags);
        if (ret < 0) {
131 132 133
                if (errno == EINTR) {
                        goto wait_again;
                } else if (errno == ECHILD) {
Ray Strode's avatar
Ray Strode committed
134
                        already_reaped = TRUE;
135 136 137
                } else {
                        g_debug ("GdmCommon: waitpid () should not fail");
                }
138 139 140 141 142 143 144 145 146 147 148
        } else if (ret == 0) {
                num_tries--;

                if (num_tries > 0) {
                        g_usleep (G_USEC_PER_SEC / 10);
                } else {
                        char *path;
                        char *command;

                        path = g_strdup_printf ("/proc/%ld/cmdline", (long) pid);
                        if (g_file_get_contents (path, &command, NULL, NULL)) {;
149 150
                                g_warning ("GdmCommon: process (pid:%d, command '%s') isn't dying after %d seconds, now ignoring it.",
                                         (int) pid, command, timeout);
151 152
                                g_free (command);
                        } else {
153 154
                                g_warning ("GdmCommon: process (pid:%d) isn't dying after %d seconds, now ignoring it.",
                                         (int) pid, timeout);
155 156 157
                        }
                        g_free (path);

158
                        return 0;
159 160
                }
                goto wait_again;
161 162 163 164
        }

        g_debug ("GdmCommon: process (pid:%d) done (%s:%d)",
                 (int) pid,
Ray Strode's avatar
Ray Strode committed
165
                 already_reaped? "reaped earlier" :
166 167 168
                 WIFEXITED (status) ? "status"
                 : WIFSIGNALED (status) ? "signal"
                 : "unknown",
Ray Strode's avatar
Ray Strode committed
169
                 already_reaped? 1 :
170 171 172 173 174 175 176
                 WIFEXITED (status) ? WEXITSTATUS (status)
                 : WIFSIGNALED (status) ? WTERMSIG (status)
                 : -1);

        return status;
}

177 178 179
int
gdm_wait_on_pid (int pid)
{
180
    return gdm_wait_on_and_disown_pid (pid, 0);
181 182
}

183 184 185 186 187 188 189
int
gdm_signal_pid (int pid,
                int signal)
{
        int status = -1;

        /* perhaps block sigchld */
190 191
        g_debug ("GdmCommon: sending signal %d to process %d", signal, pid);
        errno = 0;
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
        status = kill (pid, signal);

        if (status < 0) {
                if (errno == ESRCH) {
                        g_warning ("Child process %d was already dead.",
                                   (int)pid);
                } else {
                        g_warning ("Couldn't kill child process %d: %s",
                                   pid,
                                   g_strerror (errno));
                }
        }

        /* perhaps unblock sigchld */

        return status;
}

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
static gboolean
_fd_is_character_device (int fd)
{
        struct stat file_info;

        if (fstat (fd, &file_info) < 0) {
                return FALSE;
        }

        return S_ISCHR (file_info.st_mode);
}

static gboolean
_read_bytes (int      fd,
             char    *bytes,
             gsize    number_of_bytes,
             GError **error)
{
        size_t bytes_left_to_read;
        size_t total_bytes_read = 0;
        gboolean premature_eof;

232
        bytes_left_to_read = number_of_bytes;
233 234 235 236
        premature_eof = FALSE;
        do {
                size_t bytes_read = 0;

237
                errno = 0;
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
                bytes_read = read (fd, ((guchar *) bytes) + total_bytes_read,
                                   bytes_left_to_read);

                if (bytes_read > 0) {
                        total_bytes_read += bytes_read;
                        bytes_left_to_read -= bytes_read;
                } else if (bytes_read == 0) {
                        premature_eof = TRUE;
                        break;
                } else if ((errno != EINTR)) {
                        break;
                }
        } while (bytes_left_to_read > 0);

        if (premature_eof) {
                g_set_error (error,
                             G_FILE_ERROR,
255 256
                             G_FILE_ERROR_FAILED,
                             "No data available");
257 258 259 260 261 262

                return FALSE;
        } else if (bytes_left_to_read > 0) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (errno),
263
                             "%s", g_strerror (errno));
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
                return FALSE;
        }

        return TRUE;
}

/**
 * Pulls a requested number of bytes from /dev/urandom
 *
 * @param size number of bytes to pull
 * @param error error if read fails
 * @returns The requested number of random bytes or #NULL if fail
 */

char *
gdm_generate_random_bytes (gsize    size,
                           GError **error)
{
        int fd;
        char *bytes;
        GError *read_error;

        /* We don't use the g_rand_* glib apis because they don't document
         * how much entropy they are seeded with, and it might be less
         * than the passed in size.
         */

291
        errno = 0;
292 293 294 295 296 297 298
        fd = open ("/dev/urandom", O_RDONLY);

        if (fd < 0) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (errno),
                             "%s", g_strerror (errno));
299
                close (fd);
300 301 302 303 304 305 306 307
                return NULL;
        }

        if (!_fd_is_character_device (fd)) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (ENODEV),
                             _("/dev/urandom is not a character device"));
308
                close (fd);
309 310 311 312 313 314 315 316
                return NULL;
        }

        bytes = g_malloc (size);
        read_error = NULL;
        if (!_read_bytes (fd, bytes, size, &read_error)) {
                g_propagate_error (error, read_error);
                g_free (bytes);
317
                close (fd);
318 319 320
                return NULL;
        }

321
        close (fd);
322 323
        return bytes;
}
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
static gboolean
create_transient_display (GDBusConnection *connection,
                          GError         **error)
{
        GError *local_error = NULL;
        GVariant *reply;
        const char     *value;

        reply = g_dbus_connection_call_sync (connection,
                                             GDM_DBUS_NAME,
                                             GDM_DBUS_LOCAL_DISPLAY_FACTORY_PATH,
                                             GDM_DBUS_LOCAL_DISPLAY_FACTORY_INTERFACE,
                                             "CreateTransientDisplay",
                                             NULL, /* parameters */
                                             G_VARIANT_TYPE ("(o)"),
                                             G_DBUS_CALL_FLAGS_NONE,
                                             -1,
                                             NULL, &local_error);
        if (reply == NULL) {
                g_warning ("Unable to create transient display: %s", local_error->message);
                g_propagate_error (error, local_error);
                return FALSE;
        }

        g_variant_get (reply, "(&o)", &value);
        g_debug ("Started %s", value);

        g_variant_unref (reply);
        return TRUE;
}

355 356 357 358
gboolean
gdm_activate_session_by_id (GDBusConnection *connection,
                            const char      *seat_id,
                            const char      *session_id)
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
{
        GError *local_error = NULL;
        GVariant *reply;

        reply = g_dbus_connection_call_sync (connection,
                                             "org.freedesktop.login1",
                                             "/org/freedesktop/login1",
                                             "org.freedesktop.login1.Manager",
                                             "ActivateSessionOnSeat",
                                             g_variant_new ("(ss)", session_id, seat_id),
                                             NULL,
                                             G_DBUS_CALL_FLAGS_NONE,
                                             -1,
                                             NULL, &local_error);
        if (reply == NULL) {
                g_warning ("Unable to activate session: %s", local_error->message);
                g_error_free (local_error);
                return FALSE;
        }

        g_variant_unref (reply);

        return TRUE;
}

384 385 386
gboolean
gdm_get_login_window_session_id (const char  *seat_id,
		                 char       **session_id)
387 388 389 390
{
        gboolean   ret;
        int        res, i;
        char     **sessions;
391
        char      *service_id;
392 393 394 395 396 397 398 399 400 401 402
        char      *service_class;
        char      *state;

        res = sd_seat_get_sessions (seat_id, &sessions, NULL, NULL);
        if (res < 0) {
                g_debug ("Failed to determine sessions: %s", strerror (-res));
                return FALSE;
        }

        if (sessions == NULL || sessions[0] == NULL) {
                *session_id = NULL;
403
                ret = FALSE;
404 405 406 407
                goto out;
        }

        for (i = 0; sessions[i]; i ++) {
408

409 410
                res = sd_session_get_class (sessions[i], &service_class);
                if (res < 0) {
411 412 413 414 415
                        if (res == -ENOENT) {
                                free (service_class);
                                continue;
                        }

416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
                        g_debug ("failed to determine class of session %s: %s", sessions[i], strerror (-res));
                        ret = FALSE;
                        goto out;
                }

                if (strcmp (service_class, "greeter") != 0) {
                        free (service_class);
                        continue;
                }

                free (service_class);

                ret = sd_session_get_state (sessions[i], &state);
                if (ret < 0) {
                        g_debug ("failed to determine state of session %s: %s", sessions[i], strerror (-res));
                        ret = FALSE;
                        goto out;
                }

                if (g_strcmp0 (state, "closing") == 0) {
                        free (state);
                        continue;
                }
                free (state);

441 442 443 444 445 446 447 448 449 450
                res = sd_session_get_service (sessions[i], &service_id);
                if (res < 0) {
                        g_debug ("failed to determine service of session %s: %s", sessions[i], strerror (-res));
                        ret = FALSE;
                        goto out;
                }

                if (strcmp (service_id, "gdm-launch-environment") == 0) {
                        *session_id = g_strdup (sessions[i]);
                        ret = TRUE;
451

452 453 454 455 456
                        free (service_id);
                        goto out;
                }

                free (service_id);
457 458 459
        }

        *session_id = NULL;
460
        ret = FALSE;
461 462

out:
463 464 465 466
        if (sessions) {
                for (i = 0; sessions[i]; i ++) {
                        free (sessions[i]);
                }
467

468 469
                free (sessions);
        }
470 471 472 473 474

        return ret;
}

static gboolean
Ray Strode's avatar
Ray Strode committed
475 476
goto_login_session (GDBusConnection  *connection,
                    GError          **error)
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
{
        gboolean        ret;
        int             res;
        char           *our_session;
        char           *session_id;
        char           *seat_id;

        ret = FALSE;
        session_id = NULL;
        seat_id = NULL;

        /* First look for any existing LoginWindow sessions on the seat.
           If none are found, create a new one. */

        /* Note that we mostly use free () here, instead of g_free ()
         * since the data allocated is from libsystemd-logind, which
         * does not use GLib's g_malloc (). */

        res = sd_pid_get_session (0, &our_session);
        if (res < 0) {
                g_debug ("failed to determine own session: %s", strerror (-res));
                g_set_error (error, GDM_COMMON_ERROR, 0, _("Could not identify the current session."));

                return FALSE;
        }

        res = sd_session_get_seat (our_session, &seat_id);
        free (our_session);
        if (res < 0) {
                g_debug ("failed to determine own seat: %s", strerror (-res));
                g_set_error (error, GDM_COMMON_ERROR, 0, _("Could not identify the current seat."));

                return FALSE;
        }

        res = sd_seat_can_multi_session (seat_id);
        if (res < 0) {
                free (seat_id);

                g_debug ("failed to determine whether seat can do multi session: %s", strerror (-res));
                g_set_error (error, GDM_COMMON_ERROR, 0, _("The system is unable to determine whether to switch to an existing login screen or start up a new login screen."));

                return FALSE;
        }

        if (res == 0) {
                free (seat_id);

                g_set_error (error, GDM_COMMON_ERROR, 0, _("The system is unable to start up a new login screen."));

                return FALSE;
        }

530
        res = gdm_get_login_window_session_id (seat_id, &session_id);
531
        if (res && session_id != NULL) {
532
                res = gdm_activate_session_by_id (connection, seat_id, session_id);
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565

                if (res) {
                        ret = TRUE;
                }
        }

        if (! ret && g_strcmp0 (seat_id, "seat0") == 0) {
                res = create_transient_display (connection, error);
                if (res) {
                        ret = TRUE;
                }
        }

        free (seat_id);
        g_free (session_id);

        return ret;
}

gboolean
gdm_goto_login_session (GError **error)
{
        GError *local_error;
        GDBusConnection *connection;

        local_error = NULL;
        connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error);
        if (connection == NULL) {
                g_debug ("Failed to connect to the D-Bus daemon: %s", local_error->message);
                g_propagate_error (error, local_error);
                return FALSE;
        }

Ray Strode's avatar
Ray Strode committed
566
        return goto_login_session (connection, error);
567
}
568 569 570 571 572 573 574 575

static void
listify_hash (const char *key,
              const char *value,
              GPtrArray  *env)
{
        char *str;
        str = g_strdup_printf ("%s=%s", key, value);
576
        g_debug ("Gdm: script environment: %s", str);
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
        g_ptr_array_add (env, str);
}

GPtrArray *
gdm_get_script_environment (const char *username,
                            const char *display_name,
                            const char *display_hostname,
                            const char *display_x11_authority_file)
{
        GPtrArray     *env;
        GHashTable    *hash;
        struct passwd *pwent;

        env = g_ptr_array_new ();

        /* create a hash table of current environment, then update keys has necessary */
        hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

        /* modify environment here */
        g_hash_table_insert (hash, g_strdup ("HOME"), g_strdup ("/"));
        g_hash_table_insert (hash, g_strdup ("PWD"), g_strdup ("/"));
        g_hash_table_insert (hash, g_strdup ("SHELL"), g_strdup ("/bin/sh"));

        if (username != NULL) {
                g_hash_table_insert (hash, g_strdup ("LOGNAME"),
                                     g_strdup (username));
                g_hash_table_insert (hash, g_strdup ("USER"),
                                     g_strdup (username));
                g_hash_table_insert (hash, g_strdup ("USERNAME"),
                                     g_strdup (username));

                gdm_get_pwent_for_name (username, &pwent);
                if (pwent != NULL) {
                        if (pwent->pw_dir != NULL && pwent->pw_dir[0] != '\0') {
                                g_hash_table_insert (hash, g_strdup ("HOME"),
                                                     g_strdup (pwent->pw_dir));
                                g_hash_table_insert (hash, g_strdup ("PWD"),
                                                     g_strdup (pwent->pw_dir));
                        }

                        g_hash_table_insert (hash, g_strdup ("SHELL"),
                                             g_strdup (pwent->pw_shell));
619 620 621 622 623 624 625

                        /* Also get group name and propagate down */
                        struct group *grent;

                        if (gdm_get_grent_for_gid (pwent->pw_gid, &grent)) {
                                g_hash_table_insert (hash, g_strdup ("GROUP"), g_strdup (grent->gr_name));
                        }
626 627 628 629 630 631 632 633
                }
        }

        if (display_hostname) {
                g_hash_table_insert (hash, g_strdup ("REMOTE_HOST"), g_strdup (display_hostname));
        }

        /* Runs as root */
634 635 636 637 638 639 640
        if (display_x11_authority_file) {
                g_hash_table_insert (hash, g_strdup ("XAUTHORITY"), g_strdup (display_x11_authority_file));
        }

        if (display_name) {
                g_hash_table_insert (hash, g_strdup ("DISPLAY"), g_strdup (display_name));
        }
641
        g_hash_table_insert (hash, g_strdup ("PATH"), g_strdup (GDM_SESSION_DEFAULT_PATH));
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754
        g_hash_table_insert (hash, g_strdup ("RUNNING_UNDER_GDM"), g_strdup ("true"));

        g_hash_table_remove (hash, "MAIL");

        g_hash_table_foreach (hash, (GHFunc)listify_hash, env);
        g_hash_table_destroy (hash);

        g_ptr_array_add (env, NULL);

        return env;
}

gboolean
gdm_run_script (const char *dir,
                const char *username,
                const char *display_name,
                const char *display_hostname,
                const char *display_x11_authority_file)
{
        char      *script;
        char     **argv;
        gint       status;
        GError    *error;
        GPtrArray *env;
        gboolean   res;
        gboolean   ret;

        ret = FALSE;

        g_assert (dir != NULL);
        g_assert (username != NULL);

        script = g_build_filename (dir, display_name, NULL);
        g_debug ("Trying script %s", script);
        if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR)
               && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) {
                g_debug ("script %s not found; skipping", script);
                g_free (script);
                script = NULL;
        }

        if (script == NULL
            && display_hostname != NULL
            && display_hostname[0] != '\0') {
                script = g_build_filename (dir, display_hostname, NULL);
                g_debug ("Trying script %s", script);
                if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR)
                       && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) {
                        g_debug ("script %s not found; skipping", script);
                        g_free (script);
                        script = NULL;
                }
        }

        if (script == NULL) {
                script = g_build_filename (dir, "Default", NULL);
                g_debug ("Trying script %s", script);
                if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR)
                       && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) {
                        g_debug ("script %s not found; skipping", script);
                        g_free (script);
                        script = NULL;
                }
        }

        if (script == NULL) {
                g_debug ("no script found");
                return TRUE;
        }

        g_debug ("Running process: %s", script);
        error = NULL;
        if (! g_shell_parse_argv (script, NULL, &argv, &error)) {
                g_warning ("Could not parse command: %s", error->message);
                g_error_free (error);
                goto out;
        }

        env = gdm_get_script_environment (username,
                                          display_name,
                                          display_hostname,
                                          display_x11_authority_file);

        res = g_spawn_sync (NULL,
                            argv,
                            (char **)env->pdata,
                            G_SPAWN_SEARCH_PATH,
                            NULL,
                            NULL,
                            NULL,
                            NULL,
                            &status,
                            &error);

        g_ptr_array_foreach (env, (GFunc)g_free, NULL);
        g_ptr_array_free (env, TRUE);
        g_strfreev (argv);

        if (! res) {
                g_warning ("Unable to run script: %s", error->message);
                g_error_free (error);
        }

        if (WIFEXITED (status)) {
                g_debug ("Process exit status: %d", WEXITSTATUS (status));
                ret = WEXITSTATUS (status) == 0;
        }

 out:
        g_free (script);

        return ret;
}
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841

gboolean
gdm_shell_var_is_valid_char (gchar c, gboolean first)
{
        return (!first && g_ascii_isdigit (c)) ||
                c == '_' ||
                g_ascii_isalpha (c);
}

/* This expands a string somewhat similar to how a shell would do it
   if it was enclosed inside double quotes.  It handles variable
   expansion like $FOO and ${FOO}, single-char escapes using \, and
   non-escaped # at the begining of a word is taken as a comment and ignored */
char *
gdm_shell_expand (const char *str,
                  GdmExpandVarFunc expand_var_func,
                  gpointer user_data)
{
        GString *s = g_string_new("");
        const gchar *p, *start;
        gchar c;
        gboolean at_new_word;

        p = str;
        at_new_word = TRUE;
        while (*p) {
                c = *p;
                if (c == '\\') {
                        p++;
                        c = *p;
                        if (c != '\0') {
                                p++;
                                switch (c) {
                                case '\\':
                                        g_string_append_c (s, '\\');
                                        break;
                                case '$':
                                        g_string_append_c (s, '$');
                                        break;
                                case '#':
                                        g_string_append_c (s, '#');
                                        break;
                                default:
                                        g_string_append_c (s, '\\');
                                        g_string_append_c (s, c);
                                        break;
                                }
                        }
                } else if (c == '#' && at_new_word) {
                        break;
                } else if (c == '$') {
                        gboolean brackets = FALSE;
                        p++;
                        if (*p == '{') {
                                brackets = TRUE;
                                p++;
                        }
                        start = p;
                        while (*p != '\0' &&
                               gdm_shell_var_is_valid_char (*p, p == start))
                                p++;
                        if (p == start || (brackets && *p != '}')) {
                                /* Invalid variable, use as-is */
                                g_string_append_c (s, '$');
                                if (brackets)
                                        g_string_append_c (s, '{');
                                g_string_append_len (s, start, p - start);
                        } else {
                                gchar *expanded;
                                gchar *var = g_strndup (start, p - start);
                                if (brackets && *p == '}')
                                        p++;

                                expanded = expand_var_func (var, user_data);
                                if (expanded)
                                        g_string_append (s, expanded);
                                g_free (var);
                                g_free (expanded);
                        }
                } else {
                        p++;
                        g_string_append_c (s, c);
                        at_new_word = g_ascii_isspace (c);
                }
        }
        return g_string_free (s, FALSE);
}