mirror of https://github.com/hak5/openwrt-owl.git
base-files: always use staged sysupgrade
Support for the -d and -p options is dropped; it may be added again at some point by adding these flags to the ubus sysupgrade call. A downside of this is that we get a lot less information about the progress of the upgrade: as soon as the actual upgrade starts, all shell sessions are killed to allow unmounting the root filesystem. Signed-off-by: Matthias Schiffer <mschiffer@universe-factory.net>owl
parent
393817df5d
commit
30f61a34b4
|
@ -56,7 +56,6 @@ run_ramfs() { # <command> [...]
|
||||||
/bin/rm /usr/bin/basename /bin/kill /bin/chmod /usr/bin/find \
|
/bin/rm /usr/bin/basename /bin/kill /bin/chmod /usr/bin/find \
|
||||||
/bin/mknod
|
/bin/mknod
|
||||||
|
|
||||||
install_bin /bin/uclient-fetch /bin/wget
|
|
||||||
install_bin /sbin/mtd
|
install_bin /sbin/mtd
|
||||||
install_bin /sbin/mount_root
|
install_bin /sbin/mount_root
|
||||||
install_bin /sbin/snapshot
|
install_bin /sbin/snapshot
|
||||||
|
@ -96,51 +95,37 @@ run_ramfs() { # <command> [...]
|
||||||
exec /bin/busybox ash -c "$*"
|
exec /bin/busybox ash -c "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
kill_remaining() { # [ <signal> ]
|
kill_remaining() { # [ <signal> [ <loop> ] ]
|
||||||
local sig="${1:-TERM}"
|
local sig="${1:-TERM}"
|
||||||
|
local loop="${2:-0}"
|
||||||
|
local run=true
|
||||||
|
local stat
|
||||||
|
|
||||||
echo -n "Sending $sig to remaining processes ... "
|
echo -n "Sending $sig to remaining processes ... "
|
||||||
|
|
||||||
local my_pid=$$
|
while $run; do
|
||||||
local my_ppid=$(cut -d' ' -f4 /proc/$my_pid/stat)
|
run=false
|
||||||
local my_ppisupgraded=
|
for stat in /proc/[0-9]*/stat; do
|
||||||
grep -q upgraded /proc/$my_ppid/cmdline >/dev/null && {
|
[ -f "$stat" ] || continue
|
||||||
local my_ppisupgraded=1
|
|
||||||
}
|
|
||||||
|
|
||||||
local stat
|
|
||||||
for stat in /proc/[0-9]*/stat; do
|
|
||||||
[ -f "$stat" ] || continue
|
|
||||||
|
|
||||||
local pid name state ppid rest
|
local pid name state ppid rest
|
||||||
read pid name state ppid rest < $stat
|
read pid name state ppid rest < $stat
|
||||||
name="${name#(}"; name="${name%)}"
|
name="${name#(}"; name="${name%)}"
|
||||||
|
|
||||||
local cmdline
|
# Skip PID1, ourself and our children
|
||||||
read cmdline < /proc/$pid/cmdline
|
[ $pid -ne 1 -a $pid -ne $$ -a $ppid -ne $$ ] || continue
|
||||||
|
|
||||||
# Skip kernel threads
|
local cmdline
|
||||||
[ -n "$cmdline" ] || continue
|
read cmdline < /proc/$pid/cmdline
|
||||||
|
|
||||||
if [ $$ -eq 1 ] || [ $my_ppid -eq 1 ] && [ -n "$my_ppisupgraded" ]; then
|
# Skip kernel threads
|
||||||
# Running as init process, kill everything except me
|
[ -n "$cmdline" ] || continue
|
||||||
if [ $pid -ne $$ ] && [ $pid -ne $my_ppid ]; then
|
|
||||||
echo -n "$name "
|
|
||||||
kill -$sig $pid 2>/dev/null
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
case "$name" in
|
|
||||||
# Skip essential services
|
|
||||||
*procd*|*ash*|*init*|*watchdog*|*ssh*|*dropbear*|*telnet*|*login*|*hostapd*|*wpa_supplicant*|*nas*|*relayd*) : ;;
|
|
||||||
|
|
||||||
# Killable process
|
echo -n "$name "
|
||||||
*)
|
kill -$sig $pid 2>/dev/null
|
||||||
if [ $pid -ne $$ ] && [ $ppid -ne $$ ]; then
|
|
||||||
echo -n "$name "
|
[ $loop -eq 1 ] && run=true
|
||||||
kill -$sig $pid 2>/dev/null
|
done
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
@ -175,28 +160,31 @@ v() {
|
||||||
[ "$VERBOSE" -ge 1 ] && echo "$@"
|
[ "$VERBOSE" -ge 1 ] && echo "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_string() {
|
||||||
|
local v="$1"
|
||||||
|
v="${v//\\/\\\\}"
|
||||||
|
v="${v//\"/\\\"}"
|
||||||
|
echo "\"$v\""
|
||||||
|
}
|
||||||
|
|
||||||
rootfs_type() {
|
rootfs_type() {
|
||||||
/bin/mount | awk '($3 ~ /^\/$/) && ($5 !~ /rootfs/) { print $5 }'
|
/bin/mount | awk '($3 ~ /^\/$/) && ($5 !~ /rootfs/) { print $5 }'
|
||||||
}
|
}
|
||||||
|
|
||||||
get_image() { # <source> [ <command> ]
|
get_image() { # <source> [ <command> ]
|
||||||
local from="$1"
|
local from="$1"
|
||||||
local conc="$2"
|
local cat="$2"
|
||||||
local cmd
|
|
||||||
|
|
||||||
case "$from" in
|
if [ -z "$cat" ]; then
|
||||||
http://*|ftp://*) cmd="wget -O- -q";;
|
local magic="$(dd if="$from" bs=2 count=1 2>/dev/null | hexdump -n 2 -e '1/1 "%02x"')"
|
||||||
*) cmd="cat";;
|
|
||||||
esac
|
|
||||||
if [ -z "$conc" ]; then
|
|
||||||
local magic="$(eval $cmd \"$from\" 2>/dev/null | dd bs=2 count=1 2>/dev/null | hexdump -n 2 -e '1/1 "%02x"')"
|
|
||||||
case "$magic" in
|
case "$magic" in
|
||||||
1f8b) conc="zcat";;
|
1f8b) cat="zcat";;
|
||||||
425a) conc="bzcat";;
|
425a) cat="bzcat";;
|
||||||
|
*) cat="cat";;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval "$cmd \"$from\" 2>/dev/null ${conc:+| $conc}"
|
$cat "$from" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
get_magic_word() {
|
get_magic_word() {
|
||||||
|
@ -320,12 +308,14 @@ default_do_upgrade() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
do_upgrade() {
|
do_upgrade_stage2() {
|
||||||
v "Performing system upgrade..."
|
v "Performing system upgrade..."
|
||||||
if type 'platform_do_upgrade' >/dev/null 2>/dev/null; then
|
if [ -n "$do_upgrade" ]; then
|
||||||
platform_do_upgrade "$ARGV"
|
$do_upgrade "$IMAGE"
|
||||||
|
elif type 'platform_do_upgrade' >/dev/null 2>/dev/null; then
|
||||||
|
platform_do_upgrade "$IMAGE"
|
||||||
else
|
else
|
||||||
default_do_upgrade "$ARGV"
|
default_do_upgrade "$IMAGE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$SAVE_CONFIG" -eq 1 ] && type 'platform_copy_config' >/dev/null 2>/dev/null; then
|
if [ "$SAVE_CONFIG" -eq 1 ] && type 'platform_copy_config' >/dev/null 2>/dev/null; then
|
||||||
|
@ -333,12 +323,11 @@ do_upgrade() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
v "Upgrade completed"
|
v "Upgrade completed"
|
||||||
[ -n "$DELAY" ] && sleep "$DELAY"
|
sleep 1
|
||||||
ask_bool 1 "Reboot" && {
|
|
||||||
v "Rebooting system..."
|
v "Rebooting system..."
|
||||||
umount -a
|
umount -a
|
||||||
reboot -f
|
reboot -f
|
||||||
sleep 5
|
sleep 5
|
||||||
echo b 2>/dev/null >/proc/sysrq-trigger
|
echo b 2>/dev/null >/proc/sysrq-trigger
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -283,7 +283,16 @@ nand_upgrade_tar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Recognize type of passed file and start the upgrade process
|
# Recognize type of passed file and start the upgrade process
|
||||||
nand_do_upgrade_stage2() {
|
nand_do_upgrade() {
|
||||||
|
if [ -n "$IS_PRE_UPGRADE" ]; then
|
||||||
|
# Previously, nand_do_upgrade was called from the platform_pre_upgrade
|
||||||
|
# hook; this piece of code handles scripts that haven't been
|
||||||
|
# updated. All scripts should gradually move to call nand_do_upgrade
|
||||||
|
# from platform_do_upgrade instead.
|
||||||
|
export do_upgrade=nand_do_upgrade
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
local file_type=$(identify $1)
|
local file_type=$(identify $1)
|
||||||
|
|
||||||
if type 'platform_nand_pre_upgrade' >/dev/null 2>/dev/null; then
|
if type 'platform_nand_pre_upgrade' >/dev/null 2>/dev/null; then
|
||||||
|
@ -299,45 +308,6 @@ nand_do_upgrade_stage2() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
nand_upgrade_stage2() {
|
|
||||||
[ $1 = "nand" ] && {
|
|
||||||
[ -f "$2" ] && {
|
|
||||||
touch /tmp/sysupgrade
|
|
||||||
|
|
||||||
killall -9 telnetd
|
|
||||||
killall -9 dropbear
|
|
||||||
killall -9 ash
|
|
||||||
|
|
||||||
kill_remaining TERM
|
|
||||||
sleep 3
|
|
||||||
kill_remaining KILL
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
if [ -n "$(rootfs_type)" ]; then
|
|
||||||
v "Switching to ramdisk..."
|
|
||||||
run_ramfs ". /lib/functions.sh; include /lib/upgrade; nand_do_upgrade_stage2 $2"
|
|
||||||
else
|
|
||||||
nand_do_upgrade_stage2 $2
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
echo "Nand upgrade failed"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nand_upgrade_stage1() {
|
|
||||||
[ -f /tmp/sysupgrade-nand-path ] && {
|
|
||||||
path="$(cat /tmp/sysupgrade-nand-path)"
|
|
||||||
[ "$SAVE_CONFIG" != 1 -a -f "$CONF_TAR" ] &&
|
|
||||||
rm $CONF_TAR
|
|
||||||
|
|
||||||
ubus call system nandupgrade "{\"prefix\": \"$RAM_ROOT\", \"path\": \"$path\" }"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if passed file is a valid one for NAND sysupgrade. Currently it accepts
|
# Check if passed file is a valid one for NAND sysupgrade. Currently it accepts
|
||||||
# 3 types of files:
|
# 3 types of files:
|
||||||
# 1) UBI - should contain an ubinized image, header is checked for the proper
|
# 1) UBI - should contain an ubinized image, header is checked for the proper
|
||||||
|
@ -364,13 +334,3 @@ nand_do_platform_check() {
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start NAND upgrade process
|
|
||||||
#
|
|
||||||
# $(1): file to be used for upgrade
|
|
||||||
nand_do_upgrade() {
|
|
||||||
echo -n $1 > /tmp/sysupgrade-nand-path
|
|
||||||
install_bin /sbin/upgraded
|
|
||||||
ln -s "$RAM_ROOT"/sbin/upgraded /tmp/upgraded
|
|
||||||
nand_upgrade_stage1
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /lib/functions/system.sh
|
||||||
|
|
||||||
|
export IMAGE="$1"
|
||||||
|
COMMAND="$2"
|
||||||
|
|
||||||
|
export ARGV="$IMAGE"
|
||||||
|
export ARGC=1
|
||||||
|
|
||||||
|
export SAVE_CONFIG=1
|
||||||
|
export SAVE_PARTITIONS=1
|
||||||
|
|
||||||
|
export INTERACTIVE=0
|
||||||
|
export VERBOSE=1
|
||||||
|
export CONFFILES=/tmp/sysupgrade.conffiles
|
||||||
|
export CONF_TAR=/tmp/sysupgrade.tgz
|
||||||
|
|
||||||
|
|
||||||
|
[ -f "$CONF_TAR" ] || export SAVE_CONFIG=0
|
||||||
|
[ -f /tmp/sysupgrade.always.overwrite.bootdisk.partmap ] && export SAVE_PARTITIONS=0
|
||||||
|
|
||||||
|
include /lib/upgrade
|
||||||
|
|
||||||
|
|
||||||
|
killall -9 telnetd
|
||||||
|
killall -9 dropbear
|
||||||
|
killall -9 ash
|
||||||
|
|
||||||
|
kill_remaining TERM
|
||||||
|
sleep 3
|
||||||
|
kill_remaining KILL 1
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
|
||||||
|
if [ -n "$IMAGE" ] && type 'platform_pre_upgrade' >/dev/null 2>/dev/null; then
|
||||||
|
IS_PRE_UPGRADE=1 platform_pre_upgrade "$IMAGE"
|
||||||
|
|
||||||
|
# Needs to be unset again because of busybox weirdness ...
|
||||||
|
IS_PRE_UPGRADE=
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$(rootfs_type)" ]; then
|
||||||
|
echo "Switching to ramdisk..."
|
||||||
|
run_ramfs "$COMMAND"
|
||||||
|
else
|
||||||
|
exec /bin/busybox ash -c "$COMMAND"
|
||||||
|
fi
|
|
@ -1,4 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ "$1" = "nand" ] && exec /lib/upgrade/stage2 "$2" "$3"
|
||||||
|
|
||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /lib/functions/system.sh
|
. /lib/functions/system.sh
|
||||||
|
|
||||||
|
@ -11,7 +14,6 @@ export VERBOSE=1
|
||||||
export SAVE_CONFIG=1
|
export SAVE_CONFIG=1
|
||||||
export SAVE_OVERLAY=0
|
export SAVE_OVERLAY=0
|
||||||
export SAVE_PARTITIONS=1
|
export SAVE_PARTITIONS=1
|
||||||
export DELAY=
|
|
||||||
export CONF_IMAGE=
|
export CONF_IMAGE=
|
||||||
export CONF_BACKUP_LIST=0
|
export CONF_BACKUP_LIST=0
|
||||||
export CONF_BACKUP=
|
export CONF_BACKUP=
|
||||||
|
@ -25,7 +27,6 @@ export TEST=0
|
||||||
while [ -n "$1" ]; do
|
while [ -n "$1" ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-i) export INTERACTIVE=1;;
|
-i) export INTERACTIVE=1;;
|
||||||
-d) export DELAY="$2"; shift;;
|
|
||||||
-v) export VERBOSE="$(($VERBOSE + 1))";;
|
-v) export VERBOSE="$(($VERBOSE + 1))";;
|
||||||
-q) export VERBOSE="$(($VERBOSE - 1))";;
|
-q) export VERBOSE="$(($VERBOSE - 1))";;
|
||||||
-n) export SAVE_CONFIG=0;;
|
-n) export SAVE_CONFIG=0;;
|
||||||
|
@ -50,10 +51,9 @@ done
|
||||||
export CONFFILES=/tmp/sysupgrade.conffiles
|
export CONFFILES=/tmp/sysupgrade.conffiles
|
||||||
export CONF_TAR=/tmp/sysupgrade.tgz
|
export CONF_TAR=/tmp/sysupgrade.tgz
|
||||||
|
|
||||||
export ARGV="$*"
|
IMAGE="$1"
|
||||||
export ARGC="$#"
|
|
||||||
|
|
||||||
[ -z "$ARGV" -a -z "$NEED_IMAGE" -o $HELP -gt 0 ] && {
|
[ -z "$IMAGE" -a -z "$NEED_IMAGE" -o $HELP -gt 0 ] && {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 [<upgrade-option>...] <image file or URL>
|
Usage: $0 [<upgrade-option>...] <image file or URL>
|
||||||
$0 [-q] [-i] <backup-command> <file>
|
$0 [-q] [-i] <backup-command> <file>
|
||||||
|
@ -90,7 +90,7 @@ EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
[ -n "$ARGV" -a -n "$NEED_IMAGE" ] && {
|
[ -n "$IMAGE" -a -n "$NEED_IMAGE" ] && {
|
||||||
cat <<-EOF
|
cat <<-EOF
|
||||||
-b|--create-backup and -r|--restore-backup do not perform a firmware upgrade.
|
-b|--create-backup and -r|--restore-backup do not perform a firmware upgrade.
|
||||||
Do not specify both -b|-r and a firmware image.
|
Do not specify both -b|-r and a firmware image.
|
||||||
|
@ -136,14 +136,13 @@ sysupgrade_pre_upgrade="fwtool_pre_upgrade"
|
||||||
|
|
||||||
include /lib/upgrade
|
include /lib/upgrade
|
||||||
|
|
||||||
[ "$1" = "nand" ] && nand_upgrade_stage2 $@
|
|
||||||
|
|
||||||
do_save_conffiles() {
|
do_save_conffiles() {
|
||||||
local conf_tar="${1:-$CONF_TAR}"
|
local conf_tar="${1:-$CONF_TAR}"
|
||||||
|
|
||||||
[ -z "$(rootfs_type)" ] && {
|
[ -z "$(rootfs_type)" ] && {
|
||||||
echo "Cannot save config while running from ramdisk."
|
echo "Cannot save config while running from ramdisk."
|
||||||
ask_bool 0 "Abort" && exit
|
ask_bool 0 "Abort" && exit
|
||||||
|
rm -f "$conf_tar"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
run_hooks "$CONFFILES" $sysupgrade_init_conffiles
|
run_hooks "$CONFFILES" $sysupgrade_init_conffiles
|
||||||
|
@ -184,8 +183,33 @@ type platform_check_image >/dev/null 2>/dev/null || {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "$IMAGE" in
|
||||||
|
http://*)
|
||||||
|
wget -O/tmp/sysupgrade.img "$IMAGE"
|
||||||
|
IMAGE=/tmp/sysupgrade.img
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
IMAGE="$(readlink -f "$IMAGE")"
|
||||||
|
|
||||||
|
case "$IMAGE" in
|
||||||
|
'')
|
||||||
|
echo "Image file not found."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
/tmp/*) ;;
|
||||||
|
*)
|
||||||
|
v "Image not in /tmp, copying..."
|
||||||
|
cp -f "$IMAGE" /tmp/sysupgrade.img
|
||||||
|
IMAGE=/tmp/sysupgrade.img
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export ARGV="$IMAGE"
|
||||||
|
export ARGC=1
|
||||||
|
|
||||||
for check in $sysupgrade_image_check; do
|
for check in $sysupgrade_image_check; do
|
||||||
( eval "$check \"\$ARGV\"" ) || {
|
( $check "$IMAGE" ) || {
|
||||||
if [ $FORCE -eq 1 ]; then
|
if [ $FORCE -eq 1 ]; then
|
||||||
echo "Image check '$check' failed but --force given - will update anyway!"
|
echo "Image check '$check' failed but --force given - will update anyway!"
|
||||||
break
|
break
|
||||||
|
@ -211,6 +235,7 @@ elif ask_bool $SAVE_CONFIG "Keep config files over reflash"; then
|
||||||
[ $TEST -eq 1 ] || do_save_conffiles
|
[ $TEST -eq 1 ] || do_save_conffiles
|
||||||
export SAVE_CONFIG=1
|
export SAVE_CONFIG=1
|
||||||
else
|
else
|
||||||
|
[ $TEST -eq 1 ] || rm -f "$CONF_TAR"
|
||||||
export SAVE_CONFIG=0
|
export SAVE_CONFIG=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -218,28 +243,18 @@ if [ $TEST -eq 1 ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ $SAVE_PARTITIONS -eq 0 ]; then
|
||||||
|
touch /tmp/sysupgrade.always.overwrite.bootdisk.partmap
|
||||||
|
else
|
||||||
|
rm -f /tmp/sysupgrade.always.overwrite.bootdisk.partmap
|
||||||
|
fi
|
||||||
|
|
||||||
run_hooks "" $sysupgrade_pre_upgrade
|
run_hooks "" $sysupgrade_pre_upgrade
|
||||||
|
|
||||||
# Some platforms/devices may want different sysupgrade process, e.g. without
|
install_bin /sbin/upgraded
|
||||||
# killing processes yet or calling ubus system upgrade method.
|
v "Commencing upgrade. All shell sessions will be closed now."
|
||||||
# This is needed e.g. on NAND devices where we just want to trigger stage1 at
|
ubus call system sysupgrade "{
|
||||||
# this point.
|
\"prefix\": \"$RAM_ROOT\",
|
||||||
if type 'platform_pre_upgrade' >/dev/null 2>/dev/null; then
|
\"path\": $(json_string "$IMAGE"),
|
||||||
platform_pre_upgrade "$ARGV"
|
\"command\": \". /lib/functions.sh; include /lib/upgrade; do_upgrade_stage2\"
|
||||||
fi
|
}"
|
||||||
|
|
||||||
ubus call system upgrade
|
|
||||||
touch /tmp/sysupgrade
|
|
||||||
|
|
||||||
if [ ! -f /tmp/failsafe ] ; then
|
|
||||||
kill_remaining TERM
|
|
||||||
sleep 3
|
|
||||||
kill_remaining KILL
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$(rootfs_type)" ]; then
|
|
||||||
v "Switching to ramdisk..."
|
|
||||||
run_ramfs '. /lib/functions.sh; include /lib/upgrade; do_upgrade'
|
|
||||||
else
|
|
||||||
do_upgrade
|
|
||||||
fi
|
|
||||||
|
|
Loading…
Reference in New Issue