#!/bin/sh

# Functions library :: for Linux Live Kit scripts
# Author: Tomas M. <http://www.linux-live.org>
#

# =================================================================
# debug and output functions
# =================================================================

debug_start()
{
   if grep -q debug /proc/cmdline; then
      DEBUG_IS_ENABLED=1
   else
      DEBUG_IS_ENABLED=
   fi
}

debug_log()
{
   if [ "$DEBUG_IS_ENABLED" ]; then
      echo "- debug: $*" >&2
      log "- debug: $*"
   fi
}

# header
# $1 = text to show
#
header()
{
   echo "[0;1m""$@""[0;0m"
}


# echo green star
#
echo_green_star()
{
   echo -ne "[0;32m""* ""[0;39m"
}

# log - store given text in /var/log/livedbg
log()
{
   echo "$@" 2>/dev/null >>/var/log/livedbg
}

echolog()
{
   echo "$@"
   log "$@"
}


# show information about the debug shell
show_debug_banner()
{
   echo
   echo "====="
   echo ": Debugging started. Here is the root shell for you."
   echo ": Type your desired commands or hit Ctrl+D to continue booting."
   echo
}


# debug_shell
# executed when debug boot parameter is present
#
debug_shell()
{
   if [ "$DEBUG_IS_ENABLED" ]; then
      show_debug_banner
      setsid sh -c 'exec sh < /dev/tty1 >/dev/tty1 2>&1'
      echo
   fi
}

fatal()
{
   echolog
   header "Fatal error occured - $1"
   echolog "Something went wrong and we can't continue. This should never happen."
   echolog "Please reboot your computer with Ctrl+Alt+Delete ..."
   echolog
   setsid sh -c 'exec sh < /dev/tty1 >/dev/tty1 2>&1'
}


# get value of commandline parameter $1
# $1 = parameter to search for
#
cmdline_value()
{
   cat /proc/cmdline | egrep -o "(^|[[:space:]])$1=[^[:space:]]+" | tr -d " " | cut -d "=" -f 2- | tail -n 1
}


# test if the script is started by root user. If not, exit
#
allow_only_root()
{
  if [ "0$UID" -ne 0 ]; then
     echo "Only root can run $(basename $0)"; exit 1
  fi
}


# Create bundle
# call mksquashfs with apropriate arguments
# $1 = directory which will be compressed to squashfs bundle
# $2 = output file
# $3..$9 = optional arguments like -keep-as-directory or -b 123456789
#
create_bundle()
{
   debug_log "create_module" "$*"
   rm -f "$2" # overwrite, never append to existing file
   mksquashfs "$1" "$2" -comp xz -b 512K $3 $4 $5 $6 $7 $8 $9>/dev/null
}


# Move entire initramfs tree to tmpfs mount.
# It's a bit tricky but is necessray to enable pivot_root
# even for initramfs boot image
#
transfer_initramfs()
{
   if [ ! -r /lib/initramfs_escaped ]; then
      echo "switch root from initramfs to ramfs"
      SWITCH=/m # one letter directory
      mkdir -p $SWITCH
      mount -t tmpfs -o size="100%" tmpfs $SWITCH
      cp -a /??* $SWITCH 2>/dev/null # only copy two-and-more-letter directories
      cd $SWITCH
      echo "This file indicates that we successfully escaped initramfs" > $SWITCH/lib/initramfs_escaped
      exec switch_root -c /dev/console . $0
   fi
}


# mount virtual filesystems like proc etc
#
init_proc_sysfs()
{
   debug_log "init_proc_sysfs" "$*"
   mkdir -p /proc /sys /etc $MEMORY
   mount -n -t proc proc /proc
   echo "0" >/proc/sys/kernel/printk
   mount -n -t sysfs sysfs /sys
   mount -n -o remount,rw rootfs /
   ln -sf /proc/mounts /etc/mtab
}


