#!/bin/sh # Copyright (C) 2016 Velocloud Inc # Copyright (C) 2016 Aleksander Morgado ################################################################################ . /lib/functions.sh . /lib/netifd/netifd-proto.sh ################################################################################ # Runtime state MODEMMANAGER_RUNDIR="/var/run/modemmanager" MODEMMANAGER_PID_FILE="${MODEMMANAGER_RUNDIR}/modemmanager.pid" MODEMMANAGER_CDCWDM_CACHE="${MODEMMANAGER_RUNDIR}/cdcwdm.cache" MODEMMANAGER_SYSFS_CACHE="${MODEMMANAGER_RUNDIR}/sysfs.cache" MODEMMANAGER_EVENTS_CACHE="${MODEMMANAGER_RUNDIR}/events.cache" ################################################################################ # Common logging mm_log() { local level="$1"; shift logger -p "daemon.${level}" -t "ModemManager[$$]" "hotplug: $*" } ################################################################################ # Receives as input argument the full sysfs path of the device # Returns the physical device sysfs path # # NOTE: this method only works when the device exists, i.e. it cannot be used # on removal hotplug events mm_find_physdev_sysfs_path() { local tmp_path="$1" while true; do tmp_path=$(dirname "${tmp_path}") # avoid infinite loops iterating [ -z "${tmp_path}" ] || [ "${tmp_path}" = "/" ] && return # For USB devices, the physical device will be that with a idVendor # and idProduct pair of files [ -f "${tmp_path}"/idVendor ] && [ -f "${tmp_path}"/idProduct ] && { tmp_path=$(readlink -f "$tmp_path") echo "${tmp_path}" return } # For PCI devices, the physical device will be that with a vendor # and device pair of files [ -f "${tmp_path}"/vendor ] && [ -f "${tmp_path}"/device ] && { tmp_path=$(readlink -f "$tmp_path") echo "${tmp_path}" return } done } ################################################################################ # Returns the cdc-wdm name retrieved from sysfs mm_track_cdcwdm() { local wwan="$1" local cdcwdm cdcwdm=$(ls "/sys/class/net/${wwan}/device/usbmisc/") [ -n "${cdcwdm}" ] || return # We have to cache it for later, as we won't be able to get the # associated cdc-wdm device on a remove event echo "${wwan} ${cdcwdm}" >> "${MODEMMANAGER_CDCWDM_CACHE}" echo "${cdcwdm}" } # Returns the cdc-wdm name retrieved from the cache mm_untrack_cdcwdm() { local wwan="$1" local cdcwdm # Look for the cached associated cdc-wdm device [ -f "${MODEMMANAGER_CDCWDM_CACHE}" ] || return cdcwdm=$(awk -v wwan="${wwan}" '!/^#/ && $0 ~ wwan { print $2 }' "${MODEMMANAGER_CDCWDM_CACHE}") [ -n "${cdcwdm}" ] || return # Remove from cache sed -i "/${wwan} ${cdcwdm}/d" "${MODEMMANAGER_CDCWDM_CACHE}" echo "${cdcwdm}" } ################################################################################ # ModemManager needs some time from the ports being added until a modem object # is exposed in DBus. With the logic here we do an explicit wait of N seconds # for ModemManager to expose the new modem object, making sure that the wait is # unique per device (i.e. per physical device sysfs path). # Gets the modem wait status as retrieved from the cache mm_get_modem_wait_status() { local sysfspath="$1" # If no sysfs cache file, we're done [ -f "${MODEMMANAGER_SYSFS_CACHE}" ] || return # Get status of the sysfs path awk -v sysfspath="${sysfspath}" '!/^#/ && $0 ~ sysfspath { print $2 }' "${MODEMMANAGER_SYSFS_CACHE}" } # Clear the modem wait status from the cache, if any mm_clear_modem_wait_status() { local sysfspath="$1" local escaped_sysfspath [ -f "${MODEMMANAGER_SYSFS_CACHE}" ] && { # escape '/', '\' and '&' for sed... escaped_sysfspath=$(echo "$sysfspath" | sed -e 's/[\/&]/\\&/g') sed -i "/${escaped_sysfspath}/d" "${MODEMMANAGER_SYSFS_CACHE}" } } # Sets the modem wait status in the cache mm_set_modem_wait_status() { local sysfspath="$1" local status="$2" # Remove sysfs line before adding the new one with the new state mm_clear_modem_wait_status "${sysfspath}" # Add the new status echo "${sysfspath} ${status}" >> "${MODEMMANAGER_SYSFS_CACHE}" } # Callback for config_foreach() mm_get_modem_config_foreach_cb() { local cfg="$1" local sysfspath="$2" local proto config_get proto "${cfg}" proto [ "${proto}" = modemmanager ] || return 0 local dev dev=$(uci_get network "${cfg}" device) [ "${dev}" = "${sysfspath}" ] || return 0 echo "${cfg}" } # Returns the name of the interface configured for this device mm_get_modem_config() { local sysfspath="$1" # Look for configuration for the given sysfs path config_load network config_foreach mm_get_modem_config_foreach_cb interface "${sysfspath}" } # Wait for a modem in the specified sysfspath mm_wait_for_modem() { local cfg="$1" local sysfspath="$2" # TODO: config max wait local n=45 local step=5 while [ $n -ge 0 ]; do [ -d "${sysfspath}" ] || { mm_log "error" "ignoring modem detection request: no device at ${sysfspath}" proto_set_available "${cfg}" 0 return 1 } # Check if the modem exists at the given sysfs path if ! mmcli -m "${sysfspath}" > /dev/null 2>&1 then mm_log "error" "modem not detected at sysfs path" else mm_log "info" "modem exported successfully at ${sysfspath}" mm_log "info" "setting interface '${cfg}' as available" proto_set_available "${cfg}" 1 return 0 fi sleep $step n=$((n-step)) done mm_log "error" "timed out waiting for the modem to get exported at ${sysfspath}" proto_set_available "${cfg}" 0 return 2 } mm_report_modem_wait() { local sysfspath=$1 local parent_sysfspath status parent_sysfspath=$(mm_find_physdev_sysfs_path "$sysfspath") [ -n "${parent_sysfspath}" ] || { mm_log "error" "parent device sysfspath not found" return } status=$(mm_get_modem_wait_status "${parent_sysfspath}") case "${status}" in "") local cfg cfg=$(mm_get_modem_config "${parent_sysfspath}") if [ -n "${cfg}" ]; then mm_log "info" "interface '${cfg}' is set to configure device '${parent_sysfspath}'" mm_log "info" "now waiting for modem at sysfs path ${parent_sysfspath}" mm_set_modem_wait_status "${parent_sysfspath}" "processed" # Launch subshell for the explicit wait ( mm_wait_for_modem "${cfg}" "${parent_sysfspath}" ) > /dev/null 2>&1 & else mm_log "info" "no need to wait for modem at sysfs path ${parent_sysfspath}" mm_set_modem_wait_status "${parent_sysfspath}" "ignored" fi ;; "processed") mm_log "info" "already waiting for modem at sysfs path ${parent_sysfspath}" ;; "ignored") ;; *) mm_log "error" "unknown status read for device at sysfs path ${parent_sysfspath}" ;; esac } ################################################################################ # Cleanup interfaces mm_cleanup_interface_cb() { local cfg="$1" local proto config_get proto "${cfg}" proto [ "${proto}" = modemmanager ] || return 0 proto_set_available "${cfg}" 0 } mm_cleanup_interfaces() { config_load network config_foreach mm_cleanup_interface_cb interface } mm_cleanup_interface_by_sysfspath() { local dev="$1" local cfg cfg=$(mm_get_modem_config "$dev") [ -n "${cfg}" ] || return mm_log "info" "setting interface '$cfg' as unavailable" proto_set_available "${cfg}" 0 } ################################################################################ # Event reporting # Receives as input the action, the device name and the subsystem mm_report_event() { local action="$1" local name="$2" local subsystem="$3" local sysfspath="$4" # Do not save virtual devices local virtual virtual="$(echo "$sysfspath" | cut -d'/' -f4)" [ "$virtual" = "virtual" ] && { mm_log "debug" "sysfspath is a virtual device ($sysfspath)" return } # Track/untrack events in cache case "${action}" in "add") # On add events, store event details in cache (if not exists yet) grep -qs "${name},${subsystem}" "${MODEMMANAGER_EVENTS_CACHE}" || \ echo "${action},${name},${subsystem},${sysfspath}" >> "${MODEMMANAGER_EVENTS_CACHE}" ;; "remove") # On remove events, remove old events from cache (match by subsystem+name) sed -i "/${name},${subsystem}/d" "${MODEMMANAGER_EVENTS_CACHE}" ;; esac # Report the event mm_log "debug" "event reported: action=${action}, name=${name}, subsystem=${subsystem}" mmcli --report-kernel-event="action=${action},name=${name},subsystem=${subsystem}" 1>/dev/null 2>&1 & # Wait for added modem if a sysfspath is given [ -n "${sysfspath}" ] && [ "$action" = "add" ] && mm_report_modem_wait "${sysfspath}" } mm_report_event_from_cache_line() { local event_line="$1" local action name subsystem sysfspath action=$(echo "${event_line}" | awk -F ',' '{ print $1 }') name=$(echo "${event_line}" | awk -F ',' '{ print $2 }') subsystem=$(echo "${event_line}" | awk -F ',' '{ print $3 }') sysfspath=$(echo "${event_line}" | awk -F ',' '{ print $4 }') mm_log "debug" "cached event found: action=${action}, name=${name}, subsystem=${subsystem}, sysfspath=${sysfspath}" mm_report_event "${action}" "${name}" "${subsystem}" "${sysfspath}" } mm_report_events_from_cache() { # Remove the sysfs cache rm -f "${MODEMMANAGER_SYSFS_CACHE}" local n=60 local step=1 local mmrunning=0 # Wait for ModemManager to be available in the bus while [ $n -ge 0 ]; do sleep $step mm_log "info" "checking if ModemManager is available..." if ! mmcli -L >/dev/null 2>&1 then mm_log "info" "ModemManager not yet available" else mmrunning=1 break fi n=$((n-step)) done [ ${mmrunning} -eq 1 ] || { mm_log "error" "couldn't report initial kernel events: ModemManager not running" return } # Report cached kernel events while IFS= read -r event_line; do mm_report_event_from_cache_line "${event_line}" done < ${MODEMMANAGER_EVENTS_CACHE} }