Sessions and the server

#

A BuildContext is a build that is still alive. Work is either synchronous or owned by an object the caller creates and destroys; nothing is started implicitly.

In this page:

BuildContext

#

A live build. Create one with Context(), use it as much as you like, and Dispose() it last.

The interface is abstract so a test can substitute a context that serves canned results, and so the implementation details - the file system instance, the caches, the watcher thread - can change without touching callers.

class BuildContext {
public:
    virtual ~BuildContext() = default;

    // Runs the build again with the options this context was created with.
    // Concurrent callers are fine: the second one waits for the build already
    // in progress and receives that result rather than starting a competing
    // build. The files are returned, not written, unless "write" was
    // requested.
    //
    // Input:  a context created with bundle = true and one entry point
    // Output: a BuildResult describing this run in full - every output file
    //         the build produced, plus any errors and warnings. Rebuilding
    //         reuses the validated options and the caches, which is what makes
    //         it cheaper than calling Build() again.
    //
    // A disposed context is inert rather than dangerous: this returns an empty
    // BuildResult instead of throwing, so a late call from another thread
    // cannot take the process down.
    virtual BuildResult Rebuild() = 0;

    // Starts watching every file the build read, plus whatever plugins asked to
    // watch, and rebuilds automatically whenever one of them changes. Watch
    // data is only collected in builds that run after this call, so enable it
    // before the first build if you want that first build to be covered.
    //
    // Input:  WatchOptions{ delay = 200 }
    // Output: nothing; changes now produce "[watch] build started"/"finished"
    //         lines on the log stream, and later Rebuild() calls return the new
    //         output.
    //
    // Throws "std::runtime_error" when the context is disposed or when watch
    // mode is already enabled.
    virtual void Watch(const WatchOptions& options) = 0;

    // Starts an HTTP server over the build's output and returns as soon as it
    // is accepting connections. Anything the server serves from the build
    // triggers a rebuild first, so editing a file and reloading the page is
    // enough to see the change. This combines with Watch(): rebuilds the
    // watcher runs on its own then reach open browsers through
    // ServeResult::notify, which the context calls for every pass that
    // finishes - that combination is what "guchho dev" is.
    //
    // Input:  ServeOptions{ servedir = "dist", port = 0 }
    // Output: ServeResult with the bound "port", the "hosts" to print, and a
    //         "stop" function that must be called to shut the server down.
    //
    // Throws "std::runtime_error" on a bad configuration (a port already in
    // use, a half-specified TLS pair, a malformed CORS origin) or on a
    // disposed context.
    virtual ServeResult Serve(const ServeOptions& options) = 0;

    // Asks the build that is running right now to stop at the next convenient
    // point, and blocks until it has actually stopped. A build that is
    // cancelled this way reports it: "errors" contains a single message saying
    // the build was cancelled and no output is produced. Calling this while
    // nothing is running is a no-op, and calling it on a disposed context is
    // also a no-op.
    virtual void Cancel() = 0;

    // Stops the watcher thread, shuts the build down and releases the caches.
    // Every other method on a disposed context either throws or does nothing,
    // so this is the last call and it must be made exactly once. Calling it
    // twice is harmless.
    virtual void Dispose() = 0;
};

Watcher

#

Polls the file system for changes among the paths the last build reported and triggers a rebuild whenever one is found.

The object owns a thread, so it is created, started and stopped explicitly; nothing here begins polling on its own. A Watcher is normally owned by a BuildContext, but it is usable on its own: give it a filesystem, a function that performs a build and returns the new watch data, and it will drive that function on every change.

