了解 xdg-open 的可用性

Towards understanding availability of xdg-open

我想打开一张图片,在 Windows 我这样做:

#include <windows.h>
..
ShellExecute(NULL, "open", "https://gsamaras.files.wordpress.com/2018/11/chronosgod.png", NULL, NULL, SW_SHOWNORMAL);

我想使用 Linux 方法,这样可以更轻松地即时 运行 一些事情。示例:

char s[100];
snprintf(s, sizeof s, "%s %s", "xdg-open", "https://gsamaras.files.wordpress.com/2018/11/chronosgod.png");
system(s);

在我的 Ubuntu 中,它有效。但是,当 运行 在 Wandbox (Live Demo) 或任何其他在线编译器中使用时,我很可能会收到错误消息:

sh: 1: xdg-open: not found

尽管这些在线编译器似乎存在于 Linux 中(checked). I don't expect the online compiler to open a browser for me, but I did expect the code to run without an error. Ah, and forget Mac(个人笔记本电脑,限制了我的机器)。

因为我没有其他 Linux 机器可以检查,我的问题是:我可以期望这段代码在大多数主要 Linux 发行版中都能工作吗?

也许它在在线编译器上失败的事实具有误导性。


PS:这是我在 God of Time 上的 post 的,所以不用担心安全问题。

xdg-openxdg-utils 的一部分。它们几乎总是与任何 Linux 发行版的 GUI 桌面一起安装。

可以在没有任何图形用户界面的情况下安装 Linux 发行版,比如在服务器上,而且很可能它们会缺少 xdg-open

而不是 system,您可以 - 并且应该 - 使用 fork + exec - 如果 exec 失败,则 xdg-open 无法执行。

在线编译器很可能没有安装任何桌面 GUI,因此缺少该实用程序。

虽然 Antti Haapala 已经完全 这个问题,但我认为关于该方法的一些评论和一个使安全使用变得微不足道的示例函数可能会有用。


xdg-open 是来自 freedesktop.org 的桌面集成实用程序的一部分,作为 Portland project. One can expect them to be available on any computer running a desktop environment participating in freedesktop.org 的一部分。这包括 GNOME、KDE ​​和 Xfce。

简单地说,这是在任何应用程序中使用桌面环境时打开资源(无论是文件还是URL)的推荐方式用户喜欢。

如果没有正在使用的桌面环境,那么也没有理由期望 xdg-open 可用。


对于Linux,我会建议使用专用函数,也许遵循以下几行。首先,几个内部辅助函数:

#define  _POSIX_C_SOURCE  200809L
#define  _GNU_SOURCE
//
// SPDX-License-Identifier: CC0-1.0
//
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

/* Number of bits in an unsigned long. */
#define  ULONG_BITS  (CHAR_BIT * sizeof (unsigned long))

/* Helper function to open /dev/null to a specific descriptor.
*/
static inline int devnullfd(const int fd)
{
    int  tempfd;

    /* Sanity check. */
    if (fd == -1)
        return errno = EINVAL;

    do {
        tempfd = open("/dev/null", O_RDWR | O_NOCTTY);
    } while (tempfd == -1 && errno == EINTR);
    if (tempfd == -1)
        return errno;

    if (tempfd != fd) {
        if (dup2(tempfd, fd) == -1) {
            const int  saved_errno = errno;
            close(tempfd);
            return errno = saved_errno;
        }
        if (close(tempfd) == -1)
            return errno;
    }

    return 0;
}

/* Helper function to close all except small descriptors
   specified in the mask. For obvious reasons, this is not
   thread safe, and is only intended to be used in recently
   forked child processes. */
static void closeall(const unsigned long  mask)
{
    DIR           *dir;
    struct dirent *ent;
    int            dfd;

    dir = opendir("/proc/self/fd/");
    if (!dir) {
        /* Cannot list open descriptors.  Just try and close all. */
        const long  fd_max = sysconf(_SC_OPEN_MAX);
        long        fd;

        for (fd = 0; fd < ULONG_BITS; fd++)
            if (!(mask & (1uL << fd)))
                close(fd);

        for (fd = ULONG_BITS; fd <= fd_max; fd++)
            close(fd);

        return;
    }

    dfd = dirfd(dir);

    while ((ent = readdir(dir)))
        if (ent->d_name[0] >= '0' && ent->d_name[0] <= '9') {
            const char *p = &ent->d_name[1];
            int         fd = ent->d_name[0] - '0';

            while (*p >= '0' && *p <= '9')
                fd = (10 * fd) + *(p++) - '0';

            if (*p)
                continue;

            if (fd == dfd)
                continue;

            if (fd < ULONG_MAX && (mask & (1uL << fd)))
                continue;

            close(fd);
        }

    closedir(dir);
}

