#!/bin/sh
# James Budiono 2011, 2013, 2015, 2017, 2018, 2020
# puppy test/compilation sandbox
# this version uses rw image
# version 2 - will ask for SFS, and now we use dialog for rw image file location/creation
# version 3 - change awk/sed script combo to one sed script
# version 4 - replace sed script with awk, make sure it will handle oddball cases (loop-N and pup_ro-N don't match)
# version 5 - add compatibility when running with pup_rw=tmpfs (step 2.a)
# version 6 - (2012) adapted to be more flexible - for Fatdog64 600
# version 7 - (2012) cleanup mounts if if we are killed
# version 8 - (2013) re-launch in terminal if we aren't in terminal
# version 9 - (2013) enable running multiple sandboxes
# version 10 - (2015) use pid/mount namespaces if available
# version 11 - (2017) use dumb-init if available, support command from arguments
# version 12 - (2020) env NO_NS=1 don't use namespace, chroot only
# version 13 - (2020) add support for mounting directory, empty SFS list, and OS_IMAGE

# 0. directory locations
. $BOOTSTATE_PATH # AUFS_ROOT_ID
[ "$NOT_USING_AUFS" ] && exit 1
XTERM="defaultterm"
SANDBOX_ROOT=/mnt/sb
FAKEROOT=$SANDBOX_ROOT/fakeroot   # mounted chroot location of sandbox - ie, the fake root
SANDBOX_IMG=$SANDBOX_ROOT/sandbox # mounted rw image of the sandbox disk image
SANDBOX_ID=
TMPFILE=$(mktemp -p /tmp)
#NO_NS=1 # don't user namespace, chroot only
#OS_IMAGE=1  # image file is a OS image

# umount all if we are accidentally killed
trap 'umountall' 1
umountall() {
	{
	umount -l $FAKEROOT/$SANDBOX_IMG
	umount -l $FAKEROOT/tmp
	umount -l $FAKEROOT/proc
	umount -l $FAKEROOT/sys
	umount -l $FAKEROOT/dev/pts $FAKEROOT/dev/shm $FAKEROOT/dev
	
	umount -l $FAKEROOT
	umount -l $SANDBOX_IMG
	rmdir $FAKEROOT
	rmdir $SANDBOX_IMG
	} 2> /dev/null
}

die() {
	echo "$1"
	exit 1
}


# 0.1 must be root
[ $(id -u) -ne 0 ] && die "You must be root to use sandbox."

# 0.2 cannot launch sandbox within sandbox
grep -q $SANDBOX_ROOT /sys/fs/aufs/$AUFS_ROOT_ID/br0 &&
die "Cannot launch sandbox within sandbox."

# 0.3 help
case "$1" in
	--help|-h)
	echo "Usage: ${0##*/} [/path/to/persistent.disk-image] [cmd [args]]"
	echo "Starts a persistent sandbox. Type 'exit' to leave."
	echo "Path is optional, you will be asked to select or create one if not specified."
	exit
esac

# 0.4 if not running from terminal but in Xorg, then launch via terminal
! [ -t 0 ] && [ -n "$DISPLAY" ] && exec $XTERM -e "$0" "$@"
! [ -t 0 ] && exit