# modprobe all modules found in initial ramdisk
# $1 = -e for match, -v for negative match
# $2 = regex pattern
#
modprobe_everything()
{
   debug_log "modprobe_everything" "$*"

   echo_green_star >&2
   echo "Probing for hardware" >&2

   find /lib/modules/ | fgrep .ko | egrep $1 $2 | xargs -n 1 modprobe 2>/dev/null
   refresh_devs
}


refresh_devs()
{
   debug_log "refresh_devs" "$*"
   if [ -r /proc/sys/kernel/hotplug ]; then
      echo /sbin/mdev > /proc/sys/kernel/hotplug
   fi
   mdev -s
}


# make sure some devices are there
init_devs()
{
   debug_log "init_devs" "$*"
   modprobe zram 2>/dev/null
   modprobe loop 2>/dev/null
   modprobe squashfs 2>/dev/null
   modprobe fuse 2>/dev/null
   refresh_devs
}


# Activate zram (auto-compression of RAM)
# Compressed RAM consumes 1/2 or even 1/4 of original size
# Setup static size of 500MB
#
init_zram()
{
   debug_log "init_zram" "$*"
   echo_green_star
   echo "Setting dynamic RAM compression using ZRAM if available"
   if [ -r /sys/block/zram0/disksize ]; then
      echo 536870912 > /sys/block/zram0/disksize # 512MB
      mkswap /dev/zram0 >/dev/null
      swapon /dev/zram0 -p 32767
      echo 100 > /proc/sys/vm/swappiness
   fi
}


# load the AUFS kernel module if needed
#
init_aufs()
{
   debug_log "init_aufs" "$*"
   # TODO maybe check here if aufs support is working at all
   # and produce useful error message if user has no aufs
   modprobe aufs 2>/dev/null
   refresh_devs
}


# Setup empty union
# $1 = changes directory (ramfs or persistent changes)
# $2 = union directory where to mount the union
#
init_union()
{
   debug_log "init_union" "$*"

   echo_green_star
   echo "Setting up union using AUFS 3"
   mkdir -p "$1"
   mkdir -p "$2"
   mount -t aufs -o xino="/.xino",trunc_xino,br="$1" aufs "$2"
}


# Return device mounted for given directory
# $1 = directory
#
mounted_device()
{
   debug_log "mounted_device" "$*"

   local MNT TARGET
   MNT="$1"
   while [ "$MNT" != "/" -a "$MNT" != "." -a "$MNT" != "" ]; do
      TARGET="$(grep -F " $MNT " /proc/mounts | cut -d " " -f 1)"
      if [ "$TARGET" != "" ]; then
         echo "$TARGET"
         return
      fi
      MNT="$(dirname "$MNT")"
   done
}


# Return mounted dir for given directory
# $1 = directory
#
mounted_dir()
{
   debug_log "mounted_dir" "$*"

   local MNT
   MNT="$1"
   while [ "$MNT" != "/" -a "$MNT" != "." -a "$MNT" != "" ]; do
      if mountpoint -q "$MNT" 2>/dev/null; then
         echo "$MNT"
         return
      fi
      MNT="$(dirname "$MNT")"
   done
}


# Make sure to mount FAT12/16/32 using vfat
# in order to support long filenames
# $1 = device
#
device_bestfs()
{
   debug_log "device_bestfs" "$*"
   local FS

   FS="$(blkid "$1" | sed -r "s/.*TYPE=//" | tr -d '"' | tr [A-Z] [a-z])"
   if [ "$FS" = "msdos" -o "$FS" = "fat" -o "$FS" = "vfat" ]; then
      FS="vfat"
   elif [ "$FS" = "ntfs" ]; then
      FS="ntfs-3g"
   fi
   echo "-t $FS"
}


# Filesystem options for mount
# $1 = filesystem or '-t filesystem'
#
fs_options()
{
   debug_log "fs_options" "$*"

   if [ "$1" = "-t" ]; then
      shift
   fi
   if [ "$1" = "vfat" ]; then
      echo "-o check=s,shortname=mixed,iocharset=utf8"
   fi
}