closeall(0) 尝试关闭所有打开的文件描述符,devnullfd(fd) 尝试打开 fd/dev/null。这些用于确保即使用户欺骗xdg-open,也不会泄露文件描述符;仅传递文件名或 URL。

在非Linux POSIXy 系统上,您可以用更合适的东西替换它们。在 BSD 上,使用 closefrom(),并在循环中处理前 ULONG_MAX 个描述符。

xdg_open(file-or-url) 函数本身类似于

/* Launch the user-preferred application to open a file or URL.
   Returns 0 if success, an errno error code otherwise.
*/ 
int xdg_open(const char *file_or_url)
{
    pid_t  child, p;
    int    status;

    /* Sanity check. */
    if (!file_or_url || !*file_or_url)
        return errno = EINVAL;

    /* Fork the child process. */
    child = fork();
    if (child == -1)
        return errno;
    else
    if (!child) {
        /* Child process. */

        uid_t  uid = getuid();  /* Real, not effective, user. */
        gid_t  gid = getgid();  /* Real, not effective, group. */

        /* Close all open file descriptors. */
        closeall(0);

        /* Redirect standard streams, if possible. */
        devnullfd(STDIN_FILENO);
        devnullfd(STDOUT_FILENO);
        devnullfd(STDERR_FILENO);

        /* Drop elevated privileges, if any. */
        if (setresgid(gid, gid, gid) == -1 ||
            setresuid(uid, uid, uid) == -1)
            _Exit(98);

        /* Have the child process execute in a new process group. */
        setsid();

        /* Execute xdg-open. */
        execlp("xdg-open", "xdg-open", file_or_url, (char *)0);

        /* Failed. xdg-open uses 0-5, we return 99. */
        _Exit(99);
    }

    /* Reap the child. */
    do {
        status = 0;
        p = waitpid(child, &status, 0);
    } while (p == -1 && errno == EINTR);
    if (p == -1)
        return errno;

    if (!WIFEXITED(status)) {
        /* Killed by a signal. Best we can do is I/O error, I think. */
        return errno = EIO;
    }

    switch (WEXITSTATUS(status)) {
    case 0: /* No error. */
        return errno = 0; /* It is unusual, but robust to explicitly clear errno. */
    case 1: /* Error in command line syntax. */
        return errno = EINVAL;      /* Invalid argument */
    case 2: /* File does not exist. */
        return errno = ENOENT;      /* No such file or directory */
    case 3: /* A required tool could not be found. */
        return errno = ENOSYS;      /* Not implemented */
    case 4: /* Action failed. */
        return errno = EPROTO;      /* Protocol error */
    case 98: /* Identity shenanigans. */
        return errno = EACCES;      /* Permission denied */
    case 99: /* xdg-open does not exist. */
        return errno = ENOPKG;      /* Package not installed */
    default:
        /* None of the other values should occur. */
        return errno = ENOSYS;      /* Not implemented */
    }
}

如前所述,它努力关闭所有打开的文件描述符,将标准流重定向到 /dev/null,确保有效和真实的身份匹配(以防在 setuid 二进制文件中使用),以及使用子进程退出状态传递 success/failure。

setresuid()setresgid() 调用仅在保存了用户和组 ID 的操作系统上可用。在其他情况下,请改用 seteuid(uid)setegid()

此实现试图平衡用户可配置性和安全性。用户可以设置 PATH 以便执行他们最喜欢的 xdg-open,但该函数会尝试确保没有敏感信息或特权泄露给该进程。

(环境变量是可以过滤的,但是一开始就不应该包含敏感信息,而且我们真的不知道桌面环境使用了哪些。所以最好不要乱用它们,以免给用户带来惊喜最低限度。)

作为最低限度的测试 main(),请尝试以下操作:

int main(int argc, char *argv[])
{
    int  arg, status;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s FILE-OR-URL ...\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This example program opens each specified file or URL\n");
        fprintf(stderr, "xdg-open(1), and outputs success or failure for each.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    status = EXIT_SUCCESS;

    for (arg = 1; arg < argc; arg++)
        if (xdg_open(argv[arg])) {
            printf("%s: %s.\n", argv[arg], strerror(errno));
            status = EXIT_FAILURE;
        } else
            printf("%s: Opened.\n", argv[arg]);

    return status;
}

如 SPDX 许可证标识符所述,此示例代码根据 Creative Commons Zero 1.0 获得许可。在您想要的任何代码中以您希望的任何方式使用它。