summaryrefslogtreecommitdiff
path: root/src/auto_detect_format.c
blob: 5e17e172316ed7b298bec5e7c3307456b7972610 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
 * vim:ts=4:sw=4:expandtab
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>

#include "i3status.h"

/*
 * Reads /proc/<pid>/stat and returns (via pointers) the name and parent pid of
 * the specified pid.
 * When false is returned, parsing failed and the contents of outname and
 * outpid are undefined.
 *
 */
static bool parse_proc_stat(pid_t pid, char **outname, pid_t *outppid) {
    char path[255];
    /* the relevant contents (for us) are:
     * <pid> (<program name>) <status> <ppid>
     * which should well fit into one page of 4096 bytes */
    char buffer[4096];

    if (snprintf(path, sizeof(path), "/proc/%d/stat", pid) == -1 ||
        !slurp(path, buffer, sizeof(buffer)))
        return false;

    char *leftbracket = strchr(buffer, '(');
    char *rightbracket = strrchr(buffer, ')');
    if (!leftbracket ||
        !rightbracket ||
        sscanf(rightbracket + 2, "%*c %d", outppid) != 1)
        return false;
    *rightbracket = '\0';
    *outname = strdup(leftbracket + 1);
    return true;
}

static char *format_for_process(const char *name) {
    if (strcasecmp(name, "i3bar") == 0)
        return "i3bar";
    else if (strcasecmp(name, "dzen2") == 0)
        return "dzen2";
    else if (strcasecmp(name, "xmobar") == 0)
        return "xmobar";
    else
        return NULL;
}

/*
 * This function tries to automatically find out where i3status is being piped
 * to and choses the appropriate output format.
 *
 * It is a little hackish but should work for most setups :).
 *
 * By iterating through /proc/<number>/stat and finding out the parent process
 * id (just like pstree(1) or ps(1) work), we can get all children of our
 * parent. When the output of i3status is being piped somewhere, the shell
 * (parent process) spawns i3status and the destination process, so we will
 * find our own process and the pipe target.
 *
 * We then check whether the pipe target’s name is known and chose the format.
 *
 */
char *auto_detect_format(void) {
    /* If stdout is a tty, we output directly to a terminal. */
    if (isatty(STDOUT_FILENO)) {
        return "term";
    }

    pid_t myppid = getppid();
    pid_t mypid = getpid();

    DIR *dir;
    struct dirent *entry;

    char *format = NULL;

    char *parentname;
    pid_t parentpid;

    if (!parse_proc_stat(myppid, &parentname, &parentpid))
        return NULL;

    if (strcmp(parentname, "sh") == 0) {
        pid_t tmp_ppid = parentpid;
        free(parentname);
        fprintf(stderr, "i3status: auto-detection: parent process is \"sh\", looking at its parent\n");
        if (!parse_proc_stat(tmp_ppid, &parentname, &parentpid))
            return NULL;
    }

    /* Some shells, for example zsh, open a pipe in a way which will make the
     * pipe target the parent process of i3status. If we detect that, we set
     * the format and we are done. */
    if ((format = format_for_process(parentname)) != NULL)
        goto out;

    if (!(dir = opendir("/proc")))
        goto out;

    while ((entry = readdir(dir)) != NULL) {
        pid_t pid = (pid_t)atoi(entry->d_name);
        if (pid == 0 || pid == mypid)
            continue;

        char *name = NULL;
        pid_t ppid;
        int loopcnt = 0;
        /* Now we need to find out the name of the process.
         * To avoid the possible race condition of the process existing already
         * but not executing the destination (shell after fork() and before
         * exec()), we check if the name equals its parent.
         *
         * We try this for up to 0.5 seconds, then we give up.
         */
        do {
            /* give the scheduler a chance between each iteration, don’t hog
             * the CPU too much */
            if (name) {
                usleep(50);
                free(name);
            }

            if (!parse_proc_stat(pid, &name, &ppid))
                break;
            if (ppid != myppid)
                break;
        } while (strcmp(parentname, name) == 0 && loopcnt++ < 10000);

        if (!name)
            continue;

        /* Check for known destination programs and set format */
        char *newfmt = format_for_process(name);
        free(name);

        if (newfmt && format && strcmp(newfmt, format) != 0) {
            fprintf(stderr, "i3status: cannot auto-configure, situation ambiguous (format \"%s\" *and* \"%s\" detected)\n", newfmt, format);
            format = NULL;
            break;
        } else {
            format = newfmt;
        }
    }

    closedir(dir);

out:
    if (parentname)
        free(parentname);

    return format;
}