From cee836f71e191e51dd48d34633bfd8c977ba3b8a Mon Sep 17 00:00:00 2001 From: B Stack Date: Fri, 25 Sep 2020 20:39:41 -0400 Subject: rename to myautomount Add annotations to conf file, separate out initialization so daemon can be per-user. Use a sane name. --- README | 14 +- automount-trayicon.py | 342 -------------------- src/etc/myautomount.conf | 21 ++ src/usr/bin/myautomount-trayicon | 4 + src/usr/bin/myautomountd | 137 ++++++++ .../libexec/myautomount/myautomount-initialize.sh | 24 ++ .../libexec/myautomount/myautomount-trayicon.py | 360 +++++++++++++++++++++ stackrpms-automount.conf | 6 - stackrpms-automount.sh | 153 --------- 9 files changed, 557 insertions(+), 504 deletions(-) delete mode 100755 automount-trayicon.py create mode 100644 src/etc/myautomount.conf create mode 100755 src/usr/bin/myautomount-trayicon create mode 100755 src/usr/bin/myautomountd create mode 100644 src/usr/libexec/myautomount/myautomount-initialize.sh create mode 100755 src/usr/libexec/myautomount/myautomount-trayicon.py delete mode 100644 stackrpms-automount.conf delete mode 100755 stackrpms-automount.sh diff --git a/README b/README index 701eb89..d3f808e 100644 --- a/README +++ b/README @@ -1,4 +1,12 @@ -Translated to shell almost line-for-line from Go source at https://github.com/project-trident/trident-utilities/blame/master/src-go/automount/main.go +asdf +Myautomount was translated to shell almost line-for-line from Go source at https://github.com/project-trident/trident-utilities/blame/master/src-go/automount/main.go but then it diverged. -Future goals: -make system tray icon version, probably that reads xdg desktop entries and displays icons for them +TODO: +write makefile +write full dependencies (including gtk3) + +# TESTING AND BUILDING +Use environment variable MYA_PREFIX for testing. + + export MYA_PREFIX=~/dev/myautomount/src + sudo DEBUG=1 MYA_PREFIX=~/dev/myautomount/src sh -x usr/libexec/myautomount/myautomount-initialize.sh diff --git a/automount-trayicon.py b/automount-trayicon.py deleted file mode 100755 index 84312e7..0000000 --- a/automount-trayicon.py +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/python3 -# File: automount-trayicon.py -# startdate: 2020-09-24 13:17 -# References: -# https://gitlab.com/bgstack15/logout-manager/-/blob/master/src/usr/bin/logout-manager-trayicon -# https://github.com/gapan/xdgmenumaker/blob/master/src/xdgmenumaker -# https://github.com/seb-m/pyinotify/blob/master/python2/examples/stats_threaded.py -# https://stackoverflow.com/questions/28279363/python-way-to-get-mounted-filesystems/28279434#28279434 -# timer https://python-gtk-3-tutorial.readthedocs.io/en/latest/spinner.html?highlight=timer#id1 -# vim: ts=3 sw=3 sts=3 -# Improve: -# move out config to separate file (read main .conf, but in shell format) -# add all headers -# document -# Dependencies: -# autofs, root running the included stackrpms-automount - -import gi, os, fnmatch, sys, pyinotify, time, subprocess -gi.require_version("Gtk","3.0") -from gi.repository import Gtk, Gdk, GLib -import xdg.DesktopEntry as dentry -import xdg.Exceptions as exc - -showmount = 1 # show the "MOUNTED" value for each drive -skip_sd_without_partitions = 1 # skip sdb sdc, etc. # NOT IMPLEMENTED YET. -only_update_on_menuitem = 0 # if 1, then disables the mouseover poll. Prevents showmount=1 from working. -AUTOMOUNT_BASEDIR="/media" -AUTOMOUNT_BROWSEDIR="/browse" - -# FUNCTIONS - -class App: - ''' - A class to keep individual app details in. - ''' - - def __init__(self, name, icon, command, path): - self.name = name - self.icon = icon - self.command = command - self.path = path - - def __repr__(self): - return repr((self.name, self.icon, self.command, self.path)) - -class MenuEntry: - ''' - A class for each menu entry. Includes the class category and app details - from the App class. - ''' - - def __init__(self, category, app): - self.category = category - self.app = app - - def __repr__(self): - return repr((self.category, self.app.name, self.app.icon, self.app.command, self.app.path)) - -class Identity(pyinotify.ProcessEvent): - activity_count = 0 - def process_default(self, event): - print(self,"Does nothing with the event number",self.activity_count) - self.activity_count += 1 - #print(event) - -def remove_command_keys(command, desktopfile, icon): - # replace the %i (icon key) if it's there. This is what freedesktop has to - # say about it: "The Icon key of the desktop entry expanded as two - # arguments, first --icon and then the value of the Icon key. Should not - # expand to any arguments if the Icon key is empty or missing." - if icon: - command = command.replace('"%i"', '--icon {}'.format(icon)) - command = command.replace("'%i'", '--icon {}'.format(icon)) - command = command.replace('%i', '--icon {}'.format(icon)) - # some KDE apps have this "-caption %c" in a few variations. %c is "The - # translated name of the application as listed in the appropriate Name key - # in the desktop entry" according to freedesktop. All apps launch without a - # problem without it as far as I can tell, so it's better to remove it than - # have to deal with extra sets of nested quotes which behave differently in - # each WM. This is not 100% failure-proof. There might be other variations - # of this out there, but we can't account for every single one. If someone - # finds one another one, I can always add it later. - command = command.replace('-caption "%c"', '') - command = command.replace("-caption '%c'", '') - command = command.replace('-caption %c', '') - # replace the %k key. This is what freedesktop says about it: "The - # location of the desktop file as either a URI (if for example gotten from - # the vfolder system) or a local filename or empty if no location is - # known." - command = command.replace('"%k"', desktopfile) - command = command.replace("'%k'", desktopfile) - command = command.replace('%k', desktopfile) - # removing any remaining keys from the command. That can potentially remove - # any other trailing options after the keys, - command = command.partition('%')[0] - return command - -def icon_strip(icon): - # strip the directory and extension from the icon name - icon = os.path.basename(icon) - main, ext = os.path.splitext(icon) - ext = ext.lower() - if ext == '.png' or ext == '.svg' or ext == '.svgz' or ext == '.xpm': - return main - return icon - -def get_entry_info(desktopfile, ico_paths=True): - # customized from gapan/xdgmenumaker - de = dentry.DesktopEntry(filename=desktopfile) - - name = de.getName().encode('utf-8') - - if True: - icon = de.getIcon() - # full resolution of path is not required in this GTK program. - #if ico_paths: - # icon = icon_full_path(icon) - #else: - # icon = icon_strip(icon) - else: - icon = None - - command = de.getExec() - command = remove_command_keys(command, desktopfile, icon) - - terminal = de.getTerminal() - if terminal: - command = '{term} -e {cmd}'.format(term=terminal_app, cmd=command) - - path = de.getPath() - if not path: - path = None - - categories = de.getCategories() - category = "Removable" # hardcoded xdg "category" for automount-trayicon - - app = App(name, icon, command, path) - mentry = MenuEntry(category, app) - return mentry - -def desktopfilelist(params): - # Ripped entirely from gapan/xdgmenumaker - dirs = [] - for i in params: - print("Checking dir",i) - i = i.rstrip('/') - if i not in dirs: - dirs.append(i) - filelist = [] - df_temp = [] - for d in dirs: - xdgdir = '{}/'.format(d) # xdg spec is to use "{}/applications" as this string, so use an applications subdir. - if os.path.isdir(xdgdir): - for root, dirnames, filenames in os.walk(xdgdir): - for i in fnmatch.filter(filenames, '*.desktop'): - # for duplicate .desktop files that exist in more - # than one locations, only keep the first occurence. - # That one should have precedence anyway (e.g. - # ~/.local/share/applications has precedence over - # /usr/share/applications - if i not in df_temp: - df_temp.append(i) - filelist.append(os.path.join(root, i)) - return filelist - -class MainIcon(Gtk.StatusIcon): - def __init__(self): - Gtk.StatusIcon.__init__(self) - self.set_from_icon_name("media-removable") - self.traymenu = Gtk.Menu() - self.connect("button-press-event", self.on_button_press_event) - self.connect("popup-menu", self.context_menu) - self.reestablish_menu() - if not only_update_on_menuitem: - self.connect("query-tooltip", self.mouseover) - # need these anyway, for when the icon is hidden. - self.wm1 = pyinotify.WatchManager() - self.s1 = pyinotify.Stats() - self.identity=Identity(self.s1) # provides self.identity.activity_counter which increments for every change to the watched directory - self.activity_count=0 # the cached value, for comparison. Update the menu when this changes! - self.notifier1 = pyinotify.ThreadedNotifier(self.wm1, default_proc_fun=self.identity) - self.notifier1.start() - self.wm1.add_watch(AUTOMOUNT_BASEDIR, pyinotify.IN_CREATE | pyinotify.IN_DELETE, rec=True, auto_add=True) - - def mouseover(self, second_self, x, y, some_bool, tooltip): - #print("Mouseover happened at",str(x)+",",y,tooltip, some_bool) - need_showmount=0 - if self.identity.activity_count != self.activity_count: - # then we need to update menu - print("Mouseover, and files have changed!") - self.activity_count = self.identity.activity_count - need_showmount=1 - if need_showmount or showmount: - self.reestablish_menu() - - def reestablish_menu(self,widget = None, silent=False): - if not silent: - print("Reestablishing the menu") - try: - if self.menuitems: - del self.menuitems - except: - pass - try: - for i in self.traymenu.get_children(): - self.traymenu.remove(i) - except: - pass - self.menuitems = [] - for entry in get_desktop_entries([AUTOMOUNT_BASEDIR]): - if not silent: - print('{} {} {} {}'.format(entry.app.name.decode("utf-8"),entry.app.icon,entry.app.path,entry.app.command)) - self.add_menuitem(str(entry.app.name.decode("utf-8")),entry.app.path,entry.app.icon,self.execute,entry.app.command) - self.add_separator_to_menu() - if only_update_on_menuitem: - self.add_menuitem("Update menu","",None,self.reestablish_menu,"re establish") # If you want a menu option for this - self.add_menuitem("Hide until next disk change","","",self.hide,"hide") - self.add_menuitem("Exit automount-trayicon","","system-logout",self.exit,"exit") - self.menuitem_count = len(self.menuitems) - 2 - self.set_tooltip_text(str(self.menuitem_count)+" mount point"+("s" if self.menuitem_count > 1 else "")) - - def execute(self, widget): - x=0 ; y=-1 - for n in widget.get_parent().get_children(): - x+=1 - if widget == n: - y=x-1 - break - if y > -1: - print(y,self.menuitems[y]) - # use subprocess to open in background so failure to mount dialogs do not block the program - subprocess.Popen(self.menuitems[y].split()) - #os.system(self.menuitems[y]) - else: - print("Some kind of error?! How did this get called without a menu entry.") - #print(repr(self.s1)) - - def add_separator_to_menu(self): - i=Gtk.SeparatorMenuItem.new() - i.set_visible(True) - self.traymenu.append(i) - - def add_menuitem(self,label_str,label_paren_str,icon_str,function_func,action_str): - self.menuitems.append(action_str) - full_label_str=label_str - mounted_str="" - # collection = [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith('/browse') and line.split()[2] != "autofs"] - if label_paren_str is not None and label_paren_str != "": - if showmount: - if label_paren_str in [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith(AUTOMOUNT_BROWSEDIR) and line.split()[2] != "autofs"]: - label_paren_str = "MOUNTED " + label_paren_str - full_label_str += " (" + label_paren_str + ")" - i = Gtk.ImageMenuItem.new_with_mnemonic(full_label_str) - j = Gtk.Image.new_from_icon_name(icon_str,32) - j.show() - i.set_image(j) - i.set_always_show_image(True) - i.show() - i.connect("activate", function_func) - self.traymenu.append(i) - - def on_button_press_event(self, b_unknown, event: Gdk.EventButton): - # for some reason the single click functionality prevents the double click functionality - if Gdk.EventType._2BUTTON_PRESS == event.type: - # not achievable if we are popping up the menu in the single click. - print("Double click") - else: - print("Single click") - self.traymenu.popup(None, None, - self.position_menu, - self, - event.button, - Gtk.get_current_event_time()) - - def context_menu(self, widget, event_button, event_time): - self.traymenu.popup(None, None, - self.position_menu, - self, - event_button, - Gtk.get_current_event_time()) - - def hide(self, widget = None): - print("Please hide self!") - self.set_visible(False) - self.start_timer() - - def start_timer(self): - """ Start the timer """ - self.counter = 1000 # milliseconds, but hardly a precise timer. Do not use this for real calculations - self.timeout_id = GLib.timeout_add(1,self.on_timeout, None) - - def stop_timer(self, reason_str): - if self.timeout_id: - GLib.source_remove(self.timeout_id) - self.timeout_id = None - #print("Timer was stopped!") - self.check_reenable_now() - - def check_reenable_now(self): - print("Need to check if anything has changed.") - print(self.identity.activity_count,self.activity_count) - if self.identity.activity_count != self.activity_count: - self.set_visible(True) - self.activity_count = self.identity.activity_count - self.reestablish_menu() - else: - print("No changes...") - self.hide() - - def on_timeout(self, *args, **kwargs): - """ A timeout function """ - self.counter -= 1 - if self.counter <= 0: - self.stop_timer("Reached time out") - return False - return True - - def exit(self, widget): - self.notifier1.stop() - quit() - - def __quit__(self): - self.notifier1.stop() - Gtk.StatusIcon.__quit__(self) - -# Normally call get_desktop_entries with (["/media"]) -def get_desktop_entries(dir_array): - entries = [] - for desktopfile in desktopfilelist(dir_array): - try: - entry = get_entry_info(desktopfile, True) - if entry is not None: - #print(entry) - entries.append(entry) - except exc.ParsingError as Ex: - print(Ex) - pass - return entries - -# MAIN -icon = MainIcon() -Gtk.main() diff --git a/src/etc/myautomount.conf b/src/etc/myautomount.conf new file mode 100644 index 0000000..eef2ca8 --- /dev/null +++ b/src/etc/myautomount.conf @@ -0,0 +1,21 @@ +# This config file is dot-sourced by a shell script, for both the daemon +# and myautomount-trayicon. + +# TMPFILE is only needed for trayicon +export AUTOMOUNT_TMPFILE="$( TMPDIR="/run/user/${UID}" mktemp -t ${USER}.automount.XXXXXXXXX )" + +# DIR and FILE are for the initialization only. These are the most +# likely to change per distro. +export AUTOMOUNT_DIR=/etc/auto.master.d +export AUTOMOUNT_DIR_FILE=/etc/auto.master.d/myautomount.autofs +export AUTOMOUNT_FILE=/etc/autofs.myautomount + +# BASEDIR is where the pseudo-xdg desktop files will go. This directory +# should not be controlled by anything else. +# The daemon places desktop files here that the trayicon uses. +export AUTOMOUNT_BASEDIR=/run/user/${UID}/automedia + +# This needs to match what you put in /etc/autofs.myautomount, and is +# where the filesystems get mounted to. This location should not be +# controlled by anything else. +export AUTOMOUNT_BROWSEDIR=/browse diff --git a/src/usr/bin/myautomount-trayicon b/src/usr/bin/myautomount-trayicon new file mode 100755 index 0000000..08be25d --- /dev/null +++ b/src/usr/bin/myautomount-trayicon @@ -0,0 +1,4 @@ +#!/bin/sh +# Part of myautomount project +. ${MYA_PREFIX}/etc/myautomount.conf +${MYA_PREFIX}/usr/libexec/myautomount/myautomount-trayicon.py diff --git a/src/usr/bin/myautomountd b/src/usr/bin/myautomountd new file mode 100755 index 0000000..d23940a --- /dev/null +++ b/src/usr/bin/myautomountd @@ -0,0 +1,137 @@ +#!/bin/sh +# File: /usr/bin/myautomountd +# Location: gitlab +# Authors: beanpole135, bgstack15 +# Startdate: 2020-09-23 +# Title: Automount in Shell +# Purpose: almost one-for-one translation of Go version +# History: +# 2020-09-23 translated by bgstack15 to shell from Go version (reference 1) +# Several translation notes: configuration split out into separate file +# and given a Fedora flavor. +# Usage: +# Invoke this at X startup. This can be from the xdg autostart mechanism, or "exec myautomountd &" in ~/fluxbox.startup +# This is the backend to myautomount-trayicon +# Reference: +# https://github.com/project-trident/trident-utilities/blob/master/src-go/automount/main.go +# Improve: +# Dependencies: +# udevadm (from systemd-udev or eudev) +# dep-devuan: eudev + +# FUNCTIONS +clean_automount() { + rm -f "${AUTOMOUNT_TMPFILE}" + kill "${AUTOMOUNT_PID}" +} + +reset_tmpfile() { + cat /dev/null > "${AUTOMOUNT_TMPFILE}" +} + +handleEvent() { + # call: handleEvent "${STRING}" + _line="${1}" + test -n "${STACKTRACE}" && echo "handleEvent \"${_line}\"" 1>&2 + echo "${_line}" | grep -qvE "^UDEV" && return # not a valid entry - possibly a startup message + test $( echo "${_line}" | wc -w ) -ne 5 && return + _deviceid= + _eventType= + for word in ${_line} ; do + # no opportunity for the bash for statement to read a blank value here from unquoted variable + if echo "${word}" | grep -qE '^(add|remove|change)$' ; + then + _eventType="${word}" + elif echo "${word}" | grep -qE "^\/devices\/" ; + then + _deviceid="$( echo "${word}" | awk -F'/' '{print $NF}' )" + fi + done + { test "${_deviceid}" = "" || test "${_eventType}" = "" ; } && return + test -n "${VERBOSE}" || test -n "${DEBUG}" && echo "Got device event: ${_eventType} ${_deviceid}" 1>&2 + _entry="${AUTOMOUNT_BASEDIR}/${_deviceid}.desktop" + case "${_eventType}" in + "add") + createEntry "${_deviceid}" "${_entry}" + ;; + *) # anything else + test -e "${_entry}" && { rm "${_entry}" || : ; } + test "${_eventType}" = "change" && createEntry "${_deviceid}" "${_entry}" + ;; + esac +} + +createEntry() { + # call: createEntry "{device}" "${filepath}" + _device="${1}" + _filepath="${2}" + test -n "${STACKTRACE}" && echo "STUB createEntry \"${_device}\" \"${_filepath}\"" 1>&2 + _fs= + _model= + _vendor= + _label= + _atracks= + _bytes="$( udevadm info "/dev/${_device}" 2>/dev/null )" + _shortbytes="$( printf "%s\n" "${_bytes}" | sed -r -e 's/^E:\s*//;' | grep -E '^(ID_FS_TYPE|ID_MODEL|ID_VENDOR|ID_FS_LABEL|ID_CDROM_MEDIA_TRACK_COUNT_AUDIO)=' )" + unset ID_FS_TYPE ID_MODEL ID_VENDOR ID_FS_LABEL ID_CDROM_MEDIA_TRACK_COUNT_AUDIO + eval "${_shortbytes}" + _fs="${ID_FS_TYPE}" + _model="${ID_MODEL}" + _vendor="${ID_VENDOR}" + _label="${ID_FS_LABEL}" + _atracks="${ID_CDROM_MEDIA_TRACK_COUNT_AUDIO}" + test -n "${DEBUG}" && echo "fs=${_fs} model=${_model} vendor=${_vendor} label=${_label} atracks=${_atracks}" 1>&2 + test "${_fs}" = "" && test "${_atracks}" = "" && return # if the fs cannot be detected + touch "${_filepath}" ; chmod 0755 "${_filepath}" + { + echo "[Desktop Entry]" + echo "Version=1.1" + if test "${_fs}" = "udf" ; then + echo "Type=Application" + echo "Exec=xdg-open dvd:///dev/${_device}" + elif test -n "${_atracks}" ; then + test -n "${_label}" && _label="Audio CD" + echo "Type=Application" + echo "Exec=xdg-open cdda:///dev/${_device}" + else + echo "Type=Application" + echo "Exec=xdg-open ${AUTOMOUNT_BROWSEDIR}/${_device}" + echo "Path=${AUTOMOUNT_BROWSEDIR}/${_device}" + fi + if test -z "${_label}" ; then + echo "Name=${_vendor} ${_model}" + else + echo "Name=${_label}" + echo "GenericName=${_vendor} ${_model}" + fi + echo "Comment=${_device} (${_fs})" + case "${_fs}" in + "cd9600") echo "Icon=media-optical" ;; + "udf") echo "Icon=media-optical-dvd" ;; + "") echo "Icon=media-optical-audio" ;; + *) echo "Icon=media-removable" ;; + esac + } > "${_filepath}" +} + +# INITIALIZE +. ${MYA_PREFIX}/etc/myautomount.conf +trap '__ec=$? ; clean_automount ; trap "" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 ; exit ${__ec} ;' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 +mkdir -p "${AUTOMOUNT_BASEDIR}" + +# MAIN +# start udevadm +udevadm monitor -u -s block 1> "${AUTOMOUNT_TMPFILE}" & +export AUTOMOUNT_PID="${!}" +test -n "${DEBUG}" && env | grep -E '^AUTOMOUNT_' 1>&2 +while ! test -e /tmp/kill-automount ; +do + tail -F "${AUTOMOUNT_TMPFILE}" 2>/dev/null | while read line ; + do + handleEvent "${line}" + _length="$( wc -l < "${AUTOMOUNT_TMPFILE}" 2>/dev/null )" ; test -z "${_length}" && _length=0 + test "${line}" = "$( tail -n1 "${AUTOMOUNT_TMPFILE}" )" && test ${_length} -gt 200 && reset_tmpfile + done + # the tail finished for some reason, so clear the file + reset_tmpfile +done diff --git a/src/usr/libexec/myautomount/myautomount-initialize.sh b/src/usr/libexec/myautomount/myautomount-initialize.sh new file mode 100644 index 0000000..fc75d36 --- /dev/null +++ b/src/usr/libexec/myautomount/myautomount-initialize.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Only needs to be run once. This is more for reference than for using. The package should handle these tasks. + +fail() { + echo "${@}" 1>&2 + exit 1 +} + +setupSystem() { + _needrestart=0 + mkdir -m0755 -p "${AUTOMOUNT_DIR}" || fail "Could not setup autofs rules! Check if this is being run as root?" + ! test -f "${AUTOMOUNT_FILE}" && { + { touch "${AUTOMOUNT_FILE}" && echo "* -fstype=auto,rw,nosuid,uid=${USER},gid=users :/dev/& " > "${AUTOMOUNT_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?" + _needrestart=1 + } + ! test -f "${AUTOMOUNT_DIR_FILE}" && { + { touch "${AUTOMOUNT_DIR_FILE}" && echo "${AUTOMOUNT_BROWSEDIR} ${AUTOMOUNT_FILE} --timeout=5 " > "${AUTOMOUNT_DIR_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?" + _needrestart=1 + } + test ${_needrestart} -eq 1 && eval "service autofs restart" +} + +. ${MYA_PREFIX}/etc/myautomount.conf +setupSystem diff --git a/src/usr/libexec/myautomount/myautomount-trayicon.py b/src/usr/libexec/myautomount/myautomount-trayicon.py new file mode 100755 index 0000000..257c54f --- /dev/null +++ b/src/usr/libexec/myautomount/myautomount-trayicon.py @@ -0,0 +1,360 @@ +#!/usr/bin/python3 +# File: /usr/libexec/myautomount/automount-trayicon.py +# License: CC-BY-SA 4.0 +# Author: bgstack15 +# Startdate: 2020-09-24 13:17 +# Title: Tray icon for automount daemon +# Purpose: Easy access to autofs-mounted removable media +# History: +# Usage: +# to be called from /usr/bin/myautomount-trayicon only, because it loads the environment variables +# Reference: +# https://gitlab.com/bgstack15/logout-manager/-/blob/master/src/usr/bin/logout-manager-trayicon +# https://github.com/gapan/xdgmenumaker/blob/master/src/xdgmenumaker +# https://github.com/seb-m/pyinotify/blob/master/python2/examples/stats_threaded.py +# https://stackoverflow.com/questions/28279363/python-way-to-get-mounted-filesystems/28279434#28279434 +# timer https://python-gtk-3-tutorial.readthedocs.io/en/latest/spinner.html?highlight=timer#id1 +# Improve: +# move out user configs to separate file in ~/.config/myautomount, but using the xdg spec. +# add all headers +# document +# add option to HIDE WHEN NO MEDIA +# Dependencies: +# dep-devuan: autofs, python3-pyinotify +# dep-fedora: +# dep-pip: inotify +# python-inotify, the seb-m one. +# And whatever provides Gtk 3.0 for python3. + +import gi, os, fnmatch, sys, pyinotify, time, subprocess +gi.require_version("Gtk","3.0") +from gi.repository import Gtk, Gdk, GLib +import xdg.DesktopEntry as dentry +import xdg.Exceptions as exc + +showmount = 1 # show the "MOUNTED" value for each drive +skip_sd_without_partitions = 1 # skip sdb sdc, etc. # NOT IMPLEMENTED YET. +only_update_on_menuitem = 0 # if 1, then disables the mouseover poll. Prevents showmount=1 from working. + +# load environment variables +AUTOMOUNT_BASEDIR = os.getenv("AUTOMOUNT_BASEDIR") # probably /run/user/${UID}/automedia +if not AUTOMOUNT_BASEDIR: + print("Need env var AUTOMOUNT_BASEDIR. Aborted.",file=sys.stderr) + sys.exit(1) +AUTOMOUNT_BROWSEDIR = os.getenv("AUTOMOUNT_BROWSEDIR") # probably /browse +if showmount and not AUTOMOUNT_BROWSEDIR: + print("Without AUTOMOUNT_BROWSEDIR, showmount option cannot be used. Continuing...",file=sys.stderr) + +# FUNCTIONS + +class App: + ''' + A class to keep individual app details in. + ''' + + def __init__(self, name, icon, command, path): + self.name = name + self.icon = icon + self.command = command + self.path = path + + def __repr__(self): + return repr((self.name, self.icon, self.command, self.path)) + +class MenuEntry: + ''' + A class for each menu entry. Includes the class category and app details + from the App class. + ''' + + def __init__(self, category, app): + self.category = category + self.app = app + + def __repr__(self): + return repr((self.category, self.app.name, self.app.icon, self.app.command, self.app.path)) + +class Identity(pyinotify.ProcessEvent): + activity_count = 0 + def process_default(self, event): + print(self,"Does nothing with the event number",self.activity_count) + self.activity_count += 1 + #print(event) + +def remove_command_keys(command, desktopfile, icon): + # replace the %i (icon key) if it's there. This is what freedesktop has to + # say about it: "The Icon key of the desktop entry expanded as two + # arguments, first --icon and then the value of the Icon key. Should not + # expand to any arguments if the Icon key is empty or missing." + if icon: + command = command.replace('"%i"', '--icon {}'.format(icon)) + command = command.replace("'%i'", '--icon {}'.format(icon)) + command = command.replace('%i', '--icon {}'.format(icon)) + # some KDE apps have this "-caption %c" in a few variations. %c is "The + # translated name of the application as listed in the appropriate Name key + # in the desktop entry" according to freedesktop. All apps launch without a + # problem without it as far as I can tell, so it's better to remove it than + # have to deal with extra sets of nested quotes which behave differently in + # each WM. This is not 100% failure-proof. There might be other variations + # of this out there, but we can't account for every single one. If someone + # finds one another one, I can always add it later. + command = command.replace('-caption "%c"', '') + command = command.replace("-caption '%c'", '') + command = command.replace('-caption %c', '') + # replace the %k key. This is what freedesktop says about it: "The + # location of the desktop file as either a URI (if for example gotten from + # the vfolder system) or a local filename or empty if no location is + # known." + command = command.replace('"%k"', desktopfile) + command = command.replace("'%k'", desktopfile) + command = command.replace('%k', desktopfile) + # removing any remaining keys from the command. That can potentially remove + # any other trailing options after the keys, + command = command.partition('%')[0] + return command + +def icon_strip(icon): + # strip the directory and extension from the icon name + icon = os.path.basename(icon) + main, ext = os.path.splitext(icon) + ext = ext.lower() + if ext == '.png' or ext == '.svg' or ext == '.svgz' or ext == '.xpm': + return main + return icon + +def get_entry_info(desktopfile, ico_paths=True): + # customized from gapan/xdgmenumaker + de = dentry.DesktopEntry(filename=desktopfile) + + name = de.getName().encode('utf-8') + + if True: + icon = de.getIcon() + # full resolution of path is not required in this GTK program. + #if ico_paths: + # icon = icon_full_path(icon) + #else: + # icon = icon_strip(icon) + else: + icon = None + + command = de.getExec() + command = remove_command_keys(command, desktopfile, icon) + + terminal = de.getTerminal() + if terminal: + command = '{term} -e {cmd}'.format(term=terminal_app, cmd=command) + + path = de.getPath() + if not path: + path = None + + categories = de.getCategories() + category = "Removable" # hardcoded xdg "category" for automount-trayicon + + app = App(name, icon, command, path) + mentry = MenuEntry(category, app) + return mentry + +def desktopfilelist(params): + # Ripped entirely from gapan/xdgmenumaker + dirs = [] + for i in params: + print("Checking dir",i) + i = i.rstrip('/') + if i not in dirs: + dirs.append(i) + filelist = [] + df_temp = [] + for d in dirs: + xdgdir = '{}/'.format(d) # xdg spec is to use "{}/applications" as this string, so use an applications subdir. + if os.path.isdir(xdgdir): + for root, dirnames, filenames in os.walk(xdgdir): + for i in fnmatch.filter(filenames, '*.desktop'): + # for duplicate .desktop files that exist in more + # than one locations, only keep the first occurence. + # That one should have precedence anyway (e.g. + # ~/.local/share/applications has precedence over + # /usr/share/applications + if i not in df_temp: + df_temp.append(i) + filelist.append(os.path.join(root, i)) + return filelist + +class MainIcon(Gtk.StatusIcon): + def __init__(self): + Gtk.StatusIcon.__init__(self) + self.set_from_icon_name("media-removable") + self.traymenu = Gtk.Menu() + self.connect("button-press-event", self.on_button_press_event) + self.connect("popup-menu", self.context_menu) + self.reestablish_menu() + if not only_update_on_menuitem: + self.connect("query-tooltip", self.mouseover) + # need these anyway, for when the icon is hidden. + self.wm1 = pyinotify.WatchManager() + self.s1 = pyinotify.Stats() + self.identity=Identity(self.s1) # provides self.identity.activity_counter which increments for every change to the watched directory + self.activity_count=0 # the cached value, for comparison. Update the menu when this changes! + self.notifier1 = pyinotify.ThreadedNotifier(self.wm1, default_proc_fun=self.identity) + self.notifier1.start() + self.wm1.add_watch(AUTOMOUNT_BASEDIR, pyinotify.IN_CREATE | pyinotify.IN_DELETE, rec=True, auto_add=True) + + def mouseover(self, second_self, x, y, some_bool, tooltip): + #print("Mouseover happened at",str(x)+",",y,tooltip, some_bool) + need_showmount=0 + if self.identity.activity_count != self.activity_count: + # then we need to update menu + print("Mouseover, and files have changed!") + self.activity_count = self.identity.activity_count + need_showmount=1 + if need_showmount or showmount: + self.reestablish_menu() + + def reestablish_menu(self,widget = None, silent=False): + if not silent: + print("Reestablishing the menu") + try: + if self.menuitems: + del self.menuitems + except: + pass + try: + for i in self.traymenu.get_children(): + self.traymenu.remove(i) + except: + pass + self.menuitems = [] + for entry in get_desktop_entries([AUTOMOUNT_BASEDIR]): + if not silent: + print('{} {} {} {}'.format(entry.app.name.decode("utf-8"),entry.app.icon,entry.app.path,entry.app.command)) + self.add_menuitem(str(entry.app.name.decode("utf-8")),entry.app.path,entry.app.icon,self.execute,entry.app.command) + self.add_separator_to_menu() + if only_update_on_menuitem: + self.add_menuitem("Update menu","",None,self.reestablish_menu,"re establish") # If you want a menu option for this + self.add_menuitem("Hide until next disk change","","",self.hide,"hide") + self.add_menuitem("Exit automount-trayicon","","system-logout",self.exit,"exit") + self.menuitem_count = len(self.menuitems) - 2 + self.set_tooltip_text(str(self.menuitem_count)+" mount point"+("s" if self.menuitem_count > 1 else "")) + + def execute(self, widget): + x=0 ; y=-1 + for n in widget.get_parent().get_children(): + x+=1 + if widget == n: + y=x-1 + break + if y > -1: + print(y,self.menuitems[y]) + # use subprocess to open in background so failure to mount dialogs do not block the program + subprocess.Popen(self.menuitems[y].split()) + #os.system(self.menuitems[y]) + else: + print("Some kind of error?! How did this get called without a menu entry.") + #print(repr(self.s1)) + + def add_separator_to_menu(self): + i=Gtk.SeparatorMenuItem.new() + i.set_visible(True) + self.traymenu.append(i) + + def add_menuitem(self,label_str,label_paren_str,icon_str,function_func,action_str): + self.menuitems.append(action_str) + full_label_str=label_str + mounted_str="" + # collection = [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith('/browse') and line.split()[2] != "autofs"] + if label_paren_str is not None and label_paren_str != "": + if showmount and AUTOMOUNT_BROWSEDIR: + if label_paren_str in [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith(AUTOMOUNT_BROWSEDIR) and line.split()[2] != "autofs"]: + label_paren_str = "MOUNTED " + label_paren_str + full_label_str += " (" + label_paren_str + ")" + i = Gtk.ImageMenuItem.new_with_mnemonic(full_label_str) + j = Gtk.Image.new_from_icon_name(icon_str,32) + j.show() + i.set_image(j) + i.set_always_show_image(True) + i.show() + i.connect("activate", function_func) + self.traymenu.append(i) + + def on_button_press_event(self, b_unknown, event: Gdk.EventButton): + # for some reason the single click functionality prevents the double click functionality + if Gdk.EventType._2BUTTON_PRESS == event.type: + # not achievable if we are popping up the menu in the single click. + print("Double click") + else: + print("Single click") + self.traymenu.popup(None, None, + self.position_menu, + self, + event.button, + Gtk.get_current_event_time()) + + def context_menu(self, widget, event_button, event_time): + self.traymenu.popup(None, None, + self.position_menu, + self, + event_button, + Gtk.get_current_event_time()) + + def hide(self, widget = None): + print("Please hide self!") + self.set_visible(False) + self.start_timer() + + def start_timer(self): + """ Start the timer """ + self.counter = 1000 # milliseconds, but hardly a precise timer. Do not use this for real calculations + self.timeout_id = GLib.timeout_add(1,self.on_timeout, None) + + def stop_timer(self, reason_str): + if self.timeout_id: + GLib.source_remove(self.timeout_id) + self.timeout_id = None + #print("Timer was stopped!") + self.check_reenable_now() + + def check_reenable_now(self): + print("Need to check if anything has changed.") + print(self.identity.activity_count,self.activity_count) + if self.identity.activity_count != self.activity_count: + self.set_visible(True) + self.activity_count = self.identity.activity_count + self.reestablish_menu() + else: + print("No changes...") + self.hide() + + def on_timeout(self, *args, **kwargs): + """ A timeout function """ + self.counter -= 1 + if self.counter <= 0: + self.stop_timer("Reached time out") + return False + return True + + def exit(self, widget): + self.notifier1.stop() + quit() + + def __quit__(self): + self.notifier1.stop() + Gtk.StatusIcon.__quit__(self) + +# Normally call get_desktop_entries with (["/media"]) +def get_desktop_entries(dir_array): + entries = [] + for desktopfile in desktopfilelist(dir_array): + try: + entry = get_entry_info(desktopfile, True) + if entry is not None: + #print(entry) + entries.append(entry) + except exc.ParsingError as Ex: + print(Ex) + pass + return entries + +# MAIN +icon = MainIcon() +Gtk.main() diff --git a/stackrpms-automount.conf b/stackrpms-automount.conf deleted file mode 100644 index a191a57..0000000 --- a/stackrpms-automount.conf +++ /dev/null @@ -1,6 +0,0 @@ -export AUTOMOUNT_TMPFILE="$( TMPDIR=/tmp mktemp -t ${USER}.automount.XXXXXXXXX )" -export AUTOMOUNT_DIR=/etc/auto.master.d -export AUTOMOUNT_FILE=/etc/autofs.stackrpms -export AUTOMOUNT_DIR_FILE=/etc/auto.master.d/stackrpms.autofs -export AUTOMOUNT_BASEDIR=/media -export AUTOMOUNT_BROWSEDIR=/browse diff --git a/stackrpms-automount.sh b/stackrpms-automount.sh deleted file mode 100755 index a3f7c09..0000000 --- a/stackrpms-automount.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/bin/sh -# File: stackrpms-automount.sh -# Location: gitlab -# Authors: beanpole135, bgstack15 -# Startdate: 2020-09-23 -# Title: Automount in Shell -# Purpose: almost one-for-one translation of Go version -# History: -# 2020-09-23 translated by bgstack15 to shell from Go version (reference 1) -# Several translation notes: configuration split out into separate file -# and given a Fedora flavor. -# Usage: -# Run as root. -# Reference: -# 1: https://github.com/project-trident/trident-utilities/blob/master/src-go/automount/main.go -# Improve: -# Dependencies: udevadm (from systemd-udev or eudev) - -# FUNCTIONS -clean_automount() { - rm -f "${AUTOMOUNT_TMPFILE}" - kill "${AUTOMOUNT_PID}" -} - -reset_tmpfile() { - cat /dev/null > "${AUTOMOUNT_TMPFILE}" -} - -handleEvent() { - # call: handleEvent "${STRING}" - _line="${1}" - test -n "${STACKTRACE}" && echo "handleEvent \"${_line}\"" 1>&2 - echo "${_line}" | grep -qvE "^UDEV" && return # not a valid entry - possibly a startup message - test $( echo "${_line}" | wc -w ) -ne 5 && return - _deviceid= - _eventType= - for word in ${_line} ; do - # no opportunity for the bash for statement to read a blank value here from unquoted variable - if echo "${word}" | grep -qE '^(add|remove|change)$' ; - then - _eventType="${word}" - elif echo "${word}" | grep -qE "^\/devices\/" ; - then - _deviceid="$( echo "${word}" | awk -F'/' '{print $NF}' )" - fi - done - { test "${_deviceid}" = "" || test "${_eventType}" = "" ; } && return - test -n "${VERBOSE}" || test -n "${DEBUG}" && echo "Got device event: ${_eventType} ${_deviceid}" 1>&2 - _entry="${AUTOMOUNT_BASEDIR}/${_deviceid}.desktop" - case "${_eventType}" in - "add") - createEntry "${_deviceid}" "${_entry}" - ;; - *) # anything else - test -e "${_entry}" && { rm "${_entry}" || : ; } - test "${_eventType}" = "change" && createEntry "${_deviceid}" "${_entry}" - ;; - esac -} - -createEntry() { - # call: createEntry "{device}" "${filepath}" - _device="${1}" - _filepath="${2}" - test -n "${STACKTRACE}" && echo "STUB createEntry \"${_device}\" \"${_filepath}\"" 1>&2 - _fs= - _model= - _vendor= - _label= - _atracks= - _bytes="$( udevadm info "/dev/${_device}" 2>/dev/null )" - _shortbytes="$( printf "%s\n" "${_bytes}" | sed -r -e 's/^E:\s*//;' | grep -E '^(ID_FS_TYPE|ID_MODEL|ID_VENDOR|ID_FS_LABEL|ID_CDROM_MEDIA_TRACK_COUNT_AUDIO)=' )" - unset ID_FS_TYPE ID_MODEL ID_VENDOR ID_FS_LABEL ID_CDROM_MEDIA_TRACK_COUNT_AUDIO - eval "${_shortbytes}" - _fs="${ID_FS_TYPE}" - _model="${ID_MODEL}" - _vendor="${ID_VENDOR}" - _label="${ID_FS_LABEL}" - _atracks="${ID_CDROM_MEDIA_TRACK_COUNT_AUDIO}" - test -n "${DEBUG}" && echo "fs=${_fs} model=${_model} vendor=${_vendor} label=${_label} atracks=${_atracks}" 1>&2 - test "${_fs}" = "" && test "${_atracks}" = "" && return # if the fs cannot be detected - touch "${_filepath}" ; chmod 0755 "${_filepath}" - { - echo "[Desktop Entry]" - echo "Version=1.1" - if test "${_fs}" = "udf" ; then - echo "Type=Application" - echo "Exec=xdg-open dvd:///dev/${_device}" - elif test -n "${_atracks}" ; then - test -n "${_label}" && _label="Audio CD" - echo "Type=Application" - echo "Exec=xdg-open cdda:///dev/${_device}" - else - echo "Type=Application" - echo "Exec=xdg-open ${AUTOMOUNT_BROWSEDIR}/${_device}" - echo "Path=${AUTOMOUNT_BROWSEDIR}/${_device}" - fi - if test -z "${_label}" ; then - echo "Name=${_vendor} ${_model}" - else - echo "Name=${_label}" - echo "GenericName=${_vendor} ${_model}" - fi - echo "Comment=${_device} (${_fs})" - case "${_fs}" in - "cd9600") echo "Icon=media-optical" ;; - "udf") echo "Icon=media-optical-dvd" ;; - "") echo "Icon=media-optical-audio" ;; - *) echo "Icon=media-removable" ;; - esac - } > "${_filepath}" -} - -setupSystem() { - _needrestart=0 - mkdir -m0755 -p "${AUTOMOUNT_DIR}" || fail "Could not setup autofs rules! Check if this is being run as root?" - ! test -f "${AUTOMOUNT_FILE}" && { - { touch "${AUTOMOUNT_FILE}" && echo "* -fstype=auto,rw,nosuid,uid=${USER},gid=users :/dev/& " > "${AUTOMOUNT_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?" - _needrestart=1 - } - ! test -f "${AUTOMOUNT_DIR_FILE}" && { - { touch "${AUTOMOUNT_DIR_FILE}" && echo "${AUTOMOUNT_BROWSEDIR} ${AUTOMOUNT_FILE} --timeout=5 " > "${AUTOMOUNT_DIR_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?" - _needrestart=1 - } - test ${_needrestart} -eq 1 && eval "service autofs restart" -} - -fail() { - echo "${@}" 1>&2 - exit 1 -} - -# INITIALIZE -. ${0%%.sh}.conf -trap '__ec=$? ; clean_automount ; trap "" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 ; exit ${__ec} ;' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 -setupSystem - -# MAIN -# start udevadm -udevadm monitor -u -s block 1> "${AUTOMOUNT_TMPFILE}" & -export AUTOMOUNT_PID="${!}" -test -n "${DEBUG}" && env | grep -E '^AUTOMOUNT_' 1>&2 -while ! test -e /tmp/kill-automount ; -do - tail -F "${AUTOMOUNT_TMPFILE}" 2>/dev/null | while read line ; - do - handleEvent "${line}" - _length="$( wc -l < "${AUTOMOUNT_TMPFILE}" 2>/dev/null )" ; test -z "${_length}" && _length=0 - test "${line}" = "$( tail -n1 "${AUTOMOUNT_TMPFILE}" )" && test ${_length} -gt 200 && reset_tmpfile - done - # the tail finished for some reason, so clear the file - reset_tmpfile -done -- cgit