# 0.5 is this the first sandbox? If not, then create another name for mountpoints
if grep -q $FAKEROOT /proc/mounts; then
	FAKEROOT=$(mktemp -d -p $SANDBOX_ROOT ${FAKEROOT##*/}.XXXXXXX)
	SANDBOX_ID=".${FAKEROOT##*.}"
	SANDBOX_IMG=$SANDBOX_ROOT/${SANDBOX_IMG##*/}${SANDBOX_ID}
	rmdir $FAKEROOT
fi

# 1. get aufs system-id for the root filesystem
[ -z "$AUFS_ROOT_ID" ] && AUFS_ROOT_ID=$(
awk '{ if ($2 == "/" && $3 == "aufs") { match($4,/si=[0-9a-f]*/); print "si_" substr($4,RSTART+3,RLENGTH-3) } }' /proc/mounts
)

# 2. get branches, then map branches to mount types or loop devices
if [ -z "$OS_IMAGE" ]; then
items=$(
{ echo ==mount==; cat /proc/mounts; 
  echo ==losetup==; losetup-FULL -a; 
  echo ==branches==; ls -v /sys/fs/aufs/$AUFS_ROOT_ID/br[0-9]* | xargs sed 's/=.*//'; } | awk '
  /==mount==/ { mode=1 }
  /==losetup==/ { mode=2 }
  /==branches==/ { mode=3 }
  {
	if (mode == 1) {
		# get list of mount points, types, and devices - index is $3 (mount points)
		mountdev[$2]=$1
		mounttypes[$2]=$3
	} else if (mode == 2) {
		# get list of loop devices and files - index is $1 (loop devs)
		sub(/:/,"",$1)
		sub(/.*\//,"",$3); sub(/)/,"",$3)
		loopdev[$1]=$3
	} else if (mode == 3) {
		# map mount types to loop files if mount devices is a loop
		for (m in mountdev) {
			if ( loopdev[mountdev[m]] != "" ) mounttypes[m]=loopdev[mountdev[m]]
		}
		# for (m in mountdev) print m " on " mountdev[m] " type " mounttypes[m]
		mode=4
	} else if (mode == 4) {
		# print the branches and its mappings
		if (mounttypes[$0])
			print $0, mounttypes[$0], "on"
		else
			print $0, "unknown", "on"
	}
  }  
'
)
# '
# 3. Ask user to choose the SFS
[ -z "$items" ] && die "No layered filesystem found."
dialog --separate-output --backtitle "rw image sandbox" --title "sandbox config" \
	--checklist "Choose which SFS you want to use" 0 60 10 $items 2> $TMPFILE ||
	{ clear && die "Cancelled, exiting."; }

chosen="$(cat $TMPFILE)"
clear
[ -z "$chosen" ] && OS_IMAGE=1 # if no SFS is chosen, we're running an OS IMAGE

else ### if OS_IMAGE
chosen=""
fi   ### if OS_IMAGE


# 4. convert chosen SFS to robranches
robranches=""
for a in $(cat $TMPFILE) ; do
	robranches=$robranches:$a=ro
done
rm $TMPFILE

# 5. get location of rw image
if [ -z "$1" ]; then
	# location not specified - then ask
	dialog --backtitle "rw image sandbox" --title "choose rw image" \
	--extra-button --extra-label "Create" --ok-label "Locate" \
	--yesno "You didn't specify the location of the rw image file. Do you want to locate existing file, or do you want to create a new one?" 0 0
	case $? in
		0) # ok - locate
			dialog --backtitle "rw image sandbox" --title "Specify location of existing rw image" --fselect $(pwd) 8 60 2> $TMPFILE
			rwbranch=$(cat $TMPFILE)
			rm $TMPFILE
			if [ -n "$rwbranch" ]; then
				[ ! -f "$rwbranch" ] && die "$rwbranch doesn't exist, exiting."
			else
				die "You didn't specify any file or you pressed cancel, exiting."
			fi
			;;
		3) # create
			#echo "create"
			dialog --backtitle "rw image sandbox" --title "Specify name and path of new the file" --fselect $(pwd) 8 60 2> $TMPFILE
			rwbranch=$(cat $TMPFILE)
			rm $TMPFILE
			if [ -n "$rwbranch" ]; then
				if [ -f "$rwbranch" ]; then
					die "$rwbranch already exist, exiting."
				else
					# get the size
					dialog --title "Create new rw image" --inputbox "Specify size (in megabytes)" 0 40 100 2> $TMPFILE
					size=$(cat $TMPFILE)
					rm $TMPFILE
					
					if [ -n "$size" ]; then
						if dd if=/dev/zero of="$rwbranch" bs=1 count=0 seek="$size"M; then
							if ! mke2fs -F "$rwbranch"; then
								die "I fail to make an ext2 filesystem at $rwbranch, exiting."
							fi
						else
							die "I fail to create a ${size}M file at $rwbranch, exiting."
						fi
					else
						die "You didn't specify the size or your press cancel, exiting."
					fi					
				fi
			else
				die "You didn't specify any file or you pressed cancel, exiting."
			fi
			;;
		1 | 255) # 1 is cancel, 255 is Escape
			;&
		*) # invalid input - treat as cancel
			die "Cancelled, exiting."
			;;
	esac
