text: /****************************************************************************
   * fs/mount/fs_automount.c
   *
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.  The
   * ASF licenses this file to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance with the
   * License.  You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
   * License for the specific language governing permissions and limitations
   * under the License.
   *
   ****************************************************************************/
  
  /****************************************************************************
   * Included Files
   ****************************************************************************/
  
  #include <nuttx/config.h>
  
  #if defined(CONFIG_FS_AUTOMOUNTER_DEBUG) && !defined(CONFIG_DEBUG_FS)
  #  define CONFIG_DEBUG_FS 1
  #endif
  
  #include <sys/mount.h>
  
  #include <stdbool.h>
  #include <errno.h>
  #include <assert.h>
  #include <debug.h>
  
  #include <nuttx/wdog.h>
  #include <nuttx/kmalloc.h>
  #include <nuttx/wqueue.h>
  #include <nuttx/fs/automount.h>
  
  #include "inode/inode.h"
  
  /****************************************************************************
   * Pre-processor Definitions
   ****************************************************************************/
  
  /* Configuration ************************************************************
   *
   * CONFIG_FS_AUTOMOUNTER - Enables AUTOMOUNT support
   */
  
  /* Pre-requisites */
  
  #ifndef CONFIG_SCHED_WORKQUEUE
  #  error Work queue support is required (CONFIG_SCHED_WORKQUEUE)
  #endif
  
  /* Return Values */
  
  #define OK_EXIST   0
  #define OK_NOENT   1
  
  /****************************************************************************
   * Private Types
   ****************************************************************************/
  
  /* This structure describes the state of the automounter */
  
  struct automounter_state_s
  {
    FAR const struct automount_lower_s *lower; /* Board level interfaces */
    struct work_s work;                        /* Work queue support */
    struct wdog_s wdog;                        /* Delay to retry un-mounts */
    bool mounted;                              /* True: Volume has been mounted */
    bool inserted;                             /* True: Media has been inserted */
  };
  
  /****************************************************************************
   * Private Function Prototypes
   ****************************************************************************/
  
  static int  automount_findinode(FAR const char *path);
  static void automount_mount(FAR struct automounter_state_s *priv);
  static int  automount_unmount(FAR struct automounter_state_s *priv);
  static void automount_timeout(wdparm_t arg);
  static void automount_worker(FAR void *arg);
  static int  automount_interrupt(FAR const struct automount_lower_s *lower,
                FAR void *arg, bool inserted);
  
  /****************************************************************************
   * Private Functions
   ****************************************************************************/
  
  /****************************************************************************
   * Name: automount_findinode
   *
   * Description:
   *   Find the mountpoint inode in the inode tree.
   *
   * Input Parameters:
   *   mntpath - Mountpoint path
   *
   * Returned Value:
   *   OK_EXIST if the inode exists
   *   OK_NOENT if the inode does not exist
   *   Negated errno if some failure occurs
   *
   ****************************************************************************/
  
  static int automount_findinode(FAR const char *path)
  {
    struct inode_search_s desc;
    int ret;
  
    /* Make sure that we were given a path */
  
    DEBUGASSERT(path != NULL);
  
    /* Get exclusive access to the in-memory inode tree. */
  
    ret = inode_semtake();
    if (ret < 0)
      {
        return ret;
      }
  
    /* Find the inode */
  
    SETUP_SEARCH(&desc, path, false);
  
    ret = inode_search(&desc);
  
    /* Did we find it? */
  
    if (ret < 0)
      {
        /* No.. Not found */
  
        ret = OK_NOENT;
      }
  
    /* Yes.. is it a mount point? */
  
    else if (INODE_IS_MOUNTPT(desc.node))
      {
        /* Yes.. we found a mountpoint at this path */
  
        ret = OK_EXIST;
      }
    else
      {
        /* No.. then something is in the way */
  
        ret = -ENOTDIR;
      }
  
    /* Relinquish our exclusive access to the inode try and return the result */
  
    inode_semgive();
    RELEASE_SEARCH(&desc);
    return ret;
  }
  
  /****************************************************************************
   * Name: automount_mount
   *
   * Description:
   *   Media has been inserted, mount the volume.
   *
   * Input Parameters:
   *   priv - A reference to out private state structure
   *
   * Returned Value:
   *   None
   *
   ****************************************************************************/
  
  static void automount_mount(FAR struct automounter_state_s *priv)
  {
    FAR const struct automount_lower_s *lower = priv->lower;
    int ret;
  
    finfo("Mounting %s\n", lower->mountpoint);
  
    /* Check if the something is already mounted at the mountpoint. */
  
    ret = automount_findinode(lower->mountpoint);
    switch (ret)
      {
      case OK_EXIST:
  
        /* REVISIT: What should we do in this case?  I think that this would
         * happen only if a previous unmount failed?  I suppose that we should
         * try to unmount again because the mount might be stale.
         */
  
        fwarn("WARNING: Mountpoint %s already exists\n", lower->mountpoint);
        ret = automount_unmount(priv);
        if (ret < 0)
          {
            /* We failed to unmount (again?).  Complain and abort. */
  
            ferr("ERROR: automount_unmount failed: %d\n", ret);
            return;
          }
  
        /* We successfully unmounted the file system.  Fall through to
         * mount it again.
         */
  
      case OK_NOENT:
  
        /* If we get here, then the volume must not be mounted */
  
        DEBUGASSERT(!priv->mounted);
  
         /* Mount the file system */
  
        ret = nx_mount(lower->blockdev, lower->mountpoint, lower->fstype,
                       0, NULL);
        if (ret < 0)
          {
            ferr("ERROR: Mount failed: %d\n", ret);
            return;
          }
  
        /* Indicate that the volume is mounted */
  
        priv->mounted = true;
        break;
  
      default:
        ferr("ERROR: automount_findinode failed: %d\n", ret);
        break;
      }
  }
  
  /****************************************************************************
   * Name: automount_unmount
   *
   * Description:
   *   Media has been removed, unmount the volume.
   *
   * Input Parameters:
   *   priv - A reference to out private state structure
   *
   * Returned Value:
   *   OK if the volume was successfully mounted.  A negated errno value
   *   otherwise.
   *
   ****************************************************************************/
  
  static int automount_unmount(FAR struct automounter_state_s *priv)
  {
    FAR const struct automount_lower_s *lower = priv->lower;
    int ret;
  
    finfo("Unmounting %s\n", lower->mountpoint);
  
    /* Check if the something is already mounted at the mountpoint. */
  
    ret = automount_findinode(lower->mountpoint);
    switch (ret)
      {
      case OK_EXIST:
  
        /* If we get here, then the volume must be mounted */
  
        DEBUGASSERT(priv->mounted);
  
        /* Un-mount the volume */
  
        ret = nx_umount2(lower->mountpoint, MNT_FORCE);
        if (ret < 0)
          {
            /* We expect the error to be EBUSY meaning that the volume could
             * not be unmounted because there are currently reference via open
             * files or directories.
             */
  
            if (ret == -EBUSY)
              {
                finfo("WARNING: Volume is busy, try again later\n");
  
                /* Start a timer to retry the umount2 after a delay */
  
                ret = wd_start(&priv->wdog, lower->udelay,
                               automount_timeout, (wdparm_t)priv);
                if (ret < 0)
                  {
                    ferr("ERROR: wd_start failed: %d\n", ret);
                    return ret;
                  }
              }
  
            /* Other errors are fatal */
  
            else
              {
                ferr("ERROR: umount2 failed: %d\n", ret);
                return ret;
              }
          }
  
        /* Fall through */
  
      case OK_NOENT:
  
        /* The mountpoint is not present.  This is normal behavior in the
         * case where the user manually un-mounted the volume before removing
         * media.  Nice job, Mr. user.
         */
  
        priv->mounted = false;
        return OK;
  
      default:
        ferr("ERROR: automount_findinode failed: %d\n", ret);
        return ret;
      }
  }
  
  /****************************************************************************
   * Name: automount_timeout
   *
   * Description:
   *   A previous unmount failed because the volume was busy... busy meaning
   *   the volume could not be unmounted because there are open references
   *   the files or directories in the volume.  When this failure occurred,
   *   the unmount logic setup a delay and this function is called as a result
   *   of that delay timeout.
   *
   *   This function will attempt the unmount again.
   *
   * Input Parameters:
   *   Standard wdog timeout parameters
   *
   * Returned Value:
   *   None
   *
   ****************************************************************************/
  
  static void automount_timeout(wdparm_t arg)
  {
    FAR struct automounter_state_s *priv =
      (FAR struct automounter_state_s *)arg;
    int ret;
  
    finfo("Timeout!\n");
    DEBUGASSERT(priv);
  
    /* Check the state of things.  This timeout at the interrupt level and
     * will cancel the timeout if there is any change in the insertion
     * state.  So we should still have the saved state as NOT inserted and
     * there should be no pending work.
     */
  
    finfo("inserted=%d\n", priv->inserted);
    DEBUGASSERT(!priv->inserted && work_available(&priv->work));
  
    /* Queue work to occur immediately. */
  
    ret = work_queue(LPWORK, &priv->work, automount_worker, priv, 0);
    if (ret < 0)
      {
        /* NOTE: Currently, work_queue only returns success */
  
        ferr("ERROR: Failed to schedule work: %d\n", ret);
      }
  }
  
  /****************************************************************************
   * Name: automount_worker
   *
   * Description:
   *   Performs auto-mount actions on the worker thread.
   *
   * Input Parameters:
   *   arg - Work argument set by work_queue()
   *
   * Returned Value:
   *   None
   *
   ****************************************************************************/
  
  static void automount_worker(FAR void *arg)
  {
    FAR struct automounter_state_s *priv =
      (FAR struct automounter_state_s *)arg;
    FAR const struct automount_lower_s *lower;
  
    DEBUGASSERT(priv && priv->lower);
    lower = priv->lower;
  
    /* Disable interrupts.  We are commit now and everything must remain
     * stable.
     */
  
    AUTOMOUNT_DISABLE(lower);
  
    /* Are we mounting or unmounting? */
  
    if (priv->inserted)
      {
        /* We are mounting */
  
        automount_mount(priv);
      }
    else
      {
        /* We are unmounting */
  
        automount_unmount(priv);
      }
  
    /* Re-enable interrupts */
  
    AUTOMOUNT_ENABLE(lower);
  }
  
  /****************************************************************************
   * Name: automount_interrupt
   *
   * Description:
   *   Called (probably from the interrupt level) when a media change event
   *   has been detected.
   *
   * Input Parameters:
   *   lower - Persistent board configuration data
   *   arg - Data associated with the auto-mounter
   *   inserted - True: Media has been inserted. False: media has been removed
   *
   * Returned Value:
   *   OK is returned on success; a negated errno value is returned on failure.
   *
   * Assumptions:
   *   Interrupts are disabled so that there is no race condition with the
   *   timer expiry.
   *
   ****************************************************************************/
  
  static int automount_interrupt(FAR const struct automount_lower_s *lower,
                                 FAR void *arg, bool inserted)
  {
    FAR struct automounter_state_s *priv =
      (FAR struct automounter_state_s *)arg;
    int ret;
  
    DEBUGASSERT(lower && priv && priv->lower == lower);
  
    finfo("inserted=%d\n", inserted);
  
    /* Cancel any pending work.  We could get called multiple times if, for
     * example there is bounce in the detection mechanism.  Work is performed
     * the low priority work queue if it is available.
     *
     * NOTE:  The return values are ignored.  The error -ENOENT means that
     * there is no work to be canceled.  No other errors are expected.
     */
  
    work_cancel(LPWORK, &priv->work);
  
    /* Set the media insertion/removal state */
  
    priv->inserted = inserted;
  
    /* Queue work to occur after a delay.  The delays performs debouncing:
     * If the insertion/removal detection logic has "chatter", then we may
     * receive this interrupt numerous times.  Each time, the previous work
     * will be canceled (above) and the new work will scheduled with the
     * delay.  So the final mount operation will not be performed until the
     * insertion state is stable for that delay.
     */
  
    ret = work_queue(LPWORK, &priv->work, automount_worker, priv,
                     priv->lower->ddelay);
    if (ret < 0)
      {
        /* NOTE: Currently, work_queue only returns success */
  
        ferr("ERROR: Failed to schedule work: %d\n", ret);
      }
    else
      {
        /* Cancel any retry delays */
  
        wd_cancel(&priv->wdog);
      }
  
    return OK;
  }
  
  /****************************************************************************
   * Public Functions
   ****************************************************************************/
  
  /****************************************************************************
   * Name: automount_initialize
   *
   * Description:
   *   Configure the auto mounter.
   *
   * Input Parameters:
   *   lower - Persistent board configuration data
   *
   * Returned Value:
   *   A void* handle.  The only use for this handle is with
   *   automount_uninitialize().  NULL is returned on any failure.
   *
   ****************************************************************************/
  
  FAR void *automount_initialize(FAR const struct automount_lower_s *lower)
  {
    FAR struct automounter_state_s *priv;
    int ret;
  
    finfo("lower=%p\n", lower);
    DEBUGASSERT(lower);
  
    /* Allocate an auto-mounter state structure */
  
    priv = (FAR struct automounter_state_s *)
      kmm_zalloc(sizeof(struct automounter_state_s));
  
    if (!priv)
      {
        ferr("ERROR: Failed to allocate state structure\n");
        return NULL;
      }
  
    /* Initialize the automounter state structure */
  
    priv->lower = lower;
  
    /* Handle the initial state of the mount on the caller's thread */
  
    priv->inserted = AUTOMOUNT_INSERTED(lower);
  
    /* Set up the first action at a delay from the initialization time (to
     * allow time for any extended block driver initialization to complete.
     */
  
    ret = work_queue(LPWORK, &priv->work, automount_worker, priv,
                     priv->lower->ddelay);
    if (ret < 0)
      {
        /* NOTE: Currently, work_queue only returns success */
  
        ferr("ERROR: Failed to schedule work: %d\n", ret);
      }
  
    /* Attach and enable automounter interrupts */
  
    ret = AUTOMOUNT_ATTACH(lower, automount_interrupt, priv);
    if (ret < 0)
      {
        ferr("ERROR: Failed to attach automount interrupt: %d\n", ret);
        automount_uninitialize(priv);
        return NULL;
      }
  
    AUTOMOUNT_ENABLE(lower);
    return priv;
  }
  
  /****************************************************************************
   * Name: automount_uninitialize
   *
   * Description:
   *   Stop the automounter and free resources that it used.  NOTE that the
   *   mount is left in its last state mounted/unmounted state.
   *
   * Input Parameters:
   *   handle - The value previously returned by automount_initialize();
   *
   * Returned Value:
   *   None
   *
   ****************************************************************************/
  
  void automount_uninitialize(FAR void *handle)
  {
    FAR struct automounter_state_s *priv =
      (FAR struct automounter_state_s *)handle;
    FAR const struct automount_lower_s *lower;
  
    DEBUGASSERT(priv && priv->lower);
    lower = priv->lower;
  
    /* Disable and detach interrupts */
  
    AUTOMOUNT_DISABLE(lower);
    AUTOMOUNT_DETACH(lower);
  
    /* Cancel the watchdog timer */
  
    wd_cancel(&priv->wdog);
  
    /* And free the state structure */
  
    kmm_free(priv);
  }
  