# echo first network device known at the moment of calling, eg. eth0
#
network_device()
{
   debug_log "network_device" "$*"
   cat /proc/net/dev | grep : | grep -v lo: | cut -d : -f 1 | tr -d " " | head -n 1
}


# Modprobe network kernel modules until a working driver is found.
# These drivers are (or used to be) probed in Slackware's initrd.
# The function returns the first device found, yet it doesn't have
# to be a working one, eg. if the computer has two network interfaces
# and ethernet cable is plugged only to one of them.
#
init_network_dev()
{
   debug_log "init_network_dev" "$*"
   local MODULE ETH

   for MODULE in 3c59x acenic e1000 e1000e e100 epic100 hp100 ne2k-pci \
   pcnet32 8139too 8139cp tulip via-rhine r8169 atl1e yellowfin tg3 \
   dl2k ns83820 atl1 b44 bnx2 skge sky2 tulip forcedeth sb1000 sis900; do
      modprobe $MODULE 2>/dev/null
      ETH="$(network_device)"
      if [ "$ETH" != "" ]; then
         echo $ETH
         return 0
      fi
      rmmod $MODULE 2>/dev/null
   done

   # If we are here, none of the above specified modules worked.
   # As a last chance, try to modprobe everything else
   modprobe_everything -e /drivers/net/
   echo $(network_device)
}


# Initialize network IP address
# either static from ip=bootparameter, or from DHCP
#
init_network_ip()
{
   debug_log "init_network_ip" "$*"
   local IP ETH SCRIPT CLIENT SERVER GW MASK

   SCRIPT=/tmp/dhcpscript
   ETH=$(init_network_dev)
   IP=$(cmdline_value ip)

   echo "* Setting up network" >&2

   if [ "$IP" != "" ]; then
      # set IP address as given by boot paramter
      echo "$IP" | while IFS=":" read CLIENT SERVER GW MASK; do
         ifconfig $ETH "$CLIENT" netmask "$MASK"
         route add default gw "$GW"
         echo nameserver "$GW" >> /etc/resolv.conf
         echo nameserver "$SERVER" >> /etc/resolv.conf
      done
   else
      # if client ip is unknown, try to get a DHCP lease
      ifconfig $ETH up
      echo -e '#!/bin/sh\nif [ "$1" != "bound" ]; then exit; fi\nifconfig $interface $ip netmask $subnet\nroute add default gw $router\necho nameserver $dns >>/etc/resolv.conf' >$SCRIPT
      chmod a+x $SCRIPT
      udhcpc -i $ETH -n -s $SCRIPT -q >/dev/null
   fi
}


