#!/bin/bash
# -*- ENCODING: UTF-8 -*-

# picalim
#
# Launch software with quotas or limits on the use of system resources.
#
# Copyright (c) 2013-2026: Alexis Puente Montiel   < pica (a) picalibre.org >
#
# Licensed according to GNU AGPL version 3.0.
#
# It is libre/free software; you can use, redistribute and/or modify it according to the terms of GNU AGPL as published by GNU, version 3.0, 19 November 2007.
#
# It is distributed in the hope that it will be useful, but without any warranty. Read GNU AGPL version 3.0 for additional details.
#
# A copy of GNU AGPL version 3.0 is available at /usr/share/doc/<software-package-name>/agpl-3.0.txt (additionally on Internet as text at https://www.gnu.org/licenses/agpl-3.0.txt and as HTML at https://www.gnu.org/licenses/agpl-3.0-standalone.html ).
#
# Note: Additionally to the official e-mails, picalibre.org is strictly the only official site for this software project, please consider using it to download, report bugs and contribute.
#
# Depends: bash, cgroup-tools, cgroupfs-mount (>= 1.4+devuan1-pica1), coreutils, findutils, gawk | mawk | original-awk, grep, sed, trickle
# Suggests: memlockd | earlyoom | nohang | oomd


### SCRIPT VARIABLES ########################################

CNAME="picalim"
VERSION="1.3.1"
TITLE="PicaLim"

# Translations
if [ "$LANG" = "" ] ; then export $(cat /etc/default/locale | grep -a 'LANG=') ; fi
TEXTDOMAIN=picalim
TEXTDOMAINDIR=/usr/share/locale/

# Write errors to log
ERRORLOG="$HOME/.${CNAME}.log"
if [ -e "$ERRORLOG" ] ; then
	mv -f $ERRORLOG ${ERRORLOG}.ant
fi
if [ -e "$ERRORLOG" ] ; then rm -rf "$ERRORLOG" ; fi

for i in /etc/pica-global.dist /etc/pica-global.orig /etc/pica-global /etc/pica-global.local ~/.pica-global ~/pica-global ; do
	if [ -f "$i" ] ; then
	cat "$i"
	source "$i"
	source <(cat $i | sed -e "s/=\(YES\|Yes\|yes\|y\|SÍ\|SI\|Sí\|Si\|sí\|si\|S\|s\)/=Y/g" -e "s/=\(No\|no\|n\)/=N/g" -e "s/=\"\(YES\|Yes\|yes\|y\|SÍ\|SI\|Sí\|Si\|sí\|si\|S\|s\)\"/=Y/g" -e "s/=\"\(No\|no\|n\)\"/=N/g")
	fi
done
if [ "$DEBUG" = "Y" ] ; then
	set -xv
	DEBUG="Y"
else
	ERRORLOG="/tmp/.${CNAME}_$(id -nu).log"
	if [ -e "$ERRORLOG" ] ; then mv -f $ERRORLOG ${ERRORLOG}.ant ; fi
	if [ -e "$ERRORLOG" ] ; then rm -rf "$ERRORLOG" ; fi
fi
if [ "$DEBUG" = "" ] ; then DEBUG="N" ; fi
if [ "$DEBUG" != "N" ] ; then
exec > >(tee -a "$ERRORLOG") 2>&1
echo "$0" "$*" >> "$ERRORLOG"
echo "$CNAME" "$VERSION" >> "$ERRORLOG"
echo $(date +%Y-%m-%d_%H:%M:%S) $"Start" >> "$ERRORLOG"
echo "env:" >> "$ERRORLOG"
env >> "$ERRORLOG"
echo "set:" >> "$ERRORLOG"
set >> "$ERRORLOG"
#else
#exec 2>>"$ERRORLOG"
fi

# Description:
BDESCRIP=$"Launch software with quotas or limits on the use of system resources."
LDESCRIP=$"$TITLE is a tool to launch software with quotas or limits on the use of system resources."

# Documentation:
docu_info () {
echo "$CNAME ($VERSION) - $BDESCRIP"
echo 
echo $"Usage:" $CNAME [$"OPTIONS"] -e $"SOFTWARE"
echo 
echo $"Options:"
echo -e "$ODESCRIP"
echo 
echo $"'man $CNAME' for more information."
echo 
}
ODESCRIP=" -f""\t"$"In case of failure, launch software without quotas or limits.""\n"" -m""\t"$"Maximum RAM memory to use (integer in MB).""\n"" -r""\t"$"Maximum % of free RAM memory to use (integer percentage without decimals or symbols, only if '-m' option is not used).""\n"" -c""\t"$"Maximum % of processor to use (integer percentage without decimals or symbols).""\n"" -d""\t"$"Maximum download bandwidth to use (integer in kB/s).""\n"" -u""\t"$"Maximum upload bandwidth to use (integer in kB/s).""\n"" -x""\t"$"Show help documentation."

