mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-08-17 17:31:44 +08:00
6283 lines
149 KiB
C++
6283 lines
149 KiB
C++
/***********************************************************************
|
|
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 <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
|
|
#ifndef VCZH_GCC
|
|
static_assert(false, "Do not build this file for Windows applications.");
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
extern Ptr<IFileStreamImpl> CreateOSFileStreamImpl(const WString& fileName, FileStream::AccessRight accessRight);
|
|
}
|
|
|
|
namespace filesystem
|
|
{
|
|
using namespace collections;
|
|
using namespace stream;
|
|
|
|
/***********************************************************************
|
|
LinuxFileSystemImpl
|
|
***********************************************************************/
|
|
|
|
class LinuxFileSystemImpl : public feature_injection::FeatureImpl<IFileSystemImpl>
|
|
{
|
|
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<wchar_t> 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<WString> 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<WString> 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<Folder>& 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<File>& 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<stream::IFileStreamImpl> 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 <pthread.h>
|
|
#include <fcntl.h>
|
|
#include <semaphore.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#if defined VCZH_APPLE
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#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<void()> procedure;
|
|
|
|
protected:
|
|
void Run()
|
|
{
|
|
procedure();
|
|
}
|
|
public:
|
|
LambdaThread(const Func<void()>& _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<void()>& 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<void()> task;
|
|
Ptr<ThreadPoolTask> next;
|
|
};
|
|
|
|
struct ThreadPoolData
|
|
{
|
|
Semaphore semaphore;
|
|
EventObject taskFinishEvent;
|
|
Ptr<ThreadPoolTask> taskBegin;
|
|
Ptr<ThreadPoolTask>* taskEnd = nullptr;
|
|
volatile bool stopping = false;
|
|
List<Thread*> taskThreads;
|
|
};
|
|
|
|
SpinLock threadPoolLock;
|
|
ThreadPoolData* threadPoolData = nullptr;
|
|
|
|
void ThreadPoolProc(Thread* thread, void* argument)
|
|
{
|
|
while (true)
|
|
{
|
|
Ptr<ThreadPoolTask> 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<void()>& 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<void()>& 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 <string.h>
|
|
|
|
#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 <liburing.h>
|
|
#include <arpa/inet.h>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
|
|
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> operationDrain;
|
|
|
|
RingOperationOwner()
|
|
: operationDrain(Ptr(new OperationDrain))
|
|
{
|
|
}
|
|
|
|
virtual void EndTargetOperation() = 0;
|
|
virtual void HandleOperationFailure(Ptr<RingOperationOwner> retainedOwner) = 0;
|
|
};
|
|
|
|
/***********************************************************************
|
|
RingOperation
|
|
***********************************************************************/
|
|
|
|
class RingOperation
|
|
{
|
|
friend class RingRuntime;
|
|
private:
|
|
Ptr<RingOperationOwner> operationOwner;
|
|
bool targetOperation = false;
|
|
|
|
public:
|
|
vuint64_t id = 0;
|
|
|
|
RingOperation(vuint64_t _id, Ptr<RingOperationOwner> _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<vuint64_t, RingOperation*> 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<RingRuntime> Create(bool startWorker)
|
|
{
|
|
auto result = Ptr(new RingRuntime);
|
|
if (startWorker)
|
|
{
|
|
result->Start(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Start(Ptr<RingRuntime> retainedRuntime)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::RingRuntime::Start(Ptr<RingRuntime>)#"
|
|
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<void()>([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<RingRuntime> 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<RingOperationOwner> retainedOwner) override
|
|
{
|
|
auto retainedState = retainedOwner.Cast<ConnectionState>();
|
|
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<typename TCallback>
|
|
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<ConnectionState> retainedState);
|
|
void BeginTerminal(Ptr<ConnectionState> retainedState, vint error, bool reportError);
|
|
void StartConnectAttempt(Ptr<ConnectionState> retainedState);
|
|
void DeliverConnectFailure(Ptr<ConnectionState> retainedState, vint error, bool fatal);
|
|
void ScheduleRetry(Ptr<ConnectionState> retainedState);
|
|
|
|
public:
|
|
ConnectionState(Ptr<RingRuntime> _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<RingRuntime>, bool, vint)#"
|
|
CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), ERROR_MESSAGE_PREFIX L"Failed to create the client wait event.");
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
ConnectionState(Ptr<RingRuntime> _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<RingRuntime>, vint)#"
|
|
CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), ERROR_MESSAGE_PREFIX L"Failed to create the client wait event.");
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void InstallCallback(Ptr<ConnectionState> retainedState, IAsyncSocketCallback* value);
|
|
void BeginReading(Ptr<ConnectionState> retainedState);
|
|
void Write(Ptr<ConnectionState> retainedState, Ptr<AsyncSocketBuffer> buffer);
|
|
void Stop(Ptr<ConnectionState> retainedState);
|
|
void WaitForServer(Ptr<ConnectionState> retainedState);
|
|
ClientStatus GetStatus();
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState::CancelOperation
|
|
***********************************************************************/
|
|
|
|
class ConnectionState::CancelOperation : public RingOperation
|
|
{
|
|
public:
|
|
Ptr<ConnectionState> connection;
|
|
vuint64_t targetId = 0;
|
|
|
|
CancelOperation(vuint64_t id, Ptr<ConnectionState> _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<ConnectionState> connection;
|
|
vint fileDescriptor = -1;
|
|
Array<vuint8_t> buffer;
|
|
|
|
ReceiveOperation(vuint64_t id, Ptr<ConnectionState> _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<ConnectionState> connection;
|
|
vint fileDescriptor = -1;
|
|
Ptr<AsyncSocketBuffer> buffer;
|
|
vint offset = 0;
|
|
bool empty = false;
|
|
|
|
WriteOperation(vuint64_t id, Ptr<ConnectionState> _connection, vint _fileDescriptor, Ptr<AsyncSocketBuffer> _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<ConnectionState> connection;
|
|
OwnedFileDescriptor ownedSocket;
|
|
sockaddr_in address = {};
|
|
|
|
ConnectOperation(vuint64_t id, Ptr<ConnectionState> _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<ConnectionState> connection;
|
|
__kernel_timespec timeout = {};
|
|
|
|
RetryOperation(vuint64_t id, Ptr<ConnectionState> _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<ConnectionState>, IAsyncSocketCallback* value)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::InstallCallback(Ptr<ConnectionState>, 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<ConnectionState> 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<ConnectionState> retainedState)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::BeginReading(Ptr<ConnectionState>)#"
|
|
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<ConnectionState> retainedState, Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::Write(Ptr<ConnectionState>, Ptr<AsyncSocketBuffer>)#"
|
|
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<ConnectionState> 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<ConnectionState> 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<ConnectionState> 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<ConnectionState> 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<ConnectionState> 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<ConnectionState> retainedState)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ConnectionState::WaitForServer(Ptr<ConnectionState>)#"
|
|
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<ConnectionState> state;
|
|
|
|
public:
|
|
AsyncSocketConnection(Ptr<ConnectionState> _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<AsyncSocketBuffer> buffer) override
|
|
{
|
|
state->Write(state, buffer);
|
|
}
|
|
|
|
void Stop() override
|
|
{
|
|
state->Stop(state);
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ServerState
|
|
***********************************************************************/
|
|
|
|
class ServerState : public RingOperationOwner
|
|
{
|
|
private:
|
|
class AcceptOperation;
|
|
class CancelOperation;
|
|
|
|
Ptr<RingRuntime> 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<Ptr<AsyncSocketConnection>> 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<RingOperationOwner> retainedOwner) override
|
|
{
|
|
auto retainedState = retainedOwner.Cast<ServerState>();
|
|
if (!retainedState)
|
|
{
|
|
std::abort();
|
|
}
|
|
HandleUnexpectedStop(retainedState);
|
|
}
|
|
|
|
void HandleUnexpectedStop(Ptr<ServerState> 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<ServerState> retainedState);
|
|
|
|
public:
|
|
ServerState(Ptr<RingRuntime> _runtime, vint _port)
|
|
: runtime(_runtime)
|
|
, port(_port)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::ServerState(Ptr<RingRuntime>, 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<ServerState> retainedState, IAsyncSocketServerCallback* value);
|
|
void Stop(Ptr<ServerState> retainedState);
|
|
bool IsStopped();
|
|
};
|
|
|
|
/***********************************************************************
|
|
ServerState::CancelOperation
|
|
***********************************************************************/
|
|
|
|
class ServerState::CancelOperation : public RingOperation
|
|
{
|
|
public:
|
|
Ptr<ServerState> server;
|
|
vuint64_t targetId = 0;
|
|
|
|
CancelOperation(vuint64_t id, Ptr<ServerState> _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<ServerState> server;
|
|
vint listener = -1;
|
|
sockaddr_storage address = {};
|
|
socklen_t addressSize = sizeof(address);
|
|
|
|
AcceptOperation(vuint64_t id, Ptr<ServerState> _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<ServerState> 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<ServerState> retainedState, IAsyncSocketServerCallback* value)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::linux_socket::ServerState::Start(Ptr<ServerState>, 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<ServerState> 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<Ptr<AsyncSocketConnection>> 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<RingRuntime> runtime;
|
|
Ptr<ServerState> 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<RingRuntime> runtime;
|
|
Ptr<ConnectionState> state;
|
|
Ptr<AsyncSocketConnection> 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<IAsyncSocketClient> 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<IAsyncSocketServer> CreateDefaultAsyncSocketServer(vint port)
|
|
{
|
|
return Ptr(new linux_socket::AsyncSocketServer(port));
|
|
}
|
|
|
|
Ptr<IAsyncSocketClient> CreateDefaultAsyncSocketClient(vint port)
|
|
{
|
|
return Ptr(new linux_socket::AsyncSocketClient(port));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.MACOS.CPP
|
|
***********************************************************************/
|
|
#if defined VCZH_GCC && defined VCZH_APPLE
|
|
|
|
#include <Network/Network.h>
|
|
#include <dispatch/dispatch.h>
|
|
|
|
#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<ConnectionState> state;
|
|
nw_connection_t connection = nullptr;
|
|
vint generation = 0;
|
|
bool started = false;
|
|
bool cancelRequested = false;
|
|
bool waitingHandled = false;
|
|
bool cancelledHandled = false;
|
|
|
|
public:
|
|
NativeConnectionContext(Ptr<ConnectionState> _state, nw_connection_t _connection);
|
|
~NativeConnectionContext();
|
|
};
|
|
|
|
NativeConnectionContext::NativeConnectionContext(Ptr<ConnectionState> _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<AsyncSocketBuffer> pendingReadBuffer;
|
|
bool writePending = false;
|
|
bool writeIssued = false;
|
|
vint writeGeneration = -1;
|
|
Ptr<AsyncSocketBuffer> writeBuffer;
|
|
dispatch_data_t writeData = nullptr;
|
|
vint pendingLocalTasks = 0;
|
|
Ptr<NativeConnectionContext> 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<typename TCallback>
|
|
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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<ConnectionState> 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<ConnectionState> 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<ConnectionState> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<NativeConnectionContext> 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<AsyncSocketBuffer> 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<NativeConnectionContext> 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<NativeConnectionContext> context, nw_error_t error)
|
|
{
|
|
Ptr<AsyncSocketBuffer> 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<AsyncSocketBuffer> 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<ConnectionState> 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<AsyncSocketBuffer> 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<AsyncSocketBuffer> 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<ConnectionState> 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<ConnectionState> state;
|
|
|
|
public:
|
|
AsyncSocketConnection(Ptr<ConnectionState> _state)
|
|
: state(_state)
|
|
{
|
|
state->AttachOwner(this);
|
|
}
|
|
|
|
~AsyncSocketConnection()
|
|
{
|
|
state->Stop();
|
|
state->DetachOwner(this);
|
|
}
|
|
|
|
Ptr<ConnectionState> 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<AsyncSocketBuffer> 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<Ptr<AsyncSocketConnection>> connections;
|
|
|
|
bool IsOnQueue()
|
|
{
|
|
return dispatch_get_specific(&serverQueueKey) == this;
|
|
}
|
|
|
|
void RequestListenerCancelLocked()
|
|
{
|
|
if (listener && !listenerCancelRequested)
|
|
{
|
|
listenerCancelRequested = true;
|
|
nw_listener_cancel(listener);
|
|
}
|
|
}
|
|
|
|
void OnListenerCancelled(Ptr<ServerState> 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<ServerState> 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<AsyncSocketConnection> 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<ServerState> retainedState, IAsyncSocketServerCallback* value)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::macos_socket::ServerState::Start(Ptr<ServerState>, 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<Ptr<AsyncSocketConnection>> 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<ServerState> 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<ConnectionState> state;
|
|
Ptr<AsyncSocketConnection> 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<IAsyncSocketClient> 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<IAsyncSocketServer> CreateDefaultAsyncSocketServer(vint port)
|
|
{
|
|
return Ptr(new macos_socket::AsyncSocketServer(port));
|
|
}
|
|
|
|
Ptr<IAsyncSocketClient> 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 <csignal>
|
|
#include <poll.h>
|
|
#include <string>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <wchar.h>
|
|
|
|
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<vuint8_t> inputBytes;
|
|
List<unittest::TuiBackendEvent> 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<unittest::ITuiBackend> CreateTuiBackend()
|
|
{
|
|
return Ptr(new PosixTuiBackend);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|