# Mount data from http using httpfs
# $1 = from URL
# $2 = target
mount_data_http()
{
   debug_log "mount_data_http" "$*"
   local CACHE

   echo_green_star >&2
   echo "Load data from $1" >&2

   CACHE=$(cmdline_value cache | sed -r "s/[^0-9]//g" | sed -r "s/^0+//g")
   if [ "$CACHE" != "" ]; then
      CACHE="-C /tmp/httpfs.cache -S "$(($CACHE*1024*1024))
   fi

   init_network_ip

   if [ "$(network_device)" != "" ]; then
      echo "* Mounting remote file..." >&2
      mkdir -p "$2"
      mount.httpfs2 -r 9999 -t 5 $CACHE -c /dev/null "$1" "$2" >/dev/null 2>/dev/null
      mount -o loop "$2"/* "$2" # self mount
      echo "$2/$LIVEKITNAME"
   fi
}


# Download data from tftp
# $1 = target (store downloaded files there)
#
download_data_pxe()
{
   debug_log "download_data_pxe" "$*"
   local IP CMD CLIENT SERVER GW MASK PORT PROTOCOL

   mkdir -p "$1/$LIVEKITNAME"
   IP="$(cmdline_value ip)"

   echo "$IP" | while IFS=":" read CLIENT SERVER GW MASK PORT; do
      echo_green_star >&2
      echo "Contacting PXE server $SERVER" >&2

      if [ "$PORT" = "" ]; then PORT="7529"; fi

      init_network_ip

      echo "* Downloading PXE file list"

      PROTOCOL=http
      wget -q -O "$1/PXEFILELIST" "http://$SERVER:$PORT/PXEFILELIST?$(uname -r):$(uname -m)"
      if [ $? -ne 0 ]; then
         echo "Error downloading from http://$SERVER:$PORT, trying TFTP" >&2
         PROTOCOL=tftp
         tftp -g -r PXEFILELIST -l "$1/PXEFILELIST" $SERVER
      fi

      echo "* Downloading files from the list"

      cat "$1/PXEFILELIST" | while read FILE; do
         if [ "$PROTOCOL" = "http" ]; then
            wget -O "$1/$LIVEKITNAME/$FILE" "http://$SERVER:$PORT/$FILE"
         else
            echo "* $FILE ..." >&2
            tftp -g -r $FILE -l "$1/$LIVEKITNAME/$FILE" $SERVER
         fi
      done
   done

   echo "$1/$LIVEKITNAME"
}


# Find LIVEKIT data by mounting all devices
# If found, keep mounted, else unmount
# $1 = data directory target (mount here)
# $2 = data directory which contains compressed bundles
#
find_data_try()
{
   debug_log "find_data_try" "$*"

   local DEVICE FS FROM OPTIONS

   mkdir -p "$1"
   blkid | sort | cut -d: -f 1 | grep -E -v "/loop|/ram|/zram" | while read DEVICE; do
      FROM="$2"
      FS="$(device_bestfs "$DEVICE")"
      OPTIONS="$(fs_options $FS)"
      mount -r "$DEVICE" "$1" $FS $OPTIONS 2>/dev/null

      # if the FROM parameter is actual file, mount it again as loop (eg. iso)
      if [ -f "$1/$FROM" ]; then
         mount -o remount,rw "$DEVICE" "$1" 2>/dev/null
         mkdir -p "$1/../iso"
         mount -o loop,ro "$1/$FROM" "$1/../iso" 2>/dev/null
         FROM="../iso/$LIVEKITNAME"
      fi

      # search for bundles in the mounted directory
      if [ "$(find "$1/$FROM" -maxdepth 1 -name "*.$BEXT" 2>/dev/null)" != "" ]; then
         # we found at least one bundle/module here
         mount -o remount,rw "$DEVICE" "$1" 2>/dev/null
         echo "$1/$FROM" | tr -s "/" | sed -r "s:/[^/]+/[.][.]/:/:g"
         return
      fi

      # unmount twice, since there could be mounted ISO as loop too. If not, it doesn't hurt
      umount "$1" 2>/dev/null
      umount "$1" 2>/dev/null
   done
}


# Retry finding LIVEKIT data several times,
# until timeouted or until data is found
# $1 = timeout
# $2 = data directory target (mount here)
#
find_data()
{
   debug_log "find_data" "$*"

   local DATA FROM

   FROM="$(cmdline_value from)"

   # boot parameter specified as from=http://server.com/file.iso
   if [ "$(echo $FROM | grep 'http://')" != "" ]; then
      mount_data_http "$FROM" "$2"
      return
   fi

   # if we got IP address as boot parameter, it means we booted over PXE
   if [ "$(cmdline_value ip)" != "" ]; then
      download_data_pxe "$2"
      return
   fi

   if [ "$FROM" = "" ]; then FROM="$LIVEKITNAME"; fi

   echo_green_star >&2
   echo -n "Looking for $LIVEKITNAME data in /$FROM .." | tr -s "/" >&2
   for timeout in $(seq 1 $1); do
      echo -n "." >&2
      refresh_devs
      DATA="$(find_data_try "$2" "$FROM")"
      if [ "$DATA" != "" ]; then
         echo "" >&2
         echo "* Found on $(mounted_device "$2")" >&2
         echo "$DATA"
         return
      fi
      sleep 1
   done
   echo "" >&2
}


# Check if data is found and exists
# $1 = data directory
#
check_data_found()
{
   if [ "$1" = "" -o ! -d "$1" ]; then
      fatal "Could not locate $LIVEKITNAME data";
   fi
}


# Activate persistent changes
# $1 = data directory
# $2 = target changes directory
#
persistent_changes()
{
   debug_log "persistent_changes" "$*"

   local CHANGES T1 T2

   CHANGES="$1/$(basename "$2")"
   T1="$CHANGES/.empty"
   T2="$T1"2

   # Setup the directory anyway, it will be used in all cases
   mkdir -p "$2"

   # If persistent changes are not requested, end here
   if grep -vq perch /proc/cmdline; then
      return
   fi

   # check if changes directory exists and is writable
   touch "$T1" 2>/dev/null && rm -f "$T1" 2>/dev/null

   # if not, simply return back
   if [ $? -ne 0 ]; then
      echo "* Persistent changes not writable or not used"
      return
   fi

   echo_green_star
   echo "Testing persistent changes for posix compatibility"
   touch "$T1" && ln -sf "$T1" "$T2" 2>/dev/null && \
   chmod +x "$T1" 2>/dev/null && test -x "$T1" && \
   chmod -x "$T1" 2>/dev/null && test ! -x "$T1" && \
   rm "$T1" "$T2" 2>/dev/null

   if [ $? -ne 0 ]; then
      echo "* Activating dynamic sized storage for persistent changes"
      rm "$T1" "$T2" 2>/dev/null

      mount.dynfilefs "$CHANGES/changes.dat" 4000 "$2"
      if [ "$(device_bestfs "$2/loop.fs" | tr -d " ")" = "-t" ]; then
         mke2fs -F "$2/loop.fs" >/dev/null
      fi
      mount -o loop,sync "$2/loop.fs" "$2"
      rmdir "$2/lost+found" 2>/dev/null
   else
      echo "* Activating native persistent changes"
      mount --bind "$CHANGES" "$2"
   fi
}


# Copy content of rootcopy directory to union
# $1 = data directory
# $2 = union directory
copy_rootcopy_content()
{
   debug_log "copy_rootcopy_content" "$*"

   if [ "$(ls -1 "$1/rootcopy/" 2>/dev/null)" != "" ]; then
      echo_green_star
      echo "Copying content of rootcopy directory..."
      cp -a "$1"/rootcopy/* "$2"
   fi
}


# Copy data to RAM if requested
# $1 = live data directory
# $2 = changes directory
#
copy_to_ram()
{
   debug_log "copy_to_ram" "$*"

   local MDIR MDEV RAM CHANGES

   if grep -vq toram /proc/cmdline; then
      echo "$1"
      return
   fi

   echo "* Copying $LIVEKITNAME data to RAM..." >&2
   RAM="$(dirname "$2")"/toram
   mkdir  -p "$RAM"
   cp -a "$1"/* "$RAM"
   echo "$RAM"

   MDIR="$(mounted_dir "$1")"
   MDEV="$(mounted_device "$1")"
   MDEV="$(losetup $MDEV 2>/dev/null | cut -d " " -f 3)"
   umount "$MDIR" 2>/dev/null

   if [ "$MDEV" ]; then # iso was mounted here, try to unmount the FS it resides on too
      MDEV="$(mounted_device "$MDEV")"
      umount "$MDEV" 2>/dev/null
   fi
}


# load filter
#
filter_load()
{
   local FILTER
   FILTER=$(cmdline_value load)
   if [ "$FILTER" = "" ]; then
      cat -
   else
      cat - | egrep "$FILTER"
   fi
}


# noload filter
#
filter_noload()
{
   local FILTER
   FILTER=$(cmdline_value noload)
   if [ "$FILTER" = "" ]; then
      cat -
   else
      cat - | egrep -v "$FILTER"
   fi
}

# sort modules by number even if they are in subdirectory
#
sortmod()
{
   cat - | sed -r "s,(.*/(.*)),\\2:\\1," | sort -n | cut -d : -f 2-
}


