From 9618291f806b79d3506ba715d58e502c796b7dfd Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 26 Oct 2016 14:07:17 -0400 Subject: [PATCH] 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 --- lib/src/child-writer/writer-proxy.js | 64 +++------- scripts/build/AppImages/desktopintegration | 140 +++++++++++++++------ 2 files changed, 117 insertions(+), 87 deletions(-) diff --git a/lib/src/child-writer/writer-proxy.js b/lib/src/child-writer/writer-proxy.js index be2e6834..ac1b6214 100644 --- a/lib/src/child-writer/writer-proxy.js +++ b/lib/src/child-writer/writer-proxy.js @@ -22,6 +22,7 @@ const isElevated = Bluebird.promisify(require('is-elevated')); const ipc = require('node-ipc'); const _ = require('lodash'); const os = require('os'); +const path = require('path'); const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')); const EXIT_CODES = require('../exit-codes'); const packageJSON = require('../../../package.json'); @@ -84,64 +85,27 @@ return isElevated().then((elevated) => { 'env', 'ELECTRON_RUN_AS_NODE=1', `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) { - const mountPoint = process.env.APPDIR + '-elevated'; - // Translate the current arguments to - // point to the new mount location. - const translatedArguments = _.map(process.argv, (argv) => { - return argv.replace(process.env.APPDIR, mountPoint); + // Translate the current arguments to point to the AppImage + // Relative paths are resolved from `/tmp/.mount_XXXXXX/usr` + const translatedArguments = _.map(_.tail(process.argv), (argv) => { + return argv.replace(path.join(process.env.APPDIR, 'usr/'), ''); }); - // We wrap the command with `sh -c` since it seems - // the only way to effectively run many commands - // 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) + return commandPrefix + .concat([ process.env.APPIMAGE ]) .concat(utils.escapeWhiteSpacesFromArguments(translatedArguments)) - .concat([ - ';', - - // We need to sleep for a little bit for `umount` to - // succeed, otherwise it complains with an `EBUSY` error. - 'sleep', - '1', - - ';', - 'umount', - mountPoint - ]).join(' ') + '\''; + .join(' '); } return commandPrefix.concat( diff --git a/scripts/build/AppImages/desktopintegration b/scripts/build/AppImages/desktopintegration index 3e78d6a0..3a7f6455 100755 --- a/scripts/build/AppImages/desktopintegration +++ b/scripts/build/AppImages/desktopintegration @@ -4,9 +4,23 @@ # 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 # 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: -# Handle icons for mime types as well using xdg-icon-resource # Handle multiple versions of the same AppImage? # Handle removed AppImages? Currently we are just setting TryExec= # See http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DELETE @@ -21,33 +35,61 @@ if [ ! -z "$DEBUG" ] ; then set -x fi +THIS="$0" +args=("$@") # http://stackoverflow.com/questions/3190818/ +NUMBER_OF_ARGS="$#" + # Please do not change $VENDORPREFIX as it will allow for desktop files # belonging to AppImages to be recognized by future AppImageKit components # such as desktop integration daemons 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 - 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 fi BIN=$(echo "$FILENAME" | sed -e 's|.wrapper||g') -if [[ ! -f $BIN ]] ; then +if [[ ! -f "$BIN" ]] ; then echo "$BIN not found" exit 0 fi -ARGS=$@ - 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() { - exec $BIN $ARGS +if [ $NUMBER_OF_ARGS -eq 0 ] ; then + exec "${BIN}" +else + exec "${BIN}" "${args[@]}" +fi } error() @@ -69,13 +111,13 @@ yesno() TITLE=$1 TEXT=$2 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 LD_LIBRARY_PATH="" kdialog --caption "Disk auswerfen?" --title "$TITLE" -yesno "$TEXT" || exit 0 elif [ -x /usr/bin/Xdialog ] ; then LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0 else - echo "zenity, kdialog, Xdialog missing. Skipping $0." + echo "zenity, kdialog, Xdialog missing. Skipping ${THIS}." exit 0 fi } @@ -106,7 +148,7 @@ check_dep() { DEP=$1 if [ -z $(which $DEP) ] ; then - echo "$DEP is missing. Skipping $0." + echo "$DEP is missing. Skipping ${THIS}." exit 0 fi } @@ -118,36 +160,41 @@ DIRNAME=$(dirname $FILENAME) check_dep desktop-file-validate check_dep update-desktop-database 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_NAME=$(basename $DESKTOPFILE) +DESKTOPFILE=$(find "$APPDIR" -maxdepth 1 -name "*.desktop" | head -n 1) +# echo "$DESKTOPFILE" +DESKTOPFILE_NAME=$(basename "${DESKTOPFILE}") 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 fi if [ -z "$APPIMAGE" ] ; then - echo "\$APPIMAGE is missing. Please run $0 from within an AppImage." - exit 0 + APPIMAGE="$APPDIR/AppRun" + # Not running from within an AppImage; hence using the AppRun for Exec= fi # Construct path to the icon according to # http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html -ABS_APPIMAGE=$(readlink -e $APPIMAGE) +ABS_APPIMAGE=$(readlink -e "$APPIMAGE") ICONURL="file://$ABS_APPIMAGE" MD5=$(echo -n $ICONURL | md5sum | cut -c -32) ICONFILE="$HOME/.cache/thumbnails/normal/$MD5.png" if [ ! -f "$ICONFILE" ] ; then - echo "$ICONFILE is missing. Please run $0 from within an AppImage." - exit 0 + echo "$ICONFILE is missing. Probably not running ${THIS} from within an AppImage." + echo "Hence falling back to using .DirIcon" + ICONFILE="$APPDIR/.DirIcon" fi # $XDG_DATA_DIRS contains the default paths /usr/local/share:/usr/share # desktop file has to be installed in an applications subdirectory # of one of the $XDG_DATA_DIRS components 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 fi @@ -165,37 +212,56 @@ fi # and if so, whether it points to the same AppImage if [ -e "$DESTINATION_DIR_DESKTOP/$VENDORPREFIX-$DESKTOPFILE_NAME" ] ; then # 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 - if [ "Exec=$APPIMAGE" == "$EXEC" ] ; then + if [ "Exec=\"$APPIMAGE\"" == "$EXEC" ] ; then exit 0 fi fi # We ask the user only if we have found no reason to skip until here 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 +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 [ -z "$SKIP" ] ; then - if [ -e ./share/mime/ ] ; then - 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 + # desktop-file-install is supposed to install .desktop files to the user's # applications directory when run as a non-root user, # and to /usr/share/applications if run as root # but that does not really work for me... - echo desktop-file-install --rebuild-mime-info-cache \ - --vendor=$VENDORPREFIX --set-key=Exec --set-value=$APPIMAGE \ - --set-key=X-AppImage-Comment --set-value="Generated by $0" \ - --set-icon=$ICONFILE --set-key=TryExec --set-value=$APPIMAGE $DESKTOPFILE \ - --dir "$DESTINATION_DIR_DESKTOP" + # + # For Exec we must use quotes + # For TryExec quotes is not supported, so, space must be replaced to \s + # https://askubuntu.com/questions/175404/how-to-add-space-to-exec-path-in-a-thumbnailer-descrption/175567 desktop-file-install --rebuild-mime-info-cache \ - --vendor=$VENDORPREFIX --set-key=Exec --set-value=$APPIMAGE \ - --set-key=X-AppImage-Comment --set-value="Generated by $0" \ - --set-icon=$ICONFILE --set-key=TryExec --set-value=$APPIMAGE $DESKTOPFILE \ + --vendor=$VENDORPREFIX --set-key=Exec --set-value="\"${APPIMAGE}\" %U" \ + --set-key=X-AppImage-Comment --set-value="Generated by ${THIS}" \ + --set-icon="$ICONFILE" --set-key=TryExec --set-value=${APPIMAGE// /\\s} "$DESKTOPFILE" \ --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 + gtk-update-icon-cache # for MIME fi