while getopts xfmrcdu OPTION ; do
	case $OPTION in
		x )   docu_info ; exit 0 ;;
		f )   FAMO="Y"           ;;
		m|r|c|d|u )  true        ;;
	esac
done

if [ "$*" = "" ] ; then
	docu_info ; exit 0
fi


### BASICFUN ########################################

clean () {
if [ "$CG" != "" ] ; then
	rmdir $(find /sys/fs/cgroup/ -type d | grep -aE "/$(whoami)/$CG/|/$(whoami)/$CG$" | awk '{print length($1)"\t"$1}' | sort -n -r | cut -f 2) 2>>"$ERRORLOG"
fi
}


### SCRIPT ########################################

VAR=$(echo "$*" | sed "s| -e .*||1")
COM=$(echo "$*" | sed "s|^$VAR -e ||1")

VARL=$(echo "$VAR" | sed "s|-[a-z]|\n&|g")
NCOM=$(echo "$COM" | cut -d ' ' -f 1 | sed "s|.*/||g")
CG=$(echo "$NCOM"_$(date +%Y%m%d%H%M%S%N))

for i in $(echo $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups) $(cat /sys/fs/cgroup/unified/cgroup.controllers 2>/dev/null) | sort -u) ; do
	cgcreate -a $(whoami):$(whoami) -t $(whoami):$(whoami) -g $i:$(whoami)/$CG
done

PRE_MEM_R=$(echo "$VARL" | grep -a "\-r" | sed 's|-r||g')
CHECK_MEM_R=$(echo "$PRE_MEM_R" | sed "s|[0-9 -]||g")
if [ "$CHECK_MEM_R" = "" ] && [ "$PRE_MEM_R" != "" ] ; then
	FREETEST=$(free -m | grep -aEi "^-/+|^buffers/cach")
	if [ "$FREETEST" != "" ] ; then
		MEM_A=$(free -m | grep -aEi "^-/+|^buffers/cach" | awk '{print $NF}')
	else
		MEM_A=$(free -m | grep -aEi "^Mem" | awk '{print $NF}')
	fi
	PRE_MEM=$(expr $MEM_A \* $PRE_MEM_R / 100)
	MEM=$(expr $PRE_MEM \* 1000000 )
fi
PRE_MEM=$(echo "$VARL" | grep -a "\-m" | sed 's|-m||g')
CHECK_MEM=$(echo "$PRE_MEM" | sed "s|[0-9 -]||g")
if [ "$CHECK_MEM" = "" ] && [ "$PRE_MEM" != "" ] ; then
	MEM=$(expr $PRE_MEM \* 1000000 )
fi
if [ "$MEM" != "" ] ; then
	if [ -d      "/sys/fs/cgroup/memory/$(whoami)/"  ] && [ ! -e "/sys/fs/cgroup/memory/$(whoami)/$CG/"  ] ; then mkdir /sys/fs/cgroup/memory/$(whoami)/$CG/ ; fi
	if [ -d      "/sys/fs/cgroup/$(whoami)/"         ] && [ ! -e "/sys/fs/cgroup/$(whoami)/$CG/"         ] ; then mkdir /sys/fs/cgroup/$(whoami)/$CG/ ; fi
	if [ -d      "/sys/fs/cgroup/unified/$(whoami)/" ] && [ ! -e "/sys/fs/cgroup/unified/$(whoami)/$CG/" ] ; then mkdir /sys/fs/cgroup/unified/$(whoami)/$CG/ ; fi
	if [ -f      "/sys/fs/cgroup/memory/$(whoami)/$CG/memory.limit_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/memory/$(whoami)/$CG/memory.limit_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/memory/$(whoami)/$CG/memory.max_usage_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/memory/$(whoami)/$CG/memory.max_usage_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/memory/$(whoami)/$CG/memory.high" ] ; then
	echo "$MEM" > /sys/fs/cgroup/memory/$(whoami)/$CG/memory.high ; fi
	if [ -f      "/sys/fs/cgroup/memory/$(whoami)/$CG/memory.max" ] ; then
	echo "$MEM" > /sys/fs/cgroup/memory/$(whoami)/$CG/memory.max ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/memory.limit_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/$(whoami)/$CG/memory.limit_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/memory.max_usage_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/$(whoami)/$CG/memory.max_usage_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/memory.high" ] ; then
	echo "$MEM" > /sys/fs/cgroup/$(whoami)/$CG/memory.high ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/memory.max" ] ; then
	echo "$MEM" > /sys/fs/cgroup/$(whoami)/$CG/memory.max ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/memory.limit_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/unified/$(whoami)/$CG/memory.limit_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/memory.max_usage_in_bytes" ] ; then
	echo "$MEM" > /sys/fs/cgroup/unified/$(whoami)/$CG/memory.max_usage_in_bytes ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/memory.high" ] ; then
	echo "$MEM" > /sys/fs/cgroup/unified/$(whoami)/$CG/memory.high ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/memory.max" ] ; then
	echo "$MEM" > /sys/fs/cgroup/unified/$(whoami)/$CG/memory.max ; fi
