Ctrl K
ComunidadGuías…

server/src/usbip_input_session.h

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * @file src/usbip_input_session.h
 * @brief The lifetime of the devices ONE stream is holding, and the promise to give them back.
 *
 * This is the file that decides whether somebody loses their keyboard. Everything under it is
 * careful and testable, but careful and testable is not the same as SAFE: attaching a device takes
 * it away from the machine it is plugged into, and the only thing standing between a user and a
 * keyboard that never comes home is that this object gets destroyed.
 *
 * So the release is not a call. It is a destructor.
 *
 * A stream session can end by many paths - the user closes the client, the client's network drops,
 * a worker thread throws, the process is killed, a stop races a start. Writing `detach()` at the
 * end of each of those means the one path nobody thought of is the one that strands the device.
 * Owning the devices in an object means every one of those paths releases them, because every one
 * of them destroys the object. There is no path that can skip a destructor.
 *
 * Terminology: the machine a device is plugged into is the EXPORTER, the machine it arrives on is
 * the IMPORTER. This file is the importer's. Never write "host" or "client" bare.
 */
#pragma once

#include <chrono>
#include <memory>
#include <string>

#include "src/usbip_input.h"
#include "src/usbip_input_policy.h"

namespace input::usbip {
  /**
   * @brief What a session wants attached, already resolved by the caller.
   *
   * The address is resolved BEFORE it gets here on purpose. Working out "which machine is this
   * stream coming from" belongs to the code that knows about streams; this file should not be able
   * to get it subtly wrong, and more importantly it should not be the place a reader has to go to
   * find out which of two addresses was used.
   */
  struct session_request_t {
    /// Straight from config. False means this whole file does nothing, and that is the default.
    bool enabled = false;

    /// The exporter to attach from. The caller resolves this: config's override if it is set,
    /// otherwise the address the stream is coming from.
    std::string exporter;

    /// The optional comma-separated busid allowlist, raw from config. Empty means everything.
    std::string busids;
  };

  /// One device this session took, and the port that gives it back.
  struct held_device_t {
    std::string busid;  ///< As reported by the exporter. May be empty (see Attached::busid).
    int port = 0;       ///< The only thing detach accepts. 0 is a VALID port on Linux.
  };

  /// How long this module waits for the port table to name a device it has just attached, before it
  /// stops waiting and records that device as unported.
  ///
  /// A live parameter rather than a constant, because the variable under test here IS time: the
  /// defect this file exists to pin down is a port table that answers seconds after the attach, and
  /// the suite drives this module against a stood-in platform edge where the timing is staged. A
  /// suite that can only express "the table never answers" by paying the production period is a
  /// suite that takes minutes and then gets switched off - so tests shorten it, and production
  /// leaves it alone.
  std::chrono::milliseconds port_table_patience();
  void set_port_table_patience(std::chrono::milliseconds patience);

  /**
   * @brief Owns the devices one stream is holding. Destroying it gives them back.
   *
   * Deliberately not copyable and not movable: two objects that believe they own the same device
   * would each try to give it back, and the second attempt would detach whatever had taken that
   * port number next. One owner, one release.
   */
  class session_holder_t {
   public:
    /**
     * @brief Attach what the exporter is offering, per the request.
     *
     * Returns a holder that may be holding nothing. That is a normal result, not a failure: the
     * feature is off by default, the client may not be set up on this PC, and the exporter may
     * simply be offering nothing. Each of those is reported in words by report() rather than by a
     * silent empty holder, because "nothing was attached and nobody said why" is the state that
     * costs an afternoon to diagnose.
     *
     * Never throws. This runs on the streaming path; a failure to move a USB device must not be
     * able to take a stream down.
     */
    static std::shared_ptr<session_holder_t> attach(const session_request_t &request);

    /**
     * @brief A holder that has not been asked for anything yet.
     *
     * The slow half of attaching is asking another machine what it is offering and waiting for an
     * answer, and the stream must not wait on that. So a session is given one of these first,
     * published while it holds it, and has its answer sent; the devices are moved on afterwards
     * with take_from(). The session therefore owns its hold from the moment it exists, and every
     * end path gives back whatever is on it, whichever side of take_from() the end lands on.
     *
     * Named for what it is rather than for what it lacks. holding() answers false, which is the
     * truth, so clients_holding_usbip_devices() does not name this session yet - correct, because
     * nothing has been taken from anybody and a quit key pressed in that window would have nothing
     * to resolve.
     */
    static std::shared_ptr<session_holder_t> holding_nothing();