class Watcher {
public:
    // fs         - the filesystem to poll through, so a test can watch an
    //              in-memory tree instead of the disk.
    // rebuild    - called on the watcher thread after a change is found; it
    //              must run the build and return the paths that build read, so
    //              the next tick watches the new graph. It runs on no other
    //              thread, and must not call back into the Watcher.
    // delay_in_ms- how long to wait after noticing a change, so a burst of
    //              saves causes one rebuild. Zero means rebuild at once.
    // should_log - whether to print the "[watch]" progress lines.
    // use_color  - whether those lines may contain ANSI escapes.
    // path_style - how paths are rendered in those lines (relative or absolute).
    Watcher(
        filesystem::Fs&                                             fs,
        std::function<filesystem::WatchData()>                      rebuild,
        std::chrono::milliseconds                                   delay_in_ms,
        bool                                                        should_log,
        logger::UseColor                                            use_color,
        logger::PathStyle                                           path_style);

    // Replaces the watch list with the one the latest build produced. Call
    // this after every build, including the first: it is what makes the
    // watcher follow the graph as it changes, dropping paths that are no
    // longer part of the build and adding the ones that just became part of
    // it. Any path that was recently changed and is still present keeps its
    // place in the recent list, and the shuffled remainder is thrown away so
    // the next pass starts from the new list rather than finishing the old one.
    //
    // When logging is on, the first call also prints the "[watch] build
    // finished, watching for changes" line, which is the point at which the
    // watcher is genuinely ready to answer.
    //
    // Input:  the WatchData returned by the previous build
    // Output: nothing; the next tick polls the new set of paths.
    void SetWatchData(filesystem::WatchData data);

    // Starts the polling thread. Call it exactly once per Watcher: the thread
    // is stored by assignment, so a second Start() while the first thread is
    // still running ends the process rather than merely confusing it. A
    // Watcher cannot be restarted after Stop() either, because the stop flag
    // stays set and the new thread would exit at once.
    void Start();

    // Asks the thread to finish and waits for it. The current tick completes
    // first, so a change noticed during the final pass can still trigger one
    // last rebuild - stopping a watcher is a request to wrap up, not an
    // abort. The wait covers the settle delay and the rebuild as well, so a
    // build already under way is never abandoned half-finished. Safe to call
    // when the thread was never started, and safe to call twice.
    void Stop();

private:
    // Checks the watch list for something that changed and returns the path
    // that changed, or an empty string when nothing did. At most one path is
    // reported per call, because one rebuild answers for all of them. For a
    // file the returned path is the file itself; for a watched directory it may
    // be the directory or the child that was actually modified. The recent list
    // is checked first and the first hit ends the call, then one slice of the
    // remaining paths is taken off the shuffled scan list. Must be called with
    // "mutex_" held; refills and re-shuffles the scan list when that list has
    // been exhausted, and marks a path that changed as recently changed.
    std::string TryToFindDirtyPath();

    // The paths to poll, each paired with a closure that reports whether that
    // entry changed and, if so, which path inside it did.
    filesystem::WatchData              data_;
    filesystem::Fs&                    fs_;
    std::function<filesystem::WatchData()> rebuild_;
    std::chrono::milliseconds          delay_in_ms_;
    std::vector<std::string>           recent_items_;  // re-checked on every tick
    std::vector<std::string>           items_to_scan_; // the shuffled remainder
    std::mutex                         mutex_;
    std::size_t                        items_per_iteration_ = 0;
    std::atomic<bool>                  should_stop_{false};
    bool                               should_log_ = false;
    logger::UseColor                   use_color_{};
    logger::PathStyle                  path_style_{};

    // Thread bookkeeping: "thread_" is the polling loop, and the trio below is
    // what lets Stop() block until that loop has actually unwound rather than
    // merely having been asked to. "running_" is guarded by "stop_mutex_" and
    // cleared by the thread itself just before it returns.
    std::thread               thread_;
    std::mutex                stop_mutex_;
    std::condition_variable   stop_cv_;
    bool                      running_ = false;
};

CORSOptions

#

Origins allowed to make cross-origin requests. An origin is a protocol, host and port ("http://localhost:3000"); "*" matches any origin, and it may appear at most once per entry.

struct CORSOptions {
    std::vector<std::string> origin;
};

Edit this page on GitHub