|
| 1 | +import std/[strutils, strformat, paths, os, dirs, files, osproc] |
| 2 | +import asyncdispatch |
| 3 | +import cligen, sweet |
| 4 | +import log, mounts |
| 5 | + |
| 6 | +let mounteds = getMounts() |
| 7 | + |
| 8 | +proc is_mountpoint(root: Path): bool = |
| 9 | + for mount in mounteds: |
| 10 | + if root == mount.mountpoint.Path: |
| 11 | + return true |
| 12 | + |
| 13 | +proc mkdir(dir: Path) = |
| 14 | + try: dir.createDir |
| 15 | + except OSError: |
| 16 | + error "Cannot create dir: " & dir.string |
| 17 | + quit 1 |
| 18 | + |
| 19 | +proc run(cmd: string) {.async.} = |
| 20 | + info "Running command: "&cmd |
| 21 | + let p = startProcess("sh", args=["-c", cmd], options={poParentStreams}) |
| 22 | + while p.running: |
| 23 | + await sleepAsync 10 |
| 24 | + let rc = p.waitForExit |
| 25 | + if !!rc: |
| 26 | + fatal "Fail to execute command: "&cmd |
| 27 | + fatal "Command returned exit code: " & $rc |
| 28 | + quit 1 |
| 29 | + |
| 30 | +proc force_mountpoint(root: Path) {.async.} = |
| 31 | + if root.is_mountpoint: return |
| 32 | + warn fmt"{root.string} is not a mountpoint, bind-mounting…" |
| 33 | + await run fmt"mount --bind {root.string.quoteShell} {root.string.quoteShell}" |
| 34 | + |
| 35 | +proc mount(path: Path, mountargs: string) {.async.} = |
| 36 | + if !path.is_mountpoint: |
| 37 | + mkdir path |
| 38 | + await run "mount "&mountargs&" "&path.string |
| 39 | + |
| 40 | +proc mount_dirs(root: Path) {.async.} = |
| 41 | + await mount(root/"proc".Path, "-t proc proc") and |
| 42 | + mount(root/"sys".Path, "-t sysfs sys") and |
| 43 | + mount(root/"dev".Path, "-o bind /dev") and |
| 44 | + mount(root/"dev/pts".Path, "-o bind /dev/pts") |
| 45 | + |
| 46 | +proc cp_resolv(root: Path) {.async.} = |
| 47 | + if !"/etc/resolv.conf".Path.fileExists: |
| 48 | + warn "/etc/resolv.conf does not exist" |
| 49 | + return |
| 50 | + let dest = root/"etc/resolv.conf".Path |
| 51 | + if !dest.fileExists: |
| 52 | + warn "Refusing to copy resolv.conf because it doesn't exist inside chroot" |
| 53 | + return |
| 54 | + await run "mount -c --bind /etc/resolv.conf "&dest.string |
| 55 | + |
| 56 | +proc umount(path: string) {.async.} = |
| 57 | + try: await run fmt"umount {path}" |
| 58 | + finally: discard |
| 59 | + |
| 60 | +proc umount_all(root: string) {.async.} = |
| 61 | + await umount(fmt"{root}/proc") and |
| 62 | + umount(fmt"{root}/sys") and |
| 63 | + umount(fmt"{root}/dev {root}/dev/pts") and |
| 64 | + umount(fmt"{root}/etc/resolv.conf") |
| 65 | + |
| 66 | +proc find_shell(root: Path): string = |
| 67 | + if fileExists root/"bin/fish".Path: |
| 68 | + return "/bin/fish" |
| 69 | + if fileExists root/"bin/zsh".Path: |
| 70 | + return "/bin/zsh" |
| 71 | + if fileExists root/"bin/bash".Path: |
| 72 | + return "/bin/bash" |
| 73 | + if fileExists root/"bin/sh".Path: |
| 74 | + return "/bin/sh" |
| 75 | + warn "Cannot detect any shell in the chroot… falling back to /bin/sh" |
| 76 | + "/bin/sh" |
| 77 | + |
| 78 | +proc dive(args: seq[string], verbosity = lvlNotice, keepresolv = false) = |
| 79 | + ## A chroot utility |
| 80 | + if !args.len: |
| 81 | + fatal "You must provide an argument for the root directory" |
| 82 | + quit 1 |
| 83 | + let root = args[0].Path |
| 84 | + let cp_resolv_fut = cp_resolv root |
| 85 | + waitFor (force_mountpoint root) and (mount_dirs root) and cp_resolv_fut |
| 86 | + waitFor cp_resolv_fut |
| 87 | + let shell = find_shell root |
| 88 | + let str_args = args[1..^1].join(" ") |
| 89 | + waitFor run fmt"SHELL={shell} chroot {root.string} {str_args}" |
| 90 | + waitFor umount_all(root.string) |
| 91 | + |
| 92 | + |
| 93 | +dispatch dive, help = { |
| 94 | + "args": "<root directory> [shell command]", |
| 95 | + "verbosity": "set the logging verbosity: {lvlAll, lvlDebug, lvlInfo, lvlNotice, lvlWarn, lvlError, lvlFatal, lvlNone}", |
| 96 | +}, short = {"keepresolv": 'r'} |
0 commit comments