else
	# use what we've been given
	rwbranch="$1"
fi
[ $# -gt 0 ] && shift

# 4. make the mountpoints if not exist  yet
mkdir -p $FAKEROOT $SANDBOX_IMG

# 5. do the magic - mount the rw image first, and then the rest with aufs
if [ -d "$rwbranch" ]; then
	mount -o bind "$rwbranch" $SANDBOX_IMG
else
	mount -o loop "$rwbranch" $SANDBOX_IMG
fi || die "unable to mount rw image: $rwbranch"

if mount -t aufs -o "br:$SANDBOX_IMG=rw$robranches" aufs $FAKEROOT; then
	# 6. record our new aufs-root-id so tools don't hack real filesystem
	SANDBOX_AUFS_ID=$(grep $FAKEROOT /proc/mounts | sed 's/.*si=/si_/; s/ .*//') #'
	sed -i -e '/AUFS_ROOT_ID/ d' $FAKEROOT/etc/BOOTSTATE 2> /dev/null
	echo AUFS_ROOT_ID=$SANDBOX_AUFS_ID >> $FAKEROOT/etc/BOOTSTATE
	
	# 7. sandbox is ready, now just need to mount other supports - pts, proc, sysfs, usb and tmp
	mount -o rbind /dev $FAKEROOT/dev
	mount -t sysfs none $FAKEROOT/sys
	mount -t proc none $FAKEROOT/proc
	mount -o bind /tmp $FAKEROOT/tmp
	mkdir -p $FAKEROOT/$SANDBOX_IMG
	mount -o bind $SANDBOX_IMG $FAKEROOT/$SANDBOX_IMG	# so we can access it within sandbox
	
	# 8. optional copy, to enable running sandbox-ed xwin 
	cp /usr/share/sandbox/* $FAKEROOT/usr/bin 2> /dev/null
	
	# 9. make sure we identify ourself as in sandbox - and we're good to go!
	sed -i -e '/^PS1/ s/^.*$/PS1="sandbox'${SANDBOX_ID}'# "/' $FAKEROOT/etc/profile # other puppies	
	grep -q "export PS1=" $FAKEROOT/etc/shinit &&
	sed -i -e "s/PS1=.*/PS1='sandbox${SANDBOX_ID}# '/" $FAKEROOT/etc/shinit ||
	echo -e '\nexport PS1="sandbox'${SANDBOX_ID}'# "' >> $FAKEROOT/etc/shinit #fatdog 600
	grep -q "export TERM=" $FAKEROOT/etc/shinit &&
	sed -i -e "s/TERM=.*/TERM=$TERM/" $FAKEROOT/etc/shinit ||
	echo "export TERM=$TERM" >> $FAKEROOT/etc/shinit  # default TERM is screwed, so use host's

	# use namespaces if available (kernel support and tool support)
	if [ -e /proc/1/ns/pid ] && [ -e /proc/1/ns/mnt ] && type unshare >/dev/null && [ -z $NO_NS ];
	then
		unset START_CMD
		for p in /usr/bin /usr/sbin /bin /sbin; do
			[ -e $FAKEROOT/$p/dumb-init ] && START_CMD="$p/dumb-init -c" && break
		done
		if [ "$START_CMD" ]; then
			[ $# -eq 0 ] && START_CMD="$START_CMD /bin/sh"
		fi
		unshare -f -C -u -p --mount-proc=$FAKEROOT/proc chroot $FAKEROOT $START_CMD "$@"
	else
		chroot $FAKEROOT "$@"
	fi

	# 10. done - clean up everything 
	umountall
	echo "Leaving sandbox."
else
	echo "Unable to mount aufs br:$SANDBOX_IMG=rw$robranches"
	umount -l $SANDBOX_IMG	
fi