    /**
     * @brief Move the devices onto THIS holder, after a session already owns it.
     *
     * The blocking half of attach(), which is now only holding_nothing() followed by this -
     * the two are the same operation split so a caller can publish the holder in between.
     *
     * Safe to call on a holder whose session has ALREADY ended, which is the whole reason it is
     * a separate method. A session that ended mid-attach has already released; the guard in
     * release_all() would make the destructor's call a no-op, so the devices taken here are given
     * straight back rather than left held on a session that no longer exists to give them back.
     *
     * Never throws, for the same reason attach() never throws.
     */
    void take_from(const session_request_t &request);

    /**
     * @brief Give every device back.
     *
     * This is the promise. It runs on every end path there is, including the ones nobody planned.
     * It never throws: a destructor that throws is a program that dies instead of releasing a
     * device, which is the exact opposite of the point.
     */
    ~session_holder_t();

    session_holder_t(const session_holder_t &) = delete;
    session_holder_t &operator=(const session_holder_t &) = delete;

    /// Whether this session is holding anything at all.
    bool holding() const {
      return !m_Held.empty();
    }

    /// What we took and what it was for, in words, for the log. Never empty.
    std::string report() const;

    /// Anything the release could not give back. Empty in every normal case; non-empty means a
    /// device is still away from the machine it belongs to and a person needs to know.
    const std::string &release_failure() const {
      return m_ReleaseFailure;
    }

    /**
     * @brief Give every device back now, and record anything that would not come back.
     *
     * The destructor calls this, so every end path still releases the devices and that guarantee is
     * unchanged. It exists so a caller that wants to REPORT a release failure can release while it can
     * still read this object: once a destructor has finished there is nothing left to ask, and "a
     * device is still away from the machine it belongs to" is exactly the state a person needs to be
     * told about rather than have written into something already gone.
     *
     * Calling it more than once is safe and the second call does nothing - without that, a caller that
     * releases explicitly and then lets the destructor run would detach the same port twice, and by
     * then that port number may well belong to something else. Never throws.
     */
    void release_all();

   private:
    session_holder_t() = default;

    /**
     * @brief Give back everything held, WITHOUT the once-only guard.
     *
     * The guard exists so a caller who releases explicitly and then lets the destructor run does
     * not detach the same port twice - and by the second call that port number may well belong to
     * something else. There is exactly one caller that must get past it: take_from(), when the
     * session released while the devices were still moving, so the release that has already been
     * recorded did not include them.
     */
    void give_back_everything();

    /// Give back one device by port. Returns false if the detach did not take.
    bool release(const held_device_t &device);

    /// Record a device we could not give back when there was no port to name it by. Same sentence
    /// shape as release(), because the caller logs both through the same channel.
    void record_give_back_failure(const std::string &busid, const std::string &reason, int code);

    std::vector<held_device_t> m_Held;

    /// Devices this session attached but could NOT find a port for. They are still ours to give
    /// back, and they are the reason this is a list rather than nothing: the release path looks for
    /// them a second time, on the way out, when the port table has settled.
    std::vector<std::string> m_Unported;

    /// Set by release_all() so a second call is a no-op. See the note on that method.
    bool m_Released = false;

    /// Devices the destructor tried and FAILED to give back, in words, for the caller to log.
    /// A non-empty value here is the one state that needs a human: a device is still away from
    /// the machine it is plugged into.
    std::string m_ReleaseFailure;

    /// Why we are holding nothing, when we are. When the state is "nothing was taken and nobody
    /// said why", this is the sentence that says why - and it is filled in even for the boring
    /// cases, because the boring cases are the ones that get reported as bugs.
    std::string m_Report;
  };

  /**
   * @brief Sweep this machine clean of anything it is holding that it did not just attach.
   *
   * The insurance policy, run once at startup. If a previous run of this process died between
   * attaching and detaching - a power cut, a kill -9, a panic - the devices are still held, and the
   * machine they were taken from is missing them. Nothing in this process remembers that; the
   * kernel does, and `usbip port` reads it.
   *
   * Safe to run when nothing is held, and safe to run on a machine that has never attached
   * anything. It only ever releases devices the kernel says are attached AND the client agrees
   * about, and it reports what it released.
   *
   * @return A sentence describing what it did. Never empty.
   */
  std::string sweep_leftovers();

}  // namespace input::usbip