summaryrefslogtreecommitdiffstats
path: root/src/shared/verbs.c
blob: a38591de1fe18a86d597bd8249dcb6797a7cb744 (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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <errno.h>
#include <getopt.h>
#include <stdbool.h>
#include <stddef.h>

#include "env-util.h"
#include "log.h"
#include "macro.h"
#include "process-util.h"
#include "string-util.h"
#include "verbs.h"
#include "virt.h"

/* Wraps running_in_chroot() which is used in various places, but also adds an environment variable check
 * so external processes can reliably force this on. */
bool running_in_chroot_or_offline(void) {
        int r;

        /* Added to support use cases like rpm-ostree, where from %post scripts we only want to execute "preset",
         * but not "start"/"restart" for example.
         *
         * See docs/ENVIRONMENT.md for docs.
         */
        r = getenv_bool("SYSTEMD_OFFLINE");
        if (r >= 0)
                return r > 0;
        if (r != -ENXIO)
                log_debug_errno(r, "Failed to parse $SYSTEMD_OFFLINE, ignoring: %m");

        /* We've had this condition check for a long time which basically checks for legacy chroot case like Fedora's
         * "mock", which is used for package builds.  We don't want to try to start systemd services there, since
         * without --new-chroot we don't even have systemd running, and even if we did, adding a concept of background
         * daemons to builds would be an enormous change, requiring considering things like how the journal output is
         * handled, etc.  And there's really not a use case today for a build talking to a service.
         *
         * Note this call itself also looks for a different variable SYSTEMD_IGNORE_CHROOT=1.
         */
        r = running_in_chroot();
        if (r < 0)
                log_debug_errno(r, "Failed to check if we're running in chroot, assuming not: %m");
        return r > 0;
}

const Verb* verbs_find_verb(const char *name, const Verb verbs[]) {
        assert(verbs);

        for (size_t i = 0; verbs[i].dispatch; i++)
                if (name ? streq(name, verbs[i].verb) : FLAGS_SET(verbs[i].flags, VERB_DEFAULT))
                        return verbs + i;

        /* At the end of the list? */
        return NULL;
}

static const Verb* verbs_find_prefix_verb(const char *name, const Verb verbs[]) {
        size_t best_distance = SIZE_MAX;
        const Verb *best = NULL;

        assert(verbs);

        if (!name)
                return NULL;

        for (size_t i = 0; verbs[i].dispatch; i++) {
                const char *e;
                size_t l;

                e = startswith(verbs[i].verb, name);
                if (!e)
                        continue;

                l = strlen(e);
                if (l < best_distance) {
                        best_distance = l;
                        best = verbs + i;
                }
        }

        return best;
}

static const Verb* verbs_find_closest_verb(const char *name, const Verb verbs[]) {
        ssize_t best_distance = SSIZE_MAX;
        const Verb *best = NULL;

        assert(verbs);

        if (!name)
                return NULL;

        for (size_t i = 0; verbs[i].dispatch; i++) {
                ssize_t distance;

                distance = strlevenshtein(verbs[i].verb, name);
                if (distance < 0) {
                        log_debug_errno(distance, "Failed to determine Levenshtein distance between %s and %s: %m", verbs[i].verb, name);
                        return NULL;
                }

                if (distance > 5) /* If the distance is just too far off, don't make a bad suggestion */
                        continue;

                if (distance < best_distance) {
                        best_distance = distance;
                        best = verbs + i;
                }
        }

        return best;
}

int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) {
        const Verb *verb;
        const char *name;
        int left;

        assert(verbs);
        assert(verbs[0].dispatch);
        assert(argc >= 0);
        assert(argv);
        assert(argc >= optind);

        left = argc - optind;
        argv += optind;
        optind = 0;
        name = argv[0];

        verb = verbs_find_verb(name, verbs);
        if (!verb) {
                if (name) {
                        /* Be helperful to the user, and give a hint what the user might have wanted to
                         * type. We search with two mechanisms: a simple prefix match and – if that didn't
                         * yield results –, a Levenshtein word distance based match. */
                        verb = verbs_find_prefix_verb(name, verbs);
                        if (!verb)
                                verb = verbs_find_closest_verb(name, verbs);
                        if (verb)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                       "Unknown command verb '%s', did you mean '%s'?", name, verb->verb);

                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command verb '%s'.", name);
                }

                _cleanup_free_ char *verb_list = NULL;
                size_t i;

                for (i = 0; verbs[i].dispatch; i++)
                        if (!strextend_with_separator(&verb_list, ", ", verbs[i].verb))
                                return log_oom();

                if (i > 2)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                               "Command verb required (one of %s).", verb_list);

                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Command verb required.");
        }

        if (!name)
                left = 1;

        if (verb->min_args != VERB_ANY &&
            (unsigned) left < verb->min_args)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments.");

        if (verb->max_args != VERB_ANY &&
            (unsigned) left > verb->max_args)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments.");

        if ((verb->flags & VERB_ONLINE_ONLY) && running_in_chroot_or_offline()) {
                log_info("Running in chroot, ignoring command '%s'", name ?: verb->verb);
                return 0;
        }

        if (!name)
                return verb->dispatch(1, STRV_MAKE(verb->verb), userdata);

        return verb->dispatch(left, argv, userdata);
        }