# Mount squashfs filesystem bundles
# and add them to union
# $1 = directory where to search for bundles
# $2 = directory where to mount bundles
# $3 = directory where union is mounted
#
union_append_bundles()
{
   debug_log "union_append_bundles" "$*"

   local BUN

   echo_green_star
   echo "Adding bundles to union"
   ( ls -1 "$1" | sort -n ; cd "$1" ; find modules/ 2>/dev/null | sortmod | filter_load) | grep '[.]'$BEXT'$' | filter_noload | while read BUNDLE; do
      echo "* $BUNDLE"
      BUN="$(basename "$BUNDLE")"
      mkdir -p "$2/$BUN"
      mount -o loop -t squashfs "$1/$BUNDLE" "$2/$BUN"
      mount -o remount,add:1:"$2/$BUN" aufs "$3"
   done
}


# Create empty fstab properly
# $1 = root directory
#
fstab_create()
{
   debug_log "fstab_create" "$*"

   local FSTAB
   FSTAB="$1/etc/fstab"
   echo aufs / aufs defaults 0 0 > $FSTAB
   echo proc /proc proc defaults 0 0 >> $FSTAB
   echo sysfs /sys sysfs defaults 0 0 >> $FSTAB
   echo devpts /dev/pts devpts gid=5,mode=620 0 0 >> $FSTAB
   echo tmpfs /dev/shm tmpfs defaults 0 0 >> $FSTAB
}


