fix: harden network and font resource lifetimes

- retain asynchronous HTTP operations until execution or queue disposal
  - make HTTP shutdown cancel and join queued and running shared-pool requests
  - avoid deadlocks when callbacks clear the HTTP pool
  - share atomic cancellation state across request copies
  - detect socket creation failures before configuring TCP or UDP sockets
  - release partially initialized mbedTLS state after failed handshakes
  - replace retained FreeType faces with bounded LRU probe-result caching
  - release non-trivial StaticLRU keys when clearing the cache
  - resolve the Texture Viewer close icon through the configured icon theme
  - add HTTP, TLS, UIWebView fan-out, font descriptor, and LRU regressions
  - update resource-refactor prerequisite and ownership planning documents
This commit is contained in:
Martín Lucas Golini
2026-07-14 02:16:01 -03:00
parent 8aa660104f
commit 7e66e77d89
19 changed files with 943 additions and 211 deletions
@@ -65,6 +65,17 @@ Regression coverage:
- Running and queued variants complete/cancel without UAF under ASAN. - Running and queued variants complete/cancel without UAF under ASAN.
- Callback re-entry does not deadlock. - Callback re-entry does not deadlock.
Status: implemented. Shared-executor requests are registered before submission and retained by a
heap-backed operation. The operation destructor unregisters it both after execution and when the
external executor discards queued work. `Http::shutdown()` atomically rejects new work,
cancels registered requests, and waits without holding Pool or request-map locks. Callback-initiated
Pool clearing cannot wait for work queued behind that callback, so it requests cancellation and
lets shared operations defer destruction until the shared queue drains. `Request` cancellation is
shared and atomic across request copies. Focused ASAN coverage exercises queued memory, stream, and
file requests, running cancellation, concurrent Pool clearing, and Pool clearing from a callback
with another request queued behind it. All six focused HTTP tests pass under ASAN and unsuppressed
TSAN.
### A3. Engine stops HTTP/resource producers too late ### A3. Engine stops HTTP/resource producers too late
Current behavior: Current behavior:
@@ -86,8 +97,9 @@ Regression coverage:
- Repeat Engine creation/destruction in the same test process. - Repeat Engine creation/destruction in the same test process.
- Assert no callback touches the destroyed scene/factory and no singleton is recreated. - Assert no callback touches the destroyed scene/factory and no singleton is recreated.
Status: Engine now clears pool-owned HTTP clients before scene/Graphics teardown. The complete Status: Engine now clears pool-owned HTTP clients before scene/Graphics teardown. Shared-executor
producer barrier remains pending A2 and A4 because shared-executor operations and static UI HTTP operations are covered by A2's explicit Pool barrier. The complete producer barrier remains
pending A4 because static UI
deliveries do not yet have complete close/reject semantics. deliveries do not yet have complete close/reject semantics.
### A4. UISceneNode static delivery queue lacks shutdown semantics ### A4. UISceneNode static delivery queue lacks shutdown semantics
@@ -264,6 +264,16 @@ Work-package exit criteria:
- No shared-pool lambda depends on an untracked raw Http lifetime. - No shared-pool lambda depends on an untracked raw Http lifetime.
- Http destruction provides a complete operation barrier without taking ownership of the executor. - Http destruction provides a complete operation barrier without taking ownership of the executor.
Status: implemented. Shared-pool operations are heap-backed, registered before executor
submission, and unregister from their destructor even when queued work is discarded. Pool-managed
Http instances remain alive through callback completion, while stack/raw
instances use the destructor barrier. Pool clearing requests cancellation and waits outside the
Pool mutex. When clearing is initiated by a shared-pool callback, it defers the shared-operation
barrier because queued work may require the current executor thread; shared operations retain the
clients until that work drains. Cancellation state is atomic and shared by request copies. Focused
ASAN coverage for all three async output forms, running cancellation, and callback-initiated
clearing with queued work passes. All six focused HTTP tests also pass under unsuppressed TSAN.
## 6. Work package 4: deterministic Engine shutdown ## 6. Work package 4: deterministic Engine shutdown
Reorder shutdown only after HTTP and loader barriers are reliable. Reorder shutdown only after HTTP and loader barriers are reliable.
@@ -318,8 +328,8 @@ first, makes the selected context current, destroys scenes, explicitly discards
TextLayout, releases high-level Graphics managers in dependency order, destroys shaders before TextLayout, releases high-level Graphics managers in dependency order, destroys shaders before
Renderer, and only then destroys windows/contexts. A focused test covers two Engine lifecycles with Renderer, and only then destroys windows/contexts. A focused test covers two Engine lifecycles with
a live UI scene, framebuffer, nine-patch, texture, font/layout cache, shaders, and pending batch. a live UI scene, framebuffer, nine-patch, texture, font/layout cache, shaders, and pending batch.
The complete asynchronous-producer exit criterion remains pending Work Packages 3 and 5. The complete asynchronous-producer exit criterion remains pending Work Package 5.
The complete ASAN unit suite passes: 746 tests passed and one opt-in visual test was skipped. The complete ASAN unit suite passes: 749 tests passed and one opt-in visual test was skipped.
## 7. Work package 5: UISceneNode async delivery lifecycle ## 7. Work package 5: UISceneNode async delivery lifecycle
@@ -43,6 +43,11 @@ The architecture contracts can proceed with these refinements:
Members are destroyed in reverse declaration order, so the loader is destroyed last and can Members are destroyed in reverse declaration order, so the loader is destroyed last and can
run against already-destroyed state. run against already-destroyed state.
These are prerequisite bugs and should be fixed independently before the ownership refactor. These are prerequisite bugs and should be fixed independently before the ownership refactor.
Status: the HTTP Pool lock-order defect and shared-ThreadPool operation lifetime defect are
fixed. Pool clearing now establishes an operation barrier without owning the executor, including
callback-initiated clearing and executor-discarded queued work. The TextureAtlasLoader lifetime
defect is also fixed and covered by sanitizer-backed regression tests.
6. `isStateful()` is unusable as a shareability test. Every current Drawable inherits mutable color 6. `isStateful()` is unusable as a shareability test. Every current Drawable inherits mutable color
and position, and several classes reporting false mutate themselves or children during draw. and position, and several classes reporting false mutate themselves or children during draw.
7. Stage 2 must migrate texture holders, loaders, ID-based construction, and queued batches in one 7. Stage 2 must migrate texture holders, loaders, ID-based construction, and queued batches in one
+31 -12
View File
@@ -22,6 +22,8 @@ template <std::size_t Capacity, typename KeyT = std::uint64_t,
typename ValueT = std::array<char, 64>> typename ValueT = std::array<char, 64>>
class DynamicLRU { class DynamicLRU {
private: private:
static_assert( Capacity > 0, "Capacity must be greater than zero for DynamicLRU" );
using ListPair = std::pair<KeyT, ValueT>; using ListPair = std::pair<KeyT, ValueT>;
std::list<ListPair> mCacheList; std::list<ListPair> mCacheList;
UnorderedMap<KeyT, typename std::list<ListPair>::iterator> mCacheMap; UnorderedMap<KeyT, typename std::list<ListPair>::iterator> mCacheMap;
@@ -76,6 +78,7 @@ template <std::size_t Capacity, typename KeyT = std::uint64_t,
typename ValueT = std::array<char, 64>> typename ValueT = std::array<char, 64>>
class StaticLRU { class StaticLRU {
public: public:
static_assert( Capacity > 0, "Capacity must be greater than zero for StaticLRU" );
static_assert( Capacity < ( 1 << 16 ) - 2, "Capacity must be less than 65534 for StaticLRU" ); static_assert( Capacity < ( 1 << 16 ) - 2, "Capacity must be less than 65534 for StaticLRU" );
static constexpr std::size_t N = Capacity; static constexpr std::size_t N = Capacity;
static constexpr std::size_t HASH_SZ = 2 * N; // Load factor <= 0.5 static constexpr std::size_t HASH_SZ = 2 * N; // Load factor <= 0.5
@@ -94,7 +97,7 @@ class StaticLRU {
std::optional<ValueT> get( const KeyT& key ) noexcept { std::optional<ValueT> get( const KeyT& key ) noexcept {
const std::uint16_t idx = find_index( key ); const std::uint16_t idx = find_index( key );
if ( idx == NONE ) if ( idx >= N )
return std::nullopt; return std::nullopt;
unlink( idx ); unlink( idx );
@@ -106,7 +109,7 @@ class StaticLRU {
const std::uint16_t idx = find_index( key ); const std::uint16_t idx = find_index( key );
// Hit: update existing entry and move to front // Hit: update existing entry and move to front
if ( idx != NONE ) { if ( idx < N ) {
vals_[idx] = std::move( value ); vals_[idx] = std::move( value );
unlink( idx ); unlink( idx );
push_front( idx ); push_front( idx );
@@ -121,9 +124,13 @@ class StaticLRU {
} else { } else {
// Cache is full, evict the least recently used item (tail) // Cache is full, evict the least recently used item (tail)
target_idx = tail_; target_idx = tail_;
if ( target_idx >= N )
return;
// O(1) removal from hash table using pre-stored position and a tombstone // O(1) removal from hash table using pre-stored position and a tombstone
const std::uint16_t old_pos = hash_pos_[target_idx]; const std::uint16_t old_pos = hash_pos_[target_idx];
if ( old_pos >= HASH_SZ )
return;
table_[old_pos].idx = DELETED_IDX; table_[old_pos].idx = DELETED_IDX;
unlink( target_idx ); unlink( target_idx );
@@ -148,8 +155,10 @@ class StaticLRU {
} }
void clear() { void clear() {
for ( std::size_t i = 0; i < used_; ++i ) for ( std::size_t i = 0; i < used_; ++i ) {
keys_[i] = KeyT{};
vals_[i] = ValueT{}; vals_[i] = ValueT{};
}
std::fill( table_.begin(), table_.end(), Entry{ NONE } ); std::fill( table_.begin(), table_.end(), Entry{ NONE } );
std::fill( prev_.begin(), prev_.end(), NONE ); std::fill( prev_.begin(), prev_.end(), NONE );
std::fill( next_.begin(), next_.end(), NONE ); std::fill( next_.begin(), next_.end(), NONE );
@@ -177,20 +186,28 @@ class StaticLRU {
alignas( 64 ) std::array<std::uint16_t, N> hash_pos_{}; alignas( 64 ) std::array<std::uint16_t, N> hash_pos_{};
void unlink( std::uint16_t i ) noexcept { void unlink( std::uint16_t i ) noexcept {
if ( prev_[i] != NONE ) if ( i >= N )
next_[prev_[i]] = next_[i]; return;
if ( next_[i] != NONE )
prev_[next_[i]] = prev_[i]; const std::uint16_t previous = prev_[i];
const std::uint16_t next = next_[i];
if ( previous < N )
next_[previous] = next;
if ( next < N )
prev_[next] = previous;
if ( head_ == i ) if ( head_ == i )
head_ = next_[i]; head_ = next;
if ( tail_ == i ) if ( tail_ == i )
tail_ = prev_[i]; tail_ = previous;
} }
void push_front( std::uint16_t i ) noexcept { void push_front( std::uint16_t i ) noexcept {
if ( i >= N )
return;
prev_[i] = NONE; prev_[i] = NONE;
next_[i] = head_; next_[i] = head_;
if ( head_ != NONE ) if ( head_ < N )
prev_[head_] = i; prev_[head_] = i;
head_ = i; head_ = i;
if ( tail_ == NONE ) if ( tail_ == NONE )
@@ -209,7 +226,7 @@ class StaticLRU {
// Empty slot means the key cannot be further down the probe chain. // Empty slot means the key cannot be further down the probe chain.
return NONE; return NONE;
} }
if ( entry.idx != DELETED_IDX && keys_[entry.idx] == key ) { if ( entry.idx < N && keys_[entry.idx] == key ) {
// Found the key. // Found the key.
return entry.idx; return entry.idx;
} }
@@ -226,6 +243,8 @@ template <std::size_t Capacity, typename KeyT = std::uint64_t,
typename ValueT = std::array<char, 64>> typename ValueT = std::array<char, 64>>
class LRUCache { class LRUCache {
private: private:
static_assert( Capacity > 0, "Capacity must be greater than zero for LRUCache" );
// Threshold for choosing the static implementation. If the estimated memory usage // Threshold for choosing the static implementation. If the estimated memory usage
// is below this, the faster, allocation-free StaticLRU is used. // is below this, the faster, allocation-free StaticLRU is used.
static constexpr std::size_t STATIC_THRESHOLD_BYTES = 1 << 20; // 1 MB static constexpr std::size_t STATIC_THRESHOLD_BYTES = 1 << 20; // 1 MB
@@ -234,7 +253,7 @@ class LRUCache {
constexpr std::size_t key_size = sizeof( KeyT ); constexpr std::size_t key_size = sizeof( KeyT );
constexpr std::size_t val_size = sizeof( ValueT ); constexpr std::size_t val_size = sizeof( ValueT );
constexpr std::size_t idx_size = sizeof( std::uint16_t ); constexpr std::size_t idx_size = sizeof( std::uint16_t );
constexpr std::size_t entry_size = sizeof( typename StaticLRU<0, KeyT, ValueT>::Entry ); constexpr std::size_t entry_size = sizeof( std::uint16_t );
constexpr std::size_t data_mem = ( key_size + val_size ) * Capacity; constexpr std::size_t data_mem = ( key_size + val_size ) * Capacity;
constexpr std::size_t lru_mem = ( idx_size * 2 ) * Capacity; // prev/next constexpr std::size_t lru_mem = ( idx_size * 2 ) * Capacity; // prev/next
+58 -16
View File
@@ -1,6 +1,7 @@
#ifndef EE_NETWORKCHTTP_HPP #ifndef EE_NETWORKCHTTP_HPP
#define EE_NETWORKCHTTP_HPP #define EE_NETWORKCHTTP_HPP
#include <condition_variable>
#include <eepp/core.hpp> #include <eepp/core.hpp>
#include <eepp/core/noncopyable.hpp> #include <eepp/core/noncopyable.hpp>
#include <eepp/network/ipaddress.hpp> #include <eepp/network/ipaddress.hpp>
@@ -13,6 +14,8 @@
#include <eepp/system/threadpool.hpp> #include <eepp/system/threadpool.hpp>
#include <eepp/system/time.hpp> #include <eepp/system/time.hpp>
#include <map> #include <map>
#include <memory>
#include <mutex>
#include <string> #include <string>
namespace EE { namespace System { namespace EE { namespace System {
@@ -24,7 +27,7 @@ using namespace EE::System;
namespace EE { namespace Network { namespace EE { namespace Network {
/** @brief A HTTP client */ /** @brief A HTTP client */
class EE_API Http : NonCopyable { class EE_API Http : NonCopyable, public std::enable_shared_from_this<Http> {
public: public:
/** @brief Define a HTTP response */ /** @brief Define a HTTP response */
class EE_API Response { class EE_API Response {
@@ -346,11 +349,12 @@ class EE_API Http : NonCopyable {
/** Get the cancel callback */ /** Get the cancel callback */
const CancelCallback& getCancelCallback() const; const CancelCallback& getCancelCallback() const;
/** Cancels the current request if being processed */ /** Cancels the current request if being processed. When resetCancelCallback is true,
* progress and cancellation callbacks are also released. */
void cancel( bool resetCancelCallback = false ); void cancel( bool resetCancelCallback = false );
/** @return True if the current request was cancelled */ /** @return True if the current request was cancelled */
const bool& isCancelled() const; bool isCancelled() const;
/** @return If requests a compressed response */ /** @return If requests a compressed response */
const bool& isCompressedResponse() const; const bool& isCompressedResponse() const;
@@ -400,7 +404,7 @@ class EE_API Http : NonCopyable {
bool mFollowRedirect; ///< Follows redirect response codes bool mFollowRedirect; ///< Follows redirect response codes
bool mCompressedResponse; ///< Request compressed response bool mCompressedResponse; ///< Request compressed response
bool mContinue; ///< Resume download bool mContinue; ///< Resume download
mutable bool mCancel; ///< Cancel state of current request std::shared_ptr<std::atomic<bool>> mCancel; ///< Cancel state shared by request copies
bool mVerbose{ false }; ///< Enable/Disable verbosity bool mVerbose{ false }; ///< Enable/Disable verbosity
ProgressCallback mProgressCallback; ///< Progress callback ProgressCallback mProgressCallback; ///< Progress callback
CancelCallback mCancelCallback; ///< Cancel callback CancelCallback mCancelCallback; ///< Cancel callback
@@ -503,26 +507,31 @@ class EE_API Http : NonCopyable {
Response downloadRequest( const Request& request, std::string writePath, Response downloadRequest( const Request& request, std::string writePath,
Time timeout = Time::Zero ); Time timeout = Time::Zero );
/** Definition of the async callback response */ /** Definition of the async callback response.
*
* Pool/shared-owned clients are retained through operation completion. Destroying any other
* Http from a different thread cancels and joins its operations. A raw or stack Http must not
* be destroyed from one of its own callbacks. */
typedef std::function<void( const Http&, Http::Request&, Http::Response& )> typedef std::function<void( const Http&, Http::Request&, Http::Response& )>
AsyncResponseCallback; AsyncResponseCallback;
/** @brief Sends the request and creates a new thread, when got the response informs the result /** @brief Schedules the request asynchronously and passes the response to the callback.
** to the callback. * This function does not lock the caller thread. ** This function does not lock the caller thread.
** @see sendRequest ** @see sendRequest
** @return Unique Id of the request added */ ** @return Unique Id of the request added */
Uint64 sendAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, Uint64 sendAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request,
Time timeout = Time::Zero ); Time timeout = Time::Zero );
/** @brief Sends the request and creates a new thread, when got the response informs the result /** @brief Schedules the request asynchronously and passes the response to the callback.
*to the callback. * This function does not lock the caller thread. ** This function does not lock the caller thread.
** The caller must keep writeTo alive until the callback or an Http/Pool operation barrier.
** @see downloadRequest ** @see downloadRequest
** @return Unique Id of the request added */ ** @return Unique Id of the request added */
Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request,
IOStream& writeTo, Time timeout = Time::Zero ); IOStream& writeTo, Time timeout = Time::Zero );
/** @brief Sends the request and creates a new thread, when got the response informs the result /** @brief Schedules the request asynchronously and passes the response to the callback.
*to the callback. * This function does not lock the caller thread. ** This function does not lock the caller thread.
** @see downloadRequest ** @see downloadRequest
** @return Unique Id of the request added */ ** @return Unique Id of the request added */
Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request,
@@ -626,7 +635,9 @@ class EE_API Http : NonCopyable {
~Pool(); ~Pool();
/** Clear all the HTTP Clients */ /** Cancel and clear all HTTP clients. Outside an HTTP callback this is an operation
* barrier. From a shared-pool callback, completion is deferred to avoid waiting for work
* queued behind the current callback. */
void clear(); void clear();
/** @return True if the client already exists in the pool /** @return True if the client already exists in the pool
@@ -709,7 +720,8 @@ class EE_API Http : NonCopyable {
/** It will try to get the proxy from the environment variables. */ /** It will try to get the proxy from the environment variables. */
static URI getEnvProxyURI(); static URI getEnvProxyURI();
/** Set the thread pool to consume for async requests, otherwise it will use its own */ /** Set the externally owned thread pool used for async requests, otherwise each Http uses its
* own threads. Http cancellation and destruction do not stop or drain this pool. */
static void setThreadPool( std::shared_ptr<ThreadPool> pool ); static void setThreadPool( std::shared_ptr<ThreadPool> pool );
private: private:
@@ -734,6 +746,10 @@ class EE_API Http : NonCopyable {
void cancel( bool resetCancelCallback = false ); void cancel( bool resetCancelCallback = false );
static AsyncRequest* current();
bool fromLocalPool() const { return mFromLocalPool; }
protected: protected:
friend class Http; friend class Http;
Uint64 mId{ 0 }; Uint64 mId{ 0 };
@@ -741,11 +757,26 @@ class EE_API Http : NonCopyable {
AsyncResponseCallback mCb; AsyncResponseCallback mCb;
Http::Request mRequest; Http::Request mRequest;
Time mTimeout; Time mTimeout;
bool mRunning;
bool mStreamed; bool mStreamed;
bool mStreamOwned; bool mStreamOwned;
bool mFromLocalPool; bool mFromLocalPool;
IOStream* mStream; IOStream* mStream;
static thread_local AsyncRequest* sCurrent;
};
class SharedRequestOperation {
public:
SharedRequestOperation( Http& http, std::shared_ptr<AsyncRequest> request );
~SharedRequestOperation();
void run();
private:
Http& mHttp;
std::shared_ptr<Http> mOwner;
std::shared_ptr<AsyncRequest> mRequest;
}; };
class HttpConnection { class HttpConnection {
@@ -797,10 +828,21 @@ class EE_API Http : NonCopyable {
bool mHostSolved; bool mHostSolved;
std::atomic<bool> mShuttingDown{ false }; std::atomic<bool> mShuttingDown{ false };
URI mProxy; URI mProxy;
Mutex mCurRequestsMutex; std::mutex mRequestsMutex;
std::condition_variable mRequestsComplete;
std::unordered_map<Uint64, AsyncRequest*> mCurRequests; std::unordered_map<Uint64, AsyncRequest*> mCurRequests;
std::unordered_map<Uint64, std::shared_ptr<AsyncRequest>> mSharedRequests;
void removeAsyncRequest( AsyncRequest* req ); bool removeAsyncRequest( AsyncRequest* req );
bool registerSharedRequest( const std::shared_ptr<AsyncRequest>& request );
void completeSharedRequest( const std::shared_ptr<AsyncRequest>& request );
bool scheduleSharedRequest( const std::shared_ptr<ThreadPool>& threadPool,
const std::shared_ptr<AsyncRequest>& request );
void shutdown( bool waitForSharedRequests = true );
Request prepareFields( const Http::Request& request ); Request prepareFields( const Http::Request& request );
+3 -2
View File
@@ -67,8 +67,9 @@ class EE_API Socket : NonCopyable {
/** @brief Create the internal representation of the socket /** @brief Create the internal representation of the socket
/// ///
** This function can only be accessed by derived classes. */ ** This function can only be accessed by derived classes.
void create(); ** @return True when a valid socket exists, false when socket creation failed. */
bool create();
/** @brief Create the internal representation of the socket from a socket handle /** @brief Create the internal representation of the socket from a socket handle
** This function can only be accessed by derived classes. ** This function can only be accessed by derived classes.
+84 -34
View File
@@ -5,6 +5,7 @@
#undef Rect #undef Rect
#endif #endif
#include <eepp/core/lrucache.hpp>
#include <eepp/core/string.hpp> #include <eepp/core/string.hpp>
#include <eepp/graphics/font.hpp> #include <eepp/graphics/font.hpp>
#include <eepp/graphics/systemfontresolver.hpp> #include <eepp/graphics/systemfontresolver.hpp>
@@ -52,42 +53,93 @@
using namespace EE::System; using namespace EE::System;
namespace EE::Graphics::SystemFontResolverDetail {
enum class FontProbeKind : Uint8 { Codepoint, SfntTable };
struct FontProbeKey {
std::string path;
FT_ULong value;
FontProbeKind kind;
bool operator==( const FontProbeKey& other ) const {
return value == other.value && kind == other.kind && path == other.path;
}
};
} // namespace EE::Graphics::SystemFontResolverDetail
namespace std {
template <> struct hash<EE::Graphics::SystemFontResolverDetail::FontProbeKey> {
std::size_t
operator()( const EE::Graphics::SystemFontResolverDetail::FontProbeKey& key ) const noexcept {
return hashCombine( std::hash<std::string>{}( key.path ),
std::hash<FT_ULong>{}( key.value ),
static_cast<std::size_t>( key.kind ) );
}
};
} // namespace std
namespace { namespace {
using FontProbeKey = EE::Graphics::SystemFontResolverDetail::FontProbeKey;
using FontProbeKind = EE::Graphics::SystemFontResolverDetail::FontProbeKind;
struct FreeTypeState { struct FreeTypeState {
static constexpr std::size_t MaxCachedProbes = 4096;
FT_Library library{ nullptr }; FT_Library library{ nullptr };
EE::UnorderedMap<std::string, FT_Face> faceCache;
Mutex mutex; Mutex mutex;
EE::LRUCache<MaxCachedProbes, FontProbeKey, bool> probeCache;
FreeTypeState() { FT_Init_FreeType( &library ); } FreeTypeState() { FT_Init_FreeType( &library ); }
~FreeTypeState() { ~FreeTypeState() {
Lock lock( mutex ); Lock lock( mutex );
if ( library )
for ( auto& pair : faceCache ) {
FT_Done_Face( pair.second );
}
if ( library ) {
FT_Done_FreeType( library ); FT_Done_FreeType( library );
} }
}
FT_Face getFace( const std::string& path ) { bool containsCodepoint( const std::string& path, EE::Uint32 codepoint ) {
Lock lock( mutex ); Lock lock( mutex );
FontProbeKey key{ path, codepoint, FontProbeKind::Codepoint };
if ( auto cached = probeCache.get( key ) )
return *cached;
auto it = faceCache.find( path ); FT_Face face{ nullptr };
if ( it != faceCache.end() ) { bool contains = false;
return it->second; if ( library && FT_New_Face( library, path.c_str(), 0, &face ) == 0 ) {
contains = FT_Get_Char_Index( face, codepoint ) != 0;
FT_Done_Face( face );
} }
FT_Face face; probeCache.put( std::move( key ), contains );
if ( FT_New_Face( library, path.c_str(), 0, &face ) == 0 ) { return contains;
faceCache[path] = face;
return face;
} }
return nullptr; bool hasSfntTable( const std::string& path, FT_ULong tag ) {
Lock lock( mutex );
FontProbeKey key{ path, tag, FontProbeKind::SfntTable };
if ( auto cached = probeCache.get( key ) )
return *cached;
FT_Face face{ nullptr };
bool found = false;
if ( library && FT_New_Face( library, path.c_str(), 0, &face ) == 0 ) {
FT_ULong length = 0;
found = FT_Load_Sfnt_Table( face, tag, 0, nullptr, &length ) == 0 && length > 0;
FT_Done_Face( face );
}
probeCache.put( std::move( key ), found );
return found;
}
void clearProbeCache() {
Lock lock( mutex );
probeCache.clear();
} }
}; };
@@ -106,17 +158,19 @@ void destroyFTState() {
gFtState.reset(); gFtState.reset();
} }
void clearFTProbeCache() {
std::shared_ptr<FreeTypeState> state;
{
Lock lock( gStateInitMutex );
state = gFtState;
}
if ( state )
state->clearProbeCache();
}
bool fontHasSfntTable( const std::string& path, FT_ULong tag ) { bool fontHasSfntTable( const std::string& path, FT_ULong tag ) {
std::shared_ptr<FreeTypeState> state = getFTState(); std::shared_ptr<FreeTypeState> state = getFTState();
if ( !state ) return state && state->hasSfntTable( path, tag );
return false;
FT_Face face = state->getFace( path );
if ( !face )
return false;
FT_ULong length = 0;
return FT_Load_Sfnt_Table( face, tag, 0, nullptr, &length ) == 0 && length > 0;
} }
bool fontHasSvgTable( const std::string& path ) { bool fontHasSvgTable( const std::string& path ) {
@@ -147,6 +201,7 @@ bool SystemFontResolver::isEnabled() {
} }
void SystemFontResolver::invalidateCache() { void SystemFontResolver::invalidateCache() {
{
Lock lock( mMutex ); Lock lock( mMutex );
mResolveCache.clear(); mResolveCache.clear();
mGenericCache.clear(); mGenericCache.clear();
@@ -154,6 +209,8 @@ void SystemFontResolver::invalidateCache() {
mFontList.clear(); mFontList.clear();
mGenericFallbacks.clear(); mGenericFallbacks.clear();
mFontListPopulated = false; mFontListPopulated = false;
}
clearFTProbeCache();
} }
static std::string normalizeFamily( const std::string& family ) { static std::string normalizeFamily( const std::string& family ) {
@@ -436,14 +493,7 @@ FontDesc SystemFontResolver::getFallbackForCodepoint( Uint32 codepoint, FontWeig
bool SystemFontResolver::fontContainsCodepoint( const std::string& path, Uint32 codepoint ) { bool SystemFontResolver::fontContainsCodepoint( const std::string& path, Uint32 codepoint ) {
std::shared_ptr<FreeTypeState> state = getFTState(); std::shared_ptr<FreeTypeState> state = getFTState();
if ( !state ) return state && state->containsCodepoint( path, codepoint );
return false;
FT_Face face = state->getFace( path );
if ( !face )
return false;
return FT_Get_Char_Index( face, codepoint ) != 0;
} }
void SystemFontResolver::populateGenericFallbacks() const { void SystemFontResolver::populateGenericFallbacks() const {
+175 -92
View File
@@ -108,7 +108,7 @@ Http::Request::Request( const std::string& uri, Method method, const std::string
mFollowRedirect( followRedirect ), mFollowRedirect( followRedirect ),
mCompressedResponse( compressedResponse ), mCompressedResponse( compressedResponse ),
mContinue( false ), mContinue( false ),
mCancel( false ), mCancel( std::make_shared<std::atomic<bool>>( false ) ),
mMaxRedirections( 10 ), mMaxRedirections( 10 ),
mRedirectionCount( 0 ) { mRedirectionCount( 0 ) {
setMethod( method ); setMethod( method );
@@ -215,14 +215,15 @@ const Http::Request::CancelCallback& Http::Request::getCancelCallback() const {
} }
void Http::Request::cancel( bool resetCancelCallback ) { void Http::Request::cancel( bool resetCancelCallback ) {
mCancel = true; mCancel->store( true, std::memory_order_release );
if ( resetCancelCallback ) {
setProgressCallback( {} ); setProgressCallback( {} );
if ( resetCancelCallback )
setCancelCallback( {} ); setCancelCallback( {} );
}
} }
const bool& Http::Request::isCancelled() const { bool Http::Request::isCancelled() const {
return mCancel; return mCancel->load( std::memory_order_acquire );
} }
std::string Http::Request::prepareTunnel( const Http& http ) { std::string Http::Request::prepareTunnel( const Http& http ) {
@@ -625,6 +626,12 @@ void Http::Response::parseFields( std::istream& in ) {
static Http::Pool sGlobalHttpPool = Http::Pool(); static Http::Pool sGlobalHttpPool = Http::Pool();
static std::shared_ptr<ThreadPool> sGlobalThreadPool = nullptr; static std::shared_ptr<ThreadPool> sGlobalThreadPool = nullptr;
static std::mutex sGlobalThreadPoolMutex;
static std::shared_ptr<ThreadPool> getGlobalThreadPool() {
std::lock_guard<std::mutex> lock( sGlobalThreadPoolMutex );
return sGlobalThreadPool;
}
Http::Response Http::request( const URI& uri, Request::Method method, const Time& timeout, Http::Response Http::request( const URI& uri, Request::Method method, const Time& timeout,
const Http::Request::ProgressCallback& progressCallback, const Http::Request::ProgressCallback& progressCallback,
@@ -709,23 +716,7 @@ Http::Http( const std::string& host, unsigned short port, bool useSSL, URI proxy
} }
Http::~Http() { Http::~Http() {
mShuttingDown = true; shutdown();
{
Lock l( mCurRequestsMutex );
for ( auto [_, req] : mCurRequests )
req->cancel();
}
{
Lock l( mThreadsMutex );
// First we wait to finish any request pending
for ( auto& thread : mThreads ) {
thread->cancel();
thread->wait();
eeDelete( thread );
}
}
// Then we destroy the last open connection // Then we destroy the last open connection
HttpConnection* connection = mConnection; HttpConnection* connection = mConnection;
@@ -796,6 +787,8 @@ Http::Response Http::sendRequest( const Http::Request& request, Time timeout ) {
static bool sendProgress( const Http& http, const Http::Request& request, static bool sendProgress( const Http& http, const Http::Request& request,
const Http::Response& response, const Http::Request::Status& status, const Http::Response& response, const Http::Request::Status& status,
const std::size_t& totalBytes, const std::size_t& currentBytes ) { const std::size_t& totalBytes, const std::size_t& currentBytes ) {
if ( request.isCancelled() )
return false;
if ( request.getProgressCallback() ) if ( request.getProgressCallback() )
return request.getProgressCallback()( http, request, response, status, totalBytes, return request.getProgressCallback()( http, request, response, status, totalBytes,
currentBytes ); currentBytes );
@@ -978,7 +971,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
requestStr.c_str(), requestStr.size() ) == Socket::Done ) { requestStr.c_str(), requestStr.size() ) == Socket::Done ) {
if ( !request.isCancelled() && if ( !request.isCancelled() &&
!sendProgress( *this, request, received, Request::Sent, 0, 0 ) ) { !sendProgress( *this, request, received, Request::Sent, 0, 0 ) ) {
request.mCancel = true; request.mCancel->store( true, std::memory_order_release );
} }
// Wait for the server's response // Wait for the server's response
@@ -1089,7 +1082,8 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
!sendProgress( *this, request, received, !sendProgress( *this, request, received,
Request::Redirect, contentLength, Request::Redirect, contentLength,
currentTotalBytes ) ) { currentTotalBytes ) ) {
request.mCancel = true; request.mCancel->store( true,
std::memory_order_release );
} else { } else {
Http::Request newRequest( request ); Http::Request newRequest( request );
newRequest.setUri( uri.getPathAndQuery() ); newRequest.setUri( uri.getPathAndQuery() );
@@ -1129,7 +1123,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
if ( !request.isCancelled() && if ( !request.isCancelled() &&
!sendProgress( *this, request, received, !sendProgress( *this, request, received,
Request::HeaderReceived, contentLength, 0 ) ) { Request::HeaderReceived, contentLength, 0 ) ) {
request.mCancel = true; request.mCancel->store( true, std::memory_order_release );
} }
// Move the response body bytes already read into the socket buffer. // Move the response body bytes already read into the socket buffer.
@@ -1155,7 +1149,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr
if ( !request.isCancelled() && if ( !request.isCancelled() &&
!sendProgress( *this, request, received, Request::ContentReceived, !sendProgress( *this, request, received, Request::ContentReceived,
contentLength, currentTotalBytes ) ) { contentLength, currentTotalBytes ) ) {
request.mCancel = true; request.mCancel->store( true, std::memory_order_release );
break; break;
} }
@@ -1241,10 +1235,12 @@ Http::Response Http::downloadRequest( const Http::Request& request, std::string
} }
void Http::setThreadPool( std::shared_ptr<ThreadPool> pool ) { void Http::setThreadPool( std::shared_ptr<ThreadPool> pool ) {
std::lock_guard<std::mutex> lock( sGlobalThreadPoolMutex );
sGlobalThreadPool = pool; sGlobalThreadPool = pool;
} }
std::atomic<Uint64> Http::AsyncRequest::IdCounter = 1; std::atomic<Uint64> Http::AsyncRequest::IdCounter = 1;
thread_local Http::AsyncRequest* Http::AsyncRequest::sCurrent = nullptr;
Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncResponseCallback& cb, Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncResponseCallback& cb,
Http::Request request, Time timeout, bool fromLocalPool ) : Http::Request request, Time timeout, bool fromLocalPool ) :
@@ -1253,7 +1249,6 @@ Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncRespon
mCb( cb ), mCb( cb ),
mRequest( request ), mRequest( request ),
mTimeout( timeout ), mTimeout( timeout ),
mRunning( true ),
mStreamed( false ), mStreamed( false ),
mStreamOwned( false ), mStreamOwned( false ),
mFromLocalPool( fromLocalPool ), mFromLocalPool( fromLocalPool ),
@@ -1267,7 +1262,6 @@ Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncRespon
mCb( cb ), mCb( cb ),
mRequest( request ), mRequest( request ),
mTimeout( timeout ), mTimeout( timeout ),
mRunning( true ),
mStreamed( true ), mStreamed( true ),
mStreamOwned( false ), mStreamOwned( false ),
mFromLocalPool( fromLocalPool ), mFromLocalPool( fromLocalPool ),
@@ -1281,7 +1275,6 @@ Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncRespon
mCb( cb ), mCb( cb ),
mRequest( request ), mRequest( request ),
mTimeout( timeout ), mTimeout( timeout ),
mRunning( true ),
mStreamed( true ), mStreamed( true ),
mStreamOwned( true ), mStreamOwned( true ),
mFromLocalPool( fromLocalPool ), mFromLocalPool( fromLocalPool ),
@@ -1297,6 +1290,9 @@ void Http::AsyncRequest::cancel( bool resetCancelCallback ) {
} }
void Http::AsyncRequest::run() { void Http::AsyncRequest::run() {
AsyncRequest* previousRequest = sCurrent;
sCurrent = this;
Http::Response response = mStreamed ? mHttp->downloadRequest( mRequest, *mStream, mTimeout ) Http::Response response = mStreamed ? mHttp->downloadRequest( mRequest, *mStream, mTimeout )
: mHttp->sendRequest( mRequest, mTimeout ); : mHttp->sendRequest( mRequest, mTimeout );
@@ -1307,27 +1303,120 @@ void Http::AsyncRequest::run() {
eeSAFE_DELETE( mStream ); eeSAFE_DELETE( mStream );
} }
mRunning = false; if ( mFromLocalPool && mHttp->removeAsyncRequest( this ) ) {
if ( mFromLocalPool && !mHttp->mShuttingDown ) {
mHttp->removeAsyncRequest( this );
auto me = this; auto me = this;
sCurrent = previousRequest;
eeSAFE_DELETE( me ); eeSAFE_DELETE( me );
return;
} }
sCurrent = previousRequest;
} }
void Http::removeAsyncRequest( AsyncRequest* req ) { Http::AsyncRequest* Http::AsyncRequest::current() {
return sCurrent;
}
Http::SharedRequestOperation::SharedRequestOperation( Http& http,
std::shared_ptr<AsyncRequest> request ) :
mHttp( http ), mOwner( http.weak_from_this().lock() ), mRequest( std::move( request ) ) {}
Http::SharedRequestOperation::~SharedRequestOperation() {
mHttp.completeSharedRequest( mRequest );
}
void Http::SharedRequestOperation::run() {
mRequest->run();
}
bool Http::removeAsyncRequest( AsyncRequest* req ) {
{ {
Lock l( mCurRequestsMutex ); std::lock_guard<std::mutex> lock( mRequestsMutex );
mCurRequests.erase( req->id() ); mCurRequests.erase( req->id() );
} }
Lock l( mThreadsMutex );
if ( mShuttingDown.load( std::memory_order_acquire ) )
return false;
auto found = std::find( mThreads.begin(), mThreads.end(), req );
if ( found == mThreads.end() )
return false;
mThreads.erase( found );
return true;
}
bool Http::registerSharedRequest( const std::shared_ptr<AsyncRequest>& request ) {
std::lock_guard<std::mutex> lock( mRequestsMutex );
if ( mShuttingDown.load( std::memory_order_acquire ) )
return false;
mCurRequests[request->id()] = request.get();
mSharedRequests[request->id()] = request;
return true;
}
void Http::completeSharedRequest( const std::shared_ptr<AsyncRequest>& request ) {
{
std::lock_guard<std::mutex> lock( mRequestsMutex );
mCurRequests.erase( request->id() );
mSharedRequests.erase( request->id() );
}
mRequestsComplete.notify_all();
}
bool Http::scheduleSharedRequest( const std::shared_ptr<ThreadPool>& threadPool,
const std::shared_ptr<AsyncRequest>& request ) {
if ( !registerSharedRequest( request ) ) {
request->cancel();
return false;
}
// The executor is externally owned and may discard queued work. The operation reports
// completion when the queued function releases it, whether or not run() was called.
auto operation = std::make_shared<SharedRequestOperation>( *this, request );
threadPool->run( [operation] { operation->run(); } );
return true;
}
void Http::shutdown( bool waitForSharedRequests ) {
std::vector<std::shared_ptr<AsyncRequest>> sharedRequests;
{
std::lock_guard<std::mutex> lock( mRequestsMutex );
mShuttingDown.store( true, std::memory_order_release );
sharedRequests.reserve( mSharedRequests.size() );
for ( const auto& [_, request] : mSharedRequests )
sharedRequests.emplace_back( request );
}
for ( const auto& request : sharedRequests )
request->cancel();
std::vector<AsyncRequest*> threads;
{ {
Lock l( mThreadsMutex ); Lock l( mThreadsMutex );
auto found = std::find( mThreads.begin(), mThreads.end(), req ); threads.swap( mThreads );
if ( found != mThreads.end() )
mThreads.erase( found );
} }
for ( auto* thread : threads ) {
thread->cancel();
thread->wait();
{
std::lock_guard<std::mutex> lock( mRequestsMutex );
mCurRequests.erase( thread->id() );
}
eeDelete( thread );
}
if ( !waitForSharedRequests )
return;
AsyncRequest* currentRequest = AsyncRequest::current();
std::unique_lock<std::mutex> lock( mRequestsMutex );
mRequestsComplete.wait( lock, [this, currentRequest] {
if ( currentRequest && currentRequest->mHttp == this && !currentRequest->mFromLocalPool ) {
return mSharedRequests.empty() ||
( mSharedRequests.size() == 1 &&
mSharedRequests.begin()->second.get() == currentRequest );
}
return mSharedRequests.empty();
} );
} }
Http::Request Http::prepareFields( const Http::Request& request ) { Http::Request Http::prepareFields( const Http::Request& request ) {
@@ -1388,7 +1477,7 @@ bool Http::isProxied() const {
} }
bool Http::setCancelRequest( Uint64 reqId, bool resetCancelCallback ) { bool Http::setCancelRequest( Uint64 reqId, bool resetCancelCallback ) {
Lock l( mCurRequestsMutex ); std::lock_guard<std::mutex> lock( mRequestsMutex );
auto found = mCurRequests.find( reqId ); auto found = mCurRequests.find( reqId );
if ( found != mCurRequests.end() ) { if ( found != mCurRequests.end() ) {
found->second->cancel( resetCancelCallback ); found->second->cancel( resetCancelCallback );
@@ -1469,30 +1558,25 @@ Uint64 Http::sendAsyncRequest( const Http::AsyncResponseCallback& cb, const Http
emscripten_async_wget2_got_error_data, NULL ); emscripten_async_wget2_got_error_data, NULL );
return id; return id;
#else #else
if ( sGlobalThreadPool ) { auto threadPool = getGlobalThreadPool();
sGlobalThreadPool->run( [this, cb, request, timeout, id] { if ( threadPool ) {
AsyncRequest asyncRequest( id, this, cb, request, timeout, false ); auto asyncRequest = std::make_shared<AsyncRequest>( id, this, cb, request, timeout, false );
{ scheduleSharedRequest( threadPool, asyncRequest );
Lock l( mCurRequestsMutex );
mCurRequests[id] = &asyncRequest;
}
asyncRequest.run();
{
Lock l( mCurRequestsMutex );
mCurRequests.erase( id );
}
} );
return id; return id;
} }
AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, timeout, true ) ); AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, timeout, true ) );
{ {
Lock l( mCurRequestsMutex ); Lock threadsLock( mThreadsMutex );
{
std::lock_guard<std::mutex> requestsLock( mRequestsMutex );
if ( mShuttingDown.load( std::memory_order_acquire ) ) {
eeDelete( thread );
return id;
}
mCurRequests[id] = thread; mCurRequests[id] = thread;
} }
thread->launch();
{
Lock l( mThreadsMutex );
mThreads.push_back( thread ); mThreads.push_back( thread );
thread->launch();
} }
return id; return id;
#endif #endif
@@ -1514,30 +1598,26 @@ Uint64 Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb,
emscripten_async_wget2_got_error_data, NULL ); emscripten_async_wget2_got_error_data, NULL );
return id; return id;
#else #else
if ( sGlobalThreadPool ) { auto threadPool = getGlobalThreadPool();
sGlobalThreadPool->run( [this, cb, request, &writeTo, timeout, id] { if ( threadPool ) {
AsyncRequest asyncRequest( id, this, cb, request, writeTo, timeout, false ); auto asyncRequest =
{ std::make_shared<AsyncRequest>( id, this, cb, request, writeTo, timeout, false );
Lock l( mCurRequestsMutex ); scheduleSharedRequest( threadPool, asyncRequest );
mCurRequests[id] = &asyncRequest;
}
asyncRequest.run();
{
Lock l( mCurRequestsMutex );
mCurRequests.erase( id );
}
} );
return id; return id;
} }
AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, writeTo, timeout, true ) ); AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, writeTo, timeout, true ) );
{ {
Lock l( mCurRequestsMutex ); Lock threadsLock( mThreadsMutex );
{
std::lock_guard<std::mutex> requestsLock( mRequestsMutex );
if ( mShuttingDown.load( std::memory_order_acquire ) ) {
eeDelete( thread );
return id;
}
mCurRequests[id] = thread; mCurRequests[id] = thread;
} }
thread->launch();
{
Lock l( mThreadsMutex );
mThreads.push_back( thread ); mThreads.push_back( thread );
thread->launch();
} }
return id; return id;
#endif #endif
@@ -1558,31 +1638,27 @@ Uint64 Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb,
emscripten_async_wget2_got_file, emscripten_async_wget2_got_error_file, emscripten_async_wget2_got_file, emscripten_async_wget2_got_error_file,
NULL ); NULL );
#else #else
if ( sGlobalThreadPool ) { auto threadPool = getGlobalThreadPool();
sGlobalThreadPool->run( [this, cb, request, writePath, timeout, id] { if ( threadPool ) {
AsyncRequest asyncRequest( id, this, cb, request, writePath, timeout, false ); auto asyncRequest =
{ std::make_shared<AsyncRequest>( id, this, cb, request, writePath, timeout, false );
Lock l( mCurRequestsMutex ); scheduleSharedRequest( threadPool, asyncRequest );
mCurRequests[id] = &asyncRequest;
}
asyncRequest.run();
{
Lock l( mCurRequestsMutex );
mCurRequests.erase( id );
}
} );
return id; return id;
} }
AsyncRequest* thread = AsyncRequest* thread =
eeNew( AsyncRequest, ( id, this, cb, request, writePath, timeout, true ) ); eeNew( AsyncRequest, ( id, this, cb, request, writePath, timeout, true ) );
{ {
Lock l( mCurRequestsMutex ); Lock threadsLock( mThreadsMutex );
{
std::lock_guard<std::mutex> requestsLock( mRequestsMutex );
if ( mShuttingDown.load( std::memory_order_acquire ) ) {
eeDelete( thread );
return id;
}
mCurRequests[id] = thread; mCurRequests[id] = thread;
} }
thread->launch();
{
Lock l( mThreadsMutex );
mThreads.push_back( thread ); mThreads.push_back( thread );
thread->launch();
} }
#endif #endif
return id; return id;
@@ -1690,8 +1766,15 @@ void Http::Pool::clear() {
Lock l( mMutex ); Lock l( mMutex );
https.swap( mHttps ); https.swap( mHttps );
} }
// Http destruction joins local request threads and callbacks can re-enter the global pool. // Requests may keep their Http alive while running on the shared ThreadPool. Establish the
// Never run either operation while holding the pool mutex. // operation barrier explicitly before releasing the Pool's ownership. Callbacks can re-enter
// the global Pool, so never wait while holding the Pool mutex. A shared-pool callback also
// cannot wait for requests queued behind itself; their shared operations retain each Http until
// drained.
AsyncRequest* currentRequest = AsyncRequest::current();
const bool waitForSharedRequests = !currentRequest || currentRequest->fromLocalPool();
for ( const auto& [_, http] : https )
http->shutdown( waitForSharedRequests );
https.clear(); https.clear();
} }
+15 -1
View File
@@ -1,3 +1,5 @@
#include <cerrno>
#include <cstring>
#include <eepp/network/platform/platformimpl.hpp> #include <eepp/network/platform/platformimpl.hpp>
#include <eepp/network/socket.hpp> #include <eepp/network/socket.hpp>
#include <eepp/system/log.hpp> #include <eepp/system/log.hpp>
@@ -28,12 +30,24 @@ SocketHandle Socket::getHandle() const {
return mSocket; return mSocket;
} }
void Socket::create() { bool Socket::create() {
// Don't create the socket if it already exists // Don't create the socket if it already exists
if ( mSocket == Private::SocketImpl::invalidSocket() ) { if ( mSocket == Private::SocketImpl::invalidSocket() ) {
SocketHandle handle = socket( PF_INET, mType == Tcp ? SOCK_STREAM : SOCK_DGRAM, 0 ); SocketHandle handle = socket( PF_INET, mType == Tcp ? SOCK_STREAM : SOCK_DGRAM, 0 );
if ( handle == Private::SocketImpl::invalidSocket() ) {
#if EE_PLATFORM == EE_PLATFORM_WIN
Log::error( "Failed to create %s socket (Winsock error %d)",
mType == Tcp ? "TCP" : "UDP", WSAGetLastError() );
#else
const int errorCode = errno;
Log::error( "Failed to create %s socket: %s (errno %d)", mType == Tcp ? "TCP" : "UDP",
std::strerror( errorCode ), errorCode );
#endif
return false;
}
create( handle ); create( handle );
} }
return true;
} }
void Socket::create( SocketHandle handle ) { void Socket::create( SocketHandle handle ) {
@@ -59,6 +59,7 @@ bool MbedTLSSocket::end() {
MbedTLSSocket::MbedTLSSocket( SSLSocket* socket ) : MbedTLSSocket::MbedTLSSocket( SSLSocket* socket ) :
SSLSocketImpl( socket ), SSLSocketImpl( socket ),
mInitialized( false ),
mConnected( false ), mConnected( false ),
mSessionOwner( false ), mSessionOwner( false ),
mStatus( Socket::Disconnected ), mStatus( Socket::Disconnected ),
@@ -110,9 +111,9 @@ int MbedTLSSocket::bio_recv( void* ctx, unsigned char* buf, size_t len ) {
Socket::Status MbedTLSSocket::connect( const IpAddress& /*remoteAddress*/, Socket::Status MbedTLSSocket::connect( const IpAddress& /*remoteAddress*/,
unsigned short /*remotePort*/, Time timeout ) { unsigned short /*remotePort*/, Time timeout ) {
if ( mConnected ) { // A previous handshake may have initialized TLS state without ever reaching Connected.
// Always release it before starting a new handshake.
disconnect(); disconnect();
}
bool isBlocking = mSSLSocket->isBlocking(); bool isBlocking = mSSLSocket->isBlocking();
if ( isBlocking && timeout != Time::Zero ) { if ( isBlocking && timeout != Time::Zero ) {
@@ -129,12 +130,15 @@ Socket::Status MbedTLSSocket::connect( const IpAddress& /*remoteAddress*/,
mbedtls_ssl_config_init( &mSSLConfig ); mbedtls_ssl_config_init( &mSSLConfig );
mbedtls_ctr_drbg_init( &mCtrDrbg ); mbedtls_ctr_drbg_init( &mCtrDrbg );
mbedtls_entropy_init( &mEntropy ); mbedtls_entropy_init( &mEntropy );
mInitialized = true;
ret = mbedtls_ctr_drbg_seed( &mCtrDrbg, mbedtls_entropy_func, &mEntropy, NULL, 0 ); ret = mbedtls_ctr_drbg_seed( &mCtrDrbg, mbedtls_entropy_func, &mEntropy, NULL, 0 );
if ( ret != 0 ) { if ( ret != 0 ) {
Log::error( " failed\n ! mbedtls_ctr_drbg_seed returned an error: %d", ret ); Log::error( " failed\n ! mbedtls_ctr_drbg_seed returned an error: %d", ret );
return Socket::Error; disconnect();
mStatus = Socket::Error;
return mStatus;
} }
mbedtls_ssl_config_defaults( &mSSLConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, mbedtls_ssl_config_defaults( &mSSLConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
@@ -167,19 +171,23 @@ Socket::Status MbedTLSSocket::connect( const IpAddress& /*remoteAddress*/,
mSSLSocket->setSendTimeout( Time::Zero ); mSSLSocket->setSendTimeout( Time::Zero );
} }
if ( mStatus == Socket::Error ) if ( mStatus == Socket::Error ) {
disconnect();
mStatus = Socket::Error;
return mStatus; return mStatus;
}
mSSLSession = (mbedtls_ssl_session*)eeMalloc( sizeof( mbedtls_ssl_session ) ); mSSLSession = (mbedtls_ssl_session*)eeMalloc( sizeof( mbedtls_ssl_session ) );
if ( mSSLSession ) {
mbedtls_ssl_session_init( mSSLSession ); mbedtls_ssl_session_init( mSSLSession );
ret = mbedtls_ssl_get_session( &mSSLContext, mSSLSession ); ret = mbedtls_ssl_get_session( &mSSLContext, mSSLSession );
if ( ret ) { if ( ret ) {
if ( ret != MBEDTLS_ERR_SSL_ALLOC_FAILED )
mbedtls_ssl_session_free( mSSLSession ); mbedtls_ssl_session_free( mSSLSession );
eeSAFE_FREE( mSSLSession ); eeSAFE_FREE( mSSLSession );
} else { } else {
mSessionOwner = true; mSessionOwner = true;
} }
}
} else { } else {
MbedTLSSocket* oldSocket = MbedTLSSocket* oldSocket =
reinterpret_cast<MbedTLSSocket*>( mSSLSocket->mRestoreSession->mImpl ); reinterpret_cast<MbedTLSSocket*>( mSSLSocket->mRestoreSession->mImpl );
@@ -193,19 +201,21 @@ Socket::Status MbedTLSSocket::connect( const IpAddress& /*remoteAddress*/,
} }
void MbedTLSSocket::disconnect() { void MbedTLSSocket::disconnect() {
if ( !mConnected ) if ( mInitialized ) {
return;
mbedtls_ssl_free( &mSSLContext ); mbedtls_ssl_free( &mSSLContext );
mbedtls_ssl_config_free( &mSSLConfig ); mbedtls_ssl_config_free( &mSSLConfig );
mbedtls_ctr_drbg_free( &mCtrDrbg ); mbedtls_ctr_drbg_free( &mCtrDrbg );
mbedtls_entropy_free( &mEntropy ); mbedtls_entropy_free( &mEntropy );
mInitialized = false;
}
if ( mSessionOwner && NULL != mSSLSession ) { if ( mSessionOwner && NULL != mSSLSession ) {
mbedtls_ssl_session_free( mSSLSession ); mbedtls_ssl_session_free( mSSLSession );
eeSAFE_FREE( mSSLSession ); eeSAFE_FREE( mSSLSession );
} }
mConnected = false;
mSessionOwner = false;
mStatus = Socket::Disconnected; mStatus = Socket::Disconnected;
} }
@@ -5,10 +5,10 @@
#ifdef EE_MBEDTLS #ifdef EE_MBEDTLS
#include <mbedtls/mbedtls_config.h>
#include <mbedtls/ctr_drbg.h> #include <mbedtls/ctr_drbg.h>
#include <mbedtls/debug.h> #include <mbedtls/debug.h>
#include <mbedtls/entropy.h> #include <mbedtls/entropy.h>
#include <mbedtls/mbedtls_config.h>
#include <mbedtls/ssl.h> #include <mbedtls/ssl.h>
namespace EE { namespace Network { namespace SSL { namespace EE { namespace Network { namespace SSL {
@@ -40,6 +40,7 @@ class EE_API MbedTLSSocket : public SSLSocketImpl {
mbedtls_ctr_drbg_context mCtrDrbg; mbedtls_ctr_drbg_context mCtrDrbg;
mbedtls_ssl_context mSSLContext; mbedtls_ssl_context mSSLContext;
mbedtls_ssl_config mSSLConfig; mbedtls_ssl_config mSSLConfig;
bool mInitialized;
bool mConnected; bool mConnected;
bool mSessionOwner; bool mSessionOwner;
Socket::Status mStatus; Socket::Status mStatus;
+2 -1
View File
@@ -27,7 +27,8 @@ Socket::Status TcpListener::listen( unsigned short port, const IpAddress& addres
close(); close();
// Create the internal socket if it doesn't exist // Create the internal socket if it doesn't exist
create(); if ( !create() )
return Error;
// Check if the address is valid // Check if the address is valid
if ( ( address == IpAddress::None ) || ( address == IpAddress::Broadcast ) ) if ( ( address == IpAddress::None ) || ( address == IpAddress::Broadcast ) )
+2 -1
View File
@@ -91,7 +91,8 @@ Socket::Status TcpSocket::connect( const IpAddress& remoteAddress, unsigned shor
disconnect(); disconnect();
// Create the internal socket if it doesn't exist // Create the internal socket if it doesn't exist
create(); if ( !create() )
return Error;
// Create the remote address // Create the remote address
sockaddr_in address = sockaddr_in address =
+4 -2
View File
@@ -31,7 +31,8 @@ Socket::Status UdpSocket::bind( unsigned short port, const IpAddress& address )
close(); close();
// Create the internal socket if it doesn't exist // Create the internal socket if it doesn't exist
create(); if ( !create() )
return Error;
// Check if the address is valid // Check if the address is valid
if ( ( address == IpAddress::None ) || ( address == IpAddress::Broadcast ) ) if ( ( address == IpAddress::None ) || ( address == IpAddress::Broadcast ) )
@@ -55,7 +56,8 @@ void UdpSocket::unbind() {
Socket::Status UdpSocket::send( const void* data, std::size_t size, const IpAddress& remoteAddress, Socket::Status UdpSocket::send( const void* data, std::size_t size, const IpAddress& remoteAddress,
unsigned short remotePort ) { unsigned short remotePort ) {
// Create the internal socket if it doesn't exist // Create the internal socket if it doesn't exist
create(); if ( !create() )
return Error;
// Make sure that all the data will fit in one datagram // Make sure that all the data will fit in one datagram
if ( size > MaxDatagramSize ) { if ( size > MaxDatagramSize ) {
+1 -1
View File
@@ -41,7 +41,7 @@ void UITextureViewer::init() {
</ScrollView> </ScrollView>
<RelativeLayout class="image_container" lw="mp" lh="mp" visible="false" enabled="false" background-color="black"> <RelativeLayout class="image_container" lw="mp" lh="mp" visible="false" enabled="false" background-color="black">
<Image lw="mp" lh="mp" scaleType="fit_inside" gravity="center" enabled="false" lg="center" /> <Image lw="mp" lh="mp" scaleType="fit_inside" gravity="center" enabled="false" lg="center" />
<TextView id="image_close" lw="wc" lh="wc" text="&#xeb99;" lg="top|right" enabled="false" /> <Image id="image_close" lw="22dp" lh="22dp" icon="icon(document-close, 22dp)" lg="top|right" enabled="false" />
</RelativeLayout> </RelativeLayout>
)xml", )xml",
this ); this );
+242
View File
@@ -8,8 +8,12 @@
#include <vector> #include <vector>
#include <eepp/network/http.hpp> #include <eepp/network/http.hpp>
#include <eepp/network/ssl/sslsocket.hpp>
#include <eepp/network/tcplistener.hpp> #include <eepp/network/tcplistener.hpp>
#include <eepp/network/tcpsocket.hpp> #include <eepp/network/tcpsocket.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/iostreamstring.hpp>
#include <eepp/system/sys.hpp>
#if EE_PLATFORM != EE_PLATFORM_WIN #if EE_PLATFORM != EE_PLATFORM_WIN
#include <fcntl.h> #include <fcntl.h>
@@ -20,6 +24,37 @@
using namespace EE; using namespace EE;
using namespace EE::Network; using namespace EE::Network;
namespace {
struct ThreadGate {
void enterAndWait() {
std::unique_lock<std::mutex> lock( mutex );
entered = true;
condition.notify_all();
condition.wait( lock, [this] { return released; } );
}
bool waitUntilEntered() {
std::unique_lock<std::mutex> lock( mutex );
return condition.wait_for( lock, std::chrono::seconds( 5 ), [this] { return entered; } );
}
void release() {
{
std::lock_guard<std::mutex> lock( mutex );
released = true;
}
condition.notify_all();
}
std::mutex mutex;
std::condition_variable condition;
bool entered{ false };
bool released{ false };
};
} // namespace
#if EE_PLATFORM != EE_PLATFORM_WIN #if EE_PLATFORM != EE_PLATFORM_WIN
namespace { namespace {
@@ -81,6 +116,42 @@ UTEST( Http, responseHeaderLineLargerThanReceiveBuffer ) {
EXPECT_TRUE( response.getBody() == "hello" ); EXPECT_TRUE( response.getBody() == "hello" );
} }
UTEST( Http, failedTlsHandshakesReleaseConnectionState ) {
if ( !SSL::SSLSocket::isSupported() )
return;
constexpr int Attempts = 16;
TcpListener listener;
ASSERT_EQ( listener.listen( Socket::AnyPort, IpAddress::LocalHost ), Socket::Done );
std::atomic<int> acceptedConnections{ 0 };
std::thread server( [&] {
for ( int i = 0; i < Attempts; ++i ) {
TcpSocket client;
if ( listener.accept( client ) != Socket::Done )
return;
char clientHello[1024];
std::size_t received = 0;
if ( client.receive( clientHello, sizeof( clientHello ), received ) != Socket::Done )
return;
acceptedConnections.fetch_add( 1, std::memory_order_release );
client.disconnect();
}
} );
Http http( "127.0.0.1", listener.getLocalPort(), true );
for ( int i = 0; i < Attempts; ++i ) {
Http::Request request( "/", Http::Request::Get, "", false, false );
Http::Response response = http.sendRequest( request, Seconds( 2 ) );
EXPECT_EQ( response.getStatus(), Http::Response::ConnectionFailed );
}
server.join();
listener.close();
EXPECT_EQ( acceptedConnections.load( std::memory_order_acquire ), Attempts );
}
UTEST( Http, poolClearAllowsCallbackReentry ) { UTEST( Http, poolClearAllowsCallbackReentry ) {
Http::setThreadPool( nullptr ); Http::setThreadPool( nullptr );
Http::Pool::getGlobal().clear(); Http::Pool::getGlobal().clear();
@@ -168,6 +239,177 @@ UTEST( Http, poolClearAllowsCallbackReentry ) {
EXPECT_TRUE( callbackResponseOk ); EXPECT_TRUE( callbackResponseOk );
} }
UTEST( Http, sharedThreadPoolJoinsQueuedRequestsOnPoolClear ) {
Http::setThreadPool( nullptr );
Http::Pool::getGlobal().clear();
auto threadPool = System::ThreadPool::createShared( 1 );
ThreadGate workerGate;
threadPool->run( [&] { workerGate.enterAndWait(); } );
ASSERT_TRUE( workerGate.waitUntilEntered() );
Http::setThreadPool( threadPool );
const URI uri( "http://127.0.0.1:1/" );
auto http = Http::Pool::getGlobal().get( uri );
System::IOStreamString stream;
const std::string outputPath =
System::Sys::getTempPath() + "eepp-http-shared-pool-shutdown.tmp";
System::FileSystem::fileRemove( outputPath );
std::atomic<int> callbacks{ 0 };
auto callback = [&]( const Http&, Http::Request&, Http::Response& ) { callbacks++; };
http->sendAsyncRequest( callback, Http::Request( "/memory" ), Seconds( 5 ) );
http->downloadAsyncRequest( callback, Http::Request( "/stream" ), stream, Seconds( 5 ) );
http->downloadAsyncRequest( callback, Http::Request( "/file" ), outputPath, Seconds( 5 ) );
http.reset();
std::atomic<bool> clearCompleted{ false };
std::thread clearThread( [&] {
Http::Pool::getGlobal().clear();
clearCompleted.store( true, std::memory_order_release );
} );
std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
EXPECT_FALSE( clearCompleted.load( std::memory_order_acquire ) );
workerGate.release();
clearThread.join();
EXPECT_TRUE( clearCompleted.load( std::memory_order_acquire ) );
EXPECT_EQ( callbacks.load(), 0 );
Http::setThreadPool( nullptr );
threadPool.reset();
System::FileSystem::fileRemove( outputPath );
}
UTEST( Http, sharedThreadPoolJoinsRunningRequestOnPoolClear ) {
Http::setThreadPool( nullptr );
Http::Pool::getGlobal().clear();
TcpListener listener;
ASSERT_EQ( listener.listen( Socket::AnyPort, IpAddress::LocalHost ), Socket::Done );
std::thread server( [&listener] {
TcpSocket client;
if ( listener.accept( client ) != Socket::Done )
return;
char buffer[1024];
std::size_t received = 0;
client.receive( buffer, sizeof( buffer ), received );
client.disconnect();
} );
auto threadPool = System::ThreadPool::createShared( 1 );
Http::setThreadPool( threadPool );
const URI uri( String::format( "http://127.0.0.1:%u/", listener.getLocalPort() ) );
auto http = Http::Pool::getGlobal().get( uri );
ThreadGate progressGate;
Http::Request request( "/" );
request.setProgressCallback( [&]( const Http&, const Http::Request&, const Http::Response&,
Http::Request::Status status, std::size_t, std::size_t ) {
if ( status == Http::Request::Connected )
progressGate.enterAndWait();
return true;
} );
std::atomic<int> callbacks{ 0 };
http->sendAsyncRequest( [&]( const Http&, Http::Request&, Http::Response& ) { callbacks++; },
request, Seconds( 5 ) );
http.reset();
ASSERT_TRUE( progressGate.waitUntilEntered() );
std::atomic<bool> clearCompleted{ false };
std::thread clearThread( [&] {
Http::Pool::getGlobal().clear();
clearCompleted.store( true, std::memory_order_release );
} );
std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
EXPECT_FALSE( clearCompleted.load( std::memory_order_acquire ) );
progressGate.release();
clearThread.join();
server.join();
listener.close();
EXPECT_TRUE( clearCompleted.load( std::memory_order_acquire ) );
EXPECT_EQ( callbacks.load(), 0 );
Http::setThreadPool( nullptr );
threadPool.reset();
}
UTEST( Http, sharedThreadPoolAllowsPoolClearFromCallback ) {
Http::setThreadPool( nullptr );
Http::Pool::getGlobal().clear();
TcpListener listener;
ASSERT_EQ( listener.listen( Socket::AnyPort, IpAddress::LocalHost ), Socket::Done );
std::thread server( [&listener] {
TcpSocket client;
if ( listener.accept( client ) != Socket::Done )
return;
std::string request;
char buffer[1024];
std::size_t received = 0;
while ( request.find( "\r\n\r\n" ) == std::string::npos ) {
if ( client.receive( buffer, sizeof( buffer ), received ) != Socket::Done )
return;
request.append( buffer, received );
}
const std::string response = "HTTP/1.1 200 OK\r\n"
"Content-Length: 2\r\n"
"Connection: close\r\n\r\nok";
client.send( response.data(), response.size() );
client.disconnect();
} );
auto threadPool = System::ThreadPool::createShared( 1 );
Http::setThreadPool( threadPool );
const URI uri( String::format( "http://127.0.0.1:%u/", listener.getLocalPort() ) );
auto http = Http::Pool::getGlobal().get( uri );
std::mutex callbackMutex;
std::condition_variable callbackCondition;
bool callbackCompleted = false;
bool callbackResponseOk = false;
std::atomic<int> queuedCallbacks{ 0 };
ThreadGate callbackGate;
http->sendAsyncRequest(
[&]( const Http&, Http::Request&, Http::Response& response ) {
callbackResponseOk = response.getStatus() == Http::Response::Ok;
callbackGate.enterAndWait();
Http::Pool::getGlobal().clear();
{
std::lock_guard<std::mutex> lock( callbackMutex );
callbackCompleted = true;
}
callbackCondition.notify_all();
},
Http::Request( "/" ), Seconds( 5 ) );
ASSERT_TRUE( callbackGate.waitUntilEntered() );
// This operation is queued behind the callback on the same one-thread executor. Pool clearing
// from the callback must not wait for work that only this worker can drain.
http->sendAsyncRequest(
[&]( const Http&, Http::Request&, Http::Response& ) { queuedCallbacks++; },
Http::Request( "/queued" ), Seconds( 5 ) );
http.reset();
callbackGate.release();
{
std::unique_lock<std::mutex> lock( callbackMutex );
ASSERT_TRUE( callbackCondition.wait_for( lock, std::chrono::seconds( 5 ),
[&] { return callbackCompleted; } ) );
}
ThreadGate completionGate;
threadPool->run( [&] { completionGate.enterAndWait(); } );
ASSERT_TRUE( completionGate.waitUntilEntered() );
completionGate.release();
server.join();
listener.close();
EXPECT_TRUE( callbackResponseOk );
EXPECT_EQ( queuedCallbacks.load(), 0 );
Http::setThreadPool( nullptr );
threadPool.reset();
Http::Pool::getGlobal().clear();
}
#if EE_PLATFORM != EE_PLATFORM_WIN #if EE_PLATFORM != EE_PLATFORM_WIN
UTEST( Http, tcpConnectTimeoutHandlesFdAboveFdSetSize ) { UTEST( Http, tcpConnectTimeoutHandlesFdAboveFdSetSize ) {
TcpListener listener; TcpListener listener;
@@ -2,6 +2,7 @@
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <eepp/core/lrucache.hpp>
#include <eepp/graphics/fontmanager.hpp> #include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp> #include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/framebuffermanager.hpp> #include <eepp/graphics/framebuffermanager.hpp>
@@ -89,6 +90,29 @@ class TestTextureAtlasLoader : public TextureAtlasLoader {
static_assert( !std::is_copy_constructible<Texture>::value, "Texture must not be copyable" ); static_assert( !std::is_copy_constructible<Texture>::value, "Texture must not be copyable" );
static_assert( !std::is_copy_assignable<Texture>::value, "Texture must not be copy-assignable" ); static_assert( !std::is_copy_assignable<Texture>::value, "Texture must not be copy-assignable" );
UTEST( ResourcePrerequisites, lruCacheEvictsOnlyLeastRecentlyUsedAndReleasesKeys ) {
LRUCache<2, int, bool> recencyCache;
recencyCache.put( 1, true );
recencyCache.put( 2, false );
ASSERT_TRUE( recencyCache.get( 1 ).has_value() );
recencyCache.put( 3, true );
EXPECT_TRUE( !recencyCache.get( 2 ).has_value() );
EXPECT_TRUE( recencyCache.get( 1 ).has_value() );
EXPECT_TRUE( recencyCache.get( 3 ).has_value() );
auto ownedKey = std::make_shared<int>( 1 );
std::weak_ptr<int> weakKey = ownedKey;
LRUCache<2, std::shared_ptr<int>, bool> owningKeyCache;
static_assert( decltype( owningKeyCache )::is_static() );
owningKeyCache.put( ownedKey, true );
ownedKey.reset();
ASSERT_TRUE( !weakKey.expired() );
owningKeyCache.clear();
EXPECT_TRUE( weakKey.expired() );
}
UTEST( ResourcePrerequisites, textureAtlasLoaderAppliesFilterToEveryTexture ) { UTEST( ResourcePrerequisites, textureAtlasLoaderAppliesFilterToEveryTexture ) {
Engine::instance()->createWindow( WindowSettings( 64, 64, "TextureAtlasLoader filter test", Engine::instance()->createWindow( WindowSettings( 64, 64, "TextureAtlasLoader filter test",
WindowStyle::Default, WindowBackend::Default, WindowStyle::Default, WindowBackend::Default,
@@ -6,6 +6,10 @@
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include <dirent.h>
#endif
using namespace EE; using namespace EE;
using namespace EE::Graphics; using namespace EE::Graphics;
using namespace EE::System; using namespace EE::System;
@@ -14,6 +18,22 @@ static std::string getFontsDir() {
return Sys::getProcessPath() + "../assets/fonts/"; return Sys::getProcessPath() + "../assets/fonts/";
} }
#if EE_PLATFORM == EE_PLATFORM_LINUX
static std::size_t getOpenFileDescriptorCount() {
DIR* directory = opendir( "/proc/self/fd" );
if ( !directory )
return 0;
std::size_t count = 0;
while ( dirent* entry = readdir( directory ) ) {
if ( entry->d_name[0] != '.' )
++count;
}
closedir( directory );
return count;
}
#endif
UTEST( SystemFontResolver, singletonLifecycle ) { UTEST( SystemFontResolver, singletonLifecycle ) {
SystemFontResolver::setEnabled( true ); SystemFontResolver::setEnabled( true );
UTEST_PRINT_STEP( "Create singleton" ); UTEST_PRINT_STEP( "Create singleton" );
@@ -242,6 +262,27 @@ UTEST( SystemFontResolver, getFallbackForCodepoint ) {
SystemFontResolver::destroySingleton(); SystemFontResolver::destroySingleton();
} }
#if EE_PLATFORM == EE_PLATFORM_LINUX
UTEST( SystemFontResolver, fallbackProbeDoesNotRetainFontDescriptors ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
resolver->enumerate();
const std::size_t descriptorsBefore = getOpenFileDescriptorCount();
resolver->getFallbackForCodepoint( 0x10FFFF, FontWeight::Normal, false );
const std::size_t descriptorsAfter = getOpenFileDescriptorCount();
EXPECT_TRUE_MSG( descriptorsAfter <= descriptorsBefore + 4,
String::format( "Fallback probing changed the descriptor count by %lld",
static_cast<long long>( descriptorsAfter ) -
static_cast<long long>( descriptorsBefore ) )
.c_str() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
#endif
UTEST( SystemFontResolver, invalidateCache ) { UTEST( SystemFontResolver, invalidateCache ) {
SystemFontResolver::setEnabled( true ); SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance(); auto* resolver = SystemFontResolver::instance();
+175 -1
View File
@@ -2,6 +2,7 @@
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <functional>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -11,11 +12,14 @@
#include <eepp/graphics/fontmanager.hpp> #include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp> #include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/image.hpp> #include <eepp/graphics/image.hpp>
#include <eepp/network/http.hpp>
#include <eepp/network/tcplistener.hpp> #include <eepp/network/tcplistener.hpp>
#include <eepp/network/tcpsocket.hpp> #include <eepp/network/tcpsocket.hpp>
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/system/clock.hpp>
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#include <eepp/system/threadpool.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp> #include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/tools/htmlformatter.hpp> #include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uiapplication.hpp> #include <eepp/ui/uiapplication.hpp>
@@ -39,7 +43,7 @@ using namespace EE::Window;
using namespace EE::Scene; using namespace EE::Scene;
using namespace EE::UI; using namespace EE::UI;
static bool readHttpRequestHeaders( TcpSocket& client ) { static bool readHttpRequestHeaders( TcpSocket& client, std::string* headers = nullptr ) {
std::string request; std::string request;
char buffer[1024]; char buffer[1024];
std::size_t received = 0; std::size_t received = 0;
@@ -49,6 +53,8 @@ static bool readHttpRequestHeaders( TcpSocket& client ) {
return false; return false;
request.append( buffer, received ); request.append( buffer, received );
} }
if ( headers )
*headers = std::move( request );
return true; return true;
} }
@@ -3100,3 +3106,171 @@ UTEST( UIWebView, NewerNavigationSupersedesStartedLoad ) {
Engine::destroySingleton(); Engine::destroySingleton();
} }
UTEST( UIWebView, RepeatedRemoteNavigationHandlesSubresourceFanOut ) {
constexpr int NavigationCount = 6;
constexpr int ImagesPerDocument = 100;
constexpr int StyleSheetsPerDocument = 4;
constexpr int ResourcesPerDocument = ImagesPerDocument + StyleSheetsPerDocument;
auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "UIWebView Subresource Fan-out Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
std::string imageData;
const std::string imagePath =
Sys::getProcessPath() + "assets/html/reddit_old_thread_files/pixel.png";
ASSERT_TRUE( FileSystem::fileGet( imagePath, imageData ) );
ASSERT_FALSE( imageData.empty() );
TcpListener listener;
ASSERT_EQ( listener.listen( Socket::AnyPort, IpAddress::LocalHost ), Socket::Done );
const unsigned short serverPort = listener.getLocalPort();
std::atomic<bool> stopServer{ false };
std::atomic<bool> serverOk{ true };
std::atomic<int> documentRequests{ 0 };
std::atomic<int> resourceRequests{ 0 };
std::thread server( [&] {
while ( !stopServer.load( std::memory_order_acquire ) ) {
TcpSocket client;
if ( listener.accept( client ) != Socket::Done ) {
if ( !stopServer.load( std::memory_order_acquire ) )
serverOk.store( false, std::memory_order_release );
break;
}
if ( stopServer.load( std::memory_order_acquire ) )
break;
std::string requestHeaders;
if ( !readHttpRequestHeaders( client, &requestHeaders ) ) {
serverOk.store( false, std::memory_order_release );
break;
}
std::string body;
std::string contentType;
if ( String::startsWith( requestHeaders, "GET /page/" ) ) {
const int document = documentRequests.fetch_add( 1, std::memory_order_acq_rel );
contentType = "text/html";
body = "<!doctype html><html><head>";
for ( int i = 0; i < StyleSheetsPerDocument; ++i )
body += "<link rel='stylesheet' href='/style.css?document=" +
String::toString( document ) + "&amp;sheet=" + String::toString( i ) +
"'>";
body += "</head><body><div id='document-" + String::toString( document ) + "'>";
for ( int i = 0; i < ImagesPerDocument; ++i )
body += "<img src='/pixel.png?document=" + String::toString( document ) +
"&amp;image=" + String::toString( i ) + "'>";
body += "</div></body></html>";
} else if ( String::startsWith( requestHeaders, "GET /style.css?" ) ) {
resourceRequests.fetch_add( 1, std::memory_order_acq_rel );
contentType = "text/css";
body = "body { color: #223344; }";
} else if ( String::startsWith( requestHeaders, "GET /pixel.png?" ) ) {
resourceRequests.fetch_add( 1, std::memory_order_acq_rel );
contentType = "image/png";
body = imageData;
} else {
serverOk.store( false, std::memory_order_release );
break;
}
const std::string response =
"HTTP/1.1 200 OK\r\nContent-Type: " + contentType +
"\r\nContent-Length: " + String::toString( static_cast<Uint64>( body.size() ) ) +
"\r\nConnection: close\r\n\r\n" + body;
if ( client.send( response.data(), response.size() ) != Socket::Done ) {
serverOk.store( false, std::memory_order_release );
break;
}
client.disconnect();
}
} );
auto httpThreadPool = ThreadPool::createShared( 8 );
Http::Pool::getGlobal().clear();
Http::setThreadPool( httpThreadPool );
UISceneNode* sceneNode = UISceneNode::New();
SceneManager::instance()->add( sceneNode );
sceneNode->getUIThemeManager()->setDefaultFont( font );
sceneNode->setThreadPool( httpThreadPool );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( 640, 420 );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
auto pumpUntil = [&]( const std::function<bool()>& condition, Time timeout ) {
Clock clock;
while ( !condition() && clock.getElapsedTime() < timeout ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
Sys::sleep( Milliseconds( 1 ) );
}
return condition();
};
bool allNavigationsStarted = true;
for ( int navigation = 0; navigation < NavigationCount; ++navigation ) {
webView->loadURI(
URI( String::format( "http://127.0.0.1:%u/page/%d", serverPort, navigation ) ) );
const int minimumResources = navigation * ResourcesPerDocument + 8;
if ( !pumpUntil(
[&] {
return documentRequests.load( std::memory_order_acquire ) >= navigation + 1 &&
resourceRequests.load( std::memory_order_acquire ) >= minimumResources;
},
Seconds( 10 ) ) ) {
allNavigationsStarted = false;
break;
}
}
const int expectedResourceRequests = NavigationCount * ResourcesPerDocument;
const bool allResourcesLoaded =
allNavigationsStarted &&
pumpUntil(
[&] {
return resourceRequests.load( std::memory_order_acquire ) ==
expectedResourceRequests;
},
Seconds( 15 ) );
for ( int i = 0; i < 30; ++i ) {
win->getInput()->update();
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
}
Http::Pool::getGlobal().clear();
Http::setThreadPool( nullptr );
httpThreadPool.reset();
stopServer.store( true, std::memory_order_release );
TcpSocket wakeServer;
wakeServer.connect( IpAddress::LocalHost, serverPort, Seconds( 1 ) );
wakeServer.disconnect();
listener.close();
if ( server.joinable() )
server.join();
EXPECT_TRUE( serverOk.load( std::memory_order_acquire ) );
EXPECT_TRUE( allNavigationsStarted );
EXPECT_TRUE( allResourcesLoaded );
EXPECT_EQ( NavigationCount, documentRequests.load( std::memory_order_acquire ) );
EXPECT_EQ( expectedResourceRequests, resourceRequests.load( std::memory_order_acquire ) );
EXPECT_TRUE( webView->getCurrentURI().toString().find( "/page/5" ) != std::string::npos );
UISceneNode* documentScene = webView->getDocumentSceneNode();
ASSERT_TRUE( documentScene != nullptr );
EXPECT_TRUE( documentScene->getRoot()->find( "document-5" ) != nullptr );
Engine::destroySingleton();
}