elif [ "$PRE_MEM" != "" ] || [ "$PRE_MEM_R" != "" ] ; then
	echo $"Input error."
	clean
	exit 1
else
	true
fi

PRE_CPU=$(echo "$VARL" | grep -a "\-c" | sed 's|-c||g')
CHECK_CPU=$(echo "$PRE_CPU" | sed "s|[0-9 -]||g")
if [ "$CHECK_CPU" = "" ] && [ "$PRE_CPU" != "" ] ; then
	CPU=$(expr 100000 \* $PRE_CPU / 100)
fi
if [ "$CPU" != "" ] ; then
	if [ -d      "/sys/fs/cgroup/cpu/$(whoami)/"     ] && [ ! -e "/sys/fs/cgroup/cpu/$(whoami)/$CG/"     ] ; then mkdir /sys/fs/cgroup/cpu/$(whoami)/$CG/ ; fi
	if [ -d      "/sys/fs/cgroup/$(whoami)/"         ] && [ ! -e "/sys/fs/cgroup/$(whoami)/$CG/"         ] ; then mkdir /sys/fs/cgroup/$(whoami)/$CG/ ; fi
	if [ -d      "/sys/fs/cgroup/unified/$(whoami)/" ] && [ ! -e "/sys/fs/cgroup/unified/$(whoami)/$CG/" ] ; then mkdir /sys/fs/cgroup/unified/$(whoami)/$CG/ ; fi
	if [ -f      "/sys/fs/cgroup/cpu/$(whoami)/$CG/cpu.cfs_quota_us" ] ; then
	echo "$CPU" > /sys/fs/cgroup/cpu/$(whoami)/$CG/cpu.cfs_quota_us ; fi
	if [ -f      "/sys/fs/cgroup/cpu/$(whoami)/$CG/cpu.max" ] ; then
	echo "$CPU" > /sys/fs/cgroup/cpu/$(whoami)/$CG/cpu.max ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/cpu.cfs_quota_us" ] ; then
	echo "$CPU" > /sys/fs/cgroup/$(whoami)/$CG/cpu.cfs_quota_us ; fi
	if [ -f      "/sys/fs/cgroup/$(whoami)/$CG/cpu.max" ] ; then
	echo "$CPU" > /sys/fs/cgroup/$(whoami)/$CG/cpu.max ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/cpu.cfs_quota_us" ] ; then
	echo "$CPU" > /sys/fs/cgroup/unified/$(whoami)/$CG/cpu.cfs_quota_us ; fi
	if [ -f      "/sys/fs/cgroup/unified/$(whoami)/$CG/cpu.max" ] ; then
	echo "$CPU" > /sys/fs/cgroup/unified/$(whoami)/$CG/cpu.max ; fi
elif [ "$PRE_CPU" != "" ] ; then
	echo $"Input error."
	clean
	exit 1
else
	true
fi

DL=$(echo "$VARL" | grep -a "\-d")
UL=$(echo "$VARL" | grep -a "\-u")

if [ "$DEBUG" = "Y" ] ; then
	export CGROUP_LOGLEVEL=DEBUG
fi

if [ "$DL" = "" ] && [ "$UL" = "" ] ; then
	if [ "$MEM" = "" ] && [ "$CPU" = "" ] ; then
		$COM &
	elif [ "$MEM" != "" ] && [ "$CPU" = "" ] ; then
		cgexec -g memory:$(whoami)/$CG $COM
	elif [ "$MEM" = "" ] && [ "$CPU" != "" ] ; then
		cgexec -g cpu:$(whoami)/$CG $COM
	elif [ "$MEM" != "" ] && [ "$CPU" != "" ] ; then
		cgexec -g cpu,memory:$(whoami)/$CG $COM
	else
		echo $"ERROR"
		false
	fi
else
	if [ "$MEM" = "" ] && [ "$CPU" = "" ] ; then
		trickle $DL $UL $COM
	elif [ "$MEM" != "" ] && [ "$CPU" = "" ] ; then
		cgexec -g memory:$(whoami)/$CG trickle $DL $UL $COM
	elif [ "$MEM" = "" ] && [ "$CPU" != "" ] ; then
		cgexec -g cpu:$(whoami)/$CG trickle $DL $UL $COM
	elif [ "$MEM" != "" ] && [ "$CPU" != "" ] ; then
		cgexec -g cpu,memory:$(whoami)/$CG trickle $DL $UL $COM
	else
		echo $"ERROR"
		false
	fi
fi

if [ "$?" -eq 0 ] ; then
	sleep 1
	clean
	exit 0
else
	if [ "$FAMO" = "Y" ] ; then
		$COM &
	fi
	sleep 1
	clean
	exit 1
fi
