/*********************************************************************** THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY DEVELOPER: Zihan Chen(vczh) ***********************************************************************/ #include "VlppOS.Linux.h" /*********************************************************************** .\FILESYSTEM.LINUX.CPP ***********************************************************************/ /*********************************************************************** Author: Zihan Chen (vczh) Licensed under https://github.com/vczh-libraries/License ***********************************************************************/ #include #include #include #ifndef VCZH_GCC static_assert(false, "Do not build this file for Windows applications."); #endif namespace vl { namespace stream { extern Ptr CreateOSFileStreamImpl(const WString& fileName, FileStream::AccessRight accessRight); } namespace filesystem { using namespace collections; using namespace stream; /*********************************************************************** LinuxFileSystemImpl ***********************************************************************/ class LinuxFileSystemImpl : public feature_injection::FeatureImpl { public: // FilePath operations implementation wchar_t GetPathDelimiter() const override { return L'/'; } const wchar_t* GetCompatibleDelimiters() const override { return L""; } WString ConcatPath(const WString& fullPath, const WString& relativePath) const override { auto delimiter = WString::FromChar(GetPathDelimiter()); if (IsRoot(fullPath)) { return delimiter + relativePath; } return fullPath + delimiter + relativePath; } void Initialize(WString& fullPath) const override { { Array buffer(fullPath.Length() + 1); wcscpy(&buffer[0], fullPath.Buffer()); FilePath::NormalizeDelimiters(buffer); fullPath = &buffer[0]; } if (fullPath.Length() == 0) fullPath = WString::FromChar(GetPathDelimiter()); if (fullPath[0] != GetPathDelimiter()) { char buffer[PATH_MAX] = { 0 }; getcwd(buffer, PATH_MAX); fullPath = atow(AString(buffer)) + WString::FromChar(GetPathDelimiter()) + fullPath; } { collections::List components; FilePath::GetPathComponents(fullPath, components); for (int i = 0; i < components.Count(); i++) { if (components[i] == L".") { components.RemoveAt(i); i--; } else if (components[i] == L"..") { if (i > 0) { components.RemoveAt(i); components.RemoveAt(i - 1); i -= 2; } else { throw ArgumentException(L"Illegal path."); } } } fullPath = FilePath::ComponentsToPath(components); } FilePath::TrimLastDelimiter(fullPath); } bool IsFile(const WString& fullPath) const override { struct stat info; AString path = wtoa(fullPath); int result = stat(path.Buffer(), &info); if(result != 0) return false; else return S_ISREG(info.st_mode); } bool IsFolder(const WString& fullPath) const override { struct stat info; AString path = wtoa(fullPath); int result = stat(path.Buffer(), &info); if(result != 0) return false; else return S_ISDIR(info.st_mode); } bool IsRoot(const WString& fullPath) const override { return fullPath == WString::FromChar(GetPathDelimiter()); } WString GetRelativePathFor(const WString& fromPath, const WString& toPath) const override { if (fromPath.Length() == 0 || toPath.Length() == 0 || fromPath[0] != toPath[0]) { return toPath; } collections::List srcComponents, tgtComponents, resultComponents; FilePath::GetPathComponents(IsFolder(fromPath) ? fromPath : FilePath(fromPath).GetFolder().GetFullPath(), srcComponents); FilePath::GetPathComponents(toPath, tgtComponents); int minLength = srcComponents.Count() <= tgtComponents.Count() ? srcComponents.Count() : tgtComponents.Count(); int lastCommonComponent = 0; for (int i = 0; i < minLength; i++) { if (srcComponents[i] == tgtComponents[i]) { lastCommonComponent = i; } else break; } for (int i = lastCommonComponent + 1; i < srcComponents.Count(); i++) { resultComponents.Add(L".."); } for (int i = lastCommonComponent + 1; i < tgtComponents.Count(); i++) { resultComponents.Add(tgtComponents[i]); } return FilePath::ComponentsToPath(resultComponents); } // File operations implementation bool FileDelete(const FilePath& filePath) const override { AString path = wtoa(filePath.GetFullPath()); return unlink(path.Buffer()) == 0; } bool FileRename(const FilePath& filePath, const WString& newName) const override { AString oldFileName = wtoa(filePath.GetFullPath()); AString newFileName = wtoa((filePath.GetFolder() / newName).GetFullPath()); return rename(oldFileName.Buffer(), newFileName.Buffer()) == 0; } // Folder operations implementation bool GetFolders(const FilePath& folderPath, collections::List& folders) const override { DIR *dir; AString searchPath = wtoa(folderPath.GetFullPath()); if ((dir = opendir(searchPath.Buffer())) == NULL) { return false; } struct dirent* entry; while ((entry = readdir(dir)) != NULL) { WString childName = atow(AString(entry->d_name)); FilePath childFullPath = folderPath / childName; if (childName != L"." && childName != L".." && childFullPath.IsFolder()) { folders.Add(Folder(childFullPath)); } } if (closedir(dir) != 0) { return false; } return true; } bool GetFiles(const FilePath& folderPath, collections::List& files) const override { DIR* dir; AString searchPath = wtoa(folderPath.GetFullPath()); if ((dir = opendir(searchPath.Buffer())) == NULL) { return false; } struct dirent* entry; while ((entry = readdir(dir)) != NULL) { FilePath childFullPath = folderPath / (atow(AString(entry->d_name))); if (childFullPath.IsFile()) { files.Add(File(childFullPath)); } } if (closedir(dir) != 0) { return false; } return true; } bool CreateFolder(const FilePath& folderPath) const override { AString path = wtoa(folderPath.GetFullPath()); return mkdir(path.Buffer(), 0777) == 0; } bool DeleteFolder(const FilePath& folderPath) const override { AString path = wtoa(folderPath.GetFullPath()); return rmdir(path.Buffer()) == 0; } bool FolderRename(const FilePath& folderPath, const WString& newName) const override { AString oldFileName = wtoa(folderPath.GetFullPath()); AString newFileName = wtoa((folderPath.GetFolder() / newName).GetFullPath()); return rename(oldFileName.Buffer(), newFileName.Buffer()) == 0; } Ptr GetFileStreamImpl(const WString& fileName, stream::FileStream::AccessRight accessRight) const override { return stream::CreateOSFileStreamImpl(fileName, accessRight); } }; /*********************************************************************** Global FileSystem Implementation ***********************************************************************/ IFileSystemImpl* GetOSFileSystemImpl() { static LinuxFileSystemImpl osFileSystemImpl; return &osFileSystemImpl; } } } /*********************************************************************** .\LOCALE.LINUX.CPP ***********************************************************************/ /*********************************************************************** Author: Zihan Chen (vczh) Licensed under https://github.com/vczh-libraries/License ***********************************************************************/ #ifndef VCZH_GCC static_assert(false, "Do not build this file for Windows applications."); #endif namespace vl { ILocaleImpl* GetOSLocaleImpl() { static EnUsLocaleImpl linuxLocaleImpl; return &linuxLocaleImpl; } } /*********************************************************************** .\THREADING.LINUX.CPP ***********************************************************************/ /*********************************************************************** Author: Zihan Chen (vczh) Licensed under https://github.com/vczh-libraries/License ***********************************************************************/ #include #include #include #include #include #if defined VCZH_APPLE #include #endif #ifndef VCZH_GCC static_assert(false, "Do not build this file for Windows applications."); #endif namespace vl { using namespace threading_internal; using namespace collections; /*********************************************************************** Thread ***********************************************************************/ namespace threading_internal { struct ThreadData { pthread_t id; EventObject ev; bool deleteAfterStopped = false; }; class ProceduredThread : public Thread { private: Thread::ThreadProcedure procedure; void* argument; protected: void Run() { procedure(this, argument); } public: ProceduredThread(Thread::ThreadProcedure _procedure, void* _argument, bool _deleteAfterStopped) :procedure(_procedure) ,argument(_argument) { internalData->deleteAfterStopped = _deleteAfterStopped; } }; class LambdaThread : public Thread { private: Func procedure; protected: void Run() { procedure(); } public: LambdaThread(const Func& _procedure, bool _deleteAfterStopped) :procedure(_procedure) { internalData->deleteAfterStopped = _deleteAfterStopped; } }; } void InternalThreadProc(Thread* thread) { auto deleteAfterStopped = thread->internalData->deleteAfterStopped; ThreadLocalStorage::FixStorages(); try { thread->Run(); ThreadLocalStorage::ClearStorages(); thread->threadState=Thread::Stopped; thread->internalData->ev.Signal(); } catch (...) { ThreadLocalStorage::ClearStorages(); throw; } if(deleteAfterStopped) { delete thread; } } void* InternalThreadProcWrapper(void* lpParameter) { InternalThreadProc((Thread*)lpParameter); return 0; } Thread::Thread() { internalData=new ThreadData; internalData->ev.CreateManualUnsignal(false); threadState=Thread::NotStarted; } Thread::~Thread() { if (internalData) { Stop(); if (threadState!=Thread::NotStarted) { pthread_detach(internalData->id); } delete internalData; } } Thread* Thread::CreateAndStart(ThreadProcedure procedure, void* argument, bool deleteAfterStopped) { if(procedure) { Thread* thread=new ProceduredThread(procedure, argument, deleteAfterStopped); if(thread->Start()) { return thread; } else { delete thread; } } return 0; } Thread* Thread::CreateAndStart(const Func& procedure, bool deleteAfterStopped) { Thread* thread=new LambdaThread(procedure, deleteAfterStopped); if(thread->Start()) { return thread; } else { delete thread; } return 0; } void Thread::Sleep(vint ms) { if (ms >= 1000) { sleep(ms / 1000); } if (ms % 1000) { usleep((ms % 1000) * 1000); } } vint Thread::GetCPUCount() { return (vint)sysconf(_SC_NPROCESSORS_ONLN); } vint Thread::GetCurrentThreadId() { return (vint)::pthread_self(); } bool Thread::Start() { if(threadState==Thread::NotStarted) { threadState=Thread::Running; if(pthread_create(&internalData->id, nullptr, &InternalThreadProcWrapper, this)==0) { return true; } threadState=Thread::NotStarted; } return false; } bool Thread::Wait() { return internalData->ev.Wait(); } bool Thread::Stop() { if (threadState==Thread::Running) { if(pthread_cancel(internalData->id)==0) { threadState=Thread::Stopped; internalData->ev.Signal(); return true; } } return false; } Thread::ThreadState Thread::GetState() { return threadState; } /*********************************************************************** Mutex ***********************************************************************/ namespace threading_internal { struct MutexData { Semaphore sem; }; }; Mutex::Mutex() { internalData = new MutexData; } Mutex::~Mutex() { delete internalData; } bool Mutex::Create(bool owned, const WString& name) { return internalData->sem.Create(owned ? 0 : 1, 1, name); } bool Mutex::Open(bool inheritable, const WString& name) { return internalData->sem.Open(inheritable, name); } bool Mutex::Release() { return internalData->sem.Release(); } bool Mutex::Wait() { return internalData->sem.Wait(); } /*********************************************************************** Semaphore ***********************************************************************/ namespace threading_internal { struct SemaphoreData { sem_t semUnnamed; sem_t* semNamed = nullptr; }; #if defined VCZH_APPLE void FailOnUnnamedSemaphore() { CHECK_FAIL(L"vl::Semaphore::~Semaphore()#Unnamed semaphores are not supported on macOS."); } #endif } Semaphore::Semaphore() :internalData(0) { } Semaphore::~Semaphore() { if (internalData) { if (internalData->semNamed) { sem_close(internalData->semNamed); } else { #if defined VCZH_APPLE threading_internal::FailOnUnnamedSemaphore(); #else sem_destroy(&internalData->semUnnamed); #endif } delete internalData; } } bool Semaphore::Create(vint initialCount, vint maxCount, const WString& name) { if (internalData) return false; if (initialCount > maxCount) return false; internalData = new SemaphoreData; #if defined VCZH_APPLE AString auuid; if(name.Length() == 0) { CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault); CFStringRef cfstr = CFUUIDCreateString(kCFAllocatorDefault, cfuuid); auuid = CFStringGetCStringPtr(cfstr, kCFStringEncodingASCII); CFRelease(cfstr); CFRelease(cfuuid); } auuid = auuid.Insert(0, "/"); // OSX SEM_NAME_LENGTH = 31 if(auuid.Length() >= 30) auuid = auuid.Sub(0, 30); if ((internalData->semNamed = sem_open(auuid.Buffer(), O_CREAT, O_RDWR, initialCount)) == SEM_FAILED) { delete internalData; internalData = 0; return false; } #else if (name == L"") { if(sem_init(&internalData->semUnnamed, 0, (int)initialCount) == -1) { delete internalData; internalData = 0; return false; } } else { AString astr = wtoa(name); if ((internalData->semNamed = sem_open(astr.Buffer(), O_CREAT, 0777, initialCount)) == SEM_FAILED) { delete internalData; internalData = 0; return false; } } #endif Release(initialCount); return true; } bool Semaphore::Open(bool inheritable, const WString& name) { if (internalData) return false; if (inheritable) return false; internalData = new SemaphoreData; if (!(internalData->semNamed = sem_open(wtoa(name).Buffer(), 0))) { delete internalData; internalData = 0; return false; } return true; } bool Semaphore::Release() { return Release(1); } vint Semaphore::Release(vint count) { for (vint i = 0; i < count; i++) { if (internalData->semNamed) { sem_post(internalData->semNamed); } else { sem_post(&internalData->semUnnamed); } } return true; } bool Semaphore::Wait() { if (internalData->semNamed) { return sem_wait(internalData->semNamed) == 0; } else { return sem_wait(&internalData->semUnnamed) == 0; } } /*********************************************************************** EventObject ***********************************************************************/ namespace threading_internal { struct EventData { bool autoReset; volatile bool signaled; CriticalSection mutex; ConditionVariable cond; atomic_vint counter = 0; vint releasedWaiters = 0; vuint64_t signalVersion = 0; }; } EventObject::EventObject() { internalData = nullptr; } EventObject::~EventObject() { if (internalData) { delete internalData; } } bool EventObject::CreateAutoUnsignal(bool signaled, const WString& name) { if (name!=L"") return false; if (internalData) return false; internalData = new EventData; internalData->autoReset = true; internalData->signaled = signaled; return true; } bool EventObject::CreateManualUnsignal(bool signaled, const WString& name) { if (name!=L"") return false; if (internalData) return false; internalData = new EventData; internalData->autoReset = false; internalData->signaled = signaled; return true; } bool EventObject::Signal() { if (!internalData) return false; internalData->mutex.Enter(); if (internalData->autoReset) { if (internalData->counter > internalData->releasedWaiters) { internalData->releasedWaiters++; internalData->cond.WakeOnePending(); } else { internalData->signaled = true; } } else { internalData->signaled = true; internalData->signalVersion++; if (internalData->counter) { internalData->cond.WakeAllPendings(); } } internalData->mutex.Leave(); return true; } bool EventObject::Unsignal() { if (!internalData) return false; internalData->mutex.Enter(); internalData->signaled = false; internalData->mutex.Leave(); return true; } bool EventObject::Wait() { if (!internalData) return false; bool result = true; internalData->mutex.Enter(); if (internalData->signaled) { if (internalData->autoReset) { internalData->signaled = false; } } else { auto signalVersion = internalData->signalVersion; INCRC(&internalData->counter); while (internalData->autoReset ? internalData->releasedWaiters == 0 : !internalData->signaled && internalData->signalVersion == signalVersion) { if (!internalData->cond.SleepWith(internalData->mutex)) { result = false; break; } } if (result && internalData->autoReset) { internalData->releasedWaiters--; } DECRC(&internalData->counter); } internalData->mutex.Leave(); return result; } bool EventObject::WaitForTime(vint ms) { if (!internalData) return false; bool result = true; internalData->mutex.Enter(); if (internalData->signaled) { if (internalData->autoReset) { internalData->signaled = false; } } else { auto signalVersion = internalData->signalVersion; timespec now; #if defined VCZH_APPLE constexpr auto waitClock = CLOCK_REALTIME; #else constexpr auto waitClock = CLOCK_MONOTONIC; #endif if (ms <= 0 || clock_gettime(waitClock, &now) != 0) { internalData->mutex.Leave(); return false; } auto deadline = (vuint64_t)now.tv_sec * 1000000000 + now.tv_nsec + (vuint64_t)ms * 1000000; auto remaining = ms; INCRC(&internalData->counter); while (internalData->autoReset ? internalData->releasedWaiters == 0 : !internalData->signaled && internalData->signalVersion == signalVersion) { if (!internalData->cond.SleepWithForTime(internalData->mutex, remaining)) { result = false; break; } if (clock_gettime(waitClock, &now) != 0) { result = false; break; } auto current = (vuint64_t)now.tv_sec * 1000000000 + now.tv_nsec; if (current >= deadline) { result = false; break; } remaining = (vint)((deadline - current + 999999) / 1000000); } if (internalData->autoReset && internalData->releasedWaiters > 0) { internalData->releasedWaiters--; result = true; } else if (!internalData->autoReset && (internalData->signaled || internalData->signalVersion != signalVersion)) { result = true; } DECRC(&internalData->counter); } internalData->mutex.Leave(); return result; } /*********************************************************************** ThreadPoolLite ***********************************************************************/ namespace threading_internal { struct ThreadPoolTask { Func task; Ptr next; }; struct ThreadPoolData { Semaphore semaphore; EventObject taskFinishEvent; Ptr taskBegin; Ptr* taskEnd = nullptr; volatile bool stopping = false; List taskThreads; }; SpinLock threadPoolLock; ThreadPoolData* threadPoolData = nullptr; void ThreadPoolProc(Thread* thread, void* argument) { while (true) { Ptr task; threadPoolData->semaphore.Wait(); SPIN_LOCK(threadPoolLock) { if (threadPoolData->taskBegin) { task = threadPoolData->taskBegin; threadPoolData->taskBegin = task->next; } if (!threadPoolData->taskBegin) { threadPoolData->taskEnd = &threadPoolData->taskBegin; threadPoolData->taskFinishEvent.Signal(); } } if (task) { ThreadLocalStorage::FixStorages(); try { task->task(); ThreadLocalStorage::ClearStorages(); } catch (...) { ThreadLocalStorage::ClearStorages(); } } else if (threadPoolData->stopping) { return; } } } bool ThreadPoolQueue(const Func& proc) { SPIN_LOCK(threadPoolLock) { if (!threadPoolData) { threadPoolData = new ThreadPoolData; threadPoolData->semaphore.Create(0, 65536); threadPoolData->taskFinishEvent.CreateManualUnsignal(false); threadPoolData->taskEnd = &threadPoolData->taskBegin; for (vint i = 0; i < Thread::GetCPUCount() * 4; i++) { threadPoolData->taskThreads.Add(Thread::CreateAndStart(&ThreadPoolProc, nullptr, false)); } } if (threadPoolData) { if (threadPoolData->stopping) { return false; } auto task = Ptr(new ThreadPoolTask); task->task = proc; *threadPoolData->taskEnd = task; threadPoolData->taskEnd = &task->next; threadPoolData->semaphore.Release(); threadPoolData->taskFinishEvent.Unsignal(); } } return true; } bool ThreadPoolStop(bool discardPendingTasks) { SPIN_LOCK(threadPoolLock) { if (!threadPoolData) return false; if (threadPoolData->stopping) return false; threadPoolData->stopping = true; if (discardPendingTasks) { threadPoolData->taskEnd = &threadPoolData->taskBegin; threadPoolData->taskBegin = nullptr; } threadPoolData->semaphore.Release(threadPoolData->taskThreads.Count()); } threadPoolData->taskFinishEvent.Wait(); // TODO: (enumerable) foreach for (vint i = 0; i < threadPoolData->taskThreads.Count(); i++) { auto thread = threadPoolData->taskThreads[i]; thread->Wait(); delete thread; } threadPoolData->taskThreads.Clear(); SPIN_LOCK(threadPoolLock) { delete threadPoolData; threadPoolData = nullptr; } return true; } } ThreadPoolLite::ThreadPoolLite() { } ThreadPoolLite::~ThreadPoolLite() { } bool ThreadPoolLite::Queue(void(*proc)(void*), void* argument) { return ThreadPoolQueue([proc, argument](){proc(argument);}); } bool ThreadPoolLite::Queue(const Func& proc) { return ThreadPoolQueue(proc); } bool ThreadPoolLite::Stop(bool discardPendingTasks) { return ThreadPoolStop(discardPendingTasks); } /*********************************************************************** CriticalSection ***********************************************************************/ namespace threading_internal { struct CriticalSectionData { pthread_mutex_t mutex; }; } CriticalSection::CriticalSection() { internalData = new CriticalSectionData; pthread_mutex_init(&internalData->mutex, nullptr); } CriticalSection::~CriticalSection() { pthread_mutex_destroy(&internalData->mutex); delete internalData; } bool CriticalSection::TryEnter() { return pthread_mutex_trylock(&internalData->mutex) == 0; } void CriticalSection::Enter() { pthread_mutex_lock(&internalData->mutex); } void CriticalSection::Leave() { pthread_mutex_unlock(&internalData->mutex); } CriticalSection::Scope::Scope(CriticalSection& _criticalSection) :criticalSection(&_criticalSection) { criticalSection->Enter(); } CriticalSection::Scope::~Scope() { criticalSection->Leave(); } /*********************************************************************** ReaderWriterLock ***********************************************************************/ namespace threading_internal { struct ReaderWriterLockData { pthread_rwlock_t rwlock; }; } ReaderWriterLock::ReaderWriterLock() { internalData = new ReaderWriterLockData; pthread_rwlock_init(&internalData->rwlock, nullptr); } ReaderWriterLock::~ReaderWriterLock() { pthread_rwlock_destroy(&internalData->rwlock); delete internalData; } bool ReaderWriterLock::TryEnterReader() { return pthread_rwlock_tryrdlock(&internalData->rwlock) == 0; } void ReaderWriterLock::EnterReader() { pthread_rwlock_rdlock(&internalData->rwlock); } void ReaderWriterLock::LeaveReader() { pthread_rwlock_unlock(&internalData->rwlock); } bool ReaderWriterLock::TryEnterWriter() { return pthread_rwlock_trywrlock(&internalData->rwlock) == 0; } void ReaderWriterLock::EnterWriter() { pthread_rwlock_wrlock(&internalData->rwlock); } void ReaderWriterLock::LeaveWriter() { pthread_rwlock_unlock(&internalData->rwlock); } ReaderWriterLock::ReaderScope::ReaderScope(ReaderWriterLock& _lock) :lock(&_lock) { lock->EnterReader(); } ReaderWriterLock::ReaderScope::~ReaderScope() { lock->LeaveReader(); } ReaderWriterLock::WriterScope::WriterScope(ReaderWriterLock& _lock) :lock(&_lock) { lock->EnterWriter(); } ReaderWriterLock::WriterScope::~WriterScope() { lock->LeaveReader(); } /*********************************************************************** ConditionVariable ***********************************************************************/ namespace threading_internal { struct ConditionVariableData { pthread_cond_t cond; }; } ConditionVariable::ConditionVariable() { #define ERROR_MESSAGE_PREFIX L"vl::ConditionVariable::ConditionVariable()#" internalData = new ConditionVariableData; #if defined VCZH_APPLE auto initResult = pthread_cond_init(&internalData->cond, nullptr); #else pthread_condattr_t attributes; auto attributeResult = pthread_condattr_init(&attributes); if (attributeResult != 0) { delete internalData; internalData = nullptr; CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to initialize condition-variable attributes."); } auto clockResult = pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC); auto initResult = clockResult == 0 ? pthread_cond_init(&internalData->cond, &attributes) : clockResult; pthread_condattr_destroy(&attributes); #endif if (initResult != 0) { delete internalData; internalData = nullptr; CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to initialize the condition variable."); } #undef ERROR_MESSAGE_PREFIX } ConditionVariable::~ConditionVariable() { pthread_cond_destroy(&internalData->cond); delete internalData; } bool ConditionVariable::SleepWith(CriticalSection& cs) { return pthread_cond_wait(&internalData->cond, &cs.internalData->mutex) == 0; } bool ConditionVariable::SleepWithForTime(CriticalSection& cs, vint ms) { if (ms < 0) return false; timespec timeout; #if defined VCZH_APPLE constexpr auto waitClock = CLOCK_REALTIME; #else constexpr auto waitClock = CLOCK_MONOTONIC; #endif if (clock_gettime(waitClock, &timeout) != 0) { return false; } timeout.tv_sec += ms / 1000; timeout.tv_nsec += (ms % 1000) * 1000000; if (timeout.tv_nsec >= 1000000000) { timeout.tv_sec++; timeout.tv_nsec -= 1000000000; } else if (timeout.tv_nsec < 0) { timeout.tv_sec--; timeout.tv_nsec += 1000000000; } return pthread_cond_timedwait(&internalData->cond, &cs.internalData->mutex, &timeout) == 0; } void ConditionVariable::WakeOnePending() { pthread_cond_signal(&internalData->cond); } void ConditionVariable::WakeAllPendings() { pthread_cond_broadcast(&internalData->cond); } /*********************************************************************** ThreadLocalStorage ***********************************************************************/ #define KEY ((pthread_key_t&)key) ThreadLocalStorage::ThreadLocalStorage(Destructor _destructor) :destructor(_destructor) { static_assert(sizeof(key) >= sizeof(pthread_key_t), "ThreadLocalStorage's key storage is not large enouth."); PushStorage(this); auto error = pthread_key_create(&KEY, destructor); CHECK_ERROR(error != EAGAIN && error != ENOMEM, L"vl::ThreadLocalStorage::ThreadLocalStorage()#Failed to create a thread local storage index."); } ThreadLocalStorage::~ThreadLocalStorage() { pthread_key_delete(KEY); } void* ThreadLocalStorage::Get() { CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Get()#Cannot access a disposed ThreadLocalStorage."); return pthread_getspecific(KEY); } void ThreadLocalStorage::Set(void* data) { CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Set()#Cannot access a disposed ThreadLocalStorage."); pthread_setspecific(KEY, data); } #undef KEY } /*********************************************************************** .\ENCODING\CHARFORMAT\CHARFORMAT.LINUX.CPP ***********************************************************************/ /*********************************************************************** Author: Zihan Chen (vczh) Licensed under https://github.com/vczh-libraries/License ***********************************************************************/ #include #ifndef VCZH_GCC static_assert(false, "Do not build this file for Windows applications."); #endif namespace vl { namespace stream { using namespace vl::encoding; bool IsMbcsLeadByte(char c) { return (vint8_t)c < 0; } void MbcsToWChar(wchar_t* wideBuffer, vint wideChars, vint wideReaded, char* mbcsBuffer, vint mbcsChars) { AString a = AString::CopyFrom(mbcsBuffer, mbcsChars); WString w = atow(a); memcpy(wideBuffer, w.Buffer(), wideReaded * sizeof(wchar_t)); } /*********************************************************************** MbcsEncoder ***********************************************************************/ vint MbcsEncoder::WriteString(wchar_t* _buffer, vint chars) { WString w = WString::CopyFrom(_buffer, chars); AString a = wtoa(w); vint length = a.Length(); vint result = stream->Write((void*)a.Buffer(), length); if (result != length) { Close(); return 0; } return chars; } /*********************************************************************** Helper Functions ***********************************************************************/ extern bool CanBeMbcs(unsigned char* buffer, vint size); extern bool CanBeUtf8(unsigned char* buffer, vint size); extern bool CanBeUtf16(unsigned char* buffer, vint size, bool& hitSurrogatePairs); extern bool CanBeUtf16BE(unsigned char* buffer, vint size, bool& hitSurrogatePairs); /*********************************************************************** TestEncoding ***********************************************************************/ extern void TestEncodingInternal( unsigned char* buffer, vint size, BomEncoder::Encoding& encoding, bool containsBom, bool utf16HitSurrogatePairs, bool utf16BEHitSurrogatePairs, bool roughMbcs, bool roughUtf8, bool roughUtf16, bool roughUtf16BE ) { if (roughUtf16 && roughUtf16BE && !roughUtf8) { if (utf16BEHitSurrogatePairs && !utf16HitSurrogatePairs) { encoding = BomEncoder::Utf16BE; } else { encoding = BomEncoder::Utf16; } } else { encoding = BomEncoder::Utf8; } } } } /*********************************************************************** .\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.LINUX.CPP ***********************************************************************/ #if defined VCZH_GCC && !defined VCZH_APPLE #include #include #include #include #include #include #include namespace vl::inter_process::async_tcp_socket::linux_socket { using namespace collections; class RingOperation; class RingRuntime; class ConnectionState; class ServerState; class AsyncSocketConnection; static thread_local RingRuntime* currentRingRuntime = nullptr; struct ConnectionCallbackFrame { ConnectionState* connection = nullptr; ConnectionCallbackFrame* previous = nullptr; }; struct ServerCallbackFrame { ServerState* server = nullptr; ServerCallbackFrame* previous = nullptr; }; static thread_local ConnectionCallbackFrame* currentConnectionCallbackFrame = nullptr; static thread_local ServerCallbackFrame* currentServerCallbackFrame = nullptr; WString LinuxSocketErrorMessage(const wchar_t* operation, vint error) { return WString::Unmanaged(operation) + L" failed with Linux error " + itow(error) + L" (" + atow(AString::Unmanaged(strerror((int)error))) + L")."; } void CloseFileDescriptor(vint fileDescriptor) { if (fileDescriptor >= 0) { // Do not retry close after EINTR: the descriptor number can already be reused. close((int)fileDescriptor); } } /*********************************************************************** OwnedFileDescriptor ***********************************************************************/ class OwnedFileDescriptor { private: vint fileDescriptor = -1; public: OwnedFileDescriptor(vint _fileDescriptor) : fileDescriptor(_fileDescriptor) { } OwnedFileDescriptor(const OwnedFileDescriptor&) = delete; OwnedFileDescriptor& operator=(const OwnedFileDescriptor&) = delete; ~OwnedFileDescriptor() { CloseFileDescriptor(fileDescriptor); } vint Get() { return fileDescriptor; } vint Detach() { auto result = fileDescriptor; fileDescriptor = -1; return result; } }; /*********************************************************************** OperationDrain ***********************************************************************/ class OperationDrain : public Object { private: CriticalSection lockDrain; vint pendingOperations = 0; EventObject eventDrained; public: OperationDrain() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::OperationDrain::OperationDrain()#" CHECK_ERROR(eventDrained.CreateManualUnsignal(true), ERROR_MESSAGE_PREFIX L"Failed to create an operation drain event."); #undef ERROR_MESSAGE_PREFIX } void Begin() { CS_LOCK(lockDrain) { if (pendingOperations++ == 0) { eventDrained.Unsignal(); } } } void End() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::OperationDrain::End()#" CS_LOCK(lockDrain) { CHECK_ERROR(pendingOperations > 0, ERROR_MESSAGE_PREFIX L"Operation drain bookkeeping became unbalanced."); if (--pendingOperations == 0) { eventDrained.Signal(); } } #undef ERROR_MESSAGE_PREFIX } void Wait() { eventDrained.Wait(); } }; /*********************************************************************** RingOperationOwner ***********************************************************************/ class RingOperationOwner : public Object { friend class RingRuntime; protected: Ptr operationDrain; RingOperationOwner() : operationDrain(Ptr(new OperationDrain)) { } virtual void EndTargetOperation() = 0; virtual void HandleOperationFailure(Ptr retainedOwner) = 0; }; /*********************************************************************** RingOperation ***********************************************************************/ class RingOperation { friend class RingRuntime; private: Ptr operationOwner; bool targetOperation = false; public: vuint64_t id = 0; RingOperation(vuint64_t _id, Ptr _operationOwner = nullptr, bool _targetOperation = false) : id(_id) , operationOwner(_operationOwner) , targetOperation(_targetOperation) { } virtual ~RingOperation() = default; virtual void Prepare(io_uring_sqe* sqe) noexcept = 0; virtual void Handle(vint result) = 0; }; /*********************************************************************** RuntimeWakeOperation ***********************************************************************/ class RuntimeWakeOperation : public RingOperation { public: RuntimeWakeOperation(vuint64_t id) : RingOperation(id) { } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_nop(sqe); } void Handle(vint) override { } }; /*********************************************************************** RingRuntime ***********************************************************************/ class RingRuntime : public Object { private: enum class FlushResult { Done, ConsumeCompletion, }; io_uring ring = {}; CriticalSection lockRuntime; ConditionVariable cvRuntimeProgress; Dictionary operations; vuint64_t nextOperationId = 1; bool ringInitialized = false; bool workerStarted = false; bool stopRequested = false; EventObject eventWorkerStopped; vuint64_t ReserveOperationIdLocked() { auto result = nextOperationId++; if (result == 0) { result = nextOperationId++; } return result; } FlushResult FlushPendingSubmissionsLocked() { while (io_uring_sq_ready(&ring)) { vint submitResult = 0; do { submitResult = io_uring_submit(&ring); } while (submitResult == -EINTR); if (submitResult == -EAGAIN) { std::abort(); } if (submitResult == -EBUSY) { return FlushResult::ConsumeCompletion; } if (submitResult <= 0) { std::abort(); } } return FlushResult::Done; } void FlushPendingSubmissionsCore(bool completionWorker) { while (true) { FlushResult result = FlushResult::Done; bool waited = true; lockRuntime.Enter(); if (!ringInitialized) { lockRuntime.Leave(); return; } result = FlushPendingSubmissionsLocked(); if (result == FlushResult::ConsumeCompletion && !completionWorker) { waited = cvRuntimeProgress.SleepWith(lockRuntime); } lockRuntime.Leave(); if (!waited) { std::abort(); } if (result == FlushResult::Done) { return; } if (completionWorker) { return; } } } bool SubmitLocked(RingOperation* operation, bool allowStopping) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::SubmitLocked(RingOperation*, bool)#" if (!ringInitialized || (!allowStopping && stopRequested) || operation->id == 0 || operations.Keys().Contains(operation->id)) { delete operation; return false; } try { operations.Add(operation->id, operation); } catch (...) { delete operation; return false; } auto sqe = io_uring_get_sqe(&ring); if (!sqe) { operations.Remove(operation->id); delete operation; return false; } operation->Prepare(sqe); io_uring_sqe_set_data64(sqe, operation->id); vint submitResult = 0; do { submitResult = io_uring_submit(&ring); } while (submitResult == -EINTR); if (submitResult == -EAGAIN || (submitResult <= 0 && submitResult != -EBUSY)) { std::abort(); } #undef ERROR_MESSAGE_PREFIX return true; } void WorkerLoop() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::WorkerLoop()#" currentRingRuntime = this; while (true) { io_uring_cqe* cqe = nullptr; vint waitResult = 0; do { waitResult = io_uring_wait_cqe(&ring, &cqe); } while (waitResult == -EINTR); if (waitResult != 0 || !cqe) { std::abort(); } auto operationId = io_uring_cqe_get_data64(cqe); RingOperation* operation = nullptr; CS_LOCK(lockRuntime) { if (operations.Keys().Contains(operationId)) { operation = operations[operationId]; } } if (!operation) { std::abort(); } bool operationFailed = false; try { operation->Handle((vint)cqe->res); } catch (...) { operationFailed = true; } io_uring_cqe_seen(&ring, cqe); CS_LOCK(lockRuntime) { operations.Remove(operationId); cvRuntimeProgress.WakeAllPendings(); } auto operationOwner = operation->operationOwner; auto operationDrain = operationOwner ? operationOwner->operationDrain : nullptr; auto targetOperation = operation->targetOperation; delete operation; if (operationOwner && targetOperation) { operationOwner->EndTargetOperation(); } if (operationOwner && operationFailed) { try { operationOwner->HandleOperationFailure(operationOwner); } catch (...) { } } operationOwner = nullptr; if (operationDrain) { operationDrain->End(); } FlushPendingSubmissionsCore(true); bool shouldStop = false; CS_LOCK(lockRuntime) { shouldStop = stopRequested && operations.Count() == 0; } if (shouldStop) { break; } } CS_LOCK(lockRuntime) { io_uring_queue_exit(&ring); ringInitialized = false; cvRuntimeProgress.WakeAllPendings(); } currentRingRuntime = nullptr; eventWorkerStopped.Signal(); #undef ERROR_MESSAGE_PREFIX } public: RingRuntime() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::RingRuntime()#" CHECK_ERROR(eventWorkerStopped.CreateManualUnsignal(false), ERROR_MESSAGE_PREFIX L"Failed to create the io_uring worker drain event."); io_uring_params parameters = {}; auto initResult = io_uring_queue_init_params(1024, &ring, ¶meters); CHECK_ERROR(initResult == 0, ERROR_MESSAGE_PREFIX L"Failed to initialize io_uring."); ringInitialized = true; auto probe = io_uring_get_probe_ring(&ring); if (!probe) { io_uring_queue_exit(&ring); ringInitialized = false; CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to probe io_uring operations."); } auto supported = io_uring_opcode_supported(probe, IORING_OP_NOP) && io_uring_opcode_supported(probe, IORING_OP_ACCEPT) && io_uring_opcode_supported(probe, IORING_OP_CONNECT) && io_uring_opcode_supported(probe, IORING_OP_RECV) && io_uring_opcode_supported(probe, IORING_OP_SEND) && io_uring_opcode_supported(probe, IORING_OP_ASYNC_CANCEL) && io_uring_opcode_supported(probe, IORING_OP_TIMEOUT); io_uring_free_probe(probe); if (!supported) { io_uring_queue_exit(&ring); ringInitialized = false; CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Requires io_uring accept, connect, receive, send, async-cancel, timeout, and nop operations."); } #undef ERROR_MESSAGE_PREFIX } ~RingRuntime() { Stop(); } static Ptr Create(bool startWorker) { auto result = Ptr(new RingRuntime); if (startWorker) { result->Start(result); } return result; } void Start(Ptr retainedRuntime) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::Start(Ptr)#" CS_LOCK(lockRuntime) { CHECK_ERROR(!workerStarted && !stopRequested && ringInitialized, ERROR_MESSAGE_PREFIX L"The io_uring runtime can only be started once before stopping."); auto worker = Thread::CreateAndStart(Func([retainedRuntime]() { retainedRuntime->WorkerLoop(); }), true); CHECK_ERROR(worker != nullptr, ERROR_MESSAGE_PREFIX L"Failed to start the io_uring completion worker."); workerStarted = true; } #undef ERROR_MESSAGE_PREFIX } vuint64_t ReserveOperationId() { vuint64_t result = 0; CS_LOCK(lockRuntime) { result = ReserveOperationIdLocked(); } return result; } bool Submit(RingOperation* operation) { bool result = false; CS_LOCK(lockRuntime) { result = SubmitLocked(operation, false); } return result; } void FlushPendingSubmissions() { if (currentRingRuntime != this) { FlushPendingSubmissionsCore(false); } } void Stop(); }; void RingRuntime::Stop() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::Stop()#" bool waitForWorker = false; bool signalWithoutWorker = false; bool wakeFailed = false; bool wakeSubmitted = false; CS_LOCK(lockRuntime) { if (!stopRequested) { if (workerStarted) { if (currentRingRuntime == this) { stopRequested = true; } else { auto operation = new RuntimeWakeOperation(ReserveOperationIdLocked()); if (SubmitLocked(operation, true)) { stopRequested = true; wakeSubmitted = true; } else { wakeFailed = true; } } } else { stopRequested = true; if (ringInitialized) { io_uring_queue_exit(&ring); ringInitialized = false; cvRuntimeProgress.WakeAllPendings(); } signalWithoutWorker = true; } } waitForWorker = stopRequested && workerStarted && currentRingRuntime != this; } CHECK_ERROR(!wakeFailed, ERROR_MESSAGE_PREFIX L"Failed to wake the stopping io_uring worker."); if (wakeSubmitted) { FlushPendingSubmissions(); } if (signalWithoutWorker) { eventWorkerStopped.Signal(); } if (waitForWorker) { eventWorkerStopped.Wait(); } #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** ConnectionState ***********************************************************************/ class ConnectionState : public RingOperationOwner { friend class AsyncSocketConnection; private: class ReceiveOperation; class WriteOperation; class ConnectOperation; class RetryOperation; class CancelOperation; Ptr runtime; IAsyncSocketConnection* owner = nullptr; // covers all fields below, callback counts, and target operation counts CriticalSection lockState; ConditionVariable cvCallbacks; vint fileDescriptor = -1; IAsyncSocketCallback* callback = nullptr; bool connected = false; bool stopping = false; bool reading = false; bool writePending = false; bool terminalPending = false; bool disconnectedNotified = false; vint activeCallbacks = 0; vuint64_t readOperationId = 0; vuint64_t writeOperationId = 0; vuint64_t connectOperationId = 0; vuint64_t retryOperationId = 0; bool readCancelRequested = false; bool writeCancelRequested = false; bool connectCancelRequested = false; bool retryCancelRequested = false; vint targetOperations = 0; bool clientMode = false; vint clientPort = 0; ClientStatus clientStatus = ClientStatus::Ready; vint clientAttempts = 0; EventObject eventWaitForServer; void BeginTargetLocked() { targetOperations++; operationDrain->Begin(); } void BeginCancelLocked() { operationDrain->Begin(); } void RollbackTargetLocked() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::RollbackTargetLocked()#" CHECK_ERROR(targetOperations > 0, ERROR_MESSAGE_PREFIX L"Target-operation bookkeeping became unbalanced."); targetOperations--; operationDrain->End(); #undef ERROR_MESSAGE_PREFIX } void RollbackCancelLocked() { operationDrain->End(); } void EndTargetOperation() override { CS_LOCK(lockState) { if (targetOperations <= 0) { std::abort(); } targetOperations--; if (stopping && targetOperations == 0 && fileDescriptor >= 0) { CloseFileDescriptor(fileDescriptor); fileDescriptor = -1; } } } void HandleOperationFailure(Ptr retainedOwner) override { auto retainedState = retainedOwner.Cast(); if (!retainedState) { std::abort(); } try { BeginTerminal(retainedState, EIO, true); } catch (...) { Stop(retainedState); } } vint CurrentCallbackDepth() { vint result = 0; for (auto frame = currentConnectionCallbackFrame; frame; frame = frame->previous) { if (frame->connection == this) { result++; } } return result; } IAsyncSocketCallback* BeginCallback(bool terminal) { IAsyncSocketCallback* result = nullptr; CS_LOCK(lockState) { if (callback && (terminal || (!stopping && !terminalPending))) { result = callback; activeCallbacks++; } } return result; } void EndCallback() { CS_LOCK(lockState) { activeCallbacks--; cvCallbacks.WakeAllPendings(); } } template bool InvokeCallback(bool terminal, TCallback&& invoke) { auto installed = BeginCallback(terminal); if (!installed) { return false; } ConnectionCallbackFrame frame{ this, currentConnectionCallbackFrame }; currentConnectionCallbackFrame = &frame; try { invoke(installed); } catch (...) { } currentConnectionCallbackFrame = frame.previous; EndCallback(); return true; } void PostRead(Ptr retainedState); void BeginTerminal(Ptr retainedState, vint error, bool reportError); void StartConnectAttempt(Ptr retainedState); void DeliverConnectFailure(Ptr retainedState, vint error, bool fatal); void ScheduleRetry(Ptr retainedState); public: ConnectionState(Ptr _runtime, bool _clientMode, vint _clientPort) : runtime(_runtime) , clientMode(_clientMode) , clientPort(_clientPort) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::ConnectionState(Ptr, bool, vint)#" CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), ERROR_MESSAGE_PREFIX L"Failed to create the client wait event."); #undef ERROR_MESSAGE_PREFIX } ConnectionState(Ptr _runtime, vint _fileDescriptor) : runtime(_runtime) , fileDescriptor(_fileDescriptor) , connected(true) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::ConnectionState(Ptr, vint)#" CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), ERROR_MESSAGE_PREFIX L"Failed to create the client wait event."); #undef ERROR_MESSAGE_PREFIX } void InstallCallback(Ptr retainedState, IAsyncSocketCallback* value); void BeginReading(Ptr retainedState); void Write(Ptr retainedState, Ptr buffer); void Stop(Ptr retainedState); void WaitForServer(Ptr retainedState); ClientStatus GetStatus(); }; /*********************************************************************** ConnectionState::CancelOperation ***********************************************************************/ class ConnectionState::CancelOperation : public RingOperation { public: Ptr connection; vuint64_t targetId = 0; CancelOperation(vuint64_t id, Ptr _connection, vuint64_t _targetId) : RingOperation(id, _connection, false) , connection(_connection) , targetId(_targetId) { } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_cancel64(sqe, targetId, 0); } void Handle(vint) override { CS_LOCK(connection->lockState) { } } }; /*********************************************************************** ConnectionState::ReceiveOperation ***********************************************************************/ class ConnectionState::ReceiveOperation : public RingOperation { public: Ptr connection; vint fileDescriptor = -1; Array buffer; ReceiveOperation(vuint64_t id, Ptr _connection, vint _fileDescriptor) : RingOperation(id, _connection, true) , connection(_connection) , fileDescriptor(_fileDescriptor) { buffer.Resize(65536); } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_recv(sqe, (int)fileDescriptor, &buffer[0], (size_t)buffer.Count(), 0); } void Handle(vint result) override { bool active = false; CS_LOCK(connection->lockState) { if (connection->readOperationId == id) { connection->readOperationId = 0; connection->readCancelRequested = false; active = !connection->stopping && connection->connected && connection->reading; } } if (!active) { return; } if (result > 0 && result <= buffer.Count()) { auto invoked = connection->InvokeCallback(false, [&](IAsyncSocketCallback* installed) { installed->OnRead(&buffer[0], result); }); if (invoked) { connection->PostRead(connection); } } else if (result == 0) { connection->BeginTerminal(connection, 0, false); } else { auto error = result < 0 ? -result : EIO; connection->BeginTerminal(connection, error, true); } } }; /*********************************************************************** ConnectionState::WriteOperation ***********************************************************************/ class ConnectionState::WriteOperation : public RingOperation { public: Ptr connection; vint fileDescriptor = -1; Ptr buffer; vint offset = 0; bool empty = false; WriteOperation(vuint64_t id, Ptr _connection, vint _fileDescriptor, Ptr _buffer, vint _offset) : RingOperation(id, _connection, true) , connection(_connection) , fileDescriptor(_fileDescriptor) , buffer(_buffer) , offset(_offset) , empty(_buffer->data.Count() == 0) { } void Prepare(io_uring_sqe* sqe) noexcept override { if (empty) { io_uring_prep_nop(sqe); } else { io_uring_prep_send( sqe, (int)fileDescriptor, &buffer->data[offset], (size_t)(buffer->data.Count() - offset), MSG_NOSIGNAL ); } } void Handle(vint result) override { if (empty) { bool deliver = false; CS_LOCK(connection->lockState) { if (connection->writeOperationId == id) { connection->writeOperationId = 0; connection->writeCancelRequested = false; if (!connection->stopping && connection->connected && result == 0) { connection->writePending = false; deliver = true; } } } if (deliver) { connection->InvokeCallback(false, [&](IAsyncSocketCallback* installed) { installed->OnWriteCompleted(buffer); }); } else if (result < 0 && result != -ECANCELED) { connection->BeginTerminal(connection, -result, true); } return; } if (result > 0 && result <= buffer->data.Count() - offset) { auto nextOffset = offset + result; if (nextOffset == buffer->data.Count()) { bool deliver = false; CS_LOCK(connection->lockState) { if (connection->writeOperationId == id) { connection->writeOperationId = 0; connection->writeCancelRequested = false; if (!connection->stopping && connection->connected) { connection->writePending = false; deliver = true; } } } if (deliver) { connection->InvokeCallback(false, [&](IAsyncSocketCallback* installed) { installed->OnWriteCompleted(buffer); }); } } else { auto nextId = connection->runtime->ReserveOperationId(); auto next = new WriteOperation(nextId, connection, fileDescriptor, buffer, nextOffset); bool attempted = false; bool submissionFailed = false; CS_LOCK(connection->lockState) { if (connection->writeOperationId == id && !connection->stopping && connection->connected) { attempted = true; connection->writeOperationId = nextId; connection->writeCancelRequested = false; connection->BeginTargetLocked(); if (!connection->runtime->Submit(next)) { connection->writeOperationId = 0; connection->RollbackTargetLocked(); submissionFailed = true; } } } if (attempted && !submissionFailed) { connection->runtime->FlushPendingSubmissions(); } if (!attempted) { delete next; } else if (submissionFailed) { connection->BeginTerminal(connection, EIO, true); } } } else { bool active = false; CS_LOCK(connection->lockState) { if (connection->writeOperationId == id) { connection->writeOperationId = 0; connection->writeCancelRequested = false; active = !connection->stopping && connection->connected; } } if (active) { auto error = result < 0 ? -result : EPIPE; connection->BeginTerminal(connection, error, true); } } } }; /*********************************************************************** ConnectionState::ConnectOperation ***********************************************************************/ class ConnectionState::ConnectOperation : public RingOperation { public: Ptr connection; OwnedFileDescriptor ownedSocket; sockaddr_in address = {}; ConnectOperation(vuint64_t id, Ptr _connection, vint _fileDescriptor, vint port) : RingOperation(id, _connection, true) , connection(_connection) , ownedSocket(_fileDescriptor) { address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = htons((vuint16_t)port); } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_connect(sqe, (int)ownedSocket.Get(), (sockaddr*)&address, sizeof(address)); } void Handle(vint result) override { bool accepted = false; bool failed = false; bool fatal = false; CS_LOCK(connection->lockState) { if (connection->connectOperationId == id) { connection->connectOperationId = 0; connection->connectCancelRequested = false; if (!connection->stopping && connection->clientStatus == ClientStatus::WaitingForServer) { if (result == 0) { connection->connected = true; connection->clientStatus = ClientStatus::Connected; ownedSocket.Detach(); accepted = true; } else { connection->fileDescriptor = -1; failed = true; fatal = connection->clientAttempts >= AsyncSocketClientRetryCount; } } else { if (connection->fileDescriptor == ownedSocket.Get()) { connection->fileDescriptor = -1; } } } } if (accepted) { connection->InvokeCallback(false, [](IAsyncSocketCallback* installed) { installed->OnConnected(); }); connection->eventWaitForServer.Signal(); } else if (failed) { connection->DeliverConnectFailure(connection, result < 0 ? -result : EIO, fatal); } } }; /*********************************************************************** ConnectionState::RetryOperation ***********************************************************************/ class ConnectionState::RetryOperation : public RingOperation { public: Ptr connection; __kernel_timespec timeout = {}; RetryOperation(vuint64_t id, Ptr _connection) : RingOperation(id, _connection, true) , connection(_connection) { timeout.tv_sec = AsyncSocketClientRetryDelay / 1000; timeout.tv_nsec = (AsyncSocketClientRetryDelay % 1000) * 1000000; } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_timeout(sqe, &timeout, 0, 0); } void Handle(vint result) override { bool active = false; CS_LOCK(connection->lockState) { if (connection->retryOperationId == id) { connection->retryOperationId = 0; connection->retryCancelRequested = false; active = !connection->stopping && connection->clientStatus == ClientStatus::WaitingForServer; } } if (!active) { return; } if (result == -ETIME) { connection->StartConnectAttempt(connection); } else if (result != -ECANCELED) { connection->BeginTerminal(connection, result < 0 ? -result : EIO, true); } } }; /*********************************************************************** ConnectionState ***********************************************************************/ void ConnectionState::InstallCallback(Ptr, IAsyncSocketCallback* value) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::InstallCallback(Ptr, IAsyncSocketCallback*)#" auto callbackDepth = CurrentCallbackDepth(); if (!value) { CS_LOCK(lockState) { callback = nullptr; while (activeCallbacks > callbackDepth) { cvCallbacks.SleepWith(lockState); } } return; } bool canInstall = false; CS_LOCK(lockState) { canInstall = callback == nullptr && !stopping && owner != nullptr; if (canInstall) { callback = value; activeCallbacks++; } } CHECK_ERROR(canInstall, ERROR_MESSAGE_PREFIX L"Cannot replace a callback or install one on a stopped connection."); ConnectionCallbackFrame frame{ this, currentConnectionCallbackFrame }; currentConnectionCallbackFrame = &frame; try { value->OnInstalled(owner); } catch (...) { } currentConnectionCallbackFrame = frame.previous; EndCallback(); #undef ERROR_MESSAGE_PREFIX } void ConnectionState::PostRead(Ptr retainedState) { auto operationId = runtime->ReserveOperationId(); ReceiveOperation* operation = nullptr; bool submitted = false; bool submissionFailed = false; CS_LOCK(lockState) { if (connected && !stopping && !terminalPending && reading && readOperationId == 0) { operation = new ReceiveOperation(operationId, retainedState, fileDescriptor); readOperationId = operationId; readCancelRequested = false; BeginTargetLocked(); submitted = runtime->Submit(operation); if (!submitted) { readOperationId = 0; RollbackTargetLocked(); submissionFailed = true; } } } if (submitted) { runtime->FlushPendingSubmissions(); } if (submissionFailed) { BeginTerminal(retainedState, EIO, true); } } void ConnectionState::BeginReading(Ptr retainedState) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::BeginReading(Ptr)#" CS_LOCK(lockState) { CHECK_ERROR(connected && !stopping && !terminalPending, ERROR_MESSAGE_PREFIX L"Requires a connected connection."); CHECK_ERROR(callback != nullptr, ERROR_MESSAGE_PREFIX L"Requires an installed callback."); CHECK_ERROR(!reading, ERROR_MESSAGE_PREFIX L"Can only be called once."); reading = true; } PostRead(retainedState); #undef ERROR_MESSAGE_PREFIX } void ConnectionState::Write(Ptr retainedState, Ptr buffer) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::Write(Ptr, Ptr)#" CHECK_ERROR(buffer, ERROR_MESSAGE_PREFIX L"Requires a buffer."); auto operationId = runtime->ReserveOperationId(); bool submitted = false; CS_LOCK(lockState) { CHECK_ERROR(connected && !stopping && !terminalPending && !writePending, ERROR_MESSAGE_PREFIX L"Requires a connected connection with no outstanding write."); auto operation = new WriteOperation(operationId, retainedState, fileDescriptor, buffer, 0); writePending = true; writeOperationId = operationId; writeCancelRequested = false; BeginTargetLocked(); submitted = runtime->Submit(operation); if (!submitted) { writePending = false; writeOperationId = 0; RollbackTargetLocked(); } } if (submitted) { runtime->FlushPendingSubmissions(); } CHECK_ERROR(submitted, ERROR_MESSAGE_PREFIX L"Failed to submit the write operation."); #undef ERROR_MESSAGE_PREFIX } void ConnectionState::BeginTerminal(Ptr retainedState, vint error, bool reportError) { bool claimed = false; CS_LOCK(lockState) { if (!stopping && !terminalPending) { terminalPending = true; claimed = true; } } if (!claimed) { return; } if (reportError) { InvokeCallback(true, [&](IAsyncSocketCallback* installed) { installed->OnError(LinuxSocketErrorMessage(L"Asynchronous socket operation", error), true); }); } Stop(retainedState); } void ConnectionState::StartConnectAttempt(Ptr retainedState) { bool active = false; bool fatal = false; CS_LOCK(lockState) { if (clientMode && !stopping && clientStatus == ClientStatus::WaitingForServer && connectOperationId == 0 && retryOperationId == 0) { clientAttempts++; fatal = clientAttempts >= AsyncSocketClientRetryCount; active = true; } } if (!active) { return; } OwnedFileDescriptor createdSocket(socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP)); if (createdSocket.Get() < 0) { DeliverConnectFailure(retainedState, errno, fatal); return; } vint reuseAddress = 1; if (setsockopt(createdSocket.Get(), SOL_SOCKET, SO_REUSEADDR, &reuseAddress, sizeof(reuseAddress)) != 0) { DeliverConnectFailure(retainedState, errno, fatal); return; } auto operationId = runtime->ReserveOperationId(); auto createdSocketValue = createdSocket.Get(); auto operation = new ConnectOperation(operationId, retainedState, createdSocketValue, clientPort); createdSocket.Detach(); bool attempted = false; bool submissionFailed = false; CS_LOCK(lockState) { if (!stopping && clientStatus == ClientStatus::WaitingForServer && connectOperationId == 0 && retryOperationId == 0) { attempted = true; fileDescriptor = createdSocketValue; connectOperationId = operationId; connectCancelRequested = false; BeginTargetLocked(); if (!runtime->Submit(operation)) { fileDescriptor = -1; connectOperationId = 0; RollbackTargetLocked(); submissionFailed = true; } } } if (attempted && !submissionFailed) { runtime->FlushPendingSubmissions(); } if (!attempted) { delete operation; } else if (submissionFailed) { DeliverConnectFailure(retainedState, EIO, fatal); } } void ConnectionState::DeliverConnectFailure(Ptr retainedState, vint error, bool fatal) { bool deliver = false; CS_LOCK(lockState) { deliver = !stopping && clientStatus == ClientStatus::WaitingForServer; } if (!deliver) { return; } InvokeCallback(false, [&](IAsyncSocketCallback* installed) { installed->OnError(LinuxSocketErrorMessage(L"io_uring connect", error), fatal); }); if (fatal) { Stop(retainedState); } else { ScheduleRetry(retainedState); } } void ConnectionState::ScheduleRetry(Ptr retainedState) { auto operationId = runtime->ReserveOperationId(); auto operation = new RetryOperation(operationId, retainedState); bool attempted = false; bool submissionFailed = false; CS_LOCK(lockState) { if (!stopping && clientStatus == ClientStatus::WaitingForServer && retryOperationId == 0 && connectOperationId == 0) { attempted = true; retryOperationId = operationId; retryCancelRequested = false; BeginTargetLocked(); if (!runtime->Submit(operation)) { retryOperationId = 0; RollbackTargetLocked(); submissionFailed = true; } } } if (attempted && !submissionFailed) { runtime->FlushPendingSubmissions(); } if (!attempted) { delete operation; } else if (submissionFailed) { BeginTerminal(retainedState, EIO, true); } } void ConnectionState::Stop(Ptr retainedState) { bool firstStop = false; bool cancellationSubmitted = false; CS_LOCK(lockState) { if (!stopping) { stopping = true; firstStop = true; connected = false; reading = false; writePending = false; terminalPending = false; if (clientMode) { clientStatus = ClientStatus::Disconnected; } } auto collect = [&](vuint64_t operationId, bool& cancelRequested) { if (operationId != 0 && !cancelRequested) { auto operation = new CancelOperation(runtime->ReserveOperationId(), retainedState, operationId); cancelRequested = true; BeginCancelLocked(); if (!runtime->Submit(operation)) { cancelRequested = false; RollbackCancelLocked(); } else { cancellationSubmitted = true; } } }; collect(readOperationId, readCancelRequested); collect(writeOperationId, writeCancelRequested); collect(connectOperationId, connectCancelRequested); collect(retryOperationId, retryCancelRequested); if (firstStop && fileDescriptor >= 0) { shutdown((int)fileDescriptor, SHUT_RDWR); if (targetOperations == 0) { CloseFileDescriptor(fileDescriptor); fileDescriptor = -1; } } } if (cancellationSubmitted) { runtime->FlushPendingSubmissions(); } if (currentRingRuntime != runtime.Obj()) { operationDrain->Wait(); } auto callbackDepth = CurrentCallbackDepth(); IAsyncSocketCallback* installed = nullptr; lockState.Enter(); while (activeCallbacks > callbackDepth) { cvCallbacks.SleepWith(lockState); } if (!disconnectedNotified) { disconnectedNotified = true; if (callback) { installed = callback; activeCallbacks++; } } lockState.Leave(); if (installed) { ConnectionCallbackFrame frame{ this, currentConnectionCallbackFrame }; currentConnectionCallbackFrame = &frame; try { installed->OnDisconnected(); } catch (...) { } currentConnectionCallbackFrame = frame.previous; EndCallback(); } if (callbackDepth == 0) { CS_LOCK(lockState) { while (activeCallbacks > 0) { cvCallbacks.SleepWith(lockState); } } } if (clientMode) { eventWaitForServer.Signal(); } } void ConnectionState::WaitForServer(Ptr retainedState) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::WaitForServer(Ptr)#" bool begin = false; CS_LOCK(lockState) { if (clientMode && clientStatus == ClientStatus::Ready && !stopping) { clientStatus = ClientStatus::WaitingForServer; begin = true; } } CHECK_ERROR(begin, ERROR_MESSAGE_PREFIX L"Can only be called once while the client is ready."); StartConnectAttempt(retainedState); eventWaitForServer.Wait(); #undef ERROR_MESSAGE_PREFIX } ClientStatus ConnectionState::GetStatus() { ClientStatus result; CS_LOCK(lockState) { result = clientStatus; } return result; } /*********************************************************************** AsyncSocketConnection ***********************************************************************/ class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection { private: Ptr state; public: AsyncSocketConnection(Ptr _state) : state(_state) { state->owner = this; } ~AsyncSocketConnection() { state->Stop(state); state->owner = nullptr; } void InstallCallback(IAsyncSocketCallback* callback) override { state->InstallCallback(state, callback); } void BeginReadingLoopUnsafe() override { state->BeginReading(state); } void WriteAsync(Ptr buffer) override { state->Write(state, buffer); } void Stop() override { state->Stop(state); } }; /*********************************************************************** ServerState ***********************************************************************/ class ServerState : public RingOperationOwner { private: class AcceptOperation; class CancelOperation; Ptr runtime; vint port = 0; // covers all fields below, callback counts, and target operation counts CriticalSection lockState; ConditionVariable cvCallbacks; IAsyncSocketServerCallback* callback = nullptr; vint listener = -1; bool startCalled = false; bool starting = false; bool started = false; bool stopping = false; bool stopped = false; bool unexpectedStopNotified = false; EventObject eventStartFinished; vuint64_t acceptOperationId = 0; bool acceptCancelRequested = false; vint targetOperations = 0; vint activeCallbacks = 0; List> connections; void FinishStart() { CS_LOCK(lockState) { starting = false; eventStartFinished.Signal(); } } class StartScope { private: ServerState* server = nullptr; public: StartScope(ServerState* _server) : server(_server) { } ~StartScope() { server->FinishStart(); } }; void BeginTargetLocked() { targetOperations++; operationDrain->Begin(); } void BeginCancelLocked() { operationDrain->Begin(); } void RollbackTargetLocked() { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::RollbackTargetLocked()#" CHECK_ERROR(targetOperations > 0, ERROR_MESSAGE_PREFIX L"Target-operation bookkeeping became unbalanced."); targetOperations--; operationDrain->End(); #undef ERROR_MESSAGE_PREFIX } void RollbackCancelLocked() { operationDrain->End(); } void EndTargetOperation() override { CS_LOCK(lockState) { if (targetOperations <= 0) { std::abort(); } targetOperations--; if (stopping && targetOperations == 0 && listener >= 0) { CloseFileDescriptor(listener); listener = -1; } } } void HandleOperationFailure(Ptr retainedOwner) override { auto retainedState = retainedOwner.Cast(); if (!retainedState) { std::abort(); } HandleUnexpectedStop(retainedState); } void HandleUnexpectedStop(Ptr retainedState) { IAsyncSocketServerCallback* installedCallback = nullptr; CS_LOCK(lockState) { if (started && !stopping && !unexpectedStopNotified) { unexpectedStopNotified = true; installedCallback = callback; if (installedCallback) { activeCallbacks++; } } } if (installedCallback) { ServerCallbackFrame frame{ this, currentServerCallbackFrame }; currentServerCallbackFrame = &frame; try { installedCallback->OnServerStopped(); } catch (...) { } currentServerCallbackFrame = frame.previous; EndCallback(); } Stop(retainedState); } vint CurrentCallbackDepth() { vint result = 0; for (auto frame = currentServerCallbackFrame; frame; frame = frame->previous) { if (frame->server == this) { result++; } } return result; } void EndCallback() { CS_LOCK(lockState) { activeCallbacks--; cvCallbacks.WakeAllPendings(); } } bool PostAccept(Ptr retainedState); public: ServerState(Ptr _runtime, vint _port) : runtime(_runtime) , port(_port) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::ServerState(Ptr, vint)#" CHECK_ERROR(eventStartFinished.CreateManualUnsignal(true), ERROR_MESSAGE_PREFIX L"Failed to create the server startup drain event."); #undef ERROR_MESSAGE_PREFIX } void Start(Ptr retainedState, IAsyncSocketServerCallback* value); void Stop(Ptr retainedState); bool IsStopped(); }; /*********************************************************************** ServerState::CancelOperation ***********************************************************************/ class ServerState::CancelOperation : public RingOperation { public: Ptr server; vuint64_t targetId = 0; CancelOperation(vuint64_t id, Ptr _server, vuint64_t _targetId) : RingOperation(id, _server, false) , server(_server) , targetId(_targetId) { } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_cancel64(sqe, targetId, 0); } void Handle(vint) override { CS_LOCK(server->lockState) { } } }; /*********************************************************************** ServerState::AcceptOperation ***********************************************************************/ class ServerState::AcceptOperation : public RingOperation { public: Ptr server; vint listener = -1; sockaddr_storage address = {}; socklen_t addressSize = sizeof(address); AcceptOperation(vuint64_t id, Ptr _server, vint _listener) : RingOperation(id, _server, true) , server(_server) , listener(_listener) { } void Prepare(io_uring_sqe* sqe) noexcept override { io_uring_prep_accept(sqe, (int)listener, (sockaddr*)&address, &addressSize, SOCK_CLOEXEC); } void Handle(vint result) override { bool running = false; CS_LOCK(server->lockState) { if (server->acceptOperationId == id) { server->acceptOperationId = 0; server->acceptCancelRequested = false; running = server->started && !server->stopping; } } if (result < 0) { bool recoverable = false; switch (-result) { case EINTR: case EAGAIN: case ECONNABORTED: case EPROTO: case ENETDOWN: case ENOPROTOOPT: case EHOSTDOWN: case ENONET: case EHOSTUNREACH: case EOPNOTSUPP: case ENETUNREACH: recoverable = true; } if (running && recoverable) { if (!server->PostAccept(server)) { server->HandleUnexpectedStop(server); } } else if (running && result != -ECANCELED) { server->HandleUnexpectedStop(server); } return; } OwnedFileDescriptor acceptedSocket(result); if (!running) { return; } // Keep exactly one accept pending, and rearm before invoking user code. if (!server->PostAccept(server)) { server->HandleUnexpectedStop(server); return; } auto connectionState = Ptr(new ConnectionState(server->runtime, acceptedSocket.Get())); auto connection = Ptr(new AsyncSocketConnection(connectionState)); acceptedSocket.Detach(); IAsyncSocketServerCallback* installedCallback = nullptr; CS_LOCK(server->lockState) { if (server->started && !server->stopping && server->callback) { server->connections.Add(connection); installedCallback = server->callback; server->activeCallbacks++; } } if (!installedCallback) { connection->Stop(); return; } WaitForClientResult acceptResult = WaitForClientResult::Reject; ServerCallbackFrame frame{ server.Obj(), currentServerCallbackFrame }; currentServerCallbackFrame = &frame; try { acceptResult = installedCallback->OnClientConnected(connection.Obj()); } catch (...) { } currentServerCallbackFrame = frame.previous; bool accepted = false; CS_LOCK(server->lockState) { accepted = acceptResult == WaitForClientResult::Accept && server->started && !server->stopping; if (!accepted) { server->connections.Remove(connection.Obj()); } } server->EndCallback(); if (!accepted) { connection->Stop(); } } }; /*********************************************************************** ServerState ***********************************************************************/ bool ServerState::PostAccept(Ptr retainedState) { auto operationId = runtime->ReserveOperationId(); AcceptOperation* operation = nullptr; bool submitted = false; bool submissionFailed = false; CS_LOCK(lockState) { if (started && !stopping && listener >= 0 && acceptOperationId == 0) { operation = new AcceptOperation(operationId, retainedState, listener); acceptOperationId = operationId; acceptCancelRequested = false; BeginTargetLocked(); submitted = runtime->Submit(operation); if (!submitted) { acceptOperationId = 0; RollbackTargetLocked(); submissionFailed = true; } } } if (submitted) { runtime->FlushPendingSubmissions(); } return !submissionFailed; } void ServerState::Start(Ptr retainedState, IAsyncSocketServerCallback* value) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::Start(Ptr, IAsyncSocketServerCallback*)#" CHECK_ERROR(value != nullptr, ERROR_MESSAGE_PREFIX L"Requires a callback."); CS_LOCK(lockState) { CHECK_ERROR(!startCalled && !stopping, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping."); startCalled = true; starting = true; callback = value; eventStartFinished.Unsignal(); } StartScope startScope(this); OwnedFileDescriptor createdListener(socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP)); vint setupError = createdListener.Get() < 0 ? errno : 0; if (setupError == 0) { vint reuseAddress = 1; if (setsockopt(createdListener.Get(), SOL_SOCKET, SO_REUSEADDR, &reuseAddress, sizeof(reuseAddress)) != 0) { setupError = errno; } } sockaddr_in address = {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = htons((vuint16_t)port); if (setupError == 0 && bind(createdListener.Get(), (sockaddr*)&address, sizeof(address)) != 0) { setupError = errno; } if (setupError == 0 && listen(createdListener.Get(), SOMAXCONN) != 0) { setupError = errno; } if (setupError != 0) { CS_LOCK(lockState) { stopping = true; stopped = true; callback = nullptr; } runtime->Stop(); auto failure = setupError == EADDRINUSE ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; throw AsyncSocketServerStartException(failure, LinuxSocketErrorMessage(L"AsyncSocketServer listener setup", setupError)); } bool committed = false; try { CS_LOCK(lockState) { if (!stopping) { runtime->Start(runtime); listener = createdListener.Detach(); started = true; committed = true; } } } catch (...) { CS_LOCK(lockState) { started = false; stopping = true; stopped = true; callback = nullptr; } runtime->Stop(); throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to start the listener runtime."); } if (!committed) { throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"The listener was stopped during startup."); } if (!PostAccept(retainedState)) { CS_LOCK(lockState) { started = false; stopping = true; stopped = true; callback = nullptr; if (listener >= 0) { shutdown((int)listener, SHUT_RDWR); if (targetOperations == 0) { CloseFileDescriptor(listener); listener = -1; } } } runtime->Stop(); throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to submit the first accept operation."); } #undef ERROR_MESSAGE_PREFIX } void ServerState::Stop(Ptr retainedState) { bool waitForStart = false; bool cancellationSubmitted = false; CS_LOCK(lockState) { if (!stopping) { stopping = true; stopped = true; started = false; } waitForStart = starting; } if (waitForStart) { eventStartFinished.Wait(); } CS_LOCK(lockState) { if (acceptOperationId != 0 && !acceptCancelRequested) { auto operation = new CancelOperation(runtime->ReserveOperationId(), retainedState, acceptOperationId); acceptCancelRequested = true; BeginCancelLocked(); if (!runtime->Submit(operation)) { acceptCancelRequested = false; RollbackCancelLocked(); } else { cancellationSubmitted = true; } } if (listener >= 0) { shutdown((int)listener, SHUT_RDWR); if (targetOperations == 0) { CloseFileDescriptor(listener); listener = -1; } } } if (cancellationSubmitted) { runtime->FlushPendingSubmissions(); } if (currentRingRuntime != runtime.Obj()) { operationDrain->Wait(); } auto callbackDepth = CurrentCallbackDepth(); CS_LOCK(lockState) { while (activeCallbacks > callbackDepth) { cvCallbacks.SleepWith(lockState); } } List> retainedConnections; CS_LOCK(lockState) { for (auto connection : connections) { retainedConnections.Add(connection); } } for (auto connection : retainedConnections) { connection->Stop(); } runtime->Stop(); if (callbackDepth == 0) { CS_LOCK(lockState) { callback = nullptr; } } } bool ServerState::IsStopped() { bool result = false; CS_LOCK(lockState) { result = stopped; } return result; } /*********************************************************************** AsyncSocketServer::Impl ***********************************************************************/ class AsyncSocketServer::Impl : public Object { private: vint port = 0; Ptr runtime; Ptr state; public: Impl(vint _port) : port(_port) , runtime(RingRuntime::Create(false)) , state(Ptr(new ServerState(runtime, _port))) { } ~Impl() { Stop(); } vint GetPort() { return port; } void Start(IAsyncSocketServerCallback* callback) { state->Start(state, callback); } void Stop() { state->Stop(state); } bool IsStopped() { return state->IsStopped(); } }; /*********************************************************************** AsyncSocketServer ***********************************************************************/ AsyncSocketServer::AsyncSocketServer(vint port) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketServer::AsyncSocketServer(vint)#" CHECK_ERROR(1 <= port && port <= 65535, ERROR_MESSAGE_PREFIX L"The port must be in 1..65535."); #undef ERROR_MESSAGE_PREFIX impl = new Impl(port); } AsyncSocketServer::~AsyncSocketServer() { delete impl; } vint AsyncSocketServer::GetPort() { return impl->GetPort(); } void AsyncSocketServer::Start(IAsyncSocketServerCallback* callback) { impl->Start(callback); } void AsyncSocketServer::Stop() { impl->Stop(); } bool AsyncSocketServer::IsStopped() { return impl->IsStopped(); } /*********************************************************************** AsyncSocketClient::Impl ***********************************************************************/ class AsyncSocketClient::Impl : public Object { private: vint port = 0; Ptr runtime; Ptr state; Ptr connection; public: Impl(vint _port) : port(_port) , runtime(RingRuntime::Create(true)) , state(Ptr(new ConnectionState(runtime, true, _port))) , connection(Ptr(new AsyncSocketConnection(state))) { } ~Impl() { Stop(); } vint GetPort() { return port; } void Stop() { connection->Stop(); runtime->Stop(); } IAsyncSocketConnection* GetConnection() { return connection.Obj(); } void WaitForServer() { state->WaitForServer(state); } ClientStatus GetStatus() { return state->GetStatus(); } }; /*********************************************************************** AsyncSocketClient ***********************************************************************/ AsyncSocketClient::AsyncSocketClient(vint port) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::AsyncSocketClient::AsyncSocketClient(vint)#" CHECK_ERROR(1 <= port && port <= 65535, ERROR_MESSAGE_PREFIX L"The port must be in 1..65535."); #undef ERROR_MESSAGE_PREFIX impl = new Impl(port); } AsyncSocketClient::~AsyncSocketClient() { delete impl; } vint AsyncSocketClient::GetPort() { return impl->GetPort(); } Ptr AsyncSocketClient::CreateSameEndpointClient() { return Ptr(new AsyncSocketClient(GetPort())); } IAsyncSocketConnection* AsyncSocketClient::GetConnection() { return impl->GetConnection(); } void AsyncSocketClient::WaitForServer() { impl->WaitForServer(); } ClientStatus AsyncSocketClient::GetStatus() { return impl->GetStatus(); } } namespace vl::inter_process::async_tcp_socket { Ptr CreateDefaultAsyncSocketServer(vint port) { return Ptr(new linux_socket::AsyncSocketServer(port)); } Ptr CreateDefaultAsyncSocketClient(vint port) { return Ptr(new linux_socket::AsyncSocketClient(port)); } } #endif /*********************************************************************** .\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.MACOS.CPP ***********************************************************************/ #if defined VCZH_GCC && defined VCZH_APPLE #include #include #if !defined VCZH_GCC || !defined VCZH_APPLE static_assert(false, "Do not build this file for non-macOS applications."); #endif namespace vl::inter_process::async_tcp_socket::macos_socket { using namespace collections; class ConnectionState; class AsyncSocketConnection; struct CallbackFrame { ConnectionState* connection = nullptr; CallbackFrame* previous = nullptr; }; static thread_local CallbackFrame* currentCallbackFrame = nullptr; static char connectionQueueKey; static char serverQueueKey; /*********************************************************************** NativeConnectionContext ***********************************************************************/ class NativeConnectionContext : public Object { friend class ConnectionState; private: Ptr state; nw_connection_t connection = nullptr; vint generation = 0; bool started = false; bool cancelRequested = false; bool waitingHandled = false; bool cancelledHandled = false; public: NativeConnectionContext(Ptr _state, nw_connection_t _connection); ~NativeConnectionContext(); }; NativeConnectionContext::NativeConnectionContext(Ptr _state, nw_connection_t _connection) : state(_state) , connection(_connection) { } NativeConnectionContext::~NativeConnectionContext() { if (connection) { nw_release(connection); } } /*********************************************************************** ConnectionState ***********************************************************************/ class ConnectionState : public Object { friend class NativeConnectionContext; friend class AsyncSocketConnection; private: // covers all fields below CriticalSection lockState; ConditionVariable cvState; dispatch_queue_t queue = nullptr; IAsyncSocketConnection* owner = nullptr; IAsyncSocketCallback* callback = nullptr; bool clientMode = false; vint port = 0; bool logicalConnected = false; bool nativeReady = false; bool stopping = false; bool terminalPending = false; bool disconnectedNotified = false; vint activeCallbacks = 0; bool callbackExecuting = false; bool readingRequested = false; bool receivePending = false; bool receiveHandling = false; vint receiveGeneration = -1; Ptr pendingReadBuffer; bool writePending = false; bool writeIssued = false; vint writeGeneration = -1; Ptr writeBuffer; dispatch_data_t writeData = nullptr; vint pendingLocalTasks = 0; Ptr nativeContext; vint pendingNativeContexts = 0; vint nextGeneration = 0; ClientStatus clientStatus = ClientStatus::Ready; vint clientAttempts = 0; vint retryAfterGeneration = -1; dispatch_source_t retryTimer = nullptr; vint retryTimerGeneration = -1; bool retryTimerFired = false; bool retryTimerCancelRequested = false; vint pendingRetryTimers = 0; EventObject eventWaitForServer; vint CountCurrentCallbackFrames() { vint count = 0; for (auto frame = currentCallbackFrame; frame; frame = frame->previous) { if (frame->connection == this) { count++; } } return count; } bool IsOnQueue() { return dispatch_get_specific(&connectionQueueKey) == this; } enum class CallbackType { Ordinary, FatalError, Disconnected, }; bool CanInvokeCallbackLocked(CallbackType type) { if (!callback) { return false; } switch (type) { case CallbackType::Ordinary: return !stopping && !terminalPending; case CallbackType::FatalError: return !stopping; case CallbackType::Disconnected: return true; default: return false; } } IAsyncSocketCallback* BeginCallback(CallbackType type, bool& ownsExecution) { IAsyncSocketCallback* installed = nullptr; auto nested = CountCurrentCallbackFrames() > 0; ownsExecution = false; lockState.Enter(); while (!nested && callbackExecuting && CanInvokeCallbackLocked(type)) { cvState.SleepWith(lockState); } if (CanInvokeCallbackLocked(type)) { installed = callback; activeCallbacks++; if (!nested) { callbackExecuting = true; ownsExecution = true; } } lockState.Leave(); return installed; } void EndCallback(bool ownsExecution) { CS_LOCK(lockState) { activeCallbacks--; if (ownsExecution) { callbackExecuting = false; } cvState.WakeAllPendings(); } } template bool InvokeCallback(CallbackType type, TCallback&& invoke) { bool ownsExecution = false; auto installed = BeginCallback(type, ownsExecution); if (!installed) { return false; } CallbackFrame frame{ this, currentCallbackFrame }; currentCallbackFrame = &frame; try { invoke(installed); } catch (...) { } currentCallbackFrame = frame.previous; EndCallback(ownsExecution); return true; } void WaitForOtherCallbacks(vint currentFrames) { lockState.Enter(); while (activeCallbacks > currentFrames) { cvState.SleepWith(lockState); } lockState.Leave(); } void NotifyDisconnected() { bool notify = false; lockState.Enter(); if (!disconnectedNotified) { disconnectedNotified = true; notify = callback != nullptr; } lockState.Leave(); if (notify) { InvokeCallback(CallbackType::Disconnected, [](IAsyncSocketCallback* installed) { installed->OnDisconnected(); }); } } void WaitForDrain() { lockState.Enter(); while ( activeCallbacks > 0 || pendingNativeContexts > 0 || pendingRetryTimers > 0 || pendingLocalTasks > 0 ) { cvState.SleepWith(lockState); } lockState.Leave(); } void RequestNativeCancelLocked(Ptr context) { if (!context || !context->connection || context->cancelRequested) { return; } if (!context->started) { context->started = true; nw_connection_start(context->connection); } context->cancelRequested = true; nw_connection_cancel(context->connection); } void RequestRetryTimerCancelLocked() { if (retryTimer && !retryTimerCancelRequested) { retryTimerCancelRequested = true; dispatch_source_cancel(retryTimer); } } WString NetworkErrorMessage(const wchar_t* operation, nw_error_t error) { if (!error) { return WString(operation) + L" failed without a Network.framework error."; } return WString(operation) + L" failed with Network.framework domain " + itow((vint)nw_error_get_error_domain(error)) + L", code " + itow((vint)nw_error_get_error_code(error)) + L"."; } void ConfigureNativeLocked(Ptr context, bool start) { nativeContext = context; pendingNativeContexts++; context->generation = ++nextGeneration; nw_connection_set_queue(context->connection, queue); auto retainedContext = context; nw_connection_set_state_changed_handler(context->connection, ^(nw_connection_state_t state, nw_error_t error) { retainedContext->state->OnNativeState(retainedContext, state, error); }); if (start) { context->started = true; nw_connection_start(context->connection); } } void IssueReceiveLocked(Ptr context) { if ( !context || context != nativeContext || !context->connection || !logicalConnected || !nativeReady || stopping || terminalPending || !callback || !readingRequested || receivePending || receiveHandling || pendingReadBuffer ) { return; } receivePending = true; receiveGeneration = context->generation; auto retainedContext = context; nw_connection_receive(context->connection, 1, 65536, ^(dispatch_data_t content, nw_content_context_t, bool isComplete, nw_error_t error) { retainedContext->state->OnReceive(retainedContext, content, isComplete, error); }); } void IssueWriteLocked(Ptr context) { if ( !context || context != nativeContext || !context->connection || !logicalConnected || !nativeReady || stopping || terminalPending || !writePending || writeIssued ) { return; } writeIssued = true; writeGeneration = context->generation; if (writeBuffer->data.Count() == 0) { pendingLocalTasks++; auto retainedState = context->state; auto generation = context->generation; dispatch_async(queue, ^ { retainedState->CompleteEmptyWrite(generation); }); } else { auto retainedContext = context; nw_connection_send( context->connection, writeData, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t error) { retainedContext->state->OnSendCompleted(retainedContext, error); } ); } } void EnterTerminal(Ptr context, const WString& error, bool reportError) { bool claim = false; CS_LOCK(lockState) { if (!stopping && !terminalPending && (!context || context == nativeContext)) { terminalPending = true; claim = true; } } if (!claim) { return; } if (reportError) { InvokeCallback(CallbackType::FatalError, [&](IAsyncSocketCallback* installed) { installed->OnError(error, true); }); } Stop(); } void StartClientAttempt(Ptr retainedState) { bool canStart = false; CS_LOCK(lockState) { canStart = clientMode && !stopping && clientStatus == ClientStatus::WaitingForServer && !nativeContext; } if (!canStart) { return; } auto portText = itoa(port); auto endpoint = nw_endpoint_create_host("127.0.0.1", portText.Buffer()); auto parameters = nw_parameters_create_secure_tcp( NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION ); nw_connection_t connection = nullptr; if (endpoint && parameters) { connection = nw_connection_create(endpoint, parameters); } if (endpoint) { nw_release(endpoint); } if (parameters) { nw_release(parameters); } if (!connection) { EnterTerminal(nullptr, L"AsyncSocketClient failed to create a Network.framework connection.", true); return; } auto context = Ptr(new NativeConnectionContext(retainedState, connection)); bool installed = false; lockState.Enter(); if (clientMode && !stopping && clientStatus == ClientStatus::WaitingForServer && !nativeContext) { clientAttempts++; ConfigureNativeLocked(context, true); installed = true; } lockState.Leave(); if (!installed) { return; } } void ScheduleRetry(Ptr retainedState, vint generation) { auto source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); if (!source) { EnterTerminal(nullptr, L"AsyncSocketClient failed to create its retry timer.", true); return; } auto retainedForEvent = retainedState; dispatch_source_set_event_handler(source, ^ { bool cancel = false; retainedForEvent->lockState.Enter(); if (retainedForEvent->retryTimer == source && !retainedForEvent->retryTimerCancelRequested) { retainedForEvent->retryTimerFired = true; retainedForEvent->retryTimerCancelRequested = true; cancel = true; } retainedForEvent->lockState.Leave(); if (cancel) { dispatch_source_cancel(source); } }); auto retainedForCancel = retainedState; dispatch_source_set_cancel_handler(source, ^ { auto retainedForSentinel = retainedForCancel; dispatch_async(retainedForCancel->queue, ^ { retainedForSentinel->FinishRetryTimer(retainedForSentinel, source, generation); }); }); dispatch_source_set_timer( source, dispatch_time(DISPATCH_TIME_NOW, (int64_t)AsyncSocketClientRetryDelay * NSEC_PER_MSEC), DISPATCH_TIME_FOREVER, 0 ); bool installed = false; lockState.Enter(); if ( clientMode && !stopping && clientStatus == ClientStatus::WaitingForServer && retryAfterGeneration == generation && !retryTimer ) { retryTimer = source; retryTimerGeneration = generation; retryTimerFired = false; retryTimerCancelRequested = false; pendingRetryTimers++; installed = true; } lockState.Leave(); if (installed) { dispatch_activate(source); } else { dispatch_source_set_event_handler(source, nullptr); dispatch_source_set_cancel_handler(source, nullptr); dispatch_activate(source); dispatch_source_cancel(source); dispatch_release(source); } } void FinishRetryTimer(Ptr retainedState, dispatch_source_t source, vint generation) { bool start = false; lockState.Enter(); if (retryTimer == source) { start = retryTimerFired && !stopping && clientStatus == ClientStatus::WaitingForServer && retryTimerGeneration == generation; retryTimer = nullptr; retryTimerGeneration = -1; retryTimerFired = false; retryTimerCancelRequested = false; } lockState.Leave(); dispatch_release(source); if (start) { StartClientAttempt(retainedState); } CS_LOCK(lockState) { pendingRetryTimers--; cvState.WakeAllPendings(); } } void OnNativeReady(Ptr context) { bool notifyConnected = false; lockState.Enter(); if ( context == nativeContext && !context->cancelRequested && retryAfterGeneration != context->generation && !stopping && !terminalPending ) { nativeReady = true; logicalConnected = true; if (clientMode && clientStatus == ClientStatus::WaitingForServer) { clientStatus = ClientStatus::Connected; notifyConnected = true; } } lockState.Leave(); if (notifyConnected) { InvokeCallback(CallbackType::Ordinary, [](IAsyncSocketCallback* installed) { installed->OnConnected(); }); eventWaitForServer.Signal(); } lockState.Enter(); if (context == nativeContext && nativeReady && logicalConnected && !stopping && !terminalPending) { IssueReceiveLocked(context); IssueWriteLocked(context); } lockState.Leave(); } void OnNativeWaiting(Ptr context, nw_error_t error) { bool retryable = false; bool exhausted = false; lockState.Enter(); if ( clientMode && context == nativeContext && !context->waitingHandled && !stopping && clientStatus == ClientStatus::WaitingForServer ) { context->waitingHandled = true; exhausted = clientAttempts >= AsyncSocketClientRetryCount; retryable = !exhausted; if (retryable) { retryAfterGeneration = context->generation; } } lockState.Leave(); if (!retryable && !exhausted) { return; } auto message = NetworkErrorMessage(L"AsyncSocketClient connection attempt", error); if (exhausted) { EnterTerminal(context, message, true); return; } InvokeCallback(CallbackType::Ordinary, [&](IAsyncSocketCallback* installed) { installed->OnError(message, false); }); lockState.Enter(); if ( context == nativeContext && retryAfterGeneration == context->generation && !stopping && clientStatus == ClientStatus::WaitingForServer ) { RequestNativeCancelLocked(context); } lockState.Leave(); } void OnNativeCancelled(Ptr context) { bool schedule = false; lockState.Enter(); if (!context->cancelledHandled) { context->cancelledHandled = true; if (context->connection) { nw_connection_set_state_changed_handler(context->connection, nullptr); nw_release(context->connection); context->connection = nullptr; } schedule = true; } lockState.Leave(); if (schedule) { auto retainedContext = context; dispatch_async(queue, ^ { retainedContext->state->FinishNativeCancellation(retainedContext); }); } } void FinishNativeCancellation(Ptr context) { bool retry = false; lockState.Enter(); if (nativeContext == context) { retry = clientMode && !stopping && clientStatus == ClientStatus::WaitingForServer && retryAfterGeneration == context->generation; nativeContext = nullptr; nativeReady = false; receivePending = false; receiveGeneration = -1; } lockState.Leave(); if (retry) { ScheduleRetry(context->state, context->generation); } CS_LOCK(lockState) { pendingNativeContexts--; cvState.WakeAllPendings(); } } void OnNativeState(Ptr context, nw_connection_state_t state, nw_error_t error) { switch (state) { case nw_connection_state_waiting: OnNativeWaiting(context, error); break; case nw_connection_state_ready: OnNativeReady(context); break; case nw_connection_state_failed: { bool intentionalCancellation = false; CS_LOCK(lockState) { intentionalCancellation = context != nativeContext || context->cancelRequested || retryAfterGeneration == context->generation || stopping; } if (!intentionalCancellation) { EnterTerminal(context, NetworkErrorMessage(L"Asynchronous socket connection", error), true); } } break; case nw_connection_state_cancelled: OnNativeCancelled(context); break; default: break; } } void ResumeReading() { while (true) { Ptr buffered; lockState.Enter(); if ( pendingReadBuffer && callback && !receiveHandling && logicalConnected && !stopping && !terminalPending ) { buffered = pendingReadBuffer; pendingReadBuffer = nullptr; receiveHandling = true; } else { IssueReceiveLocked(nativeContext); } lockState.Leave(); if (!buffered) { return; } auto invoked = InvokeCallback(CallbackType::Ordinary, [&](IAsyncSocketCallback* installed) { installed->OnRead(&buffered->data[0], buffered->data.Count()); }); bool continueReading = false; lockState.Enter(); if ( !invoked && !pendingReadBuffer && logicalConnected && !stopping && !terminalPending ) { pendingReadBuffer = buffered; } receiveHandling = false; continueReading = invoked && logicalConnected && !stopping && !terminalPending; lockState.Leave(); if (!continueReading) { return; } } } void OnReceive(Ptr context, dispatch_data_t content, bool isComplete, nw_error_t error) { bool claimed = false; bool deliver = false; lockState.Enter(); if (context == nativeContext && receivePending && receiveGeneration == context->generation) { receivePending = false; receiveHandling = true; receiveGeneration = -1; claimed = true; deliver = !stopping && !terminalPending && logicalConnected; } lockState.Leave(); if (!deliver) { if (claimed) { CS_LOCK(lockState) { receiveHandling = false; } } return; } auto undelivered = Ptr(new AsyncSocketBuffer); bool buffering = false; auto bufferingRef = &buffering; if (content) { dispatch_data_apply(content, ^bool(dispatch_data_t, size_t, const void* buffer, size_t size) { if (size == 0) { return true; } if (!*bufferingRef) { auto invoked = InvokeCallback(CallbackType::Ordinary, [&](IAsyncSocketCallback* installed) { installed->OnRead((const vuint8_t*)buffer, (vint)size); }); if (invoked) { return true; } *bufferingRef = true; } auto oldSize = undelivered->data.Count(); undelivered->data.Resize(oldSize + (vint)size); std::memcpy(&undelivered->data[oldSize], buffer, size); return true; }); } if (error) { EnterTerminal(context, NetworkErrorMessage(L"Asynchronous socket receive", error), true); } else if (isComplete) { EnterTerminal(context, WString::Empty, false); } bool resume = false; lockState.Enter(); if ( !error && !isComplete && context == nativeContext && !stopping && !terminalPending && logicalConnected ) { if (undelivered->data.Count() > 0) { pendingReadBuffer = undelivered; } resume = true; } receiveHandling = false; lockState.Leave(); if (resume) { ResumeReading(); } } void OnSendCompleted(Ptr context, nw_error_t error) { Ptr completedBuffer; dispatch_data_t completedData = nullptr; bool reportCompletion = false; bool reportError = false; lockState.Enter(); if (writePending && writeIssued && writeGeneration == context->generation) { completedBuffer = writeBuffer; completedData = writeData; writeBuffer = nullptr; writeData = nullptr; writePending = false; writeIssued = false; writeGeneration = -1; reportCompletion = !error && !stopping && !terminalPending && context == nativeContext; reportError = error && !stopping && !terminalPending && context == nativeContext; } lockState.Leave(); if (completedData) { dispatch_release(completedData); } if (reportCompletion) { InvokeCallback(CallbackType::Ordinary, [&](IAsyncSocketCallback* installed) { installed->OnWriteCompleted(completedBuffer); }); } else if (reportError) { EnterTerminal(context, NetworkErrorMessage(L"Asynchronous socket send", error), true); } } void CompleteEmptyWrite(vint generation) { Ptr completedBuffer; bool reportCompletion = false; lockState.Enter(); if (writePending && writeIssued && writeGeneration == generation) { completedBuffer = writeBuffer; writeBuffer = nullptr; writePending = false; writeIssued = false; writeGeneration = -1; reportCompletion = !stopping && !terminalPending && nativeContext && nativeContext->generation == generation; } lockState.Leave(); if (reportCompletion) { InvokeCallback(CallbackType::Ordinary, [&](IAsyncSocketCallback* installed) { installed->OnWriteCompleted(completedBuffer); }); } CS_LOCK(lockState) { pendingLocalTasks--; cvState.WakeAllPendings(); } } public: ConnectionState(bool _clientMode, vint _port) : clientMode(_clientMode) , port(_port) { queue = dispatch_queue_create("vlppos.async-socket.connection", DISPATCH_QUEUE_SERIAL); CHECK_ERROR(queue != nullptr, L"IAsyncSocketConnection failed to create its dispatch queue."); try { CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), L"IAsyncSocketClient failed to create its wait event."); dispatch_queue_set_specific(queue, &connectionQueueKey, this, nullptr); } catch (...) { dispatch_release(queue); queue = nullptr; throw; } } ~ConnectionState() { if (writeData) { dispatch_release(writeData); } dispatch_queue_set_specific(queue, &connectionQueueKey, nullptr, nullptr); dispatch_release(queue); } void AttachOwner(IAsyncSocketConnection* value) { CS_LOCK(lockState) { owner = value; } } void DetachOwner(IAsyncSocketConnection* value) { CS_LOCK(lockState) { if (owner == value) { owner = nullptr; } } } bool RequiresDeferredDrain() { return CountCurrentCallbackFrames() > 0 || IsOnQueue(); } void ConfigureServerNative(Ptr retainedState, nw_connection_t connection) { auto context = Ptr(new NativeConnectionContext(retainedState, connection)); CS_LOCK(lockState) { CHECK_ERROR(!clientMode && !nativeContext && !stopping, L"The accepted async socket connection is already configured."); logicalConnected = true; ConfigureNativeLocked(context, false); } } void StartAcceptedConnection() { CS_LOCK(lockState) { if (nativeContext && !nativeContext->started && !stopping) { nativeContext->started = true; nw_connection_start(nativeContext->connection); } } } void InstallCallback(IAsyncSocketCallback* value) { if (!value) { auto currentFrames = CountCurrentCallbackFrames(); lockState.Enter(); callback = nullptr; cvState.WakeAllPendings(); while (activeCallbacks > currentFrames) { cvState.SleepWith(lockState); } lockState.Leave(); return; } IAsyncSocketConnection* installedOwner = nullptr; bool canInstall = false; bool ownsExecution = false; auto nested = CountCurrentCallbackFrames() > 0; lockState.Enter(); while (!nested && callbackExecuting && !stopping) { cvState.SleepWith(lockState); } if (!callback && !stopping && owner) { callback = value; installedOwner = owner; activeCallbacks++; if (!nested) { callbackExecuting = true; ownsExecution = true; } canInstall = true; } lockState.Leave(); CHECK_ERROR(canInstall, L"IAsyncSocketConnection::InstallCallback cannot replace a callback or install one on a stopped connection."); CallbackFrame frame{ this, currentCallbackFrame }; currentCallbackFrame = &frame; try { value->OnInstalled(installedOwner); } catch (...) { } currentCallbackFrame = frame.previous; EndCallback(ownsExecution); ResumeReading(); } void BeginReading() { CS_LOCK(lockState) { CHECK_ERROR(logicalConnected && !stopping && !terminalPending, L"IAsyncSocketConnection::BeginReadingLoopUnsafe requires a connected connection."); CHECK_ERROR(callback != nullptr, L"IAsyncSocketConnection::BeginReadingLoopUnsafe requires an installed callback."); CHECK_ERROR(!readingRequested, L"IAsyncSocketConnection::BeginReadingLoopUnsafe can only be called once."); readingRequested = true; IssueReceiveLocked(nativeContext); } } void Write(Ptr buffer) { CHECK_ERROR(buffer, L"IAsyncSocketConnection::WriteAsync requires a buffer."); dispatch_data_t data = nullptr; if (buffer->data.Count() > 0) { auto bytes = std::malloc((size_t)buffer->data.Count()); CHECK_ERROR(bytes != nullptr, L"IAsyncSocketConnection::WriteAsync failed to allocate native write storage."); std::memcpy(bytes, &buffer->data[0], (size_t)buffer->data.Count()); data = dispatch_data_create(bytes, (size_t)buffer->data.Count(), nullptr, DISPATCH_DATA_DESTRUCTOR_FREE); if (!data) { std::free(bytes); } CHECK_ERROR(data != nullptr, L"IAsyncSocketConnection::WriteAsync failed to create native write data."); } bool canWrite = false; lockState.Enter(); if (logicalConnected && !stopping && !terminalPending && !writePending) { writePending = true; writeBuffer = buffer; writeData = data; canWrite = true; IssueWriteLocked(nativeContext); } lockState.Leave(); if (!canWrite && data) { dispatch_release(data); } CHECK_ERROR(canWrite, L"IAsyncSocketConnection::WriteAsync requires a connected connection with no outstanding write."); } void Stop() { auto currentFrames = CountCurrentCallbackFrames(); auto deferredDrain = RequiresDeferredDrain(); Ptr cancelledBuffer; dispatch_data_t cancelledData = nullptr; lockState.Enter(); if (!stopping) { stopping = true; terminalPending = true; logicalConnected = false; nativeReady = false; readingRequested = false; pendingReadBuffer = nullptr; if (clientMode) { clientStatus = ClientStatus::Disconnected; } if (writePending && !writeIssued) { cancelledBuffer = writeBuffer; cancelledData = writeData; writeBuffer = nullptr; writeData = nullptr; writePending = false; writeGeneration = -1; } } RequestRetryTimerCancelLocked(); RequestNativeCancelLocked(nativeContext); cvState.WakeAllPendings(); lockState.Leave(); if (cancelledData) { dispatch_release(cancelledData); } if (clientMode) { eventWaitForServer.Signal(); } WaitForOtherCallbacks(currentFrames); NotifyDisconnected(); if (!deferredDrain) { WaitForDrain(); } } void WaitForServer(Ptr retainedState) { bool begin = false; CS_LOCK(lockState) { if (clientMode && clientStatus == ClientStatus::Ready && !stopping) { clientStatus = ClientStatus::WaitingForServer; begin = true; } } CHECK_ERROR(begin, L"IAsyncSocketClient::WaitForServer can only be called once while the client is ready."); StartClientAttempt(retainedState); eventWaitForServer.Wait(); } ClientStatus GetStatus() { ClientStatus result; CS_LOCK(lockState) { result = clientStatus; } return result; } }; /*********************************************************************** AsyncSocketConnection ***********************************************************************/ class AsyncSocketConnection : public Object, public virtual IAsyncSocketConnection { private: Ptr state; public: AsyncSocketConnection(Ptr _state) : state(_state) { state->AttachOwner(this); } ~AsyncSocketConnection() { state->Stop(); state->DetachOwner(this); } Ptr GetState() { return state; } void StartAcceptedConnection() { state->StartAcceptedConnection(); } bool RequiresDeferredDrain() { return state->RequiresDeferredDrain(); } void InstallCallback(IAsyncSocketCallback* callback) override { state->InstallCallback(callback); } void BeginReadingLoopUnsafe() override { state->BeginReading(); } void WriteAsync(Ptr buffer) override { state->Write(buffer); } void Stop() override { state->Stop(); } }; /*********************************************************************** ServerState ***********************************************************************/ class ServerState : public Object { private: // covers all fields below CriticalSection lockState; ConditionVariable cvState; dispatch_queue_t queue = nullptr; IAsyncSocketServerCallback* callback = nullptr; vint port = 0; bool startCalled = false; bool starting = false; bool startupResolved = false; bool startupReady = false; AsyncSocketServerStartFailure startupFailure = AsyncSocketServerStartFailure::Other; vint startupError = 0; bool started = false; bool stopping = false; bool stopped = false; bool unexpectedStopNotified = false; bool stopFinalizing = false; bool stopCompleted = false; nw_listener_t listener = nullptr; bool listenerCancelRequested = false; bool listenerCancelledHandled = false; vint pendingListener = 0; List> connections; bool IsOnQueue() { return dispatch_get_specific(&serverQueueKey) == this; } void RequestListenerCancelLocked() { if (listener && !listenerCancelRequested) { listenerCancelRequested = true; nw_listener_cancel(listener); } } void OnListenerCancelled(Ptr retainedState) { bool schedule = false; lockState.Enter(); if (!listenerCancelledHandled) { listenerCancelledHandled = true; if (listener) { nw_listener_set_new_connection_handler(listener, nullptr); nw_listener_set_state_changed_handler(listener, nullptr); nw_release(listener); listener = nullptr; } schedule = true; } lockState.Leave(); if (schedule) { dispatch_async(queue, ^ { retainedState->FinishListenerCancellation(); }); } } void FinishListenerCancellation() { CS_LOCK(lockState) { pendingListener = 0; cvState.WakeAllPendings(); } } void OnListenerState(Ptr retainedState, nw_listener_state_t state, nw_error_t error) { switch (state) { case nw_listener_state_waiting: { bool startupFailed = false; CS_LOCK(lockState) { if (starting && !startupResolved) { starting = false; startupResolved = true; startupReady = false; startupError = error ? (vint)nw_error_get_error_code(error) : 0; startupFailure = error && nw_error_get_error_domain(error) == nw_error_domain_posix && startupError == EADDRINUSE ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; startupFailed = true; cvState.WakeAllPendings(); } } if (startupFailed) { Stop(); } } break; case nw_listener_state_ready: CS_LOCK(lockState) { if (starting && !startupResolved && !stopping) { starting = false; startupResolved = true; startupReady = true; started = true; cvState.WakeAllPendings(); } } break; case nw_listener_state_failed: { IAsyncSocketServerCallback* installedCallback = nullptr; lockState.Enter(); if (starting && !startupResolved) { starting = false; startupResolved = true; startupReady = false; startupError = error ? (vint)nw_error_get_error_code(error) : 0; startupFailure = error && nw_error_get_error_domain(error) == nw_error_domain_posix && startupError == EADDRINUSE ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other; cvState.WakeAllPendings(); } else if (started && !stopping && !unexpectedStopNotified) { unexpectedStopNotified = true; installedCallback = callback; } lockState.Leave(); if (installedCallback) { try { installedCallback->OnServerStopped(); } catch (...) { } } } Stop(); break; case nw_listener_state_cancelled: CS_LOCK(lockState) { if (starting && !startupResolved) { starting = false; startupResolved = true; startupReady = false; startupFailure = AsyncSocketServerStartFailure::Other; cvState.WakeAllPendings(); } } OnListenerCancelled(retainedState); break; default: break; } } Ptr CreateConnection(nw_connection_t connection) { nw_retain(connection); auto connectionState = Ptr(new ConnectionState(false, 0)); auto wrapper = Ptr(new AsyncSocketConnection(connectionState)); connectionState->ConfigureServerNative(connectionState, connection); return wrapper; } void OnNewConnection(nw_connection_t connection) { auto wrapper = CreateConnection(connection); IAsyncSocketServerCallback* installedCallback = nullptr; CS_LOCK(lockState) { if (started && !stopping) { installedCallback = callback; } } WaitForClientResult result = WaitForClientResult::Reject; if (installedCallback) { try { result = installedCallback->OnClientConnected(wrapper.Obj()); } catch (...) { } } bool accepted = false; lockState.Enter(); if (result == WaitForClientResult::Accept && started && !stopping) { connections.Add(wrapper); accepted = true; } lockState.Leave(); if (accepted) { wrapper->StartAcceptedConnection(); } else { wrapper->Stop(); } } public: ServerState(vint _port) : port(_port) { queue = dispatch_queue_create("vlppos.async-socket.listener", DISPATCH_QUEUE_SERIAL); CHECK_ERROR(queue != nullptr, L"AsyncSocketServer failed to create its dispatch queue."); dispatch_queue_set_specific(queue, &serverQueueKey, this, nullptr); } ~ServerState() { dispatch_queue_set_specific(queue, &serverQueueKey, nullptr, nullptr); dispatch_release(queue); } void Start(Ptr retainedState, IAsyncSocketServerCallback* value) { #define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::macos_socket::ServerState::Start(Ptr, IAsyncSocketServerCallback*)#" CHECK_ERROR(value != nullptr, ERROR_MESSAGE_PREFIX L"Requires a callback."); bool begin = false; CS_LOCK(lockState) { if (!startCalled && !stopping) { startCalled = true; callback = value; begin = true; } } CHECK_ERROR(begin, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping."); auto portText = itoa(port); auto endpoint = nw_endpoint_create_host("127.0.0.1", portText.Buffer()); auto parameters = nw_parameters_create_secure_tcp( NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION ); nw_listener_t createdListener = nullptr; if (endpoint && parameters) { nw_parameters_set_local_endpoint(parameters, endpoint); createdListener = nw_listener_create(parameters); } if (endpoint) { nw_release(endpoint); } if (parameters) { nw_release(parameters); } if (!createdListener) { CS_LOCK(lockState) { stopping = true; stopped = true; callback = nullptr; } throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"Failed to create the Network.framework listener."); } bool installed = false; lockState.Enter(); if (!stopping) { listener = createdListener; pendingListener = 1; starting = true; nw_listener_set_queue(listener, queue); auto retainedForState = retainedState; nw_listener_set_state_changed_handler(listener, ^(nw_listener_state_t state, nw_error_t error) { retainedForState->OnListenerState(retainedForState, state, error); }); auto retainedForConnection = retainedState; nw_listener_set_new_connection_handler(listener, ^(nw_connection_t connection) { retainedForConnection->OnNewConnection(connection); }); nw_listener_start(listener); installed = true; } lockState.Leave(); if (!installed) { nw_release(createdListener); throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, ERROR_MESSAGE_PREFIX L"The listener was stopped during startup."); } AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other; vint error = 0; bool ready = false; lockState.Enter(); while (!startupResolved) { cvState.SleepWith(lockState); } ready = startupReady; failure = startupFailure; error = startupError; lockState.Leave(); if (!ready) { Stop(); throw AsyncSocketServerStartException(failure, ERROR_MESSAGE_PREFIX L"Network.framework listener startup failed with error " + itow(error) + L"."); } #undef ERROR_MESSAGE_PREFIX } void Stop() { auto deferredDrain = IsOnQueue(); List> stoppingConnections; lockState.Enter(); if (!stopping) { stopping = true; started = false; stopped = true; if (starting && !startupResolved) { starting = false; startupResolved = true; startupReady = false; startupFailure = AsyncSocketServerStartFailure::Other; cvState.WakeAllPendings(); } } RequestListenerCancelLocked(); for (auto connection : connections) { stoppingConnections.Add(connection); } lockState.Leave(); if (!deferredDrain) { for (auto connection : stoppingConnections) { if (connection->RequiresDeferredDrain()) { deferredDrain = true; break; } } } if (deferredDrain) { for (auto connection : stoppingConnections) { connection->Stop(); } return; } bool finalizeHere = false; lockState.Enter(); if (!stopCompleted && !stopFinalizing) { stopFinalizing = true; finalizeHere = true; } while (!finalizeHere && !stopCompleted) { cvState.SleepWith(lockState); } lockState.Leave(); if (!finalizeHere) { return; } lockState.Enter(); while (pendingListener > 0) { cvState.SleepWith(lockState); } lockState.Leave(); for (auto connection : stoppingConnections) { connection->Stop(); } CS_LOCK(lockState) { connections.Clear(); callback = nullptr; stopFinalizing = false; stopCompleted = true; cvState.WakeAllPendings(); } } bool IsStopped() { bool result = false; CS_LOCK(lockState) { result = stopped; } return result; } }; /*********************************************************************** AsyncSocketServer::Impl ***********************************************************************/ class AsyncSocketServer::Impl : public Object { private: vint port = 0; Ptr state; public: Impl(vint _port) : port(_port) , state(Ptr(new ServerState(_port))) { } ~Impl() { state->Stop(); } vint GetPort() { return port; } void Start(IAsyncSocketServerCallback* callback) { state->Start(state, callback); } void Stop() { state->Stop(); } bool IsStopped() { return state->IsStopped(); } }; /*********************************************************************** AsyncSocketServer ***********************************************************************/ AsyncSocketServer::AsyncSocketServer(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketServer requires a port in 1..65535."); impl = new Impl(port); } AsyncSocketServer::~AsyncSocketServer() { delete impl; } vint AsyncSocketServer::GetPort() { return impl->GetPort(); } void AsyncSocketServer::Start(IAsyncSocketServerCallback* callback) { impl->Start(callback); } void AsyncSocketServer::Stop() { impl->Stop(); } bool AsyncSocketServer::IsStopped() { return impl->IsStopped(); } /*********************************************************************** AsyncSocketClient::Impl ***********************************************************************/ class AsyncSocketClient::Impl : public Object { private: vint port = 0; Ptr state; Ptr connection; public: Impl(vint _port) : port(_port) , state(Ptr(new ConnectionState(true, _port))) , connection(Ptr(new AsyncSocketConnection(state))) { } ~Impl() { connection->Stop(); } vint GetPort() { return port; } IAsyncSocketConnection* GetConnection() { return connection.Obj(); } void WaitForServer() { state->WaitForServer(state); } ClientStatus GetStatus() { return state->GetStatus(); } }; /*********************************************************************** AsyncSocketClient ***********************************************************************/ AsyncSocketClient::AsyncSocketClient(vint port) { CHECK_ERROR(1 <= port && port <= 65535, L"AsyncSocketClient requires a port in 1..65535."); impl = new Impl(port); } AsyncSocketClient::~AsyncSocketClient() { delete impl; } vint AsyncSocketClient::GetPort() { return impl->GetPort(); } Ptr AsyncSocketClient::CreateSameEndpointClient() { return Ptr(new AsyncSocketClient(GetPort())); } IAsyncSocketConnection* AsyncSocketClient::GetConnection() { return impl->GetConnection(); } void AsyncSocketClient::WaitForServer() { impl->WaitForServer(); } ClientStatus AsyncSocketClient::GetStatus() { return impl->GetStatus(); } } namespace vl::inter_process::async_tcp_socket { Ptr CreateDefaultAsyncSocketServer(vint port) { return Ptr(new macos_socket::AsyncSocketServer(port)); } Ptr CreateDefaultAsyncSocketClient(vint port) { return Ptr(new macos_socket::AsyncSocketClient(port)); } } #endif /*********************************************************************** .\TUI\TUI.LINUX.CPP ***********************************************************************/ /*********************************************************************** Author: Zihan Chen (vczh) Licensed under https://github.com/vczh-libraries/License ***********************************************************************/ #ifdef VCZH_GCC #include #include #include #include #include #include using namespace vl; using namespace vl::collections; namespace vl { namespace console { vint TUI::MeasureChar(char32_t code) { if (!tui_internal::IsScalar(code)) return 0; auto width = wcwidth((wchar_t)code); return width < 0 ? 0 : width; } namespace tui_internal { int resizePipe[2] = { -1, -1 }; void ResizeSignalHandler(int) { auto savedError = errno; char byte = 1; while (write(resizePipe[1], &byte, 1) == -1 && errno == EINTR) { } errno = savedError; } void EnsureResizePipe() { if (resizePipe[0] != -1) return; CHECK_ERROR(pipe(resizePipe) == 0, L"vl::console::TUI POSIX backend failed to create its resize pipe."); for (vint i = 0; i < 2; i++) { auto descriptorFlags = fcntl(resizePipe[i], F_GETFD); auto statusFlags = fcntl(resizePipe[i], F_GETFL); CHECK_ERROR(descriptorFlags != -1 && statusFlags != -1, L"vl::console::TUI POSIX backend failed to query resize-pipe flags."); CHECK_ERROR(fcntl(resizePipe[i], F_SETFD, descriptorFlags | FD_CLOEXEC) != -1, L"vl::console::TUI POSIX backend failed to make the resize pipe close-on-exec."); CHECK_ERROR(fcntl(resizePipe[i], F_SETFL, statusFlags | O_NONBLOCK) != -1, L"vl::console::TUI POSIX backend failed to make the resize pipe nonblocking."); } } void WriteAll(int descriptor, const char* text, vint length) { vint written = 0; while (written < length) { auto count = write(descriptor, text + written, (size_t)(length - written)); if (count == -1 && errno == EINTR) continue; CHECK_ERROR(count > 0, L"vl::console::TUI POSIX backend failed while writing terminal output."); written += count; } } void AppendUtf8(std::string& output, char32_t code) { if (code <= 0x7F) { output.push_back((char)code); } else if (code <= 0x7FF) { output.push_back((char)(0xC0 | (code >> 6))); output.push_back((char)(0x80 | (code & 0x3F))); } else if (code <= 0xFFFF) { output.push_back((char)(0xE0 | (code >> 12))); output.push_back((char)(0x80 | ((code >> 6) & 0x3F))); output.push_back((char)(0x80 | (code & 0x3F))); } else { output.push_back((char)(0xF0 | (code >> 18))); output.push_back((char)(0x80 | ((code >> 12) & 0x3F))); output.push_back((char)(0x80 | ((code >> 6) & 0x3F))); output.push_back((char)(0x80 | (code & 0x3F))); } } void AppendNumber(std::string& output, vint value) { output += std::to_string((long long)value); } class PosixTuiBackend : public unittest::ITuiBackend { private: termios savedTermios = {}; struct sigaction savedAction = {}; sigset_t savedMask = {}; List inputBytes; List pendingEvents; vuint64_t escapeDeadline = 0; vuint64_t lastClickTime = 0; vint lastClickX = -1; vint lastClickY = -1; TuiMouseButton lastClickButton = TuiMouseButton::Left; bool left = false; bool middle = false; bool right = false; bool started = false; bool termiosChanged = false; bool signalInstalled = false; void QueueChar(wchar_t code, bool alt = false) { unittest::TuiBackendEvent event; event.type = unittest::TuiBackendEventType::Char; event.charInfo.code = code; event.charInfo.alt = alt; pendingEvents.Add(event); } void DrainResizePipe() { char buffer[64]; for (;;) { auto count = read(resizePipe[0], buffer, sizeof(buffer)); if (count > 0) continue; if (count == -1 && errno == EINTR) continue; break; } } void QueueResize() { vint width = 0; vint height = 0; if (TryGetConsoleSize(width, height)) { unittest::TuiBackendEvent event; event.type = unittest::TuiBackendEventType::Resize; event.width = width; event.height = height; pendingEvents.Add(event); } } bool TryParseMouse() { if (inputBytes.Count() < 4 || inputBytes[0] != 0x1B || inputBytes[1] != '[' || inputBytes[2] != '<') return false; vint end = -1; for (vint i = 3; i < inputBytes.Count(); i++) { if (inputBytes[i] == 'M' || inputBytes[i] == 'm') { end = i; break; } if (!(inputBytes[i] == ';' || (inputBytes[i] >= '0' && inputBytes[i] <= '9'))) { inputBytes.RemoveRange(0, i + 1); QueueChar(L'\uFFFD'); return true; } } if (end == -1) return false; vint values[3] = {}; vint valueIndex = 0; for (vint i = 3; i < end; i++) { if (inputBytes[i] == ';') { valueIndex++; if (valueIndex >= 3) break; } else { values[valueIndex] = values[valueIndex] * 10 + inputBytes[i] - '0'; } } auto final = inputBytes[end]; inputBytes.RemoveRange(0, end + 1); escapeDeadline = 0; if (valueIndex != 2) return true; auto cb = values[0]; TuiMouseInfo info; info.x = values[1] - 1; info.y = values[2] - 1; info.shift = (cb & 4) != 0; info.alt = (cb & 8) != 0; info.ctrl = (cb & 16) != 0; auto motion = (cb & 32) != 0; auto base = cb & ~(4 | 8 | 16 | 32); info.left = left; info.middle = middle; info.right = right; unittest::TuiBackendEvent event; event.mouseInfo = info; if (base >= 64 && base <= 67) { if (base == 64 || base == 65) { event.type = unittest::TuiBackendEventType::MouseVerticalWheel; event.mouseInfo.wheel = base == 64 ? 120 : -120; } else { event.type = unittest::TuiBackendEventType::MouseHorizontalWheel; event.mouseInfo.wheel = base == 66 ? 120 : -120; } pendingEvents.Add(event); return true; } if (motion) { event.type = unittest::TuiBackendEventType::MouseMove; pendingEvents.Add(event); return true; } auto released = final == 'm' || base == 3; auto button = base == 0 ? TuiMouseButton::Left : base == 1 ? TuiMouseButton::Middle : TuiMouseButton::Right; event.mouseButton = button; if (released) { event.type = unittest::TuiBackendEventType::MouseUp; if (button == TuiMouseButton::Left) left = false; if (button == TuiMouseButton::Middle) middle = false; if (button == TuiMouseButton::Right) right = false; } else { auto now = GetMonotonicTime(); auto doubleClick = lastClickTime != 0 && now - lastClickTime <= 500 && lastClickX == info.x && lastClickY == info.y && lastClickButton == button; event.type = doubleClick ? unittest::TuiBackendEventType::MouseDoubleClick : unittest::TuiBackendEventType::MouseDown; if (button == TuiMouseButton::Left) left = true; if (button == TuiMouseButton::Middle) middle = true; if (button == TuiMouseButton::Right) right = true; if (doubleClick) { lastClickTime = 0; } else { lastClickTime = now; lastClickX = info.x; lastClickY = info.y; lastClickButton = button; } } event.mouseInfo.left = left; event.mouseInfo.middle = middle; event.mouseInfo.right = right; pendingEvents.Add(event); return true; } bool TryConsumeSpecialSequence() { if (inputBytes.Count() < 2 || inputBytes[0] != 0x1B || inputBytes[1] != '[') return false; for (vint i = 2; i < inputBytes.Count(); i++) { auto byte = inputBytes[i]; if (byte >= 0x40 && byte <= 0x7E) { inputBytes.RemoveRange(0, i + 1); escapeDeadline = 0; return true; } } return false; } bool TryDecodeUtf8(bool alt = false) { auto offset = alt ? 1 : 0; if (inputBytes.Count() <= offset) return false; auto first = inputBytes[offset]; vint length = 0; char32_t code = 0; if (first < 0x80) { length = 1; code = first; } else if (first >= 0xC2 && first <= 0xDF) { length = 2; code = first & 0x1F; } else if (first >= 0xE0 && first <= 0xEF) { length = 3; code = first & 0x0F; } else if (first >= 0xF0 && first <= 0xF4) { length = 4; code = first & 0x07; } else { inputBytes.RemoveRange(0, offset + 1); QueueChar(L'\uFFFD', alt); return true; } if (inputBytes.Count() < offset + length) return false; for (vint i = 1; i < length; i++) { auto next = inputBytes[offset + i]; if ((next & 0xC0) != 0x80) { inputBytes.RemoveRange(0, offset + 1); QueueChar(L'\uFFFD', alt); return true; } code = (code << 6) | (next & 0x3F); } auto minimum = length == 1 ? 0 : length == 2 ? 0x80 : length == 3 ? 0x800 : 0x10000; if (code < minimum || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) { inputBytes.RemoveRange(0, offset + length); QueueChar(L'\uFFFD', alt); return true; } inputBytes.RemoveRange(0, offset + length); escapeDeadline = 0; QueueChar((wchar_t)code, alt); return true; } bool ParseInput() { if (inputBytes.Count() == 0) return false; if (inputBytes[0] == 0x1B) { if (TryParseMouse()) return true; if (TryConsumeSpecialSequence()) return true; if (inputBytes.Count() > 1 && inputBytes[1] != '[') { return TryDecodeUtf8(true); } if (escapeDeadline == 0) escapeDeadline = GetMonotonicTime() + 30; if (GetMonotonicTime() >= escapeDeadline) { inputBytes.RemoveAt(0); escapeDeadline = 0; QueueChar((wchar_t)0x1B); return true; } return false; } return TryDecodeUtf8(); } void AppendColor(std::string& output, TuiColor foreground, TuiColor background, TuiColorMode colorMode) { output += "\x1B["; if (colorMode == TuiColorMode::TrueColor) { output += "38;2;"; AppendNumber(output, foreground.r); output += ";"; AppendNumber(output, foreground.g); output += ";"; AppendNumber(output, foreground.b); output += ";48;2;"; AppendNumber(output, background.r); output += ";"; AppendNumber(output, background.g); output += ";"; AppendNumber(output, background.b); } else if (colorMode == TuiColorMode::Color256) { output += "38;5;"; AppendNumber(output, QuantizeColor(foreground, colorMode)); output += ";48;5;"; AppendNumber(output, QuantizeColor(background, colorMode)); } else { auto foregroundIndex = QuantizeColor(foreground, colorMode); auto backgroundIndex = QuantizeColor(background, colorMode); AppendNumber(output, foregroundIndex < 8 ? 30 + foregroundIndex : 90 + foregroundIndex - 8); output += ";"; AppendNumber(output, backgroundIndex < 8 ? 40 + backgroundIndex : 100 + backgroundIndex - 8); } output += "m"; } public: TuiColorMode Start(const TuiStartOptions& options) override { CHECK_ERROR(!started, L"vl::console::TUI POSIX backend is already active."); CHECK_ERROR(isatty(STDIN_FILENO) && isatty(STDOUT_FILENO), L"vl::console::TUI requires interactive terminal descriptors."); EnsureResizePipe(); DrainResizePipe(); CHECK_ERROR(tcgetattr(STDIN_FILENO, &savedTermios) == 0, L"vl::console::TUI failed to query terminal attributes."); started = true; try { auto raw = savedTermios; cfmakeraw(&raw); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; CHECK_ERROR(tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0, L"vl::console::TUI failed to activate raw terminal input."); termiosChanged = true; sigset_t blocked; sigemptyset(&blocked); sigaddset(&blocked, SIGWINCH); CHECK_ERROR(pthread_sigmask(SIG_BLOCK, &blocked, &savedMask) == 0, L"vl::console::TUI failed to block SIGWINCH during handler installation."); struct sigaction action = {}; action.sa_handler = ResizeSignalHandler; sigemptyset(&action.sa_mask); auto signalResult = sigaction(SIGWINCH, &action, &savedAction); signalInstalled = signalResult == 0; auto maskResult = pthread_sigmask(SIG_SETMASK, &savedMask, nullptr); CHECK_ERROR(signalResult == 0, L"vl::console::TUI failed to install its SIGWINCH handler."); CHECK_ERROR(maskResult == 0, L"vl::console::TUI failed to restore the owner thread signal mask."); const char sequence[] = "\x1B[?1049h\x1B[?25l\x1B[?1003h\x1B[?1006h"; WriteAll(STDOUT_FILENO, sequence, sizeof(sequence) - 1); if (options.colorMode != TuiColorMode::Auto) return options.colorMode; auto colorTerm = getenv("COLORTERM"); if (colorTerm && (strstr(colorTerm, "truecolor") || strstr(colorTerm, "24bit"))) return TuiColorMode::TrueColor; auto term = getenv("TERM"); if (term && strstr(term, "256color")) return TuiColorMode::Color256; return TuiColorMode::Color16; } catch (...) { Stop(); throw; } } void Stop() override { if (!started) return; const char sequence[] = "\x1B[?1006l\x1B[?1003l\x1B[0m\x1B[?25h\x1B[?1049l"; auto ignored = write(STDOUT_FILENO, sequence, sizeof(sequence) - 1); (void)ignored; if (termiosChanged) tcsetattr(STDIN_FILENO, TCSANOW, &savedTermios); if (signalInstalled) { sigset_t blocked; sigemptyset(&blocked); sigaddset(&blocked, SIGWINCH); pthread_sigmask(SIG_BLOCK, &blocked, nullptr); sigaction(SIGWINCH, &savedAction, nullptr); pthread_sigmask(SIG_SETMASK, &savedMask, nullptr); } inputBytes.Clear(); pendingEvents.Clear(); escapeDeadline = 0; left = middle = right = false; termiosChanged = false; signalInstalled = false; started = false; } bool TryGetConsoleSize(vint& width, vint& height) override { winsize size = {}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) != 0) return false; width = size.ws_col; height = size.ws_row; return width > 0 && height > 0; } vuint64_t GetMonotonicTime() override { timespec time = {}; clock_gettime(CLOCK_MONOTONIC, &time); return (vuint64_t)time.tv_sec * 1000 + time.tv_nsec / 1000000; } bool ReadEvent(vint milliseconds, unittest::TuiBackendEvent& event) override { if (pendingEvents.Count() == 0) ParseInput(); if (pendingEvents.Count() == 0) { auto wait = milliseconds; if (escapeDeadline != 0) { auto now = GetMonotonicTime(); auto remaining = escapeDeadline <= now ? 0 : (vint)(escapeDeadline - now); if (wait < 0 || remaining < wait) wait = remaining; } pollfd descriptors[] = { { STDIN_FILENO, POLLIN, 0 }, { resizePipe[0], POLLIN, 0 }, }; vint result = 0; do { result = poll(descriptors, sizeof(descriptors) / sizeof(*descriptors), wait); } while (result == -1 && errno == EINTR); CHECK_ERROR(result >= 0, L"vl::console::TUI POSIX backend failed while waiting for terminal input."); if (descriptors[1].revents & POLLIN) { DrainResizePipe(); QueueResize(); } if (descriptors[0].revents & POLLIN) { vuint8_t bytes[256]; auto count = read(STDIN_FILENO, bytes, sizeof(bytes)); if (count > 0) { for (vint i = 0; i < count; i++) inputBytes.Add(bytes[i]); } } if (pendingEvents.Count() == 0) ParseInput(); } if (pendingEvents.Count() == 0) return false; event = pendingEvents[0]; pendingEvents.RemoveAt(0); return true; } void Render(const TuiPixel* buffer, vint width, vint height, TuiColorMode colorMode) override { std::string output; TuiColor lastForeground; TuiColor lastBackground; bool hasLastColor = false; for (vint y = 0; y < height; y++) { output += "\x1B["; AppendNumber(output, y + 1); output += ";1H"; for (vint x = 0; x < width; x++) { auto& pixel = buffer[y * width + x]; if (pixel.glyph == TuiPixelGlyph::WideCharContinuation) continue; if (!hasLastColor || pixel.foregroundColor != lastForeground || pixel.backgroundColor != lastBackground) { AppendColor(output, pixel.foregroundColor, pixel.backgroundColor, colorMode); lastForeground = pixel.foregroundColor; lastBackground = pixel.backgroundColor; hasLastColor = true; } auto code = pixel.GetChar32(); AppendUtf8(output, code == 0 ? U' ' : code); } } output += "\x1B[0m"; WriteAll(STDOUT_FILENO, output.data(), (vint)output.size()); } }; Ptr CreateTuiBackend() { return Ptr(new PosixTuiBackend); } } } } #endif