mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 11:16:39 +00:00
feat(GUI): spawn CLI directly from the AppImage in GNU/Linux (#775)
If we pass relative paths as arguments to the AppImage, they get resolved from `/tmp/.mount_XXXXXX/usr`. We can exploit this to run the Etcher CLI directly from the AppImage, rather than having to mount it ourselves: ```sh ELECTRON_RUN_AS_NODE=1 Etcher-linux-x64.AppImage bin/resources/app.asar [OPTIONS] ``` By using this little trick, we get rid of both our custom mounting logic and the need of surrounding the command to run in single quotes, therefore avoiding a whole new kind of escaping issues. Since running the CLI directly from the AppImage means that the `desktopintegration` will be ran for the CLI, we pass the `SKIP` environment variable to avoid having it prompt the user for installation. We also updated the `desktopintegration` script to the latest version, which reduces the amount of logging plus other minor fixes. Fixes: https://github.com/resin-io/etcher/issues/637 Change-Type: patch Changelog-Entry: Fix Etcher leaving zombie processes behind in GNU/Linux. Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
parent
bcada80def
commit
9618291f80
@ -22,6 +22,7 @@ const isElevated = Bluebird.promisify(require('is-elevated'));
|
|||||||
const ipc = require('node-ipc');
|
const ipc = require('node-ipc');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
|
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
|
||||||
const EXIT_CODES = require('../exit-codes');
|
const EXIT_CODES = require('../exit-codes');
|
||||||
const packageJSON = require('../../../package.json');
|
const packageJSON = require('../../../package.json');
|
||||||
@ -84,64 +85,27 @@ return isElevated().then((elevated) => {
|
|||||||
'env',
|
'env',
|
||||||
'ELECTRON_RUN_AS_NODE=1',
|
'ELECTRON_RUN_AS_NODE=1',
|
||||||
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
||||||
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`
|
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
|
||||||
|
|
||||||
|
// This environment variable prevents the AppImages
|
||||||
|
// desktop integration script from presenting the
|
||||||
|
// "installation" dialog.
|
||||||
|
'SKIP=1'
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Executing a binary from inside an AppImage as other user
|
|
||||||
// (e.g: `root`) fails with a permission error because of a
|
|
||||||
// security measure imposed by FUSE.
|
|
||||||
//
|
|
||||||
// As a workaround, if we're inside an AppImage, we re-mount
|
|
||||||
// the same AppImage to another temporary location without
|
|
||||||
// FUSE, and re-call to writer proxy as `root` from there.
|
|
||||||
|
|
||||||
if (process.env.APPIMAGE && process.env.APPDIR) {
|
if (process.env.APPIMAGE && process.env.APPDIR) {
|
||||||
const mountPoint = process.env.APPDIR + '-elevated';
|
|
||||||
|
|
||||||
// Translate the current arguments to
|
// Translate the current arguments to point to the AppImage
|
||||||
// point to the new mount location.
|
// Relative paths are resolved from `/tmp/.mount_XXXXXX/usr`
|
||||||
const translatedArguments = _.map(process.argv, (argv) => {
|
const translatedArguments = _.map(_.tail(process.argv), (argv) => {
|
||||||
return argv.replace(process.env.APPDIR, mountPoint);
|
return argv.replace(path.join(process.env.APPDIR, 'usr/'), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
// We wrap the command with `sh -c` since it seems
|
return commandPrefix
|
||||||
// the only way to effectively run many commands
|
.concat([ process.env.APPIMAGE ])
|
||||||
// with a graphical sudo interface,
|
|
||||||
return 'sh -c \'' + [
|
|
||||||
|
|
||||||
'mkdir',
|
|
||||||
'-p',
|
|
||||||
mountPoint,
|
|
||||||
'&&',
|
|
||||||
'mount',
|
|
||||||
'-o',
|
|
||||||
'loop',
|
|
||||||
|
|
||||||
// We re-mount the AppImage as "read-only", since `mount`
|
|
||||||
// will refuse to mount the same AppImage in two different
|
|
||||||
// locations otherwise.
|
|
||||||
'-o',
|
|
||||||
'ro',
|
|
||||||
|
|
||||||
process.env.APPIMAGE,
|
|
||||||
mountPoint,
|
|
||||||
'&&'
|
|
||||||
]
|
|
||||||
.concat(commandPrefix)
|
|
||||||
.concat(utils.escapeWhiteSpacesFromArguments(translatedArguments))
|
.concat(utils.escapeWhiteSpacesFromArguments(translatedArguments))
|
||||||
.concat([
|
.join(' ');
|
||||||
';',
|
|
||||||
|
|
||||||
// We need to sleep for a little bit for `umount` to
|
|
||||||
// succeed, otherwise it complains with an `EBUSY` error.
|
|
||||||
'sleep',
|
|
||||||
'1',
|
|
||||||
|
|
||||||
';',
|
|
||||||
'umount',
|
|
||||||
mountPoint
|
|
||||||
]).join(' ') + '\'';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return commandPrefix.concat(
|
return commandPrefix.concat(
|
||||||
|
@ -4,9 +4,23 @@
|
|||||||
# into the host system without special help from the host system.
|
# into the host system without special help from the host system.
|
||||||
# If you want to use it, then place this in usr/bin/$APPNAME.wrapper
|
# If you want to use it, then place this in usr/bin/$APPNAME.wrapper
|
||||||
# and set it as the Exec= line of the .desktop file in the AppImage.
|
# and set it as the Exec= line of the .desktop file in the AppImage.
|
||||||
|
#
|
||||||
|
# For example, to install the appropriate icons for Scribus,
|
||||||
|
# put them into the AppDir at the following locations:
|
||||||
|
#
|
||||||
|
# ./usr/share/icons/default/128x128/apps/scribus.png
|
||||||
|
# ./usr/share/icons/default/128x128/mimetypes/application-vnd.scribus.png
|
||||||
|
#
|
||||||
|
# Note that the filename application-vnd.scribus.png is derived from
|
||||||
|
# and must be match MimeType=application/vnd.scribus; in scribus.desktop
|
||||||
|
# (with "/" characters replaced by "-").
|
||||||
|
#
|
||||||
|
# Then, change Exec=scribus to Exec=scribus.wrapper and place the script
|
||||||
|
# below in usr/bin/scribus.wrapper and make it executable.
|
||||||
|
# When you run AppRun, then AppRun runs the wrapper script below
|
||||||
|
# which in turn will run the main application.
|
||||||
|
#
|
||||||
# TODO:
|
# TODO:
|
||||||
# Handle icons for mime types as well using xdg-icon-resource
|
|
||||||
# Handle multiple versions of the same AppImage?
|
# Handle multiple versions of the same AppImage?
|
||||||
# Handle removed AppImages? Currently we are just setting TryExec=
|
# Handle removed AppImages? Currently we are just setting TryExec=
|
||||||
# See http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DELETE
|
# See http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DELETE
|
||||||
@ -21,33 +35,61 @@ if [ ! -z "$DEBUG" ] ; then
|
|||||||
set -x
|
set -x
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
THIS="$0"
|
||||||
|
args=("$@") # http://stackoverflow.com/questions/3190818/
|
||||||
|
NUMBER_OF_ARGS="$#"
|
||||||
|
|
||||||
# Please do not change $VENDORPREFIX as it will allow for desktop files
|
# Please do not change $VENDORPREFIX as it will allow for desktop files
|
||||||
# belonging to AppImages to be recognized by future AppImageKit components
|
# belonging to AppImages to be recognized by future AppImageKit components
|
||||||
# such as desktop integration daemons
|
# such as desktop integration daemons
|
||||||
VENDORPREFIX=appimagekit
|
VENDORPREFIX=appimagekit
|
||||||
|
|
||||||
FILENAME=$(readlink -f "${0}")
|
find-up () {
|
||||||
|
path="$(dirname "$(readlink -f "${THIS}")")"
|
||||||
|
while [[ "$path" != "" && ! -e "$path/$1" ]]; do
|
||||||
|
path=${path%/*}
|
||||||
|
done
|
||||||
|
# echo "$path"
|
||||||
|
}
|
||||||
|
|
||||||
echo $FILENAME
|
if [ -z $APPDIR ] ; then
|
||||||
|
# Find the AppDir. It is the directory that contains AppRun.
|
||||||
|
# This assumes that this script resides inside the AppDir or a subdirectory.
|
||||||
|
# If this script is run inside an AppImage, then the AppImage runtime
|
||||||
|
# likely has already set $APPDIR
|
||||||
|
APPDIR=$(find-up "AppRun")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# echo "$APPDIR"
|
||||||
|
|
||||||
|
FILENAME="$(readlink -f "${THIS}")"
|
||||||
|
|
||||||
|
# echo "$FILENAME"
|
||||||
|
|
||||||
if [[ "$FILENAME" != *.wrapper ]] ; then
|
if [[ "$FILENAME" != *.wrapper ]] ; then
|
||||||
echo "$0 is not named correctly. It should be named \$Exec.wrapper"
|
echo "${THIS} is not named correctly. It should be named \$Exec.wrapper"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BIN=$(echo "$FILENAME" | sed -e 's|.wrapper||g')
|
BIN=$(echo "$FILENAME" | sed -e 's|.wrapper||g')
|
||||||
if [[ ! -f $BIN ]] ; then
|
if [[ ! -f "$BIN" ]] ; then
|
||||||
echo "$BIN not found"
|
echo "$BIN not found"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ARGS=$@
|
|
||||||
|
|
||||||
trap atexit EXIT
|
trap atexit EXIT
|
||||||
|
|
||||||
|
# Note that the following handles 0, 1 or more arguments (file paths)
|
||||||
|
# which can include blanks but uses a bashism; can the same be achieved
|
||||||
|
# in POSIX-shell? (FIXME)
|
||||||
|
# http://stackoverflow.com/questions/3190818
|
||||||
atexit()
|
atexit()
|
||||||
{
|
{
|
||||||
exec $BIN $ARGS
|
if [ $NUMBER_OF_ARGS -eq 0 ] ; then
|
||||||
|
exec "${BIN}"
|
||||||
|
else
|
||||||
|
exec "${BIN}" "${args[@]}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
error()
|
error()
|
||||||
@ -69,13 +111,13 @@ yesno()
|
|||||||
TITLE=$1
|
TITLE=$1
|
||||||
TEXT=$2
|
TEXT=$2
|
||||||
if [ -x /usr/bin/zenity ] ; then
|
if [ -x /usr/bin/zenity ] ; then
|
||||||
LD_LIBRARY_PATH="" zenity --question --title="$TITLE" --text="$TEXT" || exit 0
|
LD_LIBRARY_PATH="" zenity --question --title="$TITLE" --text="$TEXT" 2>/dev/null || exit 0
|
||||||
elif [ -x /usr/bin/kdialog ] ; then
|
elif [ -x /usr/bin/kdialog ] ; then
|
||||||
LD_LIBRARY_PATH="" kdialog --caption "Disk auswerfen?" --title "$TITLE" -yesno "$TEXT" || exit 0
|
LD_LIBRARY_PATH="" kdialog --caption "Disk auswerfen?" --title "$TITLE" -yesno "$TEXT" || exit 0
|
||||||
elif [ -x /usr/bin/Xdialog ] ; then
|
elif [ -x /usr/bin/Xdialog ] ; then
|
||||||
LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0
|
LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0
|
||||||
else
|
else
|
||||||
echo "zenity, kdialog, Xdialog missing. Skipping $0."
|
echo "zenity, kdialog, Xdialog missing. Skipping ${THIS}."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -106,7 +148,7 @@ check_dep()
|
|||||||
{
|
{
|
||||||
DEP=$1
|
DEP=$1
|
||||||
if [ -z $(which $DEP) ] ; then
|
if [ -z $(which $DEP) ] ; then
|
||||||
echo "$DEP is missing. Skipping $0."
|
echo "$DEP is missing. Skipping ${THIS}."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -118,36 +160,41 @@ DIRNAME=$(dirname $FILENAME)
|
|||||||
check_dep desktop-file-validate
|
check_dep desktop-file-validate
|
||||||
check_dep update-desktop-database
|
check_dep update-desktop-database
|
||||||
check_dep desktop-file-install
|
check_dep desktop-file-install
|
||||||
|
check_dep xdg-icon-resource
|
||||||
|
check_dep xdg-mime
|
||||||
|
check_dep xdg-desktop-menu
|
||||||
|
|
||||||
DESKTOPFILE=$(find ../ -name "*.desktop" | head -n 1)
|
DESKTOPFILE=$(find "$APPDIR" -maxdepth 1 -name "*.desktop" | head -n 1)
|
||||||
DESKTOPFILE_NAME=$(basename $DESKTOPFILE)
|
# echo "$DESKTOPFILE"
|
||||||
|
DESKTOPFILE_NAME=$(basename "${DESKTOPFILE}")
|
||||||
|
|
||||||
if [ ! -f "$DESKTOPFILE" ] ; then
|
if [ ! -f "$DESKTOPFILE" ] ; then
|
||||||
echo "Desktop file is missing. Please run $0 from within an AppImage."
|
echo "Desktop file is missing. Please run ${THIS} from within an AppImage."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$APPIMAGE" ] ; then
|
if [ -z "$APPIMAGE" ] ; then
|
||||||
echo "\$APPIMAGE is missing. Please run $0 from within an AppImage."
|
APPIMAGE="$APPDIR/AppRun"
|
||||||
exit 0
|
# Not running from within an AppImage; hence using the AppRun for Exec=
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Construct path to the icon according to
|
# Construct path to the icon according to
|
||||||
# http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
|
# http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
|
||||||
ABS_APPIMAGE=$(readlink -e $APPIMAGE)
|
ABS_APPIMAGE=$(readlink -e "$APPIMAGE")
|
||||||
ICONURL="file://$ABS_APPIMAGE"
|
ICONURL="file://$ABS_APPIMAGE"
|
||||||
MD5=$(echo -n $ICONURL | md5sum | cut -c -32)
|
MD5=$(echo -n $ICONURL | md5sum | cut -c -32)
|
||||||
ICONFILE="$HOME/.cache/thumbnails/normal/$MD5.png"
|
ICONFILE="$HOME/.cache/thumbnails/normal/$MD5.png"
|
||||||
if [ ! -f "$ICONFILE" ] ; then
|
if [ ! -f "$ICONFILE" ] ; then
|
||||||
echo "$ICONFILE is missing. Please run $0 from within an AppImage."
|
echo "$ICONFILE is missing. Probably not running ${THIS} from within an AppImage."
|
||||||
exit 0
|
echo "Hence falling back to using .DirIcon"
|
||||||
|
ICONFILE="$APPDIR/.DirIcon"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# $XDG_DATA_DIRS contains the default paths /usr/local/share:/usr/share
|
# $XDG_DATA_DIRS contains the default paths /usr/local/share:/usr/share
|
||||||
# desktop file has to be installed in an applications subdirectory
|
# desktop file has to be installed in an applications subdirectory
|
||||||
# of one of the $XDG_DATA_DIRS components
|
# of one of the $XDG_DATA_DIRS components
|
||||||
if [ -z "$XDG_DATA_DIRS" ] ; then
|
if [ -z "$XDG_DATA_DIRS" ] ; then
|
||||||
echo "\$XDG_DATA_DIRS is missing. Please run $0 from within an AppImage."
|
echo "\$XDG_DATA_DIRS is missing. Please run ${THIS} from within an AppImage."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -165,37 +212,56 @@ fi
|
|||||||
# and if so, whether it points to the same AppImage
|
# and if so, whether it points to the same AppImage
|
||||||
if [ -e "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME" ] ; then
|
if [ -e "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME" ] ; then
|
||||||
# echo "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME already there"
|
# echo "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME already there"
|
||||||
EXEC=$(grep "^Exec=" "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME" | head -n 1)
|
EXEC=$(grep "^Exec=" "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME" | head -n 1 | cut -d " " -f 1)
|
||||||
# echo $EXEC
|
# echo $EXEC
|
||||||
if [ "Exec=$APPIMAGE" == "$EXEC" ] ; then
|
if [ "Exec=\"$APPIMAGE\"" == "$EXEC" ] ; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# We ask the user only if we have found no reason to skip until here
|
# We ask the user only if we have found no reason to skip until here
|
||||||
if [ -z "$SKIP" ] ; then
|
if [ -z "$SKIP" ] ; then
|
||||||
yesno "Install" "Should a desktop file for $APPIMAGE be installed?"
|
yesno "Install" "Would you like to integrate $APPIMAGE with your system?\n\nThis will add it to your applications menu and install icons.\nIf you don't do this you can still launch the application by double-clicking on the AppImage."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
APP=$(echo "$DESKTOPFILE_NAME" | sed -e 's/.desktop//g')
|
||||||
|
|
||||||
# If the user has agreed, rewrite and install the desktop file, and the MIME information
|
# If the user has agreed, rewrite and install the desktop file, and the MIME information
|
||||||
if [ -z "$SKIP" ] ; then
|
if [ -z "$SKIP" ] ; then
|
||||||
if [ -e ./share/mime/ ] ; then
|
# desktop-file-install is supposed to install .desktop files to the user's
|
||||||
find ./share/mime/ -type f -name *xml -exec xdg-mime install $SYSTEM_WIDE --novendor {} \;
|
|
||||||
fi
|
|
||||||
# desktop-file-install is supposed to install
|
|
||||||
# .desktop files to the user's
|
|
||||||
# applications directory when run as a non-root user,
|
# applications directory when run as a non-root user,
|
||||||
# and to /usr/share/applications if run as root
|
# and to /usr/share/applications if run as root
|
||||||
# but that does not really work for me...
|
# but that does not really work for me...
|
||||||
echo desktop-file-install --rebuild-mime-info-cache \
|
#
|
||||||
--vendor=$VENDORPREFIX --set-key=Exec --set-value=$APPIMAGE \
|
# For Exec we must use quotes
|
||||||
--set-key=X-AppImage-Comment --set-value="Generated by $0" \
|
# For TryExec quotes is not supported, so, space must be replaced to \s
|
||||||
--set-icon=$ICONFILE --set-key=TryExec --set-value=$APPIMAGE $DESKTOPFILE \
|
# https://askubuntu.com/questions/175404/how-to-add-space-to-exec-path-in-a-thumbnailer-descrption/175567
|
||||||
--dir "$DESTINATION_DIR_DESKTOP"
|
|
||||||
desktop-file-install --rebuild-mime-info-cache \
|
desktop-file-install --rebuild-mime-info-cache \
|
||||||
--vendor=$VENDORPREFIX --set-key=Exec --set-value=$APPIMAGE \
|
--vendor=$VENDORPREFIX --set-key=Exec --set-value="\"${APPIMAGE}\" %U" \
|
||||||
--set-key=X-AppImage-Comment --set-value="Generated by $0" \
|
--set-key=X-AppImage-Comment --set-value="Generated by ${THIS}" \
|
||||||
--set-icon=$ICONFILE --set-key=TryExec --set-value=$APPIMAGE $DESKTOPFILE \
|
--set-icon="$ICONFILE" --set-key=TryExec --set-value=${APPIMAGE// /\\s} "$DESKTOPFILE" \
|
||||||
--dir "$DESTINATION_DIR_DESKTOP"
|
--dir "$DESTINATION_DIR_DESKTOP"
|
||||||
|
chmod a+x "$DESTINATION_DIR_DESKTOP/"*
|
||||||
|
RESOURCE_NAME=$(echo "$VENDORPREFIX-$DESKTOPFILE_NAME" | sed -e 's/.desktop//g')
|
||||||
|
# echo $RESOURCE_NAME
|
||||||
|
|
||||||
|
# Install the icon files for the application; TODO: scalable
|
||||||
|
ICONS=$(find "${APPDIR}/usr/share/icons/" -wholename "*/apps/${APP}.png" 2>/dev/null || true)
|
||||||
|
for ICON in $ICONS ; do
|
||||||
|
ICON_SIZE=$(echo "${ICON}" | rev | cut -d "/" -f 3 | rev | cut -d "x" -f 1)
|
||||||
|
xdg-icon-resource install --context apps --size ${ICON_SIZE} "${ICON}" "${RESOURCE_NAME}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Install mime type
|
||||||
|
find "${APPDIR}/usr/share/mime/" -type f -name *xml -exec xdg-mime install $SYSTEM_WIDE --novendor {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install the icon files for the mime type; TODO: scalable
|
||||||
|
ICONS=$(find "${APPDIR}/usr/share/icons/" -wholename "*/mimetypes/*.png" 2>/dev/null || true)
|
||||||
|
for ICON in $ICONS ; do
|
||||||
|
ICON_SIZE=$(echo "${ICON}" | rev | cut -d "/" -f 3 | rev | cut -d "x" -f 1)
|
||||||
|
xdg-icon-resource install --context mimetypes --size ${ICON_SIZE} "${ICON}" $(basename $ICON | sed -e 's/.png//g')
|
||||||
|
done
|
||||||
|
|
||||||
xdg-desktop-menu forceupdate
|
xdg-desktop-menu forceupdate
|
||||||
|
gtk-update-icon-cache # for MIME
|
||||||
fi
|
fi
|
||||||
|
Loading…
x
Reference in New Issue
Block a user