Merge pull request #26663 from poettering/vpick
add new "vpick" concept for automatically picking newest resource from .v/ dir containing versioned files
This commit is contained in:
@@ -516,3 +516,25 @@ const char* inode_type_to_string(mode_t m) {
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mode_t inode_type_from_string(const char *s) {
|
||||
if (!s)
|
||||
return MODE_INVALID;
|
||||
|
||||
if (streq(s, "reg"))
|
||||
return S_IFREG;
|
||||
if (streq(s, "dir"))
|
||||
return S_IFDIR;
|
||||
if (streq(s, "lnk"))
|
||||
return S_IFLNK;
|
||||
if (streq(s, "chr"))
|
||||
return S_IFCHR;
|
||||
if (streq(s, "blk"))
|
||||
return S_IFBLK;
|
||||
if (streq(s, "fifo"))
|
||||
return S_IFIFO;
|
||||
if (streq(s, "sock"))
|
||||
return S_IFSOCK;
|
||||
|
||||
return MODE_INVALID;
|
||||
}
|
||||
|
||||
@@ -114,3 +114,4 @@ int inode_compare_func(const struct stat *a, const struct stat *b);
|
||||
extern const struct hash_ops inode_hash_ops;
|
||||
|
||||
const char* inode_type_to_string(mode_t m);
|
||||
mode_t inode_type_from_string(const char *s);
|
||||
|
||||
@@ -1519,3 +1519,26 @@ ssize_t strlevenshtein(const char *x, const char *y) {
|
||||
|
||||
return t1[yl];
|
||||
}
|
||||
|
||||
char *strrstr(const char *haystack, const char *needle) {
|
||||
const char *f = NULL;
|
||||
size_t l;
|
||||
|
||||
/* Like strstr() but returns the last rather than the first occurence of "needle" in "haystack". */
|
||||
|
||||
if (!haystack || !needle)
|
||||
return NULL;
|
||||
|
||||
l = strlen(needle);
|
||||
|
||||
/* Special case: for the empty string we return the very last possible occurence, i.e. *after* the
|
||||
* last char, not before. */
|
||||
if (l == 0)
|
||||
return strchr(haystack, 0);
|
||||
|
||||
for (const char *p = haystack; *p; p++)
|
||||
if (strncmp(p, needle, l) == 0)
|
||||
f = p;
|
||||
|
||||
return (char*) f;
|
||||
}
|
||||
|
||||
@@ -322,3 +322,5 @@ static inline int strdup_or_null(const char *s, char **ret) {
|
||||
*ret = c;
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *strrstr(const char *haystack, const char *needle);
|
||||
|
||||
@@ -905,3 +905,13 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const
|
||||
}
|
||||
|
||||
DEFINE_HASH_OPS_FULL(string_strv_hash_ops, char, string_hash_func, string_compare_func, free, char*, strv_free);
|
||||
|
||||
char* strv_endswith(const char *s, char **l) {
|
||||
STRV_FOREACH(i, l) {
|
||||
char *e = endswith(s, *i);
|
||||
if (e)
|
||||
return (char*) e;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -252,3 +252,5 @@ int _string_strv_hashmap_put(Hashmap **h, const char *key, const char *value HA
|
||||
int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value HASHMAP_DEBUG_PARAMS);
|
||||
#define string_strv_hashmap_put(h, k, v) _string_strv_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS)
|
||||
#define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS)
|
||||
|
||||
char* strv_endswith(const char *s, char **l);
|
||||
|
||||
+128
-26
@@ -59,6 +59,7 @@
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "utmp-wtmp.h"
|
||||
#include "vpick.h"
|
||||
|
||||
#define IDLE_TIMEOUT_USEC (5*USEC_PER_SEC)
|
||||
#define IDLE_TIMEOUT2_USEC (1*USEC_PER_SEC)
|
||||
@@ -2859,13 +2860,33 @@ static bool insist_on_sandboxing(
|
||||
return false;
|
||||
}
|
||||
|
||||
static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
|
||||
static int setup_ephemeral(
|
||||
const ExecContext *context,
|
||||
ExecRuntime *runtime,
|
||||
char **root_image, /* both input and output! modified if ephemeral logic enabled */
|
||||
char **root_directory) { /* ditto */
|
||||
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
_cleanup_free_ char *new_root = NULL;
|
||||
int r;
|
||||
|
||||
assert(context);
|
||||
assert(root_image);
|
||||
assert(root_directory);
|
||||
|
||||
if (!*root_image && !*root_directory)
|
||||
return 0;
|
||||
|
||||
if (!runtime || !runtime->ephemeral_copy)
|
||||
return 0;
|
||||
|
||||
assert(runtime->ephemeral_storage_socket[0] >= 0);
|
||||
assert(runtime->ephemeral_storage_socket[1] >= 0);
|
||||
|
||||
new_root = strdup(runtime->ephemeral_copy);
|
||||
if (!new_root)
|
||||
return log_oom_debug();
|
||||
|
||||
r = posix_lock(runtime->ephemeral_storage_socket[0], LOCK_EX);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to lock ephemeral storage socket: %m");
|
||||
@@ -2876,28 +2897,23 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
|
||||
if (fd >= 0)
|
||||
/* We got an fd! That means ephemeral has already been set up, so nothing to do here. */
|
||||
return 0;
|
||||
|
||||
if (fd != -EAGAIN)
|
||||
return log_debug_errno(fd, "Failed to receive file descriptor queued on ephemeral storage socket: %m");
|
||||
|
||||
log_debug("Making ephemeral snapshot of %s to %s",
|
||||
context->root_image ?: context->root_directory, runtime->ephemeral_copy);
|
||||
if (*root_image) {
|
||||
log_debug("Making ephemeral copy of %s to %s", *root_image, new_root);
|
||||
|
||||
if (context->root_image)
|
||||
fd = copy_file(context->root_image, runtime->ephemeral_copy, O_EXCL, 0600,
|
||||
COPY_LOCK_BSD|COPY_REFLINK|COPY_CRTIME);
|
||||
else
|
||||
fd = btrfs_subvol_snapshot_at(AT_FDCWD, context->root_directory,
|
||||
AT_FDCWD, runtime->ephemeral_copy,
|
||||
BTRFS_SNAPSHOT_FALLBACK_COPY |
|
||||
BTRFS_SNAPSHOT_FALLBACK_DIRECTORY |
|
||||
BTRFS_SNAPSHOT_RECURSIVE |
|
||||
BTRFS_SNAPSHOT_LOCK_BSD);
|
||||
if (fd < 0)
|
||||
return log_debug_errno(fd, "Failed to snapshot %s to %s: %m",
|
||||
context->root_image ?: context->root_directory, runtime->ephemeral_copy);
|
||||
fd = copy_file(*root_image,
|
||||
new_root,
|
||||
O_EXCL,
|
||||
0600,
|
||||
COPY_LOCK_BSD|
|
||||
COPY_REFLINK|
|
||||
COPY_CRTIME);
|
||||
if (fd < 0)
|
||||
return log_debug_errno(fd, "Failed to copy image %s to %s: %m",
|
||||
*root_image, new_root);
|
||||
|
||||
if (context->root_image) {
|
||||
/* A root image might be subject to lots of random writes so let's try to disable COW on it
|
||||
* which tends to not perform well in combination with lots of random writes.
|
||||
*
|
||||
@@ -2906,13 +2922,35 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
|
||||
*/
|
||||
r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
|
||||
if (r < 0)
|
||||
log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", runtime->ephemeral_copy);
|
||||
log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", new_root);
|
||||
} else {
|
||||
assert(*root_directory);
|
||||
|
||||
log_debug("Making ephemeral snapshot of %s to %s", *root_directory, new_root);
|
||||
|
||||
fd = btrfs_subvol_snapshot_at(
|
||||
AT_FDCWD, *root_directory,
|
||||
AT_FDCWD, new_root,
|
||||
BTRFS_SNAPSHOT_FALLBACK_COPY |
|
||||
BTRFS_SNAPSHOT_FALLBACK_DIRECTORY |
|
||||
BTRFS_SNAPSHOT_RECURSIVE |
|
||||
BTRFS_SNAPSHOT_LOCK_BSD);
|
||||
if (fd < 0)
|
||||
return log_debug_errno(fd, "Failed to snapshot directory %s to %s: %m",
|
||||
*root_directory, new_root);
|
||||
}
|
||||
|
||||
r = send_one_fd(runtime->ephemeral_storage_socket[1], fd, MSG_DONTWAIT);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to queue file descriptor on ephemeral storage socket: %m");
|
||||
|
||||
if (*root_image)
|
||||
free_and_replace(*root_image, new_root);
|
||||
else {
|
||||
assert(*root_directory);
|
||||
free_and_replace(*root_directory, new_root);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2972,6 +3010,63 @@ static int verity_settings_prepare(
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pick_versions(
|
||||
const ExecContext *context,
|
||||
const ExecParameters *params,
|
||||
char **ret_root_image,
|
||||
char **ret_root_directory) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(context);
|
||||
assert(params);
|
||||
assert(ret_root_image);
|
||||
assert(ret_root_directory);
|
||||
|
||||
if (context->root_image) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
context->root_image,
|
||||
&pick_filter_image_raw,
|
||||
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
|
||||
&result);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!result.path)
|
||||
return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_image);
|
||||
|
||||
*ret_root_image = TAKE_PTR(result.path);
|
||||
*ret_root_directory = NULL;
|
||||
return r;
|
||||
}
|
||||
|
||||
if (context->root_directory) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
context->root_directory,
|
||||
&pick_filter_image_dir,
|
||||
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
|
||||
&result);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!result.path)
|
||||
return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_directory);
|
||||
|
||||
*ret_root_image = NULL;
|
||||
*ret_root_directory = TAKE_PTR(result.path);
|
||||
return r;
|
||||
}
|
||||
|
||||
*ret_root_image = *ret_root_directory = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_mount_namespace(
|
||||
ExecCommandFlags command_flags,
|
||||
const ExecContext *context,
|
||||
@@ -2984,8 +3079,8 @@ static int apply_mount_namespace(
|
||||
_cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL,
|
||||
**read_write_paths_cleanup = NULL;
|
||||
_cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL,
|
||||
*extension_dir = NULL, *host_os_release_stage = NULL;
|
||||
const char *root_dir = NULL, *root_image = NULL, *tmp_dir = NULL, *var_tmp_dir = NULL;
|
||||
*extension_dir = NULL, *host_os_release_stage = NULL, *root_image = NULL, *root_dir = NULL;
|
||||
const char *tmp_dir = NULL, *var_tmp_dir = NULL;
|
||||
char **read_write_paths;
|
||||
bool needs_sandboxing, setup_os_release_symlink;
|
||||
BindMount *bind_mounts = NULL;
|
||||
@@ -2997,14 +3092,21 @@ static int apply_mount_namespace(
|
||||
CLEANUP_ARRAY(bind_mounts, n_bind_mounts, bind_mount_free_many);
|
||||
|
||||
if (params->flags & EXEC_APPLY_CHROOT) {
|
||||
r = setup_ephemeral(context, runtime);
|
||||
r = pick_versions(
|
||||
context,
|
||||
params,
|
||||
&root_image,
|
||||
&root_dir);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (context->root_image)
|
||||
root_image = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_image;
|
||||
else
|
||||
root_dir = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_directory;
|
||||
r = setup_ephemeral(
|
||||
context,
|
||||
runtime,
|
||||
&root_image,
|
||||
&root_dir);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
r = compile_bind_mounts(context, params, &bind_mounts, &n_bind_mounts, &empty_directories);
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "tmpfile-util.h"
|
||||
#include "uid-alloc-range.h"
|
||||
#include "user-util.h"
|
||||
#include "vpick.h"
|
||||
|
||||
static enum {
|
||||
ACTION_DISSECT,
|
||||
@@ -1817,6 +1818,16 @@ static int run(int argc, char *argv[]) {
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
if (arg_image) {
|
||||
r = path_pick_update_warn(
|
||||
&arg_image,
|
||||
&pick_filter_image_raw,
|
||||
PICK_ARCHITECTURE|PICK_TRIES,
|
||||
/* ret_result= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
switch (arg_action) {
|
||||
case ACTION_UMOUNT:
|
||||
return action_umount(arg_path);
|
||||
|
||||
+66
-3
@@ -112,6 +112,7 @@
|
||||
#include "umask-util.h"
|
||||
#include "unit-name.h"
|
||||
#include "user-util.h"
|
||||
#include "vpick.h"
|
||||
|
||||
/* The notify socket inside the container it can use to talk to nspawn using the sd_notify(3) protocol */
|
||||
#define NSPAWN_NOTIFY_SOCKET_PATH "/run/host/notify"
|
||||
@@ -2911,14 +2912,72 @@ static int on_request_stop(sd_bus_message *m, void *userdata, sd_bus_error *erro
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pick_paths(void) {
|
||||
int r;
|
||||
|
||||
if (arg_directory) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
PickFilter filter = pick_filter_image_dir;
|
||||
|
||||
filter.architecture = arg_architecture;
|
||||
|
||||
r = path_pick_update_warn(
|
||||
&arg_directory,
|
||||
&filter,
|
||||
PICK_ARCHITECTURE|PICK_TRIES,
|
||||
&result);
|
||||
if (r < 0) {
|
||||
/* Accept ENOENT here so that the --template= logic can work */
|
||||
if (r != -ENOENT)
|
||||
return r;
|
||||
} else
|
||||
arg_architecture = result.architecture;
|
||||
}
|
||||
|
||||
if (arg_image) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
PickFilter filter = pick_filter_image_raw;
|
||||
|
||||
filter.architecture = arg_architecture;
|
||||
|
||||
r = path_pick_update_warn(
|
||||
&arg_image,
|
||||
&filter,
|
||||
PICK_ARCHITECTURE|PICK_TRIES,
|
||||
&result);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
arg_architecture = result.architecture;
|
||||
}
|
||||
|
||||
if (arg_template) {
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
PickFilter filter = pick_filter_image_dir;
|
||||
|
||||
filter.architecture = arg_architecture;
|
||||
|
||||
r = path_pick_update_warn(
|
||||
&arg_template,
|
||||
&filter,
|
||||
PICK_ARCHITECTURE,
|
||||
&result);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
arg_architecture = result.architecture;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int determine_names(void) {
|
||||
int r;
|
||||
|
||||
if (arg_template && !arg_directory && arg_machine) {
|
||||
|
||||
/* If --template= was specified then we should not
|
||||
* search for a machine, but instead create a new one
|
||||
* in /var/lib/machine. */
|
||||
/* If --template= was specified then we should not search for a machine, but instead create a
|
||||
* new one in /var/lib/machine. */
|
||||
|
||||
arg_directory = path_join("/var/lib/machines", arg_machine);
|
||||
if (!arg_directory)
|
||||
@@ -5406,6 +5465,10 @@ static int run(int argc, char *argv[]) {
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = pick_paths();
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = determine_names();
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
+272
-57
@@ -45,6 +45,7 @@
|
||||
#include "strv.h"
|
||||
#include "time-util.h"
|
||||
#include "utf8.h"
|
||||
#include "vpick.h"
|
||||
#include "xattr-util.h"
|
||||
|
||||
static const char* const image_search_path[_IMAGE_CLASS_MAX] = {
|
||||
@@ -215,40 +216,60 @@ static int image_new(
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int extract_pretty(
|
||||
static int extract_image_basename(
|
||||
const char *path,
|
||||
const char *class_suffix,
|
||||
const char *format_suffix,
|
||||
char **ret) {
|
||||
const char *class_suffix, /* e.g. ".sysext" (this is an optional suffix) */
|
||||
char **format_suffixes, /* e.g. ".raw" (one of these will be required) */
|
||||
char **ret_basename,
|
||||
char **ret_suffix) {
|
||||
|
||||
_cleanup_free_ char *name = NULL;
|
||||
_cleanup_free_ char *name = NULL, *suffix = NULL;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(ret);
|
||||
|
||||
r = path_extract_filename(path, &name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (format_suffix) {
|
||||
char *e = endswith(name, format_suffix);
|
||||
if (format_suffixes) {
|
||||
char *e = strv_endswith(name, format_suffixes);
|
||||
if (!e) /* Format suffix is required */
|
||||
return -EINVAL;
|
||||
|
||||
if (ret_suffix) {
|
||||
suffix = strdup(e);
|
||||
if (!suffix)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*e = 0;
|
||||
}
|
||||
|
||||
if (class_suffix) {
|
||||
char *e = endswith(name, class_suffix);
|
||||
if (e) /* Class suffix is optional */
|
||||
if (e) { /* Class suffix is optional */
|
||||
if (ret_suffix) {
|
||||
_cleanup_free_ char *j = strjoin(e, suffix);
|
||||
if (!j)
|
||||
return -ENOMEM;
|
||||
|
||||
free_and_replace(suffix, j);
|
||||
}
|
||||
|
||||
*e = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!image_name_is_valid(name))
|
||||
return -EINVAL;
|
||||
|
||||
*ret = TAKE_PTR(name);
|
||||
if (ret_suffix)
|
||||
*ret_suffix = TAKE_PTR(suffix);
|
||||
|
||||
if (ret_basename)
|
||||
*ret_basename = TAKE_PTR(name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -303,7 +324,12 @@ static int image_make(
|
||||
return 0;
|
||||
|
||||
if (!pretty) {
|
||||
r = extract_pretty(filename, image_class_suffix_to_string(c), NULL, &pretty_buffer);
|
||||
r = extract_image_basename(
|
||||
filename,
|
||||
image_class_suffix_to_string(c),
|
||||
/* format_suffix= */ NULL,
|
||||
&pretty_buffer,
|
||||
/* ret_suffix= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -390,7 +416,12 @@ static int image_make(
|
||||
(void) fd_getcrtime_at(dfd, filename, AT_SYMLINK_FOLLOW, &crtime);
|
||||
|
||||
if (!pretty) {
|
||||
r = extract_pretty(filename, image_class_suffix_to_string(c), ".raw", &pretty_buffer);
|
||||
r = extract_image_basename(
|
||||
filename,
|
||||
image_class_suffix_to_string(c),
|
||||
STRV_MAKE(".raw"),
|
||||
&pretty_buffer,
|
||||
/* ret_suffix= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -424,7 +455,12 @@ static int image_make(
|
||||
return 0;
|
||||
|
||||
if (!pretty) {
|
||||
r = extract_pretty(filename, NULL, NULL, &pretty_buffer);
|
||||
r = extract_image_basename(
|
||||
filename,
|
||||
/* class_suffix= */ NULL,
|
||||
/* format_suffix= */ NULL,
|
||||
&pretty_buffer,
|
||||
/* ret_suffix= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -488,6 +524,37 @@ static const char *pick_image_search_path(ImageClass class) {
|
||||
return in_initrd() && image_search_path_initrd[class] ? image_search_path_initrd[class] : image_search_path[class];
|
||||
}
|
||||
|
||||
static char **make_possible_filenames(ImageClass class, const char *image_name) {
|
||||
_cleanup_strv_free_ char **l = NULL;
|
||||
|
||||
assert(image_name);
|
||||
|
||||
FOREACH_STRING(v_suffix, "", ".v")
|
||||
FOREACH_STRING(format_suffix, "", ".raw") {
|
||||
_cleanup_free_ char *j = NULL;
|
||||
const char *class_suffix;
|
||||
|
||||
class_suffix = image_class_suffix_to_string(class);
|
||||
if (class_suffix) {
|
||||
j = strjoin(image_name, class_suffix, format_suffix, v_suffix);
|
||||
if (!j)
|
||||
return NULL;
|
||||
|
||||
if (strv_consume(&l, TAKE_PTR(j)) < 0)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
j = strjoin(image_name, format_suffix, v_suffix);
|
||||
if (!j)
|
||||
return NULL;
|
||||
|
||||
if (strv_consume(&l, TAKE_PTR(j)) < 0)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return TAKE_PTR(l);
|
||||
}
|
||||
|
||||
int image_find(ImageClass class,
|
||||
const char *name,
|
||||
const char *root,
|
||||
@@ -503,6 +570,10 @@ int image_find(ImageClass class,
|
||||
if (!image_name_is_valid(name))
|
||||
return -ENOENT;
|
||||
|
||||
_cleanup_strv_free_ char **names = make_possible_filenames(class, name);
|
||||
if (!names)
|
||||
return -ENOMEM;
|
||||
|
||||
NULSTR_FOREACH(path, pick_image_search_path(class)) {
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
_cleanup_closedir_ DIR *d = NULL;
|
||||
@@ -519,43 +590,97 @@ int image_find(ImageClass class,
|
||||
* to symlink block devices into the search path. (For now, we disable that when operating
|
||||
* relative to some root directory.) */
|
||||
flags = root ? AT_SYMLINK_NOFOLLOW : 0;
|
||||
if (fstatat(dirfd(d), name, &st, flags) < 0) {
|
||||
_cleanup_free_ char *raw = NULL;
|
||||
|
||||
if (errno != ENOENT)
|
||||
return -errno;
|
||||
STRV_FOREACH(n, names) {
|
||||
_cleanup_free_ char *fname_buf = NULL;
|
||||
const char *fname = *n;
|
||||
|
||||
raw = strjoin(name, ".raw");
|
||||
if (!raw)
|
||||
return -ENOMEM;
|
||||
if (fstatat(dirfd(d), fname, &st, flags) < 0) {
|
||||
if (errno != ENOENT)
|
||||
return -errno;
|
||||
|
||||
if (fstatat(dirfd(d), raw, &st, flags) < 0) {
|
||||
if (errno == ENOENT)
|
||||
continue;
|
||||
|
||||
return -errno;
|
||||
continue; /* Vanished while we were looking at it */
|
||||
}
|
||||
|
||||
if (!S_ISREG(st.st_mode))
|
||||
if (endswith(fname, ".raw")) {
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname);
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (endswith(fname, ".v")) {
|
||||
|
||||
if (!S_ISDIR(st.st_mode)) {
|
||||
log_debug("Ignoring non-directory file '%s' with .v suffix.", fname);
|
||||
continue;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *suffix = NULL;
|
||||
suffix = strdup(ASSERT_PTR(startswith(fname, name)));
|
||||
if (!suffix)
|
||||
return -ENOMEM;
|
||||
|
||||
*ASSERT_PTR(endswith(suffix, ".v")) = 0;
|
||||
|
||||
_cleanup_free_ char *vp = path_join(resolved, fname);
|
||||
if (!vp)
|
||||
return -ENOMEM;
|
||||
|
||||
PickFilter filter = {
|
||||
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
|
||||
.basename = name,
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = suffix,
|
||||
};
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(root,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
vp,
|
||||
&filter,
|
||||
PICK_ARCHITECTURE|PICK_TRIES,
|
||||
&result);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
|
||||
continue;
|
||||
}
|
||||
if (!result.path) {
|
||||
log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Refresh the stat data for the discovered target */
|
||||
st = result.st;
|
||||
|
||||
_cleanup_free_ char *bn = NULL;
|
||||
r = path_extract_filename(result.path, &bn);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
fname_buf = path_join(fname, bn);
|
||||
if (!fname_buf)
|
||||
return log_oom();
|
||||
|
||||
fname = fname_buf;
|
||||
|
||||
} else if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) {
|
||||
log_debug("Ignoring non-directory and non-block device file '%s' without suffix.", fname);
|
||||
continue;
|
||||
}
|
||||
|
||||
r = image_make(class, name, dirfd(d), resolved, raw, &st, ret);
|
||||
|
||||
} else {
|
||||
if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode))
|
||||
r = image_make(class, name, dirfd(d), resolved, fname, &st, ret);
|
||||
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
|
||||
continue;
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = image_make(class, name, dirfd(d), resolved, name, &st, ret);
|
||||
if (ret)
|
||||
(*ret)->discoverable = true;
|
||||
|
||||
return 1;
|
||||
}
|
||||
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
|
||||
continue;
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (ret)
|
||||
(*ret)->discoverable = true;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (class == IMAGE_MACHINE && streq(name, ".host")) {
|
||||
@@ -566,7 +691,7 @@ int image_find(ImageClass class,
|
||||
if (ret)
|
||||
(*ret)->discoverable = true;
|
||||
|
||||
return r;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -ENOENT;
|
||||
@@ -613,43 +738,133 @@ int image_discover(
|
||||
return r;
|
||||
|
||||
FOREACH_DIRENT_ALL(de, d, return -errno) {
|
||||
_cleanup_free_ char *pretty = NULL, *fname_buf = NULL;
|
||||
_cleanup_(image_unrefp) Image *image = NULL;
|
||||
_cleanup_free_ char *pretty = NULL;
|
||||
const char *fname = de->d_name;
|
||||
struct stat st;
|
||||
int flags;
|
||||
|
||||
if (dot_or_dot_dot(de->d_name))
|
||||
if (dot_or_dot_dot(fname))
|
||||
continue;
|
||||
|
||||
/* As mentioned above, we follow symlinks on this fstatat(), because we want to
|
||||
* permit people to symlink block devices into the search path. */
|
||||
flags = root ? AT_SYMLINK_NOFOLLOW : 0;
|
||||
if (fstatat(dirfd(d), de->d_name, &st, flags) < 0) {
|
||||
if (fstatat(dirfd(d), fname, &st, flags) < 0) {
|
||||
if (errno == ENOENT)
|
||||
continue;
|
||||
|
||||
return -errno;
|
||||
}
|
||||
|
||||
if (S_ISREG(st.st_mode))
|
||||
r = extract_pretty(de->d_name, image_class_suffix_to_string(class), ".raw", &pretty);
|
||||
else if (S_ISDIR(st.st_mode))
|
||||
r = extract_pretty(de->d_name, image_class_suffix_to_string(class), NULL, &pretty);
|
||||
else if (S_ISBLK(st.st_mode))
|
||||
r = extract_pretty(de->d_name, NULL, NULL, &pretty);
|
||||
else {
|
||||
log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", de->d_name);
|
||||
continue;
|
||||
}
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", de->d_name);
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
r = extract_image_basename(
|
||||
fname,
|
||||
image_class_suffix_to_string(class),
|
||||
STRV_MAKE(".raw"),
|
||||
&pretty,
|
||||
/* suffix= */ NULL);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
|
||||
continue;
|
||||
}
|
||||
} else if (S_ISDIR(st.st_mode)) {
|
||||
const char *v;
|
||||
|
||||
v = endswith(fname, ".v");
|
||||
if (v) {
|
||||
_cleanup_free_ char *suffix = NULL, *nov = NULL;
|
||||
|
||||
nov = strndup(fname, v - fname); /* Chop off the .v */
|
||||
if (!nov)
|
||||
return -ENOMEM;
|
||||
|
||||
r = extract_image_basename(
|
||||
nov,
|
||||
image_class_suffix_to_string(class),
|
||||
STRV_MAKE(".raw", ""),
|
||||
&pretty,
|
||||
&suffix);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like a versioned image.", fname);
|
||||
continue;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *vp = path_join(resolved, fname);
|
||||
if (!vp)
|
||||
return -ENOMEM;
|
||||
|
||||
PickFilter filter = {
|
||||
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
|
||||
.basename = pretty,
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = suffix,
|
||||
};
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(root,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
vp,
|
||||
&filter,
|
||||
PICK_ARCHITECTURE|PICK_TRIES,
|
||||
&result);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
|
||||
continue;
|
||||
}
|
||||
if (!result.path) {
|
||||
log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Refresh the stat data for the discovered target */
|
||||
st = result.st;
|
||||
|
||||
_cleanup_free_ char *bn = NULL;
|
||||
r = path_extract_filename(result.path, &bn);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
fname_buf = path_join(fname, bn);
|
||||
if (!fname_buf)
|
||||
return log_oom();
|
||||
|
||||
fname = fname_buf;
|
||||
} else {
|
||||
r = extract_image_basename(
|
||||
fname,
|
||||
image_class_suffix_to_string(class),
|
||||
/* format_suffix= */ NULL,
|
||||
&pretty,
|
||||
/* ret_suffix= */ NULL);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (S_ISBLK(st.st_mode)) {
|
||||
r = extract_image_basename(
|
||||
fname,
|
||||
/* class_suffix= */ NULL,
|
||||
/* format_suffix= */ NULL,
|
||||
&pretty,
|
||||
/* ret_v_suffix= */ NULL);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", fname);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hashmap_contains(h, pretty))
|
||||
continue;
|
||||
|
||||
r = image_make(class, pretty, dirfd(d), resolved, de->d_name, &st, &image);
|
||||
r = image_make(class, pretty, dirfd(d), resolved, fname, &st, &image);
|
||||
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
|
||||
continue;
|
||||
if (r < 0)
|
||||
|
||||
@@ -189,6 +189,7 @@ shared_sources = files(
|
||||
'verbs.c',
|
||||
'vlan-util.c',
|
||||
'volatile-util.c',
|
||||
'vpick.c',
|
||||
'wall.c',
|
||||
'watchdog.c',
|
||||
'web-util.c',
|
||||
|
||||
@@ -0,0 +1,694 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "architecture.h"
|
||||
#include "chase.h"
|
||||
#include "fd-util.h"
|
||||
#include "fs-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "path-util.h"
|
||||
#include "recurse-dir.h"
|
||||
#include "vpick.h"
|
||||
|
||||
void pick_result_done(PickResult *p) {
|
||||
assert(p);
|
||||
|
||||
free(p->path);
|
||||
safe_close(p->fd);
|
||||
free(p->version);
|
||||
|
||||
*p = PICK_RESULT_NULL;
|
||||
}
|
||||
|
||||
static int format_fname(
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
char **ret) {
|
||||
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
int r;
|
||||
|
||||
assert(filter);
|
||||
assert(ret);
|
||||
|
||||
if (FLAGS_SET(flags, PICK_TRIES) || !filter->version) /* Underspecified? */
|
||||
return -ENOEXEC;
|
||||
|
||||
/* The format for names we match goes like this:
|
||||
*
|
||||
* <basename><suffix>
|
||||
* or:
|
||||
* <basename>_<version><suffix>
|
||||
* or:
|
||||
* <basename>_<version>_<architecture><suffix>
|
||||
* or:
|
||||
* <basename>_<architecture><suffix>
|
||||
*
|
||||
* (Note that basename can be empty, in which case the leading "_" is suppressed)
|
||||
*
|
||||
* Examples: foo.raw, foo_1.3-7.raw, foo_1.3-7_x86-64.raw, foo_x86-64.raw
|
||||
*
|
||||
* Why use "_" as separator here? Primarily because it is not used by Semver 2.0. In RPM it is used
|
||||
* for "unsortable" versions, i.e. doesn't show up in "sortable" versions, which we matter for this
|
||||
* usecase here. In Debian the underscore is not allowed (and it uses it itself for separating
|
||||
* fields).
|
||||
*
|
||||
* This is very close to Debian's way to name packages, but allows arbitrary suffixes, and makes the
|
||||
* architecture field redundant.
|
||||
*
|
||||
* Compare with RPM's "NEVRA" concept. Here we have "BVAS" (basename, version, architecture, suffix).
|
||||
*/
|
||||
|
||||
if (filter->basename) {
|
||||
fn = strdup(filter->basename);
|
||||
if (!fn)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
if (filter->version) {
|
||||
if (isempty(fn)) {
|
||||
r = free_and_strdup(&fn, filter->version);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else if (!strextend(&fn, "_", filter->version))
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, PICK_ARCHITECTURE) && filter->architecture >= 0) {
|
||||
const char *as = ASSERT_PTR(architecture_to_string(filter->architecture));
|
||||
if (isempty(fn)) {
|
||||
r = free_and_strdup(&fn, as);
|
||||
if (r < 0)
|
||||
return r;
|
||||
} else if (!strextend(&fn, "_", as))
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
if (filter->suffix && !strextend(&fn, filter->suffix))
|
||||
return -ENOMEM;
|
||||
|
||||
if (!filename_is_valid(fn))
|
||||
return -EINVAL;
|
||||
|
||||
*ret = TAKE_PTR(fn);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int errno_from_mode(uint32_t type_mask, mode_t found) {
|
||||
/* Returns the most appropriate error code if we are lookging for an inode of type of those in the
|
||||
* 'type_mask' but found 'found' instead.
|
||||
*
|
||||
* type_mask is a mask of 1U << DT_REG, 1U << DT_DIR, … flags, while found is a S_IFREG, S_IFDIR, …
|
||||
* mode value. */
|
||||
|
||||
if (type_mask == 0) /* type doesn't matter */
|
||||
return 0;
|
||||
|
||||
if (FLAGS_SET(type_mask, UINT32_C(1) << IFTODT(found)))
|
||||
return 0;
|
||||
|
||||
if (type_mask == (UINT32_C(1) << DT_BLK))
|
||||
return -ENOTBLK;
|
||||
if (type_mask == (UINT32_C(1) << DT_DIR))
|
||||
return -ENOTDIR;
|
||||
if (type_mask == (UINT32_C(1) << DT_SOCK))
|
||||
return -ENOTSOCK;
|
||||
|
||||
if (S_ISLNK(found))
|
||||
return -ELOOP;
|
||||
if (S_ISDIR(found))
|
||||
return -EISDIR;
|
||||
|
||||
return -EBADF;
|
||||
}
|
||||
|
||||
static int pin_choice(
|
||||
const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *inode_path,
|
||||
int _inode_fd, /* we always take ownership of the fd, even on failure */
|
||||
unsigned tries_left,
|
||||
unsigned tries_done,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret) {
|
||||
|
||||
_cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
|
||||
_cleanup_free_ char *resolved_path = NULL;
|
||||
int r;
|
||||
|
||||
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
|
||||
assert(inode_path);
|
||||
assert(filter);
|
||||
|
||||
toplevel_path = strempty(toplevel_path);
|
||||
|
||||
if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
|
||||
r = chaseat(toplevel_fd,
|
||||
inode_path,
|
||||
CHASE_AT_RESOLVE_IN_ROOT,
|
||||
FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : 0,
|
||||
inode_fd < 0 ? &inode_fd : NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (resolved_path)
|
||||
inode_path = resolved_path;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (fstat(inode_fd, &st) < 0)
|
||||
return log_debug_errno(errno, "Failed to stat discovered inode '%s/%s': %m", toplevel_path, inode_path);
|
||||
|
||||
if (filter->type_mask != 0 &&
|
||||
!FLAGS_SET(filter->type_mask, UINT32_C(1) << IFTODT(st.st_mode)))
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(errno_from_mode(filter->type_mask, st.st_mode)),
|
||||
"Inode '%s/%s' has wrong type, found '%s'.",
|
||||
toplevel_path, inode_path,
|
||||
inode_type_to_string(st.st_mode));
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = {
|
||||
.fd = TAKE_FD(inode_fd),
|
||||
.st = st,
|
||||
.architecture = filter->architecture,
|
||||
.tries_left = tries_left,
|
||||
.tries_done = tries_done,
|
||||
};
|
||||
|
||||
result.path = strdup(inode_path);
|
||||
if (!result.path)
|
||||
return log_oom_debug();
|
||||
|
||||
if (filter->version) {
|
||||
result.version = strdup(filter->version);
|
||||
if (!result.version)
|
||||
return log_oom_debug();
|
||||
}
|
||||
|
||||
*ret = TAKE_PICK_RESULT(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_tries(const char *s, unsigned *ret_tries_left, unsigned *ret_tries_done) {
|
||||
unsigned left, done;
|
||||
size_t n;
|
||||
|
||||
assert(s);
|
||||
assert(ret_tries_left);
|
||||
assert(ret_tries_done);
|
||||
|
||||
if (s[0] != '+')
|
||||
goto nomatch;
|
||||
|
||||
s++;
|
||||
|
||||
n = strspn(s, DIGITS);
|
||||
if (n == 0)
|
||||
goto nomatch;
|
||||
|
||||
if (s[n] == 0) {
|
||||
if (safe_atou(s, &left) < 0)
|
||||
goto nomatch;
|
||||
|
||||
done = 0;
|
||||
} else if (s[n] == '-') {
|
||||
_cleanup_free_ char *c = NULL;
|
||||
|
||||
c = strndup(s, n);
|
||||
if (!c)
|
||||
return -ENOMEM;
|
||||
|
||||
if (safe_atou(c, &left) < 0)
|
||||
goto nomatch;
|
||||
|
||||
s += n + 1;
|
||||
|
||||
if (!in_charset(s, DIGITS))
|
||||
goto nomatch;
|
||||
|
||||
if (safe_atou(s, &done) < 0)
|
||||
goto nomatch;
|
||||
} else
|
||||
goto nomatch;
|
||||
|
||||
*ret_tries_left = left;
|
||||
*ret_tries_done = done;
|
||||
return 1;
|
||||
|
||||
nomatch:
|
||||
*ret_tries_left = *ret_tries_done = UINT_MAX;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int make_choice(
|
||||
const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *inode_path,
|
||||
int _inode_fd, /* we always take ownership of the fd, even on failure */
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret) {
|
||||
|
||||
static const Architecture local_architectures[] = {
|
||||
/* In order of preference */
|
||||
native_architecture(),
|
||||
#ifdef ARCHITECTURE_SECONDARY
|
||||
ARCHITECTURE_SECONDARY,
|
||||
#endif
|
||||
_ARCHITECTURE_INVALID, /* accept any arch, as last resort */
|
||||
};
|
||||
|
||||
_cleanup_free_ DirectoryEntries *de = NULL;
|
||||
_cleanup_free_ char *best_version = NULL, *best_filename = NULL, *p = NULL, *j = NULL;
|
||||
_cleanup_close_ int dir_fd = -EBADF, object_fd = -EBADF, inode_fd = TAKE_FD(_inode_fd);
|
||||
const Architecture *architectures;
|
||||
unsigned best_tries_left = UINT_MAX, best_tries_done = UINT_MAX;
|
||||
size_t n_architectures, best_architecture_index = SIZE_MAX;
|
||||
int r;
|
||||
|
||||
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
|
||||
assert(inode_path);
|
||||
assert(filter);
|
||||
|
||||
toplevel_path = strempty(toplevel_path);
|
||||
|
||||
if (inode_fd < 0) {
|
||||
r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Maybe the filter is fully specified? Then we can generate the file name directly */
|
||||
r = format_fname(filter, flags, &j);
|
||||
if (r >= 0) {
|
||||
_cleanup_free_ char *object_path = NULL;
|
||||
|
||||
/* Yay! This worked! */
|
||||
p = path_join(inode_path, j);
|
||||
if (!p)
|
||||
return log_oom_debug();
|
||||
|
||||
r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd);
|
||||
if (r < 0) {
|
||||
if (r != -ENOENT)
|
||||
return log_debug_errno(r, "Failed to open '%s/%s': %m", toplevel_path, p);
|
||||
|
||||
*ret = PICK_RESULT_NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pin_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
|
||||
TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
|
||||
/* tries_left= */ UINT_MAX,
|
||||
/* tries_done= */ UINT_MAX,
|
||||
filter,
|
||||
flags & ~PICK_RESOLVE,
|
||||
ret);
|
||||
|
||||
} else if (r != -ENOEXEC)
|
||||
return log_debug_errno(r, "Failed to format file name: %m");
|
||||
|
||||
/* Underspecified, so we do our enumeration dance */
|
||||
|
||||
/* Convert O_PATH to a regular directory fd */
|
||||
dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
|
||||
if (dir_fd < 0)
|
||||
return log_debug_errno(dir_fd, "Failed to reopen '%s/%s' as directory: %m", toplevel_path, inode_path);
|
||||
|
||||
r = readdir_all(dir_fd, 0, &de);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read directory '%s/%s': %m", toplevel_path, inode_path);
|
||||
|
||||
if (filter->architecture < 0) {
|
||||
architectures = local_architectures;
|
||||
n_architectures = ELEMENTSOF(local_architectures);
|
||||
} else {
|
||||
architectures = &filter->architecture;
|
||||
n_architectures = 1;
|
||||
}
|
||||
|
||||
FOREACH_ARRAY(entry, de->entries, de->n_entries) {
|
||||
unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX;
|
||||
_cleanup_free_ char *chopped = NULL;
|
||||
size_t found_architecture_index = SIZE_MAX;
|
||||
const char *e;
|
||||
|
||||
if (!isempty(filter->basename)) {
|
||||
e = startswith((*entry)->d_name, filter->basename);
|
||||
if (!e)
|
||||
continue;
|
||||
|
||||
if (e[0] != '_')
|
||||
continue;
|
||||
|
||||
e++;
|
||||
} else
|
||||
e = (*entry)->d_name;
|
||||
|
||||
if (!isempty(filter->suffix)) {
|
||||
const char *sfx;
|
||||
|
||||
sfx = endswith(e, filter->suffix);
|
||||
if (!sfx)
|
||||
continue;
|
||||
|
||||
chopped = strndup(e, sfx - e);
|
||||
if (!chopped)
|
||||
return log_oom_debug();
|
||||
|
||||
e = chopped;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, PICK_TRIES)) {
|
||||
char *plus = strrchr(e, '+');
|
||||
if (plus) {
|
||||
r = parse_tries(plus, &found_tries_left, &found_tries_done);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) /* Found and parsed, now chop off */
|
||||
*plus = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
|
||||
char *underscore = strrchr(e, '_');
|
||||
Architecture a;
|
||||
|
||||
a = underscore ? architecture_from_string(underscore + 1) : _ARCHITECTURE_INVALID;
|
||||
|
||||
for (size_t i = 0; i < n_architectures; i++)
|
||||
if (architectures[i] == a) {
|
||||
found_architecture_index = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (found_architecture_index == SIZE_MAX) { /* No matching arch found */
|
||||
log_debug("Found entry with architecture '%s' which is not what we are looking for, ignoring entry.", a < 0 ? "any" : architecture_to_string(a));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Chop off architecture from string */
|
||||
if (underscore)
|
||||
*underscore = 0;
|
||||
}
|
||||
|
||||
if (!version_is_valid(e)) {
|
||||
log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter->version && !streq(filter->version, e)) {
|
||||
log_debug("Found entry with version string '%s', but was looking for '%s', ignoring entry.", e, filter->version);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (best_filename) { /* Already found one matching entry? Then figure out the better one */
|
||||
int d = 0;
|
||||
|
||||
/* First, prefer entries with tries left over those without */
|
||||
if (FLAGS_SET(flags, PICK_TRIES))
|
||||
d = CMP(found_tries_left != 0, best_tries_left != 0);
|
||||
|
||||
/* Second, prefer newer versions */
|
||||
if (d == 0)
|
||||
d = strverscmp_improved(e, best_version);
|
||||
|
||||
/* Third, prefer native architectures over secondary architectures */
|
||||
if (d == 0 &&
|
||||
FLAGS_SET(flags, PICK_ARCHITECTURE) &&
|
||||
found_architecture_index != SIZE_MAX && best_architecture_index != SIZE_MAX)
|
||||
d = -CMP(found_architecture_index, best_architecture_index);
|
||||
|
||||
/* Fourth, prefer entries with more tries left */
|
||||
if (FLAGS_SET(flags, PICK_TRIES)) {
|
||||
if (d == 0)
|
||||
d = CMP(found_tries_left, best_tries_left);
|
||||
|
||||
/* Fifth, prefer entries with fewer attempts done so far */
|
||||
if (d == 0)
|
||||
d = -CMP(found_tries_done, best_tries_done);
|
||||
}
|
||||
|
||||
/* Last, just compare the filenames as strings */
|
||||
if (d == 0)
|
||||
d = strcmp((*entry)->d_name, best_filename);
|
||||
|
||||
if (d < 0) {
|
||||
log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", (*entry)->d_name, best_filename);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
r = free_and_strdup_warn(&best_version, e);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = free_and_strdup_warn(&best_filename, (*entry)->d_name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
best_architecture_index = found_architecture_index;
|
||||
best_tries_left = found_tries_left;
|
||||
best_tries_done = found_tries_done;
|
||||
}
|
||||
|
||||
if (!best_filename) { /* Everything was good, but we didn't find any suitable entry */
|
||||
*ret = PICK_RESULT_NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
p = path_join(inode_path, best_filename);
|
||||
if (!p)
|
||||
return log_oom_debug();
|
||||
|
||||
object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH);
|
||||
if (object_fd < 0)
|
||||
return log_debug_errno(errno, "Failed to open '%s/%s': %m", toplevel_path, p);
|
||||
|
||||
return pin_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
p,
|
||||
TAKE_FD(object_fd),
|
||||
best_tries_left,
|
||||
best_tries_done,
|
||||
&(const PickFilter) {
|
||||
.type_mask = filter->type_mask,
|
||||
.basename = filter->basename,
|
||||
.version = empty_to_null(best_version),
|
||||
.architecture = best_architecture_index != SIZE_MAX ? architectures[best_architecture_index] : _ARCHITECTURE_INVALID,
|
||||
.suffix = filter->suffix,
|
||||
},
|
||||
flags,
|
||||
ret);
|
||||
}
|
||||
|
||||
int path_pick(const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *path,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret) {
|
||||
|
||||
_cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL;
|
||||
const char *filter_suffix, *enumeration_path;
|
||||
uint32_t filter_type_mask;
|
||||
int r;
|
||||
|
||||
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
|
||||
assert(path);
|
||||
|
||||
toplevel_path = strempty(toplevel_path);
|
||||
|
||||
/* Given a path, resolve .v/ subdir logic (if used!), and returns the choice made. This supports
|
||||
* three ways to be called:
|
||||
*
|
||||
* • with a path referring a directory of any name, and filter→basename *explicitly* specified, in
|
||||
* which case we'll use a pattern "<filter→basename>_*<filter→suffix>" on the directory's files.
|
||||
*
|
||||
* • with no filter→basename explicitly specified and a path referring to a directory named in format
|
||||
* "<somestring><filter→suffix>.v" . In this case the filter basename to search for inside the dir
|
||||
* is derived from the directory name. Example: "/foo/bar/baz.suffix.v" → we'll search for
|
||||
* "/foo/bar/baz.suffix.v/baz_*.suffix".
|
||||
*
|
||||
* • with a path whose penultimate component ends in ".v/". In this case the final component of the
|
||||
* path refers to the pattern. Example: "/foo/bar/baz.v/waldo__.suffix" → we'll search for
|
||||
* "/foo/bar/baz.v/waldo_*.suffix".
|
||||
*/
|
||||
|
||||
/* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
|
||||
if (filter->basename)
|
||||
return make_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
filter,
|
||||
flags,
|
||||
ret);
|
||||
|
||||
r = path_extract_filename(path, &fname);
|
||||
if (r < 0) {
|
||||
if (r != -EADDRNOTAVAIL) /* root dir or "." */
|
||||
return r;
|
||||
|
||||
/* If there's no path element we can derive a pattern off, the don't */
|
||||
goto bypass;
|
||||
}
|
||||
|
||||
/* Remember if the path ends in a dash suffix */
|
||||
bool slash_suffix = r == O_DIRECTORY;
|
||||
|
||||
const char *e = endswith(fname, ".v");
|
||||
if (e) {
|
||||
/* So a path in the form /foo/bar/baz.v is specified. In this case our search basename is
|
||||
* "baz", possibly with a suffix chopped off if there's one specified. */
|
||||
filter_bname = strndup(fname, e - fname);
|
||||
if (!filter_bname)
|
||||
return -ENOMEM;
|
||||
|
||||
if (filter->suffix) {
|
||||
/* Chop off suffix, if specified */
|
||||
char *f = endswith(filter_bname, filter->suffix);
|
||||
if (f)
|
||||
*f = 0;
|
||||
}
|
||||
|
||||
filter_suffix = filter->suffix;
|
||||
filter_type_mask = filter->type_mask;
|
||||
|
||||
enumeration_path = path;
|
||||
} else {
|
||||
/* The path does not end in '.v', hence see if the last element is a pattern. */
|
||||
|
||||
char *wildcard = strrstr(fname, "___");
|
||||
if (!wildcard)
|
||||
goto bypass; /* Not a pattern, then bypass */
|
||||
|
||||
/* We found the '___' wildcard, hence evertyhing after it is our filter suffix, and
|
||||
* evertyhing before is our filter basename */
|
||||
*wildcard = 0;
|
||||
filter_suffix = empty_to_null(wildcard + 3);
|
||||
|
||||
filter_bname = TAKE_PTR(fname);
|
||||
|
||||
r = path_extract_directory(path, &dir);
|
||||
if (r < 0) {
|
||||
if (!IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */
|
||||
return r;
|
||||
|
||||
goto bypass; /* No dir extractable, can't check if parent ends in ".v" */
|
||||
}
|
||||
|
||||
r = path_extract_filename(dir, &parent);
|
||||
if (r < 0) {
|
||||
if (r != -EADDRNOTAVAIL) /* root dir or "." */
|
||||
return r;
|
||||
|
||||
goto bypass; /* Cannot extract fname from parent dir, can't check if it ends in ".v" */
|
||||
}
|
||||
|
||||
e = endswith(parent, ".v");
|
||||
if (!e)
|
||||
goto bypass; /* Doesn't end in ".v", shortcut */
|
||||
|
||||
filter_type_mask = filter->type_mask;
|
||||
if (slash_suffix) {
|
||||
/* If the pattern is suffixed by a / then we are looking for directories apparently. */
|
||||
if (filter_type_mask != 0 && !FLAGS_SET(filter_type_mask, UINT32_C(1) << DT_DIR))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(errno_from_mode(filter_type_mask, S_IFDIR)),
|
||||
"Specified pattern ends in '/', but not looking for directories, refusing.");
|
||||
filter_type_mask = UINT32_C(1) << DT_DIR;
|
||||
}
|
||||
|
||||
enumeration_path = dir;
|
||||
}
|
||||
|
||||
return make_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
enumeration_path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
&(const PickFilter) {
|
||||
.type_mask = filter_type_mask,
|
||||
.basename = filter_bname,
|
||||
.version = filter->version,
|
||||
.architecture = filter->architecture,
|
||||
.suffix = filter_suffix,
|
||||
},
|
||||
flags,
|
||||
ret);
|
||||
|
||||
bypass:
|
||||
/* Don't make any choice, but just use the passed path literally */
|
||||
return pin_choice(
|
||||
toplevel_path,
|
||||
toplevel_fd,
|
||||
path,
|
||||
/* inode_fd= */ -EBADF,
|
||||
/* tries_left= */ UINT_MAX,
|
||||
/* tries_done= */ UINT_MAX,
|
||||
filter,
|
||||
flags,
|
||||
ret);
|
||||
}
|
||||
|
||||
int path_pick_update_warn(
|
||||
char **path,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret_result) {
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(*path);
|
||||
|
||||
/* This updates the first argument if needed! */
|
||||
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
*path,
|
||||
filter,
|
||||
flags,
|
||||
&result);
|
||||
if (r == -ENOENT) {
|
||||
log_debug("Path '%s' doesn't exist, leaving as is.", *path);
|
||||
*ret_result = PICK_RESULT_NULL;
|
||||
return 0;
|
||||
}
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pick version on path '%s': %m", *path);
|
||||
if (r == 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entries in versioned directory '%s' found.", *path);
|
||||
|
||||
log_debug("Resolved versioned directory pattern '%s' to file '%s' as version '%s'.", result.path, *path, strna(result.version));
|
||||
|
||||
if (ret_result) {
|
||||
r = free_and_strdup_warn(path, result.path);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret_result = TAKE_PICK_RESULT(result);
|
||||
} else
|
||||
free_and_replace(*path, result.path);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const PickFilter pick_filter_image_raw = {
|
||||
.type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = ".raw",
|
||||
};
|
||||
|
||||
const PickFilter pick_filter_image_dir = {
|
||||
.type_mask = UINT32_C(1) << DT_DIR,
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "architecture.h"
|
||||
|
||||
typedef enum PickFlags {
|
||||
PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */
|
||||
PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */
|
||||
PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */
|
||||
} PickFlags;
|
||||
|
||||
typedef struct PickFilter {
|
||||
uint32_t type_mask; /* A mask of 1U << DT_REG, 1U << DT_DIR, … */
|
||||
const char *basename; /* Can be overriden by search pattern */
|
||||
const char *version;
|
||||
Architecture architecture;
|
||||
const char *suffix; /* Can be overriden by search pattern */
|
||||
} PickFilter;
|
||||
|
||||
typedef struct PickResult {
|
||||
char *path;
|
||||
int fd; /* O_PATH */
|
||||
struct stat st;
|
||||
char *version;
|
||||
Architecture architecture;
|
||||
unsigned tries_left;
|
||||
unsigned tries_done;
|
||||
} PickResult;
|
||||
|
||||
#define PICK_RESULT_NULL \
|
||||
(const PickResult) { \
|
||||
.fd = -EBADF, \
|
||||
.st.st_mode = MODE_INVALID, \
|
||||
.architecture = _ARCHITECTURE_INVALID, \
|
||||
.tries_left = UINT_MAX, \
|
||||
.tries_done = UINT_MAX, \
|
||||
}
|
||||
|
||||
#define TAKE_PICK_RESULT(ptr) TAKE_GENERIC(ptr, PickResult, PICK_RESULT_NULL)
|
||||
|
||||
void pick_result_done(PickResult *p);
|
||||
|
||||
int path_pick(const char *toplevel_path,
|
||||
int toplevel_fd,
|
||||
const char *path,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret);
|
||||
|
||||
int path_pick_update_warn(
|
||||
char **path,
|
||||
const PickFilter *filter,
|
||||
PickFlags flags,
|
||||
PickResult *ret);
|
||||
|
||||
extern const PickFilter pick_filter_image_raw;
|
||||
extern const PickFilter pick_filter_image_dir;
|
||||
@@ -178,6 +178,7 @@ simple_tests += files(
|
||||
'test-user-util.c',
|
||||
'test-utf8.c',
|
||||
'test-verbs.c',
|
||||
'test-vpick.c',
|
||||
'test-web-util.c',
|
||||
'test-xattr-util.c',
|
||||
'test-xml.c',
|
||||
|
||||
@@ -180,6 +180,21 @@ TEST(dir_is_empty) {
|
||||
assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) > 0);
|
||||
}
|
||||
|
||||
TEST(inode_type_from_string) {
|
||||
static const mode_t types[] = {
|
||||
S_IFREG,
|
||||
S_IFDIR,
|
||||
S_IFLNK,
|
||||
S_IFCHR,
|
||||
S_IFBLK,
|
||||
S_IFIFO,
|
||||
S_IFSOCK,
|
||||
};
|
||||
|
||||
FOREACH_ARRAY(m, types, ELEMENTSOF(types))
|
||||
assert_se(inode_type_from_string(inode_type_to_string(*m)) == *m);
|
||||
}
|
||||
|
||||
static int intro(void) {
|
||||
log_show_color(true);
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
@@ -1324,4 +1324,27 @@ TEST(strlevenshtein) {
|
||||
assert_se(strlevenshtein("sunday", "saturday") == 3);
|
||||
}
|
||||
|
||||
TEST(strrstr) {
|
||||
assert_se(!strrstr(NULL, NULL));
|
||||
assert_se(!strrstr("foo", NULL));
|
||||
assert_se(!strrstr(NULL, "foo"));
|
||||
|
||||
const char *p = "foo";
|
||||
assert_se(strrstr(p, "foo") == p);
|
||||
assert_se(strrstr(p, "fo") == p);
|
||||
assert_se(strrstr(p, "f") == p);
|
||||
assert_se(strrstr(p, "oo") == p + 1);
|
||||
assert_se(strrstr(p, "o") == p + 2);
|
||||
assert_se(strrstr(p, "") == p + strlen(p));
|
||||
assert_se(!strrstr(p, "bar"));
|
||||
|
||||
p = "xoxoxox";
|
||||
assert_se(strrstr(p, "") == p + strlen(p));
|
||||
assert_se(strrstr(p, "x") == p + 6);
|
||||
assert_se(strrstr(p, "ox") == p + 5);
|
||||
assert_se(strrstr(p, "xo") == p + 4);
|
||||
assert_se(strrstr(p, "xox") == p + 4);
|
||||
assert_se(!strrstr(p, "xx"));
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
|
||||
@@ -1006,4 +1006,12 @@ TEST(strv_find_first_field) {
|
||||
assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("i", "k", "l", "m", "d", "c", "a", "b"), haystack), "j"));
|
||||
}
|
||||
|
||||
TEST(strv_endswith) {
|
||||
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "ldo", "zzz")), "ldo"));
|
||||
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "zzz")), NULL));
|
||||
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("waldo")), "waldo"));
|
||||
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("w", "o", "ldo")), "o"));
|
||||
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("knurz", "", "waldo")), ""));
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_INFO);
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "fs-util.h"
|
||||
#include "mkdir.h"
|
||||
#include "path-util.h"
|
||||
#include "rm-rf.h"
|
||||
#include "tests.h"
|
||||
#include "tmpfile-util.h"
|
||||
#include "vpick.h"
|
||||
|
||||
TEST(path_pick) {
|
||||
_cleanup_(rm_rf_physical_and_freep) char *p = NULL;
|
||||
_cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF;
|
||||
|
||||
dfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &p);
|
||||
assert(dfd >= 0);
|
||||
|
||||
sub_dfd = open_mkdir_at(dfd, "foo.v", O_CLOEXEC, 0777);
|
||||
assert(sub_dfd >= 0);
|
||||
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_5.5.raw", "5.5", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_55.raw", "55", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_5.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_5_ia64.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_7.raw", "7", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_7_x86-64.raw", "7 64bit", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_55_x86-64.raw", "55 64bit", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_55_x86.raw", "55 32bit", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_99_x86.raw", "99 32bit", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
|
||||
/* Let's add an entry for sparc (which is a valid arch, but almost certainly not what we test
|
||||
* on). This entry should hence always be ignored */
|
||||
if (native_architecture() != ARCHITECTURE_SPARC)
|
||||
assert_se(write_string_file_at(sub_dfd, "foo_100_sparc.raw", "100 sparc", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
|
||||
assert_se(write_string_file_at(sub_dfd, "quux_1_s390.raw", "waldo1", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "quux_2_s390+4-6.raw", "waldo2", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
assert_se(write_string_file_at(sub_dfd, "quux_3_s390+0-10.raw", "waldo3", WRITE_STRING_FILE_CREATE) >= 0);
|
||||
|
||||
_cleanup_free_ char *pp = NULL;
|
||||
pp = path_join(p, "foo.v");
|
||||
assert_se(pp);
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
|
||||
PickFilter filter = {
|
||||
.architecture = _ARCHITECTURE_INVALID,
|
||||
.suffix = ".raw",
|
||||
};
|
||||
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "99"));
|
||||
assert_se(result.architecture == ARCHITECTURE_X86);
|
||||
assert_se(endswith(result.path, "/foo_99_x86.raw"));
|
||||
|
||||
pick_result_done(&result);
|
||||
}
|
||||
|
||||
filter.architecture = ARCHITECTURE_X86_64;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "55"));
|
||||
assert_se(result.architecture == ARCHITECTURE_X86_64);
|
||||
assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_IA64;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "5"));
|
||||
assert_se(result.architecture == ARCHITECTURE_IA64);
|
||||
assert_se(endswith(result.path, "/foo_5_ia64.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = _ARCHITECTURE_INVALID;
|
||||
filter.version = "5";
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "5"));
|
||||
if (native_architecture() != ARCHITECTURE_IA64) {
|
||||
assert_se(result.architecture == _ARCHITECTURE_INVALID);
|
||||
assert_se(endswith(result.path, "/foo_5.raw"));
|
||||
}
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_IA64;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "5"));
|
||||
assert_se(result.architecture == ARCHITECTURE_IA64);
|
||||
assert_se(endswith(result.path, "/foo_5_ia64.raw"));
|
||||
pick_result_done(&result);
|
||||
|
||||
filter.architecture = ARCHITECTURE_CRIS;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == 0);
|
||||
assert_se(result.st.st_mode == MODE_INVALID);
|
||||
assert_se(!result.version);
|
||||
assert_se(result.architecture < 0);
|
||||
assert_se(!result.path);
|
||||
|
||||
assert_se(unlinkat(sub_dfd, "foo_99_x86.raw", 0) >= 0);
|
||||
|
||||
filter.architecture = _ARCHITECTURE_INVALID;
|
||||
filter.version = NULL;
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "55"));
|
||||
|
||||
if (native_architecture() == ARCHITECTURE_X86_64) {
|
||||
assert_se(result.architecture == ARCHITECTURE_X86_64);
|
||||
assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
|
||||
} else {
|
||||
assert_se(result.architecture == ARCHITECTURE_X86);
|
||||
assert_se(endswith(result.path, "/foo_55_x86.raw"));
|
||||
}
|
||||
pick_result_done(&result);
|
||||
}
|
||||
|
||||
/* Test explicit patterns in last component of path not being .v */
|
||||
free(pp);
|
||||
pp = path_join(p, "foo.v/foo___.raw");
|
||||
assert_se(pp);
|
||||
|
||||
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "55"));
|
||||
assert_se(result.architecture == native_architecture());
|
||||
assert_se(endswith(result.path, ".raw"));
|
||||
assert_se(strrstr(result.path, "/foo_55_x86"));
|
||||
pick_result_done(&result);
|
||||
}
|
||||
|
||||
/* Specify an explicit path */
|
||||
free(pp);
|
||||
pp = path_join(p, "foo.v/foo_5.raw");
|
||||
assert_se(pp);
|
||||
|
||||
filter.type_mask = UINT32_C(1) << DT_DIR;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == -ENOTDIR);
|
||||
|
||||
filter.type_mask = UINT32_C(1) << DT_REG;
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(!result.version);
|
||||
assert_se(result.architecture == _ARCHITECTURE_INVALID);
|
||||
assert_se(path_equal(result.path, pp));
|
||||
pick_result_done(&result);
|
||||
|
||||
free(pp);
|
||||
pp = path_join(p, "foo.v");
|
||||
assert_se(pp);
|
||||
|
||||
filter.architecture = ARCHITECTURE_S390;
|
||||
filter.basename = "quux";
|
||||
|
||||
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
|
||||
assert_se(S_ISREG(result.st.st_mode));
|
||||
assert_se(streq_ptr(result.version, "2"));
|
||||
assert_se(result.tries_left == 4);
|
||||
assert_se(result.tries_done == 6);
|
||||
assert_se(endswith(result.path, "quux_2_s390+4-6.raw"));
|
||||
assert_se(result.architecture == ARCHITECTURE_S390);
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
@@ -0,0 +1,9 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
executables += [
|
||||
executable_template + {
|
||||
'name' : 'systemd-vpick',
|
||||
'public' : true,
|
||||
'sources' : files('vpick-tool.c'),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,350 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <getopt.h>
|
||||
|
||||
#include "architecture.h"
|
||||
#include "build.h"
|
||||
#include "format-table.h"
|
||||
#include "fs-util.h"
|
||||
#include "main-func.h"
|
||||
#include "path-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "pretty-print.h"
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "vpick.h"
|
||||
|
||||
static char *arg_filter_basename = NULL;
|
||||
static char *arg_filter_version = NULL;
|
||||
static Architecture arg_filter_architecture = _ARCHITECTURE_INVALID;
|
||||
static char *arg_filter_suffix = NULL;
|
||||
static uint32_t arg_filter_type_mask = 0;
|
||||
static enum {
|
||||
PRINT_PATH,
|
||||
PRINT_FILENAME,
|
||||
PRINT_VERSION,
|
||||
PRINT_TYPE,
|
||||
PRINT_ARCHITECTURE,
|
||||
PRINT_TRIES,
|
||||
PRINT_ALL,
|
||||
_PRINT_INVALID = -EINVAL,
|
||||
} arg_print = _PRINT_INVALID;
|
||||
static PickFlags arg_flags = PICK_ARCHITECTURE|PICK_TRIES;
|
||||
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_filter_basename, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_filter_version, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_filter_suffix, freep);
|
||||
|
||||
static int help(void) {
|
||||
_cleanup_free_ char *link = NULL;
|
||||
int r;
|
||||
|
||||
r = terminal_urlify_man("systemd-vpick", "1", &link);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
printf("%1$s [OPTIONS...] PATH...\n"
|
||||
"\n%5$sPick entry from versioned directory.%6$s\n\n"
|
||||
" -h --help Show this help\n"
|
||||
" --version Show package version\n"
|
||||
"\n%3$sLookup Keys:%4$s\n"
|
||||
" -B --basename=BASENAME\n"
|
||||
" Look for specified basename\n"
|
||||
" -V VERSION Look for specified version\n"
|
||||
" -A ARCH Look for specified architecture\n"
|
||||
" -S --suffix=SUFFIX Look for specified suffix\n"
|
||||
" -t --type=TYPE Look for specified inode type\n"
|
||||
"\n%3$sOutput:%4$s\n"
|
||||
" -p --print=filename Print selected filename rather than path\n"
|
||||
" -p --print=version Print selected version rather than path\n"
|
||||
" -p --print=type Print selected inode type rather than path\n"
|
||||
" -p --print=arch Print selected architecture rather than path\n"
|
||||
" -p --print=tries Print selected tries left/tries done rather than path\n"
|
||||
" -p --print=all Print all of the above\n"
|
||||
" --resolve=yes Canonicalize the result path\n"
|
||||
"\nSee the %2$s for details.\n",
|
||||
program_invocation_short_name,
|
||||
link,
|
||||
ansi_underline(), ansi_normal(),
|
||||
ansi_highlight(), ansi_normal());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
|
||||
enum {
|
||||
ARG_VERSION = 0x100,
|
||||
ARG_RESOLVE,
|
||||
};
|
||||
|
||||
static const struct option options[] = {
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{ "version", no_argument, NULL, ARG_VERSION },
|
||||
{ "basename", required_argument, NULL, 'B' },
|
||||
{ "suffix", required_argument, NULL, 'S' },
|
||||
{ "type", required_argument, NULL, 't' },
|
||||
{ "print", required_argument, NULL, 'p' },
|
||||
{ "resolve", required_argument, NULL, ARG_RESOLVE },
|
||||
{}
|
||||
};
|
||||
|
||||
int c, r;
|
||||
|
||||
assert(argc >= 0);
|
||||
assert(argv);
|
||||
|
||||
while ((c = getopt_long(argc, argv, "hB:V:A:S:t:p:", options, NULL)) >= 0) {
|
||||
|
||||
switch (c) {
|
||||
|
||||
case 'h':
|
||||
return help();
|
||||
|
||||
case ARG_VERSION:
|
||||
return version();
|
||||
|
||||
case 'B':
|
||||
if (!filename_part_is_valid(optarg))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid basename string: %s", optarg);
|
||||
|
||||
r = free_and_strdup_warn(&arg_filter_basename, optarg);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
break;
|
||||
|
||||
case 'V':
|
||||
if (!version_is_valid(optarg))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version string: %s", optarg);
|
||||
|
||||
r = free_and_strdup_warn(&arg_filter_version, optarg);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
break;
|
||||
|
||||
case 'A':
|
||||
if (streq(optarg, "native"))
|
||||
arg_filter_architecture = native_architecture();
|
||||
else if (streq(optarg, "secondary")) {
|
||||
#ifdef ARCHITECTURE_SECONDARY
|
||||
arg_filter_architecture = ARCHITECTURE_SECONDARY;
|
||||
#else
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Local architecture has no secondary architecture.");
|
||||
#endif
|
||||
} else if (streq(optarg, "uname"))
|
||||
arg_filter_architecture = uname_architecture();
|
||||
else if (streq(optarg, "auto"))
|
||||
arg_filter_architecture = _ARCHITECTURE_INVALID;
|
||||
else {
|
||||
arg_filter_architecture = architecture_from_string(optarg);
|
||||
if (arg_filter_architecture < 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown architecture: %s", optarg);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'S':
|
||||
if (!filename_part_is_valid(optarg))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid suffix string: %s", optarg);
|
||||
|
||||
r = free_and_strdup_warn(&arg_filter_suffix, optarg);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
break;
|
||||
|
||||
case 't':
|
||||
if (isempty(optarg))
|
||||
arg_filter_type_mask = 0;
|
||||
else {
|
||||
mode_t m;
|
||||
|
||||
m = inode_type_from_string(optarg);
|
||||
if (m == MODE_INVALID)
|
||||
return log_error_errno(m, "Unknown inode type: %s", optarg);
|
||||
|
||||
arg_filter_type_mask |= UINT32_C(1) << IFTODT(m);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
if (streq(optarg, "path"))
|
||||
arg_print = PRINT_PATH;
|
||||
else if (streq(optarg, "filename"))
|
||||
arg_print = PRINT_FILENAME;
|
||||
else if (streq(optarg, "version"))
|
||||
arg_print = PRINT_VERSION;
|
||||
else if (streq(optarg, "type"))
|
||||
arg_print = PRINT_TYPE;
|
||||
else if (STR_IN_SET(optarg, "arch", "architecture"))
|
||||
arg_print = PRINT_ARCHITECTURE;
|
||||
else if (streq(optarg, "tries"))
|
||||
arg_print = PRINT_TRIES;
|
||||
else if (streq(optarg, "all"))
|
||||
arg_print = PRINT_ALL;
|
||||
else
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown --print= argument: %s", optarg);
|
||||
|
||||
break;
|
||||
|
||||
case ARG_RESOLVE:
|
||||
r = parse_boolean(optarg);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse --resolve= value: %m");
|
||||
|
||||
SET_FLAG(arg_flags, PICK_RESOLVE, r);
|
||||
break;
|
||||
|
||||
case '?':
|
||||
return -EINVAL;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
if (arg_print < 0)
|
||||
arg_print = PRINT_PATH;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
log_show_color(true);
|
||||
log_parse_environment();
|
||||
log_open();
|
||||
|
||||
r = parse_argv(argc, argv);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
if (optind >= argc)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path to resolve must be specified.");
|
||||
|
||||
for (int i = optind; i < argc; i++) {
|
||||
_cleanup_free_ char *p = NULL;
|
||||
r = path_make_absolute_cwd(argv[i], &p);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to make path '%s' absolute: %m", argv[i]);
|
||||
|
||||
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
|
||||
r = path_pick(/* toplevel_path= */ NULL,
|
||||
/* toplevel_fd= */ AT_FDCWD,
|
||||
p,
|
||||
&(PickFilter) {
|
||||
.basename = arg_filter_basename,
|
||||
.version = arg_filter_version,
|
||||
.architecture = arg_filter_architecture,
|
||||
.suffix = arg_filter_suffix,
|
||||
.type_mask = arg_filter_type_mask,
|
||||
},
|
||||
arg_flags,
|
||||
&result);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pick version for '%s': %m", p);
|
||||
if (r == 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching version for '%s' found.", p);
|
||||
|
||||
switch (arg_print) {
|
||||
|
||||
case PRINT_PATH:
|
||||
fputs(result.path, stdout);
|
||||
if (result.st.st_mode != MODE_INVALID && S_ISDIR(result.st.st_mode) && !endswith(result.path, "/"))
|
||||
fputc('/', stdout);
|
||||
fputc('\n', stdout);
|
||||
break;
|
||||
|
||||
case PRINT_FILENAME: {
|
||||
_cleanup_free_ char *fname = NULL;
|
||||
|
||||
r = path_extract_filename(result.path, &fname);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", result.path);
|
||||
|
||||
puts(fname);
|
||||
break;
|
||||
}
|
||||
|
||||
case PRINT_VERSION:
|
||||
if (!result.version)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No version information discovered.");
|
||||
|
||||
puts(result.version);
|
||||
break;
|
||||
|
||||
case PRINT_TYPE:
|
||||
if (result.st.st_mode == MODE_INVALID)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No inode type information discovered.");
|
||||
|
||||
puts(inode_type_to_string(result.st.st_mode));
|
||||
break;
|
||||
|
||||
case PRINT_ARCHITECTURE:
|
||||
if (result.architecture < 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No architecture information discovered.");
|
||||
|
||||
puts(architecture_to_string(result.architecture));
|
||||
break;
|
||||
|
||||
case PRINT_TRIES:
|
||||
if (result.tries_left == UINT_MAX)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No tries left/tries done information discovered.");
|
||||
|
||||
printf("+%u-%u", result.tries_left, result.tries_done);
|
||||
break;
|
||||
|
||||
case PRINT_ALL: {
|
||||
_cleanup_(table_unrefp) Table *t = NULL;
|
||||
|
||||
t = table_new_vertical();
|
||||
if (!t)
|
||||
return log_oom();
|
||||
|
||||
table_set_ersatz_string(t, TABLE_ERSATZ_NA);
|
||||
|
||||
r = table_add_many(
|
||||
t,
|
||||
TABLE_FIELD, "Path",
|
||||
TABLE_PATH, result.path,
|
||||
TABLE_FIELD, "Version",
|
||||
TABLE_STRING, result.version,
|
||||
TABLE_FIELD, "Type",
|
||||
TABLE_STRING, result.st.st_mode == MODE_INVALID ? NULL : inode_type_to_string(result.st.st_mode),
|
||||
TABLE_FIELD, "Architecture",
|
||||
TABLE_STRING, result.architecture < 0 ? NULL : architecture_to_string(result.architecture));
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
|
||||
if (result.tries_left != UINT_MAX) {
|
||||
r = table_add_many(
|
||||
t,
|
||||
TABLE_FIELD, "Tries left",
|
||||
TABLE_UINT, result.tries_left,
|
||||
TABLE_FIELD, "Tries done",
|
||||
TABLE_UINT, result.tries_done);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
r = table_print(t, stdout);
|
||||
if (r < 0)
|
||||
return table_log_print_error(r);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEFINE_MAIN_FUNCTION(run);
|
||||
Reference in New Issue
Block a user