# Change root and execute init
# $1 = where to change root
#
change_root()
{
   debug_log "change_root" "$*"

   # if we are booting over httpfs, we need to copyup these files so they are
   # accessible on union without any further lookup down, else httpfs locks
   if [ "$(network_device)" != "" ]; then
      touch "$1/etc/resolv.conf"
      touch "$1/etc/hosts"
      touch "$1/etc/gai.conf"
   fi

   umount /proc
   umount /sys

   cd "$1"

   # make sure important device files and directories are in union
   mkdir -p boot dev proc sys tmp mnt run
   chmod 1777 tmp
   if [ ! -e dev/console ]; then mknod dev/console c 5 1; fi
   if [ ! -e dev/tty ]; then mknod dev/tty c 5 0; fi
   if [ ! -e dev/tty0 ]; then mknod dev/tty0 c 4 0; fi
   if [ ! -e dev/tty1 ]; then mknod dev/tty1 c 4 1; fi
   if [ ! -e dev/null ]; then mknod dev/null c 1 3; fi
   if [ ! -e sbin/fsck.aufs ]; then ln -s /bin/true sbin/fsck.aufs; fi

   # find chroot and init
   if [ -x bin/chroot -o -L bin/chroot ]; then  CHROOT=bin/chroot; fi
   if [ -x sbin/chroot -o -L sbin/chroot ]; then  CHROOT=sbin/chroot; fi
   if [ -x usr/bin/chroot -o -L usr/bin/chroot ]; then  CHROOT=usr/bin/chroot; fi
   if [ -x usr/sbin/chroot -o -L usr/sbin/chroot ]; then CHROOT=usr/sbin/chroot; fi
   if [ "$CHROOT" = "" ]; then fatal "Can't find executable chroot command"; fi

   if [ -x bin/init -o -L bin/init ]; then INIT=bin/init; fi
   if [ -x sbin/init -o -L sbin/init  ]; then INIT=sbin/init; fi
   if [ "$INIT" = "" ]; then fatal "Can't find executable init command"; fi

   mkdir -p mnt/live
   mount -n -o remount,ro aufs .
   pivot_root . mnt/live
   exec $CHROOT . $INIT < dev/console > dev/console 2>&1
}
