// SPDX-License-Identifier: Apache-2.0 // // schfs_fsck — inspect, verify and extract a SchFS image. // // "fsck" is a slight misnomer: copy-on-write means there is nothing to repair. // A crash leaves either the old tree or the new one, so a volume is either // consistent or it has no valid uberblock at all. What this does is *check* -- // and, just as usefully, read an image back out so its contents can be compared // against whatever produced it. // // schfs_fsck img --info uberblock ring and volume summary // schfs_fsck img --check full structural verification // schfs_fsck img --ls PATH list a directory // schfs_fsck img --stat PATH one object's metadata // schfs_fsck img --cat PATH a file's contents to stdout // schfs_fsck img --extract DIR the whole tree onto the host // schfs_fsck img --tree every path in the volume // // --extract is what makes the round-trip test possible, and the reason this // reads the image through the same code the kernel will use: if that code is // wrong, the extracted tree differs from the source tree and diff says so. #define _FILE_OFFSET_BITS 64 #define SCHFS_HOSTED 1 #include #include #include #include #include #include #include #include /* --extract on Windows. * * mingw has no POSIX mkdir(path, mode), no symlink() and no link(), because * Windows has no equivalent that can be created without either a privilege or * a reparse point. Everything else here -- --info, --check, --ls, --stat, * --cat, --tree -- is pure reading and works unchanged; it was only --extract * that would not compile, which took the whole tool with it and with it * `make schfs-check`. * * Directories map cleanly: _mkdir takes no mode, and Windows has no POSIX * permission bits to apply anyway. Symlinks and hard links do not map, so they * fail loudly and say why. Silently writing a regular file in their place would * produce an extracted tree that differs from the image while reporting * success, which is worse than not extracting at all -- the entire point of * --extract is comparing the result against what produced it. */ #ifdef _WIN32 #include #define mkdir(path, mode) _mkdir(path) static int symlink(const char *target, const char *path) { (void)target; fprintf(stderr, "schfs_fsck: %s: cannot create a symlink on Windows\n", path); errno = ENOSYS; return -1; } static int link(const char *from, const char *to) { (void)from; fprintf(stderr, "schfs_fsck: %s: cannot create a hard link on Windows\n", to); errno = ENOSYS; return -1; } #endif #include "../kernel/fs/schfs/btree.c" #include "../kernel/fs/schfs/space.c" #include "../kernel/fs/schfs/vol.c" #include "../kernel/fs/schfs/obj.c" /* ------------------------------------------------------------------ */ struct imgdev { FILE *f; uint64_t nblocks; }; static struct imgdev IMG; static int img_read(void *ctx, uint64_t b, void *buf) { struct imgdev *d = ctx; if (b >= d->nblocks) return -1; if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0) return -1; if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE) return -1; return 0; } static int img_write(void *ctx, uint64_t b, const void *buf) { (void)ctx; (void)b; (void)buf; return -1; /* this tool never writes */ } static uint64_t img_alloc(void *ctx) { (void)ctx; return 0; } static void img_nofree(void *ctx, uint64_t b) { (void)ctx; (void)b; } // Pages for the allocation map --check derives. Nothing is written back; this // is a check on the map, not a use of it. static void *img_palloc(void) { return calloc(1, SCHFS_BLKSIZE); } static struct schfs_vol VOL; static char CURBLK[SCHFS_BLKSIZE]; static char DATABLK[SCHFS_BLKSIZE]; // Read-only, so only the first of these is ever touched — but a volume owns all // three for its whole life, and handing it two null pointers would turn a // mistake into a crash rather than a compile error. static char CTX_A[SCHFS_BLKSIZE]; static char CTX_B[SCHFS_BLKSIZE]; static char CTX_C[SCHFS_BLKSIZE]; static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C }; static const char * typename(uint16_t t) { switch (t) { case SCHFS_T_FILE: return "file"; case SCHFS_T_DIR: return "dir"; case SCHFS_T_DEV: return "dev"; case SCHFS_T_SYMLINK: return "symlink"; default: return "?"; } } /* ------------------------------------------------------------------ */ /* --info */ /* ------------------------------------------------------------------ */ static int do_info(void) { printf("uberblock ring (%d slots from block %d)\n", SCHFS_UBER_COUNT, SCHFS_UBER_START); int valid = 0, best = -1; uint64_t bestgen = 0; for (int i = 0; i < SCHFS_UBER_COUNT; i++) { struct schfs_uberblock *u = (struct schfs_uberblock *)CTX_A; if (img_read(&IMG, SCHFS_UBER_START + i, u) != 0) continue; if (u->magic != SCHFS_MAGIC) continue; uint32_t stored = u->crc; u->crc = 0; uint32_t calc = schfs_crc32c(0, u, sizeof(*u)); u->crc = stored; if (calc != stored) { printf(" slot %3d: magic ok, CHECKSUM BAD (generation %llu)\n", i, (unsigned long long)u->generation); continue; } valid++; if (best < 0 || u->generation > bestgen) { bestgen = u->generation; best = i; } } printf(" %d slot%s valid; newest is slot %d at generation %llu\n\n", valid, valid == 1 ? "" : "s", best, (unsigned long long)bestgen); printf("volume\n"); printf(" label %s\n", VOL.label); printf(" uuid "); for (int i = 0; i < 16; i++) printf("%02x%s", VOL.uuid[i], (i == 3 || i == 5 || i == 7 || i == 9) ? "-" : ""); printf("\n"); printf(" block size %d bytes\n", SCHFS_BLKSIZE); printf(" total blocks %llu (%llu KB)\n", (unsigned long long)VOL.total_blocks, (unsigned long long)VOL.total_blocks * SCHFS_BLKSIZE / 1024); printf(" free blocks %llu (%llu KB)\n", (unsigned long long)VOL.free_blocks, (unsigned long long)VOL.free_blocks * SCHFS_BLKSIZE / 1024); printf(" tree root block %llu, generation %llu\n", (unsigned long long)VOL.tree.root, (unsigned long long)VOL.tree.generation); printf(" next object id %llu\n", (unsigned long long)VOL.next_objectid); printf(" root object id %llu\n", (unsigned long long)VOL.root_objectid); return 0; } /* ------------------------------------------------------------------ */ /* --check */ /* ------------------------------------------------------------------ */ struct tally { long inodes, dirents, extents, csums, other; long long bytes; int bad; }; static int count_rec(const struct schfs_key *k, const void *val, unsigned len, void *arg) { struct tally *t = arg; (void)val; switch (k->type) { case SCHFS_KEY_INODE: t->inodes++; if (len != sizeof(struct schfs_inode)) { fprintf(stderr, " object %llu: inode record is %u bytes, expected %zu\n", (unsigned long long)k->objectid, len, sizeof(struct schfs_inode)); t->bad++; } else { t->bytes += ((const struct schfs_inode *)val)->size; } break; case SCHFS_KEY_DIRENT: t->dirents++; break; case SCHFS_KEY_EXTENT: t->extents++; if (len != sizeof(struct schfs_extent)) { fprintf(stderr, " object %llu: extent record is %u bytes\n", (unsigned long long)k->objectid, len); t->bad++; } break; case SCHFS_KEY_CSUM: t->csums++; break; default: t->other++; break; } return 0; } /* Verify every data block against its stored checksum, by reading every file in full. This is the part a metadata-only check would miss entirely. */ static long files_checked, blocks_checked; // Chain of object ids from the root down to whatever is being checked. // // A directory that is its own ancestor makes any recursive walk run until the // stack runs out, and a segfault is a terrible way to be told the filesystem // has a loop. Directories cannot be hard-linked, so a repeat in this chain is // always a fault -- and the first version of mkschfs produced exactly one, by // handing a subdirectory the root's object id. #define MAX_DEPTH 64 static uint64_t chain[MAX_DEPTH]; static int chain_n; static int chain_push(uint64_t ino, const char *path) { for (int i = 0; i < chain_n; i++) if (chain[i] == ino) { fprintf(stderr, " %s: directory loop -- object %llu is its own " "ancestor\n", path, (unsigned long long)ino); return -1; } if (chain_n >= MAX_DEPTH) { fprintf(stderr, " %s: directory nesting deeper than %d\n", path, MAX_DEPTH); return -1; } chain[chain_n++] = ino; return 0; } static void chain_pop(void) { if (chain_n) chain_n--; } static int check_dir(uint64_t ino, const char *path); static int check_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen, void *arg) { const char *parent = arg; char path[8192]; snprintf(path, sizeof(path), "%s%s%.*s", parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name); struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_inode in; if (schfs_inode_get(&VOL, &c, ino, &in) != 0) { fprintf(stderr, " %s: entry points at object %llu, which has no inode\n", path, (unsigned long long)ino); return -1; } if (in.type != type) fprintf(stderr, " %s: dirent says %s, inode says %s\n", path, typename(type), typename(in.type)); if (in.type == SCHFS_T_DIR) return check_dir(ino, path); uint64_t off = 0; while (off < in.size) { // A full block is requested every time, even at the tail. Clamping // here instead would mean nothing ever tests that schfs_file_read // stops at end-of-file -- and the kernel's read(2) will depend on // exactly that, since a real program just asks for a bufferful. char buf[SCHFS_BLKSIZE]; unsigned want = sizeof(buf); long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK); if (got <= 0) { fprintf(stderr, " %s: read failed at offset %llu (%ld)\n", path, (unsigned long long)off, got); return -1; } off += (uint64_t)got; blocks_checked++; } files_checked++; return 0; } static int check_dir(uint64_t ino, const char *path) { if (chain_push(ino, path) != 0) return -1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); int r = schfs_dir_iterate(&VOL, &c, ino, check_entry, (void *)path); chain_pop(); return r; } static int do_check(void) { printf("checking %s\n", VOL.label); if (schfs_btree_check(&VOL.tree) != 0) { fprintf(stderr, " FAIL: tree structure is invalid\n"); return 1; } printf(" [ok] tree structure, checksums and key ordering\n"); struct tally t; memset(&t, 0, sizeof(t)); if (schfs_btree_iterate(&VOL.tree, count_rec, &t) != 0) { fprintf(stderr, " FAIL: could not walk every record\n"); return 1; } if (t.bad) { fprintf(stderr, " FAIL: %d malformed records\n", t.bad); return 1; } printf(" [ok] %ld inodes, %ld dirent items, %ld extents, %ld data checksums\n", t.inodes, t.dirents, t.extents, t.csums); if (check_dir(VOL.root_objectid, "/") != 0) { fprintf(stderr, " FAIL: walking the directory tree found a problem\n"); return 1; } printf(" [ok] %ld files read end to end, every data block matched its checksum\n", files_checked); // Rebuild the allocation map the way a mount would, and hold it against the // count the writer left in the uberblock. The map is derived rather than // stored, so nothing else would ever notice if the scan and the writer // disagreed — right up until a mounted volume handed out a block that was // still in use. if (schfs_space_init(&VOL.space, VOL.total_blocks, img_palloc) != 0) { fprintf(stderr, " FAIL: could not build an allocation map\n"); return 1; } if (schfs_space_scan(&VOL) != 0) { fprintf(stderr, " FAIL: could not walk the tree to derive free space\n"); return 1; } if (VOL.space.free_blocks != VOL.free_blocks) { fprintf(stderr, " FAIL: uberblock says %llu blocks free, walking the " "tree says %llu\n", (unsigned long long)VOL.free_blocks, (unsigned long long)VOL.space.free_blocks); return 1; } printf(" [ok] free space derived from the tree matches the uberblock " "(%llu of %llu blocks in use)\n", (unsigned long long)(VOL.total_blocks - VOL.space.free_blocks), (unsigned long long)VOL.total_blocks); return 0; } /* ------------------------------------------------------------------ */ /* --ls / --stat / --cat / --tree */ /* ------------------------------------------------------------------ */ static int print_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen, void *arg) { (void)arg; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_inode in; if (schfs_inode_get(&VOL, &c, ino, &in) != 0) { printf(" ? %.*s\n", (int)namelen, name); return 0; } printf(" %-7s %6llu %4o %2u links %.*s\n", typename(type), (unsigned long long)in.size, in.mode & 07777, in.nlink, (int)namelen, name); return 0; } static uint64_t resolve(const char *path) { struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); uint64_t ino = schfs_path_lookup(&VOL, &c, path); if (ino == 0) fprintf(stderr, "schfs_fsck: %s: no such file or directory\n", path); return ino; } static int do_ls(const char *path) { uint64_t ino = resolve(path); if (!ino) return 1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); printf("%s\n", path); return schfs_dir_iterate(&VOL, &c, ino, print_entry, NULL) != 0; } static int do_stat(const char *path) { uint64_t ino = resolve(path); if (!ino) return 1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_inode in; if (schfs_inode_get(&VOL, &c, ino, &in) != 0) { fprintf(stderr, "schfs_fsck: %s: inode unreadable\n", path); return 1; } printf("path: %s\n", path); printf("objectid: %llu\n", (unsigned long long)ino); printf("type: %s\n", typename(in.type)); printf("size: %llu\n", (unsigned long long)in.size); printf("blocks: %llu\n", (unsigned long long)in.blocks); printf("mode: %04o\n", in.mode & 07777); printf("nlink: %u\n", in.nlink); printf("uid: %u\n", in.uid); printf("gid: %u\n", in.gid); printf("mtime: %llu\n", (unsigned long long)in.mtime); printf("parent: %llu\n", (unsigned long long)in.parent); if (in.type == SCHFS_T_DEV) printf("rdev: %u:%u\n", in.rdev_major, in.rdev_minor); return 0; } static int do_cat(const char *path) { uint64_t ino = resolve(path); if (!ino) return 1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_inode in; if (schfs_inode_get(&VOL, &c, ino, &in) != 0) return 1; uint64_t off = 0; while (off < in.size) { // A full block is requested every time, even at the tail. Clamping // here instead would mean nothing ever tests that schfs_file_read // stops at end-of-file -- and the kernel's read(2) will depend on // exactly that, since a real program just asks for a bufferful. char buf[SCHFS_BLKSIZE]; unsigned want = sizeof(buf); long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK); if (got <= 0) { fprintf(stderr, "schfs_fsck: %s: read error at %llu\n", path, (unsigned long long)off); return 1; } fwrite(buf, 1, (size_t)got, stdout); off += (uint64_t)got; } return 0; } static int tree_walk(uint64_t ino, const char *path); static int tree_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen, void *arg) { const char *parent = arg; char path[8192]; snprintf(path, sizeof(path), "%s%s%.*s", parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name); printf("%-8s %s\n", typename(type), path); if (type == SCHFS_T_DIR) return tree_walk(ino, path); return 0; } static int tree_walk(uint64_t ino, const char *path) { if (chain_push(ino, path) != 0) return -1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); int r = schfs_dir_iterate(&VOL, &c, ino, tree_entry, (void *)path); chain_pop(); return r; } /* ------------------------------------------------------------------ */ /* --extract */ /* ------------------------------------------------------------------ */ // Objects already written out, so a hard link on the image becomes a hard link // on the host rather than a second copy. Without this the round-trip test's // link-count check could not distinguish dedup from duplication. struct seen_ent { uint64_t ino; char *path; }; static struct seen_ent *seen; static int seen_n, seen_cap; static const char * seen_lookup(uint64_t ino) { for (int i = 0; i < seen_n; i++) if (seen[i].ino == ino) return seen[i].path; return NULL; } static void seen_add(uint64_t ino, const char *path) { if (seen_n == seen_cap) { seen_cap = seen_cap ? seen_cap * 2 : 128; seen = realloc(seen, (size_t)seen_cap * sizeof(*seen)); if (!seen) { perror("realloc"); exit(1); } } seen[seen_n].ino = ino; seen[seen_n].path = strdup(path); seen_n++; } static long xfiles, xdirs, xlinks, xsymlinks; static int extract_dir(uint64_t ino, const char *hostpath); static int extract_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen, void *arg) { const char *parent = arg; char host[8192]; (void)type; /* the inode is authoritative, not the dirent */ snprintf(host, sizeof(host), "%s/%.*s", parent, (int)namelen, name); struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_inode in; if (schfs_inode_get(&VOL, &c, ino, &in) != 0) { fprintf(stderr, "schfs_fsck: %s: inode %llu unreadable\n", host, (unsigned long long)ino); return -1; } if (in.type == SCHFS_T_DIR) { if (mkdir(host, in.mode & 07777) != 0 && errno != EEXIST) { perror(host); return -1; } xdirs++; return extract_dir(ino, host); } if (in.type == SCHFS_T_SYMLINK) { char target[4096]; if (in.size >= sizeof(target)) { fprintf(stderr, "schfs_fsck: %s: symlink target too long\n", host); return -1; } long got = schfs_file_read(&VOL, &c, ino, 0, target, (unsigned)in.size, DATABLK); if (got < 0 || (uint64_t)got != in.size) { fprintf(stderr, "schfs_fsck: %s: could not read symlink target\n", host); return -1; } target[in.size] = '\0'; unlink(host); if (symlink(target, host) != 0) { perror(host); return -1; } xsymlinks++; return 0; } if (in.type == SCHFS_T_DEV) { // Creating a real device node needs privileges the build does not have, // so record it as an empty placeholder. The round-trip tree contains no // device nodes; --check is what validates them. FILE *f = fopen(host, "wb"); if (!f) { perror(host); return -1; } fclose(f); return 0; } // A second name for an object already written: make it a real hard link so // the extracted tree has the same link structure as the image. const char *first = seen_lookup(ino); if (first) { unlink(host); if (link(first, host) != 0) { perror(host); return -1; } xlinks++; return 0; } FILE *f = fopen(host, "wb"); if (!f) { perror(host); return -1; } uint64_t off = 0; while (off < in.size) { // A full block is requested every time, even at the tail. Clamping // here instead would mean nothing ever tests that schfs_file_read // stops at end-of-file -- and the kernel's read(2) will depend on // exactly that, since a real program just asks for a bufferful. char buf[SCHFS_BLKSIZE]; unsigned want = sizeof(buf); long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK); if (got <= 0) { fprintf(stderr, "schfs_fsck: %s: read error at %llu\n", host, (unsigned long long)off); fclose(f); return -1; } fwrite(buf, 1, (size_t)got, f); off += (uint64_t)got; } fclose(f); if (chmod(host, in.mode & 07777) != 0) perror(host); seen_add(ino, host); xfiles++; return 0; } static int extract_dir(uint64_t ino, const char *hostpath) { if (chain_push(ino, hostpath) != 0) return -1; struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); int r = schfs_dir_iterate(&VOL, &c, ino, extract_entry, (void *)hostpath); chain_pop(); return r; } static int do_extract(const char *dir) { if (mkdir(dir, 0755) != 0 && errno != EEXIST) { perror(dir); return 1; } if (extract_dir(VOL.root_objectid, dir) != 0) return 1; printf("extracted %ld dirs, %ld files, %ld hard links, %ld symlinks into %s\n", xdirs, xfiles, xlinks, xsymlinks, dir); return 0; } /* --------------------------------------------------------------------- */ /* --snapshots */ /* */ /* A snapshot is a retained tree root, stored as an ordinary record under */ /* SCHFS_SNAP_OBJECTID. Listing them is a range scan over that objectid. */ /* --------------------------------------------------------------------- */ static int do_snapshots(void) { struct schfs_cursor c; schfs_cursor_init(&c, &VOL.tree, CURBLK); struct schfs_key k; schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0); int r = schfs_cursor_seek(&c, &k); if (r == -2) { fprintf(stderr, "schfs_fsck: tree is unreadable\n"); return 1; } int n = 0; while (r == 0) { const struct schfs_key *got = schfs_cursor_key(&c); if (!got || got->objectid != SCHFS_SNAP_OBJECTID || got->type != SCHFS_KEY_SNAP) break; unsigned len; const struct schfs_snap *sn = (const struct schfs_snap *)schfs_cursor_val(&c, &len); if (sn && len == sizeof(*sn)) { if (n == 0) printf("%-6s %-24s %-12s %s\n", "id", "name", "root", "created at"); // The name is bounded but not guaranteed terminated by a bad // image, so it is printed with an explicit precision. printf("%-6llu %-24.*s %-12llu generation %llu\n", (unsigned long long)got->offset, SCHFS_SNAP_NAME_MAX, sn->name, (unsigned long long)sn->root, (unsigned long long)sn->created); n++; } r = schfs_cursor_next(&c); } if (n == 0) printf("no snapshots\n"); return 0; } /* ------------------------------------------------------------------ */ int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "usage: schfs_fsck img.schfs \n" " --info uberblock ring and volume summary\n" " --check full structural and data verification\n" " --ls PATH list a directory\n" " --stat PATH one object's metadata\n" " --cat PATH a file's contents\n" " --tree every path in the volume\n" " --extract DIR write the whole tree onto the host\n" " --snapshots list the retained tree roots\n"); return 2; } const char *imgpath = argv[1]; const char *action = argv[2]; const char *arg = argc > 3 ? argv[3] : NULL; // Must match whatever mkschfs used, or every directory lookup misses. for (int i = 3; i + 1 < argc; i++) if (strcmp(argv[i], "--hash-bits") == 0) { int bits = atoi(argv[i + 1]); schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1); if (arg == argv[i]) arg = NULL; } FILE *f = fopen(imgpath, "rb"); if (!f) { perror(imgpath); return 1; } if (fseeko(f, 0, SEEK_END) != 0) { perror(imgpath); return 1; } off_t sz = ftello(f); IMG.f = f; IMG.nblocks = (uint64_t)sz / SCHFS_BLKSIZE; struct schfs_bdev bd; bd.ctx = &IMG; bd.bread = img_read; bd.bwrite = img_write; bd.balloc = img_alloc; bd.bfree = img_nofree; bd.balloc_run = NULL; /* read-only: nothing is ever allocated */ // No page allocator: this tool never writes, and a volume without an // allocation map cannot allocate even by accident. int r = schfs_vol_open(&VOL, &bd, &CTX, NULL); if (r == -1) { fprintf(stderr, "schfs_fsck: %s: no valid uberblock -- not a SchFS " "volume, or never successfully written\n", imgpath); return 1; } if (r != 0) { fprintf(stderr, "schfs_fsck: %s: newest uberblock names an unreadable " "tree root\n", imgpath); return 1; } if (strcmp(action, "--info") == 0) return do_info(); if (strcmp(action, "--check") == 0) return do_check(); if (strcmp(action, "--tree") == 0) return tree_walk(VOL.root_objectid, ""); if (strcmp(action, "--ls") == 0 && arg) return do_ls(arg); if (strcmp(action, "--stat") == 0 && arg) return do_stat(arg); if (strcmp(action, "--cat") == 0 && arg) return do_cat(arg); if (strcmp(action, "--extract") == 0 && arg) return do_extract(arg); if (strcmp(action, "--snapshots") == 0) return do_snapshots(); fprintf(stderr, "schfs_fsck: unknown or incomplete action '%s'\n", action); return 2; }