mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-08-17 17:31:44 +08:00
7001 lines
180 KiB
C++
7001 lines
180 KiB
C++
/***********************************************************************
|
|
THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY
|
|
DEVELOPER: Zihan Chen(vczh)
|
|
***********************************************************************/
|
|
#include "VlppOS.Windows.h"
|
|
|
|
/***********************************************************************
|
|
.\FILESYSTEM.WINDOWS.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#define _WINSOCKAPI_
|
|
#include <Shlwapi.h>
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
#pragma comment(lib, "Shlwapi.lib")
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
extern Ptr<IFileStreamImpl> CreateOSFileStreamImpl(const WString& fileName, FileStream::AccessRight accessRight);
|
|
}
|
|
|
|
namespace filesystem
|
|
{
|
|
using namespace collections;
|
|
using namespace stream;
|
|
|
|
/***********************************************************************
|
|
WindowsFileSystemImpl
|
|
***********************************************************************/
|
|
|
|
class WindowsFileSystemImpl : public feature_injection::FeatureImpl<IFileSystemImpl>
|
|
{
|
|
public:
|
|
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
|
|
{
|
|
if (IsRoot(fullPath))
|
|
{
|
|
return relativePath;
|
|
}
|
|
|
|
return fullPath + WString::FromChar(GetPathDelimiter()) + relativePath;
|
|
}
|
|
|
|
void Initialize(WString& fullPath) const override
|
|
{
|
|
{
|
|
Array<wchar_t> buffer(fullPath.Length() + 1);
|
|
wcscpy_s(&buffer[0], fullPath.Length() + 1, fullPath.Buffer());
|
|
FilePath::NormalizeDelimiters(buffer);
|
|
fullPath = &buffer[0];
|
|
}
|
|
|
|
if (fullPath != L"")
|
|
{
|
|
if (fullPath.Length() < 2 || fullPath[1] != L':')
|
|
{
|
|
wchar_t buffer[MAX_PATH + 1] = { 0 };
|
|
auto result = GetCurrentDirectory(sizeof(buffer) / sizeof(*buffer), buffer);
|
|
if (result > MAX_PATH + 1 || result == 0)
|
|
{
|
|
throw ArgumentException(L"Failed to call GetCurrentDirectory.", L"vl::filesystem::FilePath::Initialize", L"");
|
|
}
|
|
fullPath = WString(buffer) + L"\\" + fullPath;
|
|
}
|
|
{
|
|
wchar_t buffer[MAX_PATH + 1] = { 0 };
|
|
if (fullPath.Length() == 2 && fullPath[1] == L':')
|
|
{
|
|
fullPath += L"\\";
|
|
}
|
|
auto result = GetFullPathName(fullPath.Buffer(), sizeof(buffer) / sizeof(*buffer), buffer, NULL);
|
|
if (result > MAX_PATH + 1 || result == 0)
|
|
{
|
|
throw ArgumentException(L"The path is illegal.", L"vl::filesystem::FilePath::FilePath", L"_filePath");
|
|
}
|
|
|
|
{
|
|
wchar_t shortPath[MAX_PATH + 1];
|
|
wchar_t longPath[MAX_PATH + 1];
|
|
if (GetShortPathName(buffer, shortPath, MAX_PATH) > 0)
|
|
{
|
|
if (GetLongPathName(shortPath, longPath, MAX_PATH) > 0)
|
|
{
|
|
memcpy(buffer, longPath, sizeof(buffer));
|
|
}
|
|
}
|
|
}
|
|
fullPath = buffer;
|
|
}
|
|
}
|
|
|
|
FilePath::TrimLastDelimiter(fullPath);
|
|
}
|
|
|
|
bool IsFile(const WString& fullPath) const override
|
|
{
|
|
WIN32_FILE_ATTRIBUTE_DATA info;
|
|
BOOL result = GetFileAttributesEx(fullPath.Buffer(), GetFileExInfoStandard, &info);
|
|
if (!result) return false;
|
|
return (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
|
}
|
|
|
|
bool IsFolder(const WString& fullPath) const override
|
|
{
|
|
WIN32_FILE_ATTRIBUTE_DATA info;
|
|
BOOL result = GetFileAttributesEx(fullPath.Buffer(), GetFileExInfoStandard, &info);
|
|
if (!result) return false;
|
|
return (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
}
|
|
|
|
bool IsRoot(const WString& fullPath) const override
|
|
{
|
|
return fullPath == L"";
|
|
}
|
|
|
|
WString GetRelativePathFor(const WString& fromPath, const WString& toPath) const override
|
|
{
|
|
if (fromPath.Length() == 0 || toPath.Length() == 0 || fromPath[0] != toPath[0])
|
|
{
|
|
return toPath;
|
|
}
|
|
|
|
wchar_t buffer[MAX_PATH + 1] = { 0 };
|
|
PathRelativePathTo(
|
|
buffer,
|
|
fromPath.Buffer(),
|
|
(IsFolder(fromPath) ? FILE_ATTRIBUTE_DIRECTORY : 0),
|
|
toPath.Buffer(),
|
|
(IsFolder(toPath) ? FILE_ATTRIBUTE_DIRECTORY : 0)
|
|
);
|
|
return buffer;
|
|
}
|
|
|
|
bool FileDelete(const FilePath& filePath) const override
|
|
{
|
|
return DeleteFile(filePath.GetFullPath().Buffer()) != 0;
|
|
}
|
|
|
|
bool FileRename(const FilePath& filePath, const WString& newName) const override
|
|
{
|
|
WString oldFileName = filePath.GetFullPath();
|
|
WString newFileName = (filePath.GetFolder() / newName).GetFullPath();
|
|
return MoveFile(oldFileName.Buffer(), newFileName.Buffer()) != 0;
|
|
}
|
|
|
|
bool GetFolders(const FilePath& folderPath, collections::List<Folder>& folders) const override
|
|
{
|
|
if (folderPath.IsRoot())
|
|
{
|
|
auto bufferSize = GetLogicalDriveStrings(0, nullptr);
|
|
if (bufferSize > 0)
|
|
{
|
|
Array<wchar_t> buffer(bufferSize);
|
|
if (GetLogicalDriveStrings((DWORD)buffer.Count(), &buffer[0]) > 0)
|
|
{
|
|
auto begin = &buffer[0];
|
|
auto end = begin + buffer.Count();
|
|
while (begin < end && *begin)
|
|
{
|
|
WString driveString = begin;
|
|
begin += driveString.Length() + 1;
|
|
folders.Add(Folder(FilePath(driveString)));
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!IsFolder(folderPath.GetFullPath())) return false;
|
|
WIN32_FIND_DATA findData;
|
|
HANDLE findHandle = INVALID_HANDLE_VALUE;
|
|
|
|
while (true)
|
|
{
|
|
if (findHandle == INVALID_HANDLE_VALUE)
|
|
{
|
|
WString searchPath = (folderPath / L"*").GetFullPath();
|
|
findHandle = FindFirstFile(searchPath.Buffer(), &findData);
|
|
if (findHandle == INVALID_HANDLE_VALUE)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BOOL result = FindNextFile(findHandle, &findData);
|
|
if (result == 0)
|
|
{
|
|
FindClose(findHandle);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
|
{
|
|
if (wcscmp(findData.cFileName, L".") != 0 && wcscmp(findData.cFileName, L"..") != 0)
|
|
{
|
|
folders.Add(Folder(folderPath / findData.cFileName));
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool GetFiles(const FilePath& folderPath, collections::List<File>& files) const override
|
|
{
|
|
if (IsRoot(folderPath.GetFullPath()))
|
|
{
|
|
return true;
|
|
}
|
|
if (!IsFolder(folderPath.GetFullPath())) return false;
|
|
WIN32_FIND_DATA findData;
|
|
HANDLE findHandle = INVALID_HANDLE_VALUE;
|
|
|
|
while (true)
|
|
{
|
|
if (findHandle == INVALID_HANDLE_VALUE)
|
|
{
|
|
WString searchPath = (folderPath / L"*").GetFullPath();
|
|
findHandle = FindFirstFile(searchPath.Buffer(), &findData);
|
|
if (findHandle == INVALID_HANDLE_VALUE)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BOOL result = FindNextFile(findHandle, &findData);
|
|
if (result == 0)
|
|
{
|
|
FindClose(findHandle);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
{
|
|
files.Add(File(folderPath / findData.cFileName));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CreateFolder(const FilePath& folderPath) const override
|
|
{
|
|
return CreateDirectory(folderPath.GetFullPath().Buffer(), NULL) != 0;
|
|
}
|
|
|
|
bool DeleteFolder(const FilePath& folderPath) const override
|
|
{
|
|
return RemoveDirectory(folderPath.GetFullPath().Buffer()) != 0;
|
|
}
|
|
|
|
bool FolderRename(const FilePath& folderPath, const WString& newName) const override
|
|
{
|
|
WString oldFileName = folderPath.GetFullPath();
|
|
WString newFileName = (folderPath.GetFolder() / newName).GetFullPath();
|
|
return MoveFile(oldFileName.Buffer(), newFileName.Buffer()) != 0;
|
|
}
|
|
|
|
Ptr<stream::IFileStreamImpl> GetFileStreamImpl(const WString& fileName, stream::FileStream::AccessRight accessRight) const override
|
|
{
|
|
return stream::CreateOSFileStreamImpl(fileName, accessRight);
|
|
}
|
|
};
|
|
|
|
IFileSystemImpl* GetOSFileSystemImpl()
|
|
{
|
|
static WindowsFileSystemImpl osFileSystemImpl;
|
|
return &osFileSystemImpl;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\LOCALE.WINDOWS.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#define _WINSOCKAPI_
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
using namespace collections;
|
|
|
|
/***********************************************************************
|
|
Locale Helper Functions
|
|
***********************************************************************/
|
|
|
|
SYSTEMTIME DateTimeToSystemTime(const DateTime& dateTime)
|
|
{
|
|
SYSTEMTIME systemTime;
|
|
systemTime.wYear = (WORD)dateTime.year;
|
|
systemTime.wMonth = (WORD)dateTime.month;
|
|
systemTime.wDayOfWeek = (WORD)dateTime.dayOfWeek;
|
|
systemTime.wDay = (WORD)dateTime.day;
|
|
systemTime.wHour = (WORD)dateTime.hour;
|
|
systemTime.wMinute = (WORD)dateTime.minute;
|
|
systemTime.wSecond = (WORD)dateTime.second;
|
|
systemTime.wMilliseconds = (WORD)dateTime.milliseconds;
|
|
return systemTime;
|
|
}
|
|
|
|
BOOL CALLBACK Locale_EnumLocalesProcEx(
|
|
_In_ LPWSTR lpLocaleString,
|
|
_In_ DWORD dwFlags,
|
|
_In_ LPARAM lParam
|
|
)
|
|
{
|
|
((List<Locale>*)lParam)->Add(Locale(lpLocaleString));
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL CALLBACK Locale_EnumDateFormatsProcExEx(
|
|
_In_ LPWSTR lpDateFormatString,
|
|
_In_ CALID CalendarID,
|
|
_In_ LPARAM lParam
|
|
)
|
|
{
|
|
((List<WString>*)lParam)->Add(lpDateFormatString);
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL CALLBACK EnumTimeFormatsProcEx(
|
|
_In_ LPWSTR lpTimeFormatString,
|
|
_In_ LPARAM lParam
|
|
)
|
|
{
|
|
((List<WString>*)lParam)->Add(lpTimeFormatString);
|
|
return TRUE;
|
|
}
|
|
|
|
WString Transform(const WString& localeName, const WString& input, DWORD flag)
|
|
{
|
|
int length = LCMapStringEx(localeName.Buffer(), flag, input.Buffer(), (int)input.Length() + 1, NULL, 0, NULL, NULL, NULL);
|
|
Array<wchar_t> buffer(length);
|
|
LCMapStringEx(localeName.Buffer(), flag, input.Buffer(), (int)input.Length() + 1, &buffer[0], (int)buffer.Count(), NULL, NULL, NULL);
|
|
return &buffer[0];
|
|
}
|
|
|
|
DWORD TranslateNormalization(Locale::Normalization normalization)
|
|
{
|
|
DWORD result = 0;
|
|
if (normalization & Locale::IgnoreCase) result |= NORM_IGNORECASE;
|
|
if (normalization & Locale::IgnoreCaseLinguistic) result |= NORM_IGNORECASE | NORM_LINGUISTIC_CASING;
|
|
if (normalization & Locale::IgnoreKanaType) result |= NORM_IGNOREKANATYPE;
|
|
if (normalization & Locale::IgnoreNonSpace) result |= NORM_IGNORENONSPACE;
|
|
if (normalization & Locale::IgnoreSymbol) result |= NORM_IGNORESYMBOLS;
|
|
if (normalization & Locale::IgnoreWidth) result |= NORM_IGNOREWIDTH;
|
|
if (normalization & Locale::DigitsAsNumbers) result |= SORT_DIGITSASNUMBERS;
|
|
if (normalization & Locale::StringSoft) result |= SORT_STRINGSORT;
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
WindowsLocaleImpl
|
|
***********************************************************************/
|
|
|
|
class WindowsLocaleImpl : public feature_injection::FeatureImpl<ILocaleImpl>
|
|
{
|
|
public:
|
|
Locale Invariant() const override
|
|
{
|
|
return Locale(LOCALE_NAME_INVARIANT);
|
|
}
|
|
|
|
Locale SystemDefault() const override
|
|
{
|
|
wchar_t buffer[LOCALE_NAME_MAX_LENGTH + 1] = { 0 };
|
|
GetSystemDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH);
|
|
return Locale(buffer);
|
|
}
|
|
|
|
Locale UserDefault() const override
|
|
{
|
|
wchar_t buffer[LOCALE_NAME_MAX_LENGTH + 1] = { 0 };
|
|
GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH);
|
|
return Locale(buffer);
|
|
}
|
|
|
|
void Enumerate(collections::List<Locale>& locales) const override
|
|
{
|
|
EnumSystemLocalesEx(&Locale_EnumLocalesProcEx, LOCALE_ALL, (LPARAM)&locales, NULL);
|
|
}
|
|
|
|
void GetShortDateFormats(const WString& localeName, collections::List<WString>& formats) const override
|
|
{
|
|
EnumDateFormatsExEx(&Locale_EnumDateFormatsProcExEx, localeName.Buffer(), DATE_SHORTDATE, (LPARAM)&formats);
|
|
}
|
|
|
|
void GetLongDateFormats(const WString& localeName, collections::List<WString>& formats) const override
|
|
{
|
|
EnumDateFormatsExEx(&Locale_EnumDateFormatsProcExEx, localeName.Buffer(), DATE_LONGDATE, (LPARAM)&formats);
|
|
}
|
|
|
|
void GetYearMonthDateFormats(const WString& localeName, collections::List<WString>& formats) const override
|
|
{
|
|
EnumDateFormatsExEx(&Locale_EnumDateFormatsProcExEx, localeName.Buffer(), DATE_YEARMONTH, (LPARAM)&formats);
|
|
}
|
|
|
|
void GetLongTimeFormats(const WString& localeName, collections::List<WString>& formats) const override
|
|
{
|
|
EnumTimeFormatsEx(&EnumTimeFormatsProcEx, localeName.Buffer(), 0, (LPARAM)&formats);
|
|
}
|
|
|
|
void GetShortTimeFormats(const WString& localeName, collections::List<WString>& formats) const override
|
|
{
|
|
EnumTimeFormatsEx(&EnumTimeFormatsProcEx, localeName.Buffer(), TIME_NOSECONDS, (LPARAM)&formats);
|
|
}
|
|
|
|
WString FormatDate(const WString& localeName, const WString& format, DateTime date) const override
|
|
{
|
|
SYSTEMTIME st = DateTimeToSystemTime(date);
|
|
int length = GetDateFormatEx(localeName.Buffer(), 0, &st, format.Buffer(), NULL, 0, NULL);
|
|
if (length == 0) return L"";
|
|
Array<wchar_t> buffer(length);
|
|
GetDateFormatEx(localeName.Buffer(), 0, &st, format.Buffer(), &buffer[0], (int)buffer.Count(), NULL);
|
|
return &buffer[0];
|
|
}
|
|
|
|
WString FormatTime(const WString& localeName, const WString& format, DateTime time) const override
|
|
{
|
|
SYSTEMTIME st = DateTimeToSystemTime(time);
|
|
int length = GetTimeFormatEx(localeName.Buffer(), 0, &st, format.Buffer(), NULL, 0);
|
|
if (length == 0) return L"";
|
|
Array<wchar_t> buffer(length);
|
|
GetTimeFormatEx(localeName.Buffer(), 0, &st, format.Buffer(), &buffer[0], (int)buffer.Count());
|
|
return &buffer[0];
|
|
}
|
|
|
|
WString FormatNumber(const WString& localeName, const WString& number) const override
|
|
{
|
|
int length = GetNumberFormatEx(localeName.Buffer(), 0, number.Buffer(), NULL, NULL, 0);
|
|
if (length == 0) return L"";
|
|
Array<wchar_t> buffer(length);
|
|
GetNumberFormatEx(localeName.Buffer(), 0, number.Buffer(), NULL, &buffer[0], (int)buffer.Count());
|
|
return &buffer[0];
|
|
}
|
|
|
|
WString FormatCurrency(const WString& localeName, const WString& currency) const override
|
|
{
|
|
int length = GetCurrencyFormatEx(localeName.Buffer(), 0, currency.Buffer(), NULL, NULL, 0);
|
|
if (length == 0) return L"";
|
|
Array<wchar_t> buffer(length);
|
|
GetCurrencyFormatEx(localeName.Buffer(), 0, currency.Buffer(), NULL, &buffer[0], (int)buffer.Count());
|
|
return &buffer[0];
|
|
}
|
|
|
|
WString GetShortDayOfWeekName(const WString& localeName, vint dayOfWeek) const override
|
|
{
|
|
return FormatDate(localeName, L"ddd", DateTime::FromDateTime(2000, 1, 2 + dayOfWeek));
|
|
}
|
|
|
|
WString GetLongDayOfWeekName(const WString& localeName, vint dayOfWeek) const override
|
|
{
|
|
return FormatDate(localeName, L"dddd", DateTime::FromDateTime(2000, 1, 2 + dayOfWeek));
|
|
}
|
|
|
|
WString GetShortMonthName(const WString& localeName, vint month) const override
|
|
{
|
|
return FormatDate(localeName, L"MMM", DateTime::FromDateTime(2000, month, 1));
|
|
}
|
|
|
|
WString GetLongMonthName(const WString& localeName, vint month) const override
|
|
{
|
|
return FormatDate(localeName, L"MMMM", DateTime::FromDateTime(2000, month, 1));
|
|
}
|
|
|
|
WString ToLower(const WString& localeName, const WString& str) const override
|
|
{
|
|
return Transform(localeName, str, LCMAP_LOWERCASE);
|
|
}
|
|
|
|
WString ToUpper(const WString& localeName, const WString& str) const override
|
|
{
|
|
return Transform(localeName, str, LCMAP_UPPERCASE);
|
|
}
|
|
|
|
WString ToLinguisticLower(const WString& localeName, const WString& str) const override
|
|
{
|
|
return Transform(localeName, str, LCMAP_LOWERCASE | LCMAP_LINGUISTIC_CASING);
|
|
}
|
|
|
|
WString ToLinguisticUpper(const WString& localeName, const WString& str) const override
|
|
{
|
|
return Transform(localeName, str, LCMAP_UPPERCASE | LCMAP_LINGUISTIC_CASING);
|
|
}
|
|
|
|
vint Compare(const WString& localeName, const WString& s1, const WString& s2, Locale::Normalization normalization) const override
|
|
{
|
|
switch (CompareStringEx(localeName.Buffer(), TranslateNormalization(normalization), s1.Buffer(), (int)s1.Length(), s2.Buffer(), (int)s2.Length(), NULL, NULL, NULL))
|
|
{
|
|
case CSTR_LESS_THAN: return -1;
|
|
case CSTR_GREATER_THAN: return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
vint CompareOrdinal(const WString& s1, const WString& s2) const override
|
|
{
|
|
switch (CompareStringOrdinal(s1.Buffer(), (int)s1.Length(), s2.Buffer(), (int)s2.Length(), FALSE))
|
|
{
|
|
case CSTR_LESS_THAN: return -1;
|
|
case CSTR_GREATER_THAN: return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
vint CompareOrdinalIgnoreCase(const WString& s1, const WString& s2) const override
|
|
{
|
|
switch (CompareStringOrdinal(s1.Buffer(), (int)s1.Length(), s2.Buffer(), (int)s2.Length(), TRUE))
|
|
{
|
|
case CSTR_LESS_THAN: return -1;
|
|
case CSTR_GREATER_THAN: return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
collections::Pair<vint, vint> FindFirst(const WString& localeName, const WString& text, const WString& find, Locale::Normalization normalization) const override
|
|
{
|
|
int length = 0;
|
|
int result = FindNLSStringEx(localeName.Buffer(), FIND_FROMSTART | TranslateNormalization(normalization), text.Buffer(), (int)text.Length(), find.Buffer(), (int)find.Length(), &length, NULL, NULL, NULL);
|
|
return result == -1 ? collections::Pair<vint, vint>(-1, 0) : collections::Pair<vint, vint>(result, length);
|
|
}
|
|
|
|
collections::Pair<vint, vint> FindLast(const WString& localeName, const WString& text, const WString& find, Locale::Normalization normalization) const override
|
|
{
|
|
int length = 0;
|
|
int result = FindNLSStringEx(localeName.Buffer(), FIND_FROMEND | TranslateNormalization(normalization), text.Buffer(), (int)text.Length(), find.Buffer(), (int)find.Length(), &length, NULL, NULL, NULL);
|
|
return result == -1 ? collections::Pair<vint, vint>(-1, 0) : collections::Pair<vint, vint>(result, length);
|
|
}
|
|
|
|
bool StartsWith(const WString& localeName, const WString& text, const WString& find, Locale::Normalization normalization) const override
|
|
{
|
|
int result = FindNLSStringEx(localeName.Buffer(), FIND_STARTSWITH | TranslateNormalization(normalization), text.Buffer(), (int)text.Length(), find.Buffer(), (int)find.Length(), NULL, NULL, NULL, NULL);
|
|
return result != -1;
|
|
}
|
|
|
|
bool EndsWith(const WString& localeName, const WString& text, const WString& find, Locale::Normalization normalization) const override
|
|
{
|
|
int result = FindNLSStringEx(localeName.Buffer(), FIND_ENDSWITH | TranslateNormalization(normalization), text.Buffer(), (int)text.Length(), find.Buffer(), (int)find.Length(), NULL, NULL, NULL, NULL);
|
|
return result != -1;
|
|
}
|
|
};
|
|
|
|
ILocaleImpl* GetOSLocaleImpl()
|
|
{
|
|
static WindowsLocaleImpl windowsLocaleImpl;
|
|
return &windowsLocaleImpl;
|
|
}
|
|
|
|
/***********************************************************************
|
|
Locale (Windows Specific)
|
|
***********************************************************************/
|
|
|
|
WString Locale::ToFullWidth(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_FULLWIDTH);
|
|
}
|
|
|
|
WString Locale::ToHalfWidth(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_HALFWIDTH);
|
|
}
|
|
|
|
WString Locale::ToHiragana(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_HIRAGANA);
|
|
}
|
|
|
|
WString Locale::ToKatagana(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_KATAKANA);
|
|
}
|
|
|
|
WString Locale::ToSimplifiedChinese(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_SIMPLIFIED_CHINESE);
|
|
}
|
|
|
|
WString Locale::ToTraditionalChinese(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_TRADITIONAL_CHINESE);
|
|
}
|
|
|
|
WString Locale::ToTileCase(const WString& str)const
|
|
{
|
|
return Transform(localeName, str, LCMAP_TITLECASE);
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\THREADING.WINDOWS.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#define _WINSOCKAPI_
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
using namespace threading_internal;
|
|
using namespace collections;
|
|
|
|
/***********************************************************************
|
|
WaitableObject
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct WaitableData
|
|
{
|
|
HANDLE handle;
|
|
|
|
WaitableData(HANDLE _handle)
|
|
:handle(_handle)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
WaitableObject::WaitableObject()
|
|
:waitableData(0)
|
|
{
|
|
}
|
|
|
|
void WaitableObject::SetData(threading_internal::WaitableData* data)
|
|
{
|
|
waitableData=data;
|
|
}
|
|
|
|
bool WaitableObject::IsCreated()
|
|
{
|
|
return waitableData!=0;
|
|
}
|
|
|
|
bool WaitableObject::Wait()
|
|
{
|
|
return WaitForTime(INFINITE);
|
|
}
|
|
|
|
bool WaitableObject::WaitForTime(vint ms)
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
if(WaitForSingleObject(waitableData->handle, (DWORD)ms)==WAIT_OBJECT_0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool WaitableObject::WaitAll(WaitableObject** objects, vint count)
|
|
{
|
|
Array<HANDLE> handles(count);
|
|
for(vint i=0;i<count;i++)
|
|
{
|
|
handles[i]=objects[i]->waitableData->handle;
|
|
}
|
|
DWORD result=WaitForMultipleObjects((DWORD)count, &handles[0], TRUE, INFINITE);
|
|
return result==WAIT_OBJECT_0 || result==WAIT_ABANDONED_0;
|
|
|
|
}
|
|
|
|
bool WaitableObject::WaitAllForTime(WaitableObject** objects, vint count, vint ms)
|
|
{
|
|
Array<HANDLE> handles(count);
|
|
for(vint i=0;i<count;i++)
|
|
{
|
|
handles[i]=objects[i]->waitableData->handle;
|
|
}
|
|
DWORD result=WaitForMultipleObjects((DWORD)count, &handles[0], TRUE, (DWORD)ms);
|
|
return result==WAIT_OBJECT_0 || result==WAIT_ABANDONED_0;
|
|
}
|
|
|
|
vint WaitableObject::WaitAny(WaitableObject** objects, vint count, bool* abandoned)
|
|
{
|
|
Array<HANDLE> handles(count);
|
|
for(vint i=0;i<count;i++)
|
|
{
|
|
handles[i]=objects[i]->waitableData->handle;
|
|
}
|
|
DWORD result=WaitForMultipleObjects((DWORD)count, &handles[0], FALSE, INFINITE);
|
|
if(WAIT_OBJECT_0 <= result && result<WAIT_OBJECT_0+count)
|
|
{
|
|
*abandoned=false;
|
|
return result-WAIT_OBJECT_0;
|
|
}
|
|
else if(WAIT_ABANDONED_0 <= result && result<WAIT_ABANDONED_0+count)
|
|
{
|
|
*abandoned=true;
|
|
return result-WAIT_ABANDONED_0;
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
vint WaitableObject::WaitAnyForTime(WaitableObject** objects, vint count, vint ms, bool* abandoned)
|
|
{
|
|
Array<HANDLE> handles(count);
|
|
for(vint i=0;i<count;i++)
|
|
{
|
|
handles[i]=objects[i]->waitableData->handle;
|
|
}
|
|
DWORD result=WaitForMultipleObjects((DWORD)count, &handles[0], FALSE, (DWORD)ms);
|
|
if(WAIT_OBJECT_0 <= result && result<WAIT_OBJECT_0+count)
|
|
{
|
|
*abandoned=false;
|
|
return result-WAIT_OBJECT_0;
|
|
}
|
|
else if(WAIT_ABANDONED_0 <= result && result<WAIT_ABANDONED_0+count)
|
|
{
|
|
*abandoned=true;
|
|
return result-WAIT_ABANDONED_0;
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
Thread
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct ThreadData : public WaitableData
|
|
{
|
|
DWORD id;
|
|
|
|
ThreadData()
|
|
:WaitableData(NULL)
|
|
{
|
|
id=-1;
|
|
}
|
|
};
|
|
|
|
class ProceduredThread : public Thread
|
|
{
|
|
private:
|
|
Thread::ThreadProcedure procedure;
|
|
void* argument;
|
|
bool deleteAfterStopped;
|
|
|
|
protected:
|
|
void Run()
|
|
{
|
|
bool deleteAfterStopped = this->deleteAfterStopped;
|
|
ThreadLocalStorage::FixStorages();
|
|
try
|
|
{
|
|
procedure(this, argument);
|
|
threadState=Thread::Stopped;
|
|
ThreadLocalStorage::ClearStorages();
|
|
}
|
|
catch (...)
|
|
{
|
|
ThreadLocalStorage::ClearStorages();
|
|
throw;
|
|
}
|
|
if(deleteAfterStopped)
|
|
{
|
|
delete this;
|
|
}
|
|
}
|
|
public:
|
|
ProceduredThread(Thread::ThreadProcedure _procedure, void* _argument, bool _deleteAfterStopped)
|
|
:procedure(_procedure)
|
|
,argument(_argument)
|
|
,deleteAfterStopped(_deleteAfterStopped)
|
|
{
|
|
}
|
|
};
|
|
|
|
class LambdaThread : public Thread
|
|
{
|
|
private:
|
|
Func<void()> procedure;
|
|
bool deleteAfterStopped;
|
|
|
|
protected:
|
|
void Run()
|
|
{
|
|
bool deleteAfterStopped = this->deleteAfterStopped;
|
|
ThreadLocalStorage::FixStorages();
|
|
try
|
|
{
|
|
procedure();
|
|
threadState=Thread::Stopped;
|
|
ThreadLocalStorage::ClearStorages();
|
|
}
|
|
catch (...)
|
|
{
|
|
ThreadLocalStorage::ClearStorages();
|
|
throw;
|
|
}
|
|
if(deleteAfterStopped)
|
|
{
|
|
delete this;
|
|
}
|
|
}
|
|
public:
|
|
LambdaThread(const Func<void()>& _procedure, bool _deleteAfterStopped)
|
|
:procedure(_procedure)
|
|
,deleteAfterStopped(_deleteAfterStopped)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
void InternalThreadProc(Thread* thread)
|
|
{
|
|
thread->Run();
|
|
}
|
|
|
|
DWORD WINAPI InternalThreadProcWrapper(LPVOID lpParameter)
|
|
{
|
|
InternalThreadProc((Thread*)lpParameter);
|
|
return 0;
|
|
}
|
|
|
|
Thread::Thread()
|
|
{
|
|
internalData=new ThreadData;
|
|
internalData->handle=CreateThread(NULL, 0, InternalThreadProcWrapper, this, CREATE_SUSPENDED, &internalData->id);
|
|
threadState=Thread::NotStarted;
|
|
SetData(internalData);
|
|
}
|
|
|
|
Thread::~Thread()
|
|
{
|
|
if (internalData)
|
|
{
|
|
Stop();
|
|
CloseHandle(internalData->handle);
|
|
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)
|
|
{
|
|
::Sleep((DWORD)ms);
|
|
}
|
|
|
|
|
|
vint Thread::GetCPUCount()
|
|
{
|
|
SYSTEM_INFO info;
|
|
GetSystemInfo(&info);
|
|
return info.dwNumberOfProcessors;
|
|
}
|
|
|
|
vint Thread::GetCurrentThreadId()
|
|
{
|
|
return (vint)::GetCurrentThreadId();
|
|
}
|
|
|
|
bool Thread::Start()
|
|
{
|
|
if(threadState==Thread::NotStarted && internalData->handle!=NULL)
|
|
{
|
|
if(ResumeThread(internalData->handle)!=-1)
|
|
{
|
|
threadState=Thread::Running;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Thread::Stop()
|
|
{
|
|
if(internalData->handle!=NULL)
|
|
{
|
|
if (SuspendThread(internalData->handle) != -1)
|
|
{
|
|
threadState=Thread::Stopped;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Thread::ThreadState Thread::GetState()
|
|
{
|
|
return threadState;
|
|
}
|
|
|
|
void Thread::SetCPU(vint index)
|
|
{
|
|
SetThreadAffinityMask(internalData->handle, ((vint)1 << index));
|
|
}
|
|
|
|
/***********************************************************************
|
|
Mutex
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct MutexData : public WaitableData
|
|
{
|
|
MutexData(HANDLE _handle)
|
|
:WaitableData(_handle)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
Mutex::Mutex()
|
|
:internalData(0)
|
|
{
|
|
}
|
|
|
|
Mutex::~Mutex()
|
|
{
|
|
if(internalData)
|
|
{
|
|
CloseHandle(internalData->handle);
|
|
delete internalData;
|
|
}
|
|
}
|
|
|
|
bool Mutex::Create(bool owned, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aOwned=owned?TRUE:FALSE;
|
|
LPCTSTR aName=name==L""?NULL:name.Buffer();
|
|
HANDLE handle=CreateMutex(NULL, aOwned, aName);
|
|
if(handle)
|
|
{
|
|
internalData=new MutexData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool Mutex::Open(bool inheritable, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aInteritable=inheritable?TRUE:FALSE;
|
|
HANDLE handle=OpenMutex(SYNCHRONIZE, aInteritable, name.Buffer());
|
|
if(handle)
|
|
{
|
|
internalData=new MutexData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool Mutex::Release()
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
return ReleaseMutex(internalData->handle)!=0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/***********************************************************************
|
|
Semaphore
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct SemaphoreData : public WaitableData
|
|
{
|
|
SemaphoreData(HANDLE _handle)
|
|
:WaitableData(_handle)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
Semaphore::Semaphore()
|
|
:internalData(0)
|
|
{
|
|
}
|
|
|
|
Semaphore::~Semaphore()
|
|
{
|
|
if(internalData)
|
|
{
|
|
CloseHandle(internalData->handle);
|
|
delete internalData;
|
|
}
|
|
}
|
|
|
|
bool Semaphore::Create(vint initialCount, vint maxCount, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
LONG aInitial=(LONG)initialCount;
|
|
LONG aMax=(LONG)maxCount;
|
|
LPCTSTR aName=name==L""?NULL:name.Buffer();
|
|
HANDLE handle=CreateSemaphore(NULL, aInitial, aMax, aName);
|
|
if(handle)
|
|
{
|
|
internalData=new SemaphoreData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool Semaphore::Open(bool inheritable, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aInteritable=inheritable?TRUE:FALSE;
|
|
HANDLE handle=OpenSemaphore(SYNCHRONIZE, aInteritable, name.Buffer());
|
|
if(handle)
|
|
{
|
|
internalData=new SemaphoreData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool Semaphore::Release()
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
return Release(1)!=-1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
vint Semaphore::Release(vint count)
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
LONG previous=-1;
|
|
if(ReleaseSemaphore(internalData->handle, (LONG)count, &previous)!=0)
|
|
{
|
|
return (vint)previous;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/***********************************************************************
|
|
EventObject
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct EventData : public WaitableData
|
|
{
|
|
EventData(HANDLE _handle)
|
|
:WaitableData(_handle)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
EventObject::EventObject()
|
|
:internalData(0)
|
|
{
|
|
}
|
|
|
|
EventObject::~EventObject()
|
|
{
|
|
if(internalData)
|
|
{
|
|
CloseHandle(internalData->handle);
|
|
delete internalData;
|
|
}
|
|
}
|
|
|
|
bool EventObject::CreateAutoUnsignal(bool signaled, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aSignaled=signaled?TRUE:FALSE;
|
|
LPCTSTR aName=name==L""?NULL:name.Buffer();
|
|
HANDLE handle=CreateEvent(NULL, FALSE, aSignaled, aName);
|
|
if(handle)
|
|
{
|
|
internalData=new EventData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool EventObject::CreateManualUnsignal(bool signaled, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aSignaled=signaled?TRUE:FALSE;
|
|
LPCTSTR aName=name==L""?NULL:name.Buffer();
|
|
HANDLE handle=CreateEvent(NULL, TRUE, aSignaled, aName);
|
|
if(handle)
|
|
{
|
|
internalData=new EventData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool EventObject::Open(bool inheritable, const WString& name)
|
|
{
|
|
if(IsCreated())return false;
|
|
BOOL aInteritable=inheritable?TRUE:FALSE;
|
|
HANDLE handle=OpenEvent(SYNCHRONIZE, aInteritable, name.Buffer());
|
|
if(handle)
|
|
{
|
|
internalData=new EventData(handle);
|
|
SetData(internalData);
|
|
}
|
|
return IsCreated();
|
|
}
|
|
|
|
bool EventObject::Signal()
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
return SetEvent(internalData->handle)!=0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool EventObject::Unsignal()
|
|
{
|
|
if(IsCreated())
|
|
{
|
|
return ResetEvent(internalData->handle)!=0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/***********************************************************************
|
|
ThreadPoolLite
|
|
***********************************************************************/
|
|
|
|
DWORD WINAPI ThreadPoolQueueFunc(void* argument)
|
|
{
|
|
auto proc=Ptr((Func<void()>*)argument);
|
|
ThreadLocalStorage::FixStorages();
|
|
try
|
|
{
|
|
(*proc.Obj())();
|
|
ThreadLocalStorage::ClearStorages();
|
|
}
|
|
catch (...)
|
|
{
|
|
ThreadLocalStorage::ClearStorages();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ThreadPoolLite::ThreadPoolLite()
|
|
{
|
|
}
|
|
|
|
ThreadPoolLite::~ThreadPoolLite()
|
|
{
|
|
}
|
|
|
|
bool ThreadPoolLite::Queue(void(*proc)(void*), void* argument)
|
|
{
|
|
return Queue([=]() {proc(argument); });
|
|
}
|
|
|
|
bool ThreadPoolLite::Queue(const Func<void()>& proc)
|
|
{
|
|
Func<void()>* p=new Func<void()>(proc);
|
|
if(QueueUserWorkItem(&ThreadPoolQueueFunc, p, WT_EXECUTEDEFAULT))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
delete p;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
CriticalSection
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct CriticalSectionData
|
|
{
|
|
CRITICAL_SECTION criticalSection;
|
|
};
|
|
}
|
|
|
|
CriticalSection::Scope::Scope(CriticalSection& _criticalSection)
|
|
:criticalSection(&_criticalSection)
|
|
{
|
|
criticalSection->Enter();
|
|
}
|
|
|
|
CriticalSection::Scope::~Scope()
|
|
{
|
|
criticalSection->Leave();
|
|
}
|
|
|
|
CriticalSection::CriticalSection()
|
|
{
|
|
internalData=new CriticalSectionData;
|
|
InitializeCriticalSection(&internalData->criticalSection);
|
|
}
|
|
|
|
CriticalSection::~CriticalSection()
|
|
{
|
|
DeleteCriticalSection(&internalData->criticalSection);
|
|
delete internalData;
|
|
}
|
|
|
|
bool CriticalSection::TryEnter()
|
|
{
|
|
return TryEnterCriticalSection(&internalData->criticalSection)!=0;
|
|
}
|
|
|
|
void CriticalSection::Enter()
|
|
{
|
|
EnterCriticalSection(&internalData->criticalSection);
|
|
}
|
|
|
|
void CriticalSection::Leave()
|
|
{
|
|
LeaveCriticalSection(&internalData->criticalSection);
|
|
}
|
|
|
|
/***********************************************************************
|
|
ReaderWriterLock
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct ReaderWriterLockData
|
|
{
|
|
SRWLOCK lock;
|
|
};
|
|
}
|
|
|
|
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->LeaveWriter();
|
|
}
|
|
|
|
ReaderWriterLock::ReaderWriterLock()
|
|
:internalData(new threading_internal::ReaderWriterLockData)
|
|
{
|
|
InitializeSRWLock(&internalData->lock);
|
|
}
|
|
|
|
ReaderWriterLock::~ReaderWriterLock()
|
|
{
|
|
delete internalData;
|
|
}
|
|
|
|
bool ReaderWriterLock::TryEnterReader()
|
|
{
|
|
return TryAcquireSRWLockShared(&internalData->lock)!=0;
|
|
}
|
|
|
|
void ReaderWriterLock::EnterReader()
|
|
{
|
|
AcquireSRWLockShared(&internalData->lock);
|
|
}
|
|
|
|
void ReaderWriterLock::LeaveReader()
|
|
{
|
|
ReleaseSRWLockShared(&internalData->lock);
|
|
}
|
|
|
|
bool ReaderWriterLock::TryEnterWriter()
|
|
{
|
|
return TryAcquireSRWLockExclusive(&internalData->lock)!=0;
|
|
}
|
|
|
|
void ReaderWriterLock::EnterWriter()
|
|
{
|
|
AcquireSRWLockExclusive(&internalData->lock);
|
|
}
|
|
|
|
void ReaderWriterLock::LeaveWriter()
|
|
{
|
|
ReleaseSRWLockExclusive(&internalData->lock);
|
|
}
|
|
|
|
/***********************************************************************
|
|
ConditionVariable
|
|
***********************************************************************/
|
|
|
|
namespace threading_internal
|
|
{
|
|
struct ConditionVariableData
|
|
{
|
|
CONDITION_VARIABLE variable;
|
|
};
|
|
}
|
|
|
|
ConditionVariable::ConditionVariable()
|
|
:internalData(new threading_internal::ConditionVariableData)
|
|
{
|
|
InitializeConditionVariable(&internalData->variable);
|
|
}
|
|
|
|
ConditionVariable::~ConditionVariable()
|
|
{
|
|
delete internalData;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWith(CriticalSection& cs)
|
|
{
|
|
return SleepConditionVariableCS(&internalData->variable, &cs.internalData->criticalSection, INFINITE)!=0;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWithForTime(CriticalSection& cs, vint ms)
|
|
{
|
|
return SleepConditionVariableCS(&internalData->variable, &cs.internalData->criticalSection, (DWORD)ms)!=0;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWithReader(ReaderWriterLock& lock)
|
|
{
|
|
return SleepConditionVariableSRW(&internalData->variable, &lock.internalData->lock, INFINITE, CONDITION_VARIABLE_LOCKMODE_SHARED)!=0;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWithReaderForTime(ReaderWriterLock& lock, vint ms)
|
|
{
|
|
return SleepConditionVariableSRW(&internalData->variable, &lock.internalData->lock, (DWORD)ms, CONDITION_VARIABLE_LOCKMODE_SHARED)!=0;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWithWriter(ReaderWriterLock& lock)
|
|
{
|
|
return SleepConditionVariableSRW(&internalData->variable, &lock.internalData->lock, INFINITE, 0)!=0;
|
|
}
|
|
|
|
bool ConditionVariable::SleepWithWriterForTime(ReaderWriterLock& lock, vint ms)
|
|
{
|
|
return SleepConditionVariableSRW(&internalData->variable, &lock.internalData->lock, (DWORD)ms, 0)!=0;
|
|
}
|
|
|
|
void ConditionVariable::WakeOnePending()
|
|
{
|
|
WakeConditionVariable(&internalData->variable);
|
|
}
|
|
|
|
void ConditionVariable::WakeAllPendings()
|
|
{
|
|
WakeAllConditionVariable(&internalData->variable);
|
|
}
|
|
|
|
/***********************************************************************
|
|
ThreadLocalStorage
|
|
***********************************************************************/
|
|
|
|
#define KEY ((DWORD&)key)
|
|
|
|
ThreadLocalStorage::ThreadLocalStorage(Destructor _destructor)
|
|
:destructor(_destructor)
|
|
{
|
|
static_assert(sizeof(key) >= sizeof(DWORD), "ThreadLocalStorage's key storage is not large enouth.");
|
|
PushStorage(this);
|
|
KEY = TlsAlloc();
|
|
CHECK_ERROR(KEY != TLS_OUT_OF_INDEXES, L"vl::ThreadLocalStorage::ThreadLocalStorage()#Failed to alloc new thread local storage index.");
|
|
}
|
|
|
|
ThreadLocalStorage::~ThreadLocalStorage()
|
|
{
|
|
TlsFree(KEY);
|
|
}
|
|
|
|
void* ThreadLocalStorage::Get()
|
|
{
|
|
CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Get()#Cannot access a disposed ThreadLocalStorage.");
|
|
return TlsGetValue(KEY);
|
|
}
|
|
|
|
void ThreadLocalStorage::Set(void* data)
|
|
{
|
|
CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Set()#Cannot access a disposed ThreadLocalStorage.");
|
|
TlsSetValue(KEY, data);
|
|
}
|
|
|
|
#undef KEY
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\CHARFORMAT\CHARFORMAT.WINDOWS.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#define _WINSOCKAPI_
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
bool IsMbcsLeadByte(char c)
|
|
{
|
|
return IsDBCSLeadByte(c);
|
|
}
|
|
|
|
void MbcsToWChar(wchar_t* wideBuffer, vint wideChars, vint wideReaded, char* mbcsBuffer, vint mbcsChars)
|
|
{
|
|
MultiByteToWideChar(CP_THREAD_ACP, 0, mbcsBuffer, (int)mbcsChars, wideBuffer, (int)wideChars);
|
|
}
|
|
|
|
/***********************************************************************
|
|
MbcsEncoder
|
|
***********************************************************************/
|
|
|
|
vint MbcsEncoder::WriteString(wchar_t* _buffer, vint chars)
|
|
{
|
|
vint length = WideCharToMultiByte(CP_THREAD_ACP, 0, _buffer, (int)chars, NULL, NULL, NULL, NULL);
|
|
char* mbcs = new char[length];
|
|
WideCharToMultiByte(CP_THREAD_ACP, 0, _buffer, (int)chars, mbcs, (int)length, NULL, NULL);
|
|
vint result = stream->Write(mbcs, length);
|
|
delete[] mbcs;
|
|
|
|
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);
|
|
|
|
template<vint Count>
|
|
bool GetEncodingResult(int(&tests)[Count], bool(&results)[Count], int test)
|
|
{
|
|
for (vint i = 0; i < Count; i++)
|
|
{
|
|
if (tests[i] & test)
|
|
{
|
|
if (results[i]) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/***********************************************************************
|
|
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
|
|
)
|
|
{
|
|
int tests[] =
|
|
{
|
|
IS_TEXT_UNICODE_REVERSE_ASCII16,
|
|
IS_TEXT_UNICODE_REVERSE_STATISTICS,
|
|
IS_TEXT_UNICODE_REVERSE_CONTROLS,
|
|
|
|
IS_TEXT_UNICODE_ASCII16,
|
|
IS_TEXT_UNICODE_STATISTICS,
|
|
IS_TEXT_UNICODE_CONTROLS,
|
|
|
|
IS_TEXT_UNICODE_ILLEGAL_CHARS,
|
|
IS_TEXT_UNICODE_ODD_LENGTH,
|
|
IS_TEXT_UNICODE_NULL_BYTES,
|
|
};
|
|
|
|
const vint TestCount = sizeof(tests) / sizeof(*tests);
|
|
bool results[TestCount];
|
|
for (vint i = 0; i < TestCount; i++)
|
|
{
|
|
int test = tests[i];
|
|
results[i] = IsTextUnicode(buffer, (int)size, &test) != 0;
|
|
}
|
|
|
|
if (size % 2 == 0
|
|
&& !GetEncodingResult(tests, results, IS_TEXT_UNICODE_REVERSE_ASCII16)
|
|
&& !GetEncodingResult(tests, results, IS_TEXT_UNICODE_REVERSE_STATISTICS)
|
|
&& !GetEncodingResult(tests, results, IS_TEXT_UNICODE_REVERSE_CONTROLS)
|
|
)
|
|
{
|
|
for (vint i = 0; i < size; i += 2)
|
|
{
|
|
unsigned char c = buffer[i];
|
|
buffer[i] = buffer[i + 1];
|
|
buffer[i + 1] = c;
|
|
}
|
|
// 3 = (count of reverse group) = (count of unicode group)
|
|
for (vint i = 0; i < 3; i++)
|
|
{
|
|
int test = tests[i + 3];
|
|
results[i] = IsTextUnicode(buffer, (int)size, &test) != 0;
|
|
}
|
|
for (vint i = 0; i < size; i += 2)
|
|
{
|
|
unsigned char c = buffer[i];
|
|
buffer[i] = buffer[i + 1];
|
|
buffer[i + 1] = c;
|
|
}
|
|
}
|
|
|
|
if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_NOT_UNICODE_MASK))
|
|
{
|
|
if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_NOT_ASCII_MASK))
|
|
{
|
|
encoding = BomEncoder::Utf8;
|
|
}
|
|
else if (roughUtf8 || !roughMbcs)
|
|
{
|
|
encoding = BomEncoder::Utf8;
|
|
}
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_ASCII16))
|
|
{
|
|
encoding = BomEncoder::Utf16;
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_REVERSE_ASCII16))
|
|
{
|
|
encoding = BomEncoder::Utf16BE;
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_CONTROLS))
|
|
{
|
|
encoding = BomEncoder::Utf16;
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_REVERSE_CONTROLS))
|
|
{
|
|
encoding = BomEncoder::Utf16BE;
|
|
}
|
|
else
|
|
{
|
|
if (!roughUtf8)
|
|
{
|
|
if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_STATISTICS))
|
|
{
|
|
encoding = BomEncoder::Utf16;
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_STATISTICS))
|
|
{
|
|
encoding = BomEncoder::Utf16BE;
|
|
}
|
|
}
|
|
else if (GetEncodingResult(tests, results, IS_TEXT_UNICODE_NOT_UNICODE_MASK))
|
|
{
|
|
encoding = BomEncoder::Utf8;
|
|
}
|
|
else if (roughUtf8 || !roughMbcs)
|
|
{
|
|
encoding = BomEncoder::Utf8;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
#pragma comment(lib, "Ws2_32.lib")
|
|
|
|
namespace vl::inter_process::async_tcp_socket::windows_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
class IocpOperation;
|
|
class IocpRuntime;
|
|
class ConnectionState;
|
|
class AsyncSocketConnection;
|
|
|
|
struct NativeOverlapped
|
|
{
|
|
OVERLAPPED overlapped;
|
|
IocpOperation* operation = nullptr;
|
|
};
|
|
|
|
struct CallbackFrame
|
|
{
|
|
ConnectionState* connection = nullptr;
|
|
CallbackFrame* previous = nullptr;
|
|
};
|
|
|
|
static thread_local IocpRuntime* currentCompletionRuntime = nullptr;
|
|
static thread_local IocpRuntime* currentCallbackRuntime = nullptr;
|
|
static thread_local CallbackFrame* currentCallbackFrame = nullptr;
|
|
|
|
WString SocketErrorMessage(const wchar_t* operation, DWORD error)
|
|
{
|
|
return WString::Unmanaged(operation) + L" failed with Windows error " + itow((vint)error) + L".";
|
|
}
|
|
|
|
/***********************************************************************
|
|
IocpOperation
|
|
***********************************************************************/
|
|
|
|
class IocpOperation
|
|
{
|
|
public:
|
|
NativeOverlapped native;
|
|
|
|
IocpOperation()
|
|
{
|
|
ZeroMemory(&native.overlapped, sizeof(native.overlapped));
|
|
native.operation = this;
|
|
}
|
|
|
|
virtual ~IocpOperation() = default;
|
|
virtual bool Complete(DWORD bytes, DWORD error) = 0;
|
|
virtual void EndPending() = 0;
|
|
};
|
|
|
|
/***********************************************************************
|
|
IocpRuntime
|
|
***********************************************************************/
|
|
|
|
class IocpRuntime : public Object
|
|
{
|
|
private:
|
|
class CompletionWorker : public Thread
|
|
{
|
|
private:
|
|
IocpRuntime* runtime = nullptr;
|
|
protected:
|
|
void Run() override
|
|
{
|
|
currentCompletionRuntime = runtime;
|
|
while (true)
|
|
{
|
|
DWORD bytes = 0;
|
|
ULONG_PTR key = 0;
|
|
OVERLAPPED* overlapped = nullptr;
|
|
auto succeeded = GetQueuedCompletionStatus(runtime->iocp, &bytes, &key, &overlapped, INFINITE);
|
|
if (!overlapped)
|
|
{
|
|
break;
|
|
}
|
|
|
|
auto native = CONTAINING_RECORD(overlapped, NativeOverlapped, overlapped);
|
|
auto operation = native->operation;
|
|
auto error = succeeded ? ERROR_SUCCESS : GetLastError();
|
|
bool completed = true;
|
|
try
|
|
{
|
|
completed = operation->Complete(bytes, error);
|
|
}
|
|
catch (...)
|
|
{
|
|
completed = true;
|
|
}
|
|
if (completed)
|
|
{
|
|
operation->EndPending();
|
|
delete operation;
|
|
}
|
|
}
|
|
currentCompletionRuntime = nullptr;
|
|
}
|
|
|
|
public:
|
|
CompletionWorker(IocpRuntime* _runtime)
|
|
: runtime(_runtime)
|
|
{
|
|
}
|
|
};
|
|
|
|
class CallbackWorker : public Thread
|
|
{
|
|
private:
|
|
IocpRuntime* runtime = nullptr;
|
|
protected:
|
|
void Run() override
|
|
{
|
|
currentCallbackRuntime = runtime;
|
|
runtime->callbackQueue.RunTaskQueue();
|
|
currentCallbackRuntime = nullptr;
|
|
}
|
|
|
|
public:
|
|
CallbackWorker(IocpRuntime* _runtime)
|
|
: runtime(_runtime)
|
|
{
|
|
}
|
|
};
|
|
|
|
HANDLE iocp = nullptr;
|
|
TaskQueue callbackQueue;
|
|
CompletionWorker* completionWorker = nullptr;
|
|
CallbackWorker* callbackWorker = nullptr;
|
|
SpinLock lockStop;
|
|
bool stopRequested = false;
|
|
bool finalizing = false;
|
|
bool finalized = false;
|
|
EventObject eventFinalized;
|
|
bool winsockStarted = false;
|
|
|
|
public:
|
|
IocpRuntime()
|
|
{
|
|
CHECK_ERROR(eventFinalized.CreateManualUnsignal(false), L"IAsyncSocket failed to create its runtime drain event.");
|
|
bool completionStarted = false;
|
|
bool callbackStarted = false;
|
|
try
|
|
{
|
|
WSADATA data;
|
|
auto error = WSAStartup(MAKEWORD(2, 2), &data);
|
|
CHECK_ERROR(error == 0, L"IAsyncSocket failed to initialize Winsock.");
|
|
winsockStarted = true;
|
|
|
|
iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1);
|
|
CHECK_ERROR(iocp != nullptr, L"IAsyncSocket failed to create an IO completion port.");
|
|
|
|
completionWorker = new CompletionWorker(this);
|
|
callbackWorker = new CallbackWorker(this);
|
|
completionStarted = completionWorker->Start();
|
|
CHECK_ERROR(completionStarted, L"IAsyncSocket failed to start its completion worker.");
|
|
callbackStarted = callbackWorker->Start();
|
|
CHECK_ERROR(callbackStarted, L"IAsyncSocket failed to start its callback worker.");
|
|
}
|
|
catch (...)
|
|
{
|
|
if (callbackStarted)
|
|
{
|
|
callbackQueue.QueueExitTask();
|
|
callbackWorker->Wait();
|
|
}
|
|
if (completionStarted)
|
|
{
|
|
PostQueuedCompletionStatus(iocp, 0, 0, nullptr);
|
|
completionWorker->Wait();
|
|
}
|
|
delete callbackWorker;
|
|
delete completionWorker;
|
|
if (iocp)
|
|
{
|
|
CloseHandle(iocp);
|
|
}
|
|
if (winsockStarted)
|
|
{
|
|
WSACleanup();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
~IocpRuntime()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
bool Associate(SOCKET socket)
|
|
{
|
|
return CreateIoCompletionPort((HANDLE)socket, iocp, 0, 0) == iocp;
|
|
}
|
|
|
|
void QueueCallback(Func<void()> callback)
|
|
{
|
|
callbackQueue.QueueTask(callback);
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
bool requestExit = false;
|
|
bool finalizeHere = false;
|
|
bool waitForFinalization = false;
|
|
auto selfWorker = currentCallbackRuntime == this || currentCompletionRuntime == this;
|
|
SPIN_LOCK(lockStop)
|
|
{
|
|
if (finalized)
|
|
{
|
|
return;
|
|
}
|
|
if (!stopRequested)
|
|
{
|
|
stopRequested = true;
|
|
requestExit = true;
|
|
}
|
|
if (!selfWorker)
|
|
{
|
|
if (!finalizing)
|
|
{
|
|
finalizing = true;
|
|
finalizeHere = true;
|
|
}
|
|
else
|
|
{
|
|
waitForFinalization = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (requestExit)
|
|
{
|
|
callbackQueue.QueueExitTask();
|
|
PostQueuedCompletionStatus(iocp, 0, 0, nullptr);
|
|
}
|
|
if (selfWorker)
|
|
{
|
|
return;
|
|
}
|
|
if (waitForFinalization)
|
|
{
|
|
eventFinalized.Wait();
|
|
return;
|
|
}
|
|
if (!finalizeHere)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (callbackWorker)
|
|
{
|
|
callbackWorker->Wait();
|
|
}
|
|
if (completionWorker)
|
|
{
|
|
completionWorker->Wait();
|
|
}
|
|
|
|
delete callbackWorker;
|
|
callbackWorker = nullptr;
|
|
delete completionWorker;
|
|
completionWorker = nullptr;
|
|
if (iocp)
|
|
{
|
|
CloseHandle(iocp);
|
|
iocp = nullptr;
|
|
}
|
|
if (winsockStarted)
|
|
{
|
|
WSACleanup();
|
|
winsockStarted = false;
|
|
}
|
|
SPIN_LOCK(lockStop)
|
|
{
|
|
finalized = true;
|
|
}
|
|
eventFinalized.Signal();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ReadBlock
|
|
***********************************************************************/
|
|
|
|
class ReadBlock : public Object
|
|
{
|
|
public:
|
|
Array<vuint8_t> data;
|
|
|
|
ReadBlock()
|
|
{
|
|
data.Resize(65536);
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState
|
|
***********************************************************************/
|
|
|
|
class ConnectionState : public Object
|
|
{
|
|
friend class AsyncSocketConnection;
|
|
private:
|
|
class ReadOperation;
|
|
class WriteOperation;
|
|
class ConnectOperation;
|
|
|
|
IocpRuntime* runtime = nullptr;
|
|
AsyncSocketConnection* owner = nullptr;
|
|
|
|
// covers every field below, pending counts, and their events
|
|
CriticalSection lockState;
|
|
SOCKET socket = INVALID_SOCKET;
|
|
IAsyncSocketCallback* callback = nullptr;
|
|
bool connected = false;
|
|
bool stopping = false;
|
|
bool stopped = false;
|
|
bool reading = false;
|
|
bool readPending = false;
|
|
bool writePending = false;
|
|
bool terminalPending = false;
|
|
bool disconnectedNotified = false;
|
|
vint pendingIo = 0;
|
|
vint activeCallbacks = 0;
|
|
EventObject eventIoDrained;
|
|
EventObject eventCallbacksDrained;
|
|
|
|
bool clientMode = false;
|
|
vint clientPort = 0;
|
|
ClientStatus clientStatus = ClientStatus::Ready;
|
|
EventObject eventWaitForServer;
|
|
PTP_TIMER clientRetryTimer = nullptr;
|
|
vint clientAttempts = 0;
|
|
vint clientGeneration = 0;
|
|
|
|
void BeginPendingLocked()
|
|
{
|
|
if (pendingIo++ == 0)
|
|
{
|
|
eventIoDrained.Unsignal();
|
|
}
|
|
}
|
|
|
|
void EndPendingLocked()
|
|
{
|
|
if (--pendingIo == 0)
|
|
{
|
|
eventIoDrained.Signal();
|
|
}
|
|
}
|
|
|
|
void EndPending()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
EndPendingLocked();
|
|
}
|
|
}
|
|
|
|
IAsyncSocketCallback* BeginCallback(bool terminal)
|
|
{
|
|
IAsyncSocketCallback* result = nullptr;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (callback && (terminal || (!stopping && !terminalPending)))
|
|
{
|
|
result = callback;
|
|
if (activeCallbacks++ == 0)
|
|
{
|
|
eventCallbacksDrained.Unsignal();
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void EndCallback()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (--activeCallbacks == 0)
|
|
{
|
|
eventCallbacksDrained.Signal();
|
|
}
|
|
}
|
|
}
|
|
|
|
template<typename TCallback>
|
|
bool InvokeCallback(bool terminal, TCallback&& invoke)
|
|
{
|
|
auto installed = BeginCallback(terminal);
|
|
if (!installed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CallbackFrame frame;
|
|
frame.connection = this;
|
|
frame.previous = currentCallbackFrame;
|
|
currentCallbackFrame = &frame;
|
|
try
|
|
{
|
|
invoke(installed);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
currentCallbackFrame = frame.previous;
|
|
EndCallback();
|
|
return true;
|
|
}
|
|
|
|
bool IsCurrentCallback()
|
|
{
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->connection == this)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PostRead(Ptr<ConnectionState> retainedState);
|
|
Ptr<ConnectionState> Retain();
|
|
void DeliverRead(Ptr<ConnectionState> retainedState, Ptr<ReadBlock> block, vint bytes);
|
|
void DeliverWrite(Ptr<AsyncSocketBuffer> buffer);
|
|
void QueueTerminal(DWORD error, bool reportError);
|
|
void DeliverTerminal(DWORD error, bool reportError);
|
|
void StartConnectAttempt();
|
|
void CompleteConnect(SOCKET operationSocket, vint generation, DWORD error);
|
|
void QueueConnectFailure(DWORD error);
|
|
void DeliverConnectFailure(DWORD error, bool fatal);
|
|
void DeliverConnected();
|
|
void ScheduleRetry();
|
|
static VOID CALLBACK RetryTimerCallback(PTP_CALLBACK_INSTANCE, PVOID context, PTP_TIMER)
|
|
{
|
|
auto self = (ConnectionState*)context;
|
|
self->StartConnectAttempt();
|
|
}
|
|
|
|
public:
|
|
ConnectionState(IocpRuntime* _runtime, bool _clientMode, vint _clientPort)
|
|
: runtime(_runtime)
|
|
, clientMode(_clientMode)
|
|
, clientPort(_clientPort)
|
|
{
|
|
CHECK_ERROR(eventIoDrained.CreateManualUnsignal(true), L"IAsyncSocket failed to create its I/O drain event.");
|
|
CHECK_ERROR(eventCallbacksDrained.CreateManualUnsignal(true), L"IAsyncSocket failed to create its callback drain event.");
|
|
CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), L"IAsyncSocket failed to create its client wait event.");
|
|
if (clientMode)
|
|
{
|
|
clientRetryTimer = CreateThreadpoolTimer(&RetryTimerCallback, this, nullptr);
|
|
CHECK_ERROR(clientRetryTimer != nullptr, L"IAsyncSocket failed to create its retry timer.");
|
|
}
|
|
}
|
|
|
|
ConnectionState(IocpRuntime* _runtime, SOCKET _socket)
|
|
: runtime(_runtime)
|
|
, socket(_socket)
|
|
, connected(true)
|
|
{
|
|
CHECK_ERROR(eventIoDrained.CreateManualUnsignal(true), L"IAsyncSocket failed to create its I/O drain event.");
|
|
CHECK_ERROR(eventCallbacksDrained.CreateManualUnsignal(true), L"IAsyncSocket failed to create its callback drain event.");
|
|
CHECK_ERROR(eventWaitForServer.CreateManualUnsignal(false), L"IAsyncSocket failed to create its client wait event.");
|
|
}
|
|
|
|
~ConnectionState()
|
|
{
|
|
if (clientRetryTimer)
|
|
{
|
|
SetThreadpoolTimer(clientRetryTimer, nullptr, 0, 0);
|
|
WaitForThreadpoolTimerCallbacks(clientRetryTimer, TRUE);
|
|
CloseThreadpoolTimer(clientRetryTimer);
|
|
clientRetryTimer = nullptr;
|
|
}
|
|
}
|
|
|
|
void CloseRetryTimer()
|
|
{
|
|
PTP_TIMER timer = nullptr;
|
|
CS_LOCK(lockState)
|
|
{
|
|
timer = clientRetryTimer;
|
|
clientRetryTimer = nullptr;
|
|
}
|
|
if (timer)
|
|
{
|
|
SetThreadpoolTimer(timer, nullptr, 0, 0);
|
|
WaitForThreadpoolTimerCallbacks(timer, TRUE);
|
|
CloseThreadpoolTimer(timer);
|
|
}
|
|
}
|
|
|
|
void InstallCallback(IAsyncSocketCallback* value);
|
|
void BeginReading();
|
|
void Write(Ptr<AsyncSocketBuffer> buffer);
|
|
void Stop();
|
|
void WaitForServer();
|
|
ClientStatus GetStatus();
|
|
};
|
|
|
|
/***********************************************************************
|
|
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->owner = nullptr;
|
|
}
|
|
|
|
Ptr<ConnectionState> GetState()
|
|
{
|
|
return state;
|
|
}
|
|
|
|
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();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState::ReadOperation
|
|
***********************************************************************/
|
|
|
|
class ConnectionState::ReadOperation : public IocpOperation
|
|
{
|
|
public:
|
|
Ptr<ConnectionState> connection;
|
|
Ptr<ReadBlock> block;
|
|
|
|
ReadOperation(Ptr<ConnectionState> _connection)
|
|
: connection(_connection)
|
|
, block(Ptr(new ReadBlock))
|
|
{
|
|
}
|
|
|
|
bool Complete(DWORD bytes, DWORD error) override
|
|
{
|
|
bool cancelled = false;
|
|
CS_LOCK(connection->lockState)
|
|
{
|
|
connection->readPending = false;
|
|
cancelled = connection->stopping;
|
|
}
|
|
if (cancelled)
|
|
{
|
|
return true;
|
|
}
|
|
if (error != ERROR_SUCCESS)
|
|
{
|
|
connection->QueueTerminal(error, true);
|
|
}
|
|
else if (bytes == 0)
|
|
{
|
|
connection->QueueTerminal(ERROR_SUCCESS, false);
|
|
}
|
|
else
|
|
{
|
|
auto state = connection;
|
|
auto retainedBlock = block;
|
|
connection->runtime->QueueCallback(Func<void()>([state, retainedBlock, bytes]()
|
|
{
|
|
state->DeliverRead(state, retainedBlock, (vint)bytes);
|
|
}));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void EndPending() override
|
|
{
|
|
connection->EndPending();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState::WriteOperation
|
|
***********************************************************************/
|
|
|
|
class ConnectionState::WriteOperation : public IocpOperation
|
|
{
|
|
public:
|
|
Ptr<ConnectionState> connection;
|
|
Ptr<AsyncSocketBuffer> buffer;
|
|
vint offset = 0;
|
|
|
|
WriteOperation(Ptr<ConnectionState> _connection, Ptr<AsyncSocketBuffer> _buffer)
|
|
: connection(_connection)
|
|
, buffer(_buffer)
|
|
{
|
|
}
|
|
|
|
DWORD PostLocked()
|
|
{
|
|
WSABUF nativeBuffer;
|
|
nativeBuffer.buf = (CHAR*)&buffer->data[offset];
|
|
nativeBuffer.len = (ULONG)(buffer->data.Count() - offset);
|
|
DWORD sent = 0;
|
|
auto result = WSASend(connection->socket, &nativeBuffer, 1, &sent, 0, &native.overlapped, nullptr);
|
|
if (result == 0)
|
|
{
|
|
return ERROR_SUCCESS;
|
|
}
|
|
auto error = WSAGetLastError();
|
|
return error == WSA_IO_PENDING ? ERROR_SUCCESS : (DWORD)error;
|
|
}
|
|
|
|
bool Complete(DWORD bytes, DWORD error) override
|
|
{
|
|
bool queueCompleted = false;
|
|
DWORD terminalError = ERROR_SUCCESS;
|
|
connection->lockState.Enter();
|
|
if (connection->stopping)
|
|
{
|
|
connection->lockState.Leave();
|
|
return true;
|
|
}
|
|
if (error != ERROR_SUCCESS || bytes == 0)
|
|
{
|
|
terminalError = error == ERROR_SUCCESS ? WSAECONNRESET : error;
|
|
connection->lockState.Leave();
|
|
connection->QueueTerminal(terminalError, true);
|
|
return true;
|
|
}
|
|
|
|
offset += (vint)bytes;
|
|
if (offset < buffer->data.Count())
|
|
{
|
|
ZeroMemory(&native.overlapped, sizeof(native.overlapped));
|
|
auto postError = PostLocked();
|
|
connection->lockState.Leave();
|
|
if (postError == ERROR_SUCCESS)
|
|
{
|
|
return false;
|
|
}
|
|
connection->QueueTerminal(postError, true);
|
|
return true;
|
|
}
|
|
queueCompleted = true;
|
|
connection->lockState.Leave();
|
|
|
|
if (queueCompleted)
|
|
{
|
|
auto state = connection;
|
|
auto retainedBuffer = buffer;
|
|
connection->runtime->QueueCallback(Func<void()>([state, retainedBuffer]()
|
|
{
|
|
state->DeliverWrite(retainedBuffer);
|
|
}));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void EndPending() override
|
|
{
|
|
connection->EndPending();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState::ConnectOperation
|
|
***********************************************************************/
|
|
|
|
class ConnectionState::ConnectOperation : public IocpOperation
|
|
{
|
|
public:
|
|
Ptr<ConnectionState> connection;
|
|
SOCKET operationSocket = INVALID_SOCKET;
|
|
vint generation = 0;
|
|
|
|
ConnectOperation(Ptr<ConnectionState> _connection, SOCKET _operationSocket, vint _generation)
|
|
: connection(_connection)
|
|
, operationSocket(_operationSocket)
|
|
, generation(_generation)
|
|
{
|
|
}
|
|
|
|
bool Complete(DWORD, DWORD error) override
|
|
{
|
|
connection->CompleteConnect(operationSocket, generation, error);
|
|
return true;
|
|
}
|
|
|
|
void EndPending() override
|
|
{
|
|
connection->EndPending();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
ConnectionState
|
|
***********************************************************************/
|
|
|
|
Ptr<ConnectionState> ConnectionState::Retain()
|
|
{
|
|
CHECK_ERROR(owner != nullptr, L"IAsyncSocketConnection lost its canonical state owner.");
|
|
return owner->GetState();
|
|
}
|
|
|
|
void ConnectionState::InstallCallback(IAsyncSocketCallback* value)
|
|
{
|
|
if (!value)
|
|
{
|
|
bool selfCallback = IsCurrentCallback();
|
|
CS_LOCK(lockState)
|
|
{
|
|
callback = nullptr;
|
|
}
|
|
if (!selfCallback)
|
|
{
|
|
eventCallbacksDrained.Wait();
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool canInstall = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
canInstall = callback == nullptr && !stopping;
|
|
if (canInstall)
|
|
{
|
|
callback = value;
|
|
if (activeCallbacks++ == 0)
|
|
{
|
|
eventCallbacksDrained.Unsignal();
|
|
}
|
|
}
|
|
}
|
|
CHECK_ERROR(canInstall, L"IAsyncSocketConnection::InstallCallback cannot replace a callback or install one on a stopped connection.");
|
|
|
|
CallbackFrame frame;
|
|
frame.connection = this;
|
|
frame.previous = currentCallbackFrame;
|
|
currentCallbackFrame = &frame;
|
|
try
|
|
{
|
|
value->OnInstalled(owner);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
currentCallbackFrame = frame.previous;
|
|
EndCallback();
|
|
}
|
|
|
|
void ConnectionState::BeginReading()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(connected && !stopping && !terminalPending, L"IAsyncSocketConnection::BeginReadingLoopUnsafe requires a connected connection.");
|
|
CHECK_ERROR(callback != nullptr, L"IAsyncSocketConnection::BeginReadingLoopUnsafe requires an installed callback.");
|
|
CHECK_ERROR(!reading, L"IAsyncSocketConnection::BeginReadingLoopUnsafe can only be called once.");
|
|
reading = true;
|
|
}
|
|
PostRead(Retain());
|
|
}
|
|
|
|
void ConnectionState::PostRead(Ptr<ConnectionState> retainedState)
|
|
{
|
|
auto operation = new ReadOperation(retainedState);
|
|
DWORD immediateError = ERROR_SUCCESS;
|
|
lockState.Enter();
|
|
if (!connected || stopping || terminalPending || !reading || readPending)
|
|
{
|
|
lockState.Leave();
|
|
delete operation;
|
|
return;
|
|
}
|
|
|
|
readPending = true;
|
|
BeginPendingLocked();
|
|
WSABUF buffer;
|
|
buffer.buf = (CHAR*)&operation->block->data[0];
|
|
buffer.len = (ULONG)operation->block->data.Count();
|
|
DWORD flags = 0;
|
|
DWORD received = 0;
|
|
auto result = WSARecv(socket, &buffer, 1, &received, &flags, &operation->native.overlapped, nullptr);
|
|
if (result == SOCKET_ERROR)
|
|
{
|
|
auto error = WSAGetLastError();
|
|
if (error != WSA_IO_PENDING)
|
|
{
|
|
immediateError = (DWORD)error;
|
|
readPending = false;
|
|
EndPendingLocked();
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (immediateError != ERROR_SUCCESS)
|
|
{
|
|
delete operation;
|
|
QueueTerminal(immediateError, true);
|
|
}
|
|
}
|
|
|
|
void ConnectionState::DeliverRead(Ptr<ConnectionState> retainedState, Ptr<ReadBlock> block, vint bytes)
|
|
{
|
|
auto invoked = InvokeCallback(false, [&](IAsyncSocketCallback* installed)
|
|
{
|
|
installed->OnRead(&block->data[0], bytes);
|
|
});
|
|
if (invoked)
|
|
{
|
|
PostRead(retainedState);
|
|
}
|
|
}
|
|
|
|
void ConnectionState::Write(Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
CHECK_ERROR(buffer, L"IAsyncSocketConnection::WriteAsync requires a buffer.");
|
|
bool empty = false;
|
|
bool canWrite = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
canWrite = connected && !stopping && !terminalPending && !writePending;
|
|
if (canWrite)
|
|
{
|
|
writePending = true;
|
|
empty = buffer->data.Count() == 0;
|
|
}
|
|
}
|
|
CHECK_ERROR(canWrite, L"IAsyncSocketConnection::WriteAsync requires a connected connection with no outstanding write.");
|
|
|
|
if (empty)
|
|
{
|
|
auto state = Retain();
|
|
runtime->QueueCallback(Func<void()>([state, buffer]()
|
|
{
|
|
state->DeliverWrite(buffer);
|
|
}));
|
|
return;
|
|
}
|
|
|
|
auto operation = new WriteOperation(Retain(), buffer);
|
|
DWORD immediateError = ERROR_SUCCESS;
|
|
lockState.Enter();
|
|
if (stopping || terminalPending)
|
|
{
|
|
writePending = false;
|
|
lockState.Leave();
|
|
delete operation;
|
|
return;
|
|
}
|
|
BeginPendingLocked();
|
|
immediateError = operation->PostLocked();
|
|
if (immediateError != ERROR_SUCCESS)
|
|
{
|
|
EndPendingLocked();
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (immediateError != ERROR_SUCCESS)
|
|
{
|
|
delete operation;
|
|
QueueTerminal(immediateError, true);
|
|
}
|
|
}
|
|
|
|
void ConnectionState::DeliverWrite(Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
bool deliver = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (writePending && !stopping && !terminalPending)
|
|
{
|
|
writePending = false;
|
|
deliver = true;
|
|
}
|
|
}
|
|
if (deliver)
|
|
{
|
|
InvokeCallback(false, [&](IAsyncSocketCallback* installed)
|
|
{
|
|
installed->OnWriteCompleted(buffer);
|
|
});
|
|
}
|
|
}
|
|
|
|
void ConnectionState::QueueTerminal(DWORD error, bool reportError)
|
|
{
|
|
bool queue = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping && !terminalPending)
|
|
{
|
|
terminalPending = true;
|
|
queue = true;
|
|
}
|
|
}
|
|
if (queue)
|
|
{
|
|
auto state = Retain();
|
|
runtime->QueueCallback(Func<void()>([state, error, reportError]()
|
|
{
|
|
state->DeliverTerminal(error, reportError);
|
|
}));
|
|
}
|
|
}
|
|
|
|
void ConnectionState::DeliverTerminal(DWORD error, bool reportError)
|
|
{
|
|
IAsyncSocketCallback* installed = nullptr;
|
|
bool claimed = false;
|
|
lockState.Enter();
|
|
if (!stopping && terminalPending)
|
|
{
|
|
claimed = true;
|
|
if (reportError && callback)
|
|
{
|
|
installed = callback;
|
|
if (activeCallbacks++ == 0)
|
|
{
|
|
eventCallbacksDrained.Unsignal();
|
|
}
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
if (!claimed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame{ this, currentCallbackFrame };
|
|
currentCallbackFrame = &frame;
|
|
try
|
|
{
|
|
installed->OnError(SocketErrorMessage(L"Asynchronous socket operation", error), true);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
currentCallbackFrame = frame.previous;
|
|
EndCallback();
|
|
}
|
|
Stop();
|
|
}
|
|
|
|
void ConnectionState::StartConnectAttempt()
|
|
{
|
|
auto retainedState = Retain();
|
|
SOCKET createdSocket = INVALID_SOCKET;
|
|
ConnectOperation* operation = nullptr;
|
|
DWORD immediateError = ERROR_SUCCESS;
|
|
|
|
lockState.Enter();
|
|
if (!clientMode || stopping || clientStatus != ClientStatus::WaitingForServer)
|
|
{
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
clientAttempts++;
|
|
|
|
createdSocket = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED);
|
|
if (createdSocket == INVALID_SOCKET)
|
|
{
|
|
immediateError = WSAGetLastError();
|
|
}
|
|
|
|
SOCKADDR_IN localAddress = {};
|
|
localAddress.sin_family = AF_INET;
|
|
localAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
localAddress.sin_port = 0;
|
|
if (immediateError == ERROR_SUCCESS && bind(createdSocket, (SOCKADDR*)&localAddress, sizeof(localAddress)) == SOCKET_ERROR)
|
|
{
|
|
immediateError = WSAGetLastError();
|
|
}
|
|
|
|
LPFN_CONNECTEX connectEx = nullptr;
|
|
GUID connectExGuid = WSAID_CONNECTEX;
|
|
DWORD transferred = 0;
|
|
if (immediateError == ERROR_SUCCESS && WSAIoctl(
|
|
createdSocket,
|
|
SIO_GET_EXTENSION_FUNCTION_POINTER,
|
|
&connectExGuid,
|
|
sizeof(connectExGuid),
|
|
&connectEx,
|
|
sizeof(connectEx),
|
|
&transferred,
|
|
nullptr,
|
|
nullptr
|
|
) == SOCKET_ERROR)
|
|
{
|
|
immediateError = WSAGetLastError();
|
|
}
|
|
if (immediateError == ERROR_SUCCESS && !runtime->Associate(createdSocket))
|
|
{
|
|
immediateError = GetLastError();
|
|
}
|
|
|
|
if (immediateError == ERROR_SUCCESS)
|
|
{
|
|
socket = createdSocket;
|
|
connected = false;
|
|
auto generation = ++clientGeneration;
|
|
operation = new ConnectOperation(retainedState, createdSocket, generation);
|
|
BeginPendingLocked();
|
|
|
|
SOCKADDR_IN remoteAddress = {};
|
|
remoteAddress.sin_family = AF_INET;
|
|
remoteAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
remoteAddress.sin_port = htons((u_short)clientPort);
|
|
auto result = connectEx(
|
|
createdSocket,
|
|
(SOCKADDR*)&remoteAddress,
|
|
sizeof(remoteAddress),
|
|
nullptr,
|
|
0,
|
|
nullptr,
|
|
&operation->native.overlapped
|
|
);
|
|
if (!result)
|
|
{
|
|
auto error = WSAGetLastError();
|
|
if (error != WSA_IO_PENDING)
|
|
{
|
|
immediateError = error;
|
|
socket = INVALID_SOCKET;
|
|
EndPendingLocked();
|
|
}
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (immediateError != ERROR_SUCCESS)
|
|
{
|
|
if (createdSocket != INVALID_SOCKET)
|
|
{
|
|
closesocket(createdSocket);
|
|
}
|
|
delete operation;
|
|
QueueConnectFailure(immediateError);
|
|
}
|
|
}
|
|
|
|
void ConnectionState::CompleteConnect(SOCKET operationSocket, vint generation, DWORD error)
|
|
{
|
|
if (error == ERROR_SUCCESS)
|
|
{
|
|
if (setsockopt(operationSocket, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0) == SOCKET_ERROR)
|
|
{
|
|
error = WSAGetLastError();
|
|
}
|
|
}
|
|
|
|
bool accepted = false;
|
|
bool failed = false;
|
|
lockState.Enter();
|
|
if (!stopping && clientStatus == ClientStatus::WaitingForServer && socket == operationSocket && clientGeneration == generation)
|
|
{
|
|
if (error == ERROR_SUCCESS)
|
|
{
|
|
connected = true;
|
|
clientStatus = ClientStatus::Connected;
|
|
accepted = true;
|
|
}
|
|
else
|
|
{
|
|
socket = INVALID_SOCKET;
|
|
failed = true;
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (accepted)
|
|
{
|
|
auto state = Retain();
|
|
runtime->QueueCallback(Func<void()>([state]()
|
|
{
|
|
state->DeliverConnected();
|
|
}));
|
|
}
|
|
else if (failed)
|
|
{
|
|
closesocket(operationSocket);
|
|
QueueConnectFailure(error);
|
|
}
|
|
}
|
|
|
|
void ConnectionState::DeliverConnected()
|
|
{
|
|
bool deliver = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
deliver = connected && !stopping && clientStatus == ClientStatus::Connected;
|
|
}
|
|
if (deliver)
|
|
{
|
|
InvokeCallback(false, [](IAsyncSocketCallback* installed)
|
|
{
|
|
installed->OnConnected();
|
|
});
|
|
}
|
|
eventWaitForServer.Signal();
|
|
}
|
|
|
|
void ConnectionState::QueueConnectFailure(DWORD error)
|
|
{
|
|
bool queue = false;
|
|
bool fatal = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping && clientStatus == ClientStatus::WaitingForServer)
|
|
{
|
|
queue = true;
|
|
fatal = clientAttempts >= AsyncSocketClientRetryCount;
|
|
}
|
|
}
|
|
if (queue)
|
|
{
|
|
auto state = Retain();
|
|
runtime->QueueCallback(Func<void()>([state, error, fatal]()
|
|
{
|
|
state->DeliverConnectFailure(error, fatal);
|
|
}));
|
|
}
|
|
}
|
|
|
|
void ConnectionState::DeliverConnectFailure(DWORD error, bool fatal)
|
|
{
|
|
bool deliver = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
deliver = !stopping && clientStatus == ClientStatus::WaitingForServer;
|
|
}
|
|
if (!deliver)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InvokeCallback(false, [&](IAsyncSocketCallback* installed)
|
|
{
|
|
installed->OnError(SocketErrorMessage(L"ConnectEx", error), fatal);
|
|
});
|
|
if (fatal)
|
|
{
|
|
Stop();
|
|
}
|
|
else
|
|
{
|
|
ScheduleRetry();
|
|
}
|
|
}
|
|
|
|
void ConnectionState::ScheduleRetry()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping && clientStatus == ClientStatus::WaitingForServer && clientRetryTimer)
|
|
{
|
|
LARGE_INTEGER dueTime;
|
|
dueTime.QuadPart = -(LONGLONG)AsyncSocketClientRetryDelay * 10000;
|
|
FILETIME fileTime;
|
|
fileTime.dwLowDateTime = dueTime.LowPart;
|
|
fileTime.dwHighDateTime = dueTime.HighPart;
|
|
SetThreadpoolTimer(clientRetryTimer, &fileTime, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConnectionState::WaitForServer()
|
|
{
|
|
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.");
|
|
StartConnectAttempt();
|
|
eventWaitForServer.Wait();
|
|
}
|
|
|
|
ClientStatus ConnectionState::GetStatus()
|
|
{
|
|
ClientStatus result;
|
|
CS_LOCK(lockState)
|
|
{
|
|
result = clientStatus;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ConnectionState::Stop()
|
|
{
|
|
SOCKET closingSocket = INVALID_SOCKET;
|
|
PTP_TIMER timer = nullptr;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping)
|
|
{
|
|
stopping = true;
|
|
connected = false;
|
|
reading = false;
|
|
writePending = false;
|
|
terminalPending = false;
|
|
closingSocket = socket;
|
|
socket = INVALID_SOCKET;
|
|
if (clientMode)
|
|
{
|
|
clientStatus = ClientStatus::Disconnected;
|
|
timer = clientRetryTimer;
|
|
}
|
|
}
|
|
else if (clientMode)
|
|
{
|
|
timer = clientRetryTimer;
|
|
}
|
|
}
|
|
|
|
if (timer)
|
|
{
|
|
SetThreadpoolTimer(timer, nullptr, 0, 0);
|
|
WaitForThreadpoolTimerCallbacks(timer, TRUE);
|
|
}
|
|
if (closingSocket != INVALID_SOCKET)
|
|
{
|
|
// Prefer an orderly FIN for the peer while closesocket cancels this
|
|
// connection's pending overlapped operations.
|
|
shutdown(closingSocket, SD_BOTH);
|
|
closesocket(closingSocket);
|
|
}
|
|
eventIoDrained.Wait();
|
|
|
|
auto selfCallback = IsCurrentCallback();
|
|
if (!selfCallback)
|
|
{
|
|
eventCallbacksDrained.Wait();
|
|
}
|
|
|
|
IAsyncSocketCallback* installed = nullptr;
|
|
lockState.Enter();
|
|
if (!disconnectedNotified)
|
|
{
|
|
disconnectedNotified = true;
|
|
if (callback)
|
|
{
|
|
installed = callback;
|
|
if (activeCallbacks++ == 0)
|
|
{
|
|
eventCallbacksDrained.Unsignal();
|
|
}
|
|
}
|
|
}
|
|
stopped = true;
|
|
lockState.Leave();
|
|
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame{ this, currentCallbackFrame };
|
|
currentCallbackFrame = &frame;
|
|
try
|
|
{
|
|
installed->OnDisconnected();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
currentCallbackFrame = frame.previous;
|
|
EndCallback();
|
|
}
|
|
|
|
if (!selfCallback)
|
|
{
|
|
eventCallbacksDrained.Wait();
|
|
}
|
|
if (clientMode)
|
|
{
|
|
eventWaitForServer.Signal();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
AsyncSocketServer::Impl
|
|
***********************************************************************/
|
|
|
|
class AsyncSocketServer::Impl : public Object
|
|
{
|
|
private:
|
|
class AcceptOperation : public IocpOperation
|
|
{
|
|
public:
|
|
Impl* server = nullptr;
|
|
SOCKET acceptedSocket = INVALID_SOCKET;
|
|
BYTE addresses[(sizeof(SOCKADDR_IN) + 16) * 2];
|
|
|
|
AcceptOperation(Impl* _server, SOCKET _acceptedSocket)
|
|
: server(_server)
|
|
, acceptedSocket(_acceptedSocket)
|
|
{
|
|
ZeroMemory(addresses, sizeof(addresses));
|
|
}
|
|
|
|
bool Complete(DWORD, DWORD error) override
|
|
{
|
|
server->CompleteAccept(this, error);
|
|
return true;
|
|
}
|
|
|
|
void EndPending() override
|
|
{
|
|
server->EndAcceptPending();
|
|
}
|
|
};
|
|
|
|
vint port = 0;
|
|
Ptr<IocpRuntime> runtime;
|
|
CriticalSection lockState;
|
|
IAsyncSocketServerCallback* callback = nullptr;
|
|
bool startCalled = false;
|
|
bool starting = false;
|
|
vint startingThreadId = -1;
|
|
bool started = false;
|
|
bool stopping = false;
|
|
bool stopped = false;
|
|
bool unexpectedStopQueued = false;
|
|
bool unexpectedStopNotified = false;
|
|
SOCKET listener = INVALID_SOCKET;
|
|
LPFN_ACCEPTEX acceptEx = nullptr;
|
|
bool acceptPending = false;
|
|
vint pendingAccepts = 0;
|
|
EventObject eventAcceptDrained;
|
|
EventObject eventStartFinished;
|
|
EventObject eventStopped;
|
|
List<Ptr<AsyncSocketConnection>> connections;
|
|
|
|
void FinishStart()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
starting = false;
|
|
startingThreadId = -1;
|
|
eventStartFinished.Signal();
|
|
}
|
|
}
|
|
|
|
class StartScope
|
|
{
|
|
private:
|
|
Impl* server = nullptr;
|
|
|
|
public:
|
|
StartScope(Impl* _server)
|
|
: server(_server)
|
|
{
|
|
}
|
|
|
|
~StartScope()
|
|
{
|
|
server->FinishStart();
|
|
}
|
|
};
|
|
|
|
void BeginAcceptPendingLocked()
|
|
{
|
|
if (pendingAccepts++ == 0)
|
|
{
|
|
eventAcceptDrained.Unsignal();
|
|
}
|
|
}
|
|
|
|
void EndAcceptPending()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (--pendingAccepts == 0)
|
|
{
|
|
eventAcceptDrained.Signal();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool PostAccept()
|
|
{
|
|
SOCKET acceptedSocket = INVALID_SOCKET;
|
|
AcceptOperation* operation = nullptr;
|
|
DWORD immediateError = ERROR_SUCCESS;
|
|
|
|
lockState.Enter();
|
|
if (!started || stopping || acceptPending)
|
|
{
|
|
lockState.Leave();
|
|
return false;
|
|
}
|
|
acceptedSocket = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED);
|
|
if (acceptedSocket == INVALID_SOCKET)
|
|
{
|
|
immediateError = WSAGetLastError();
|
|
}
|
|
else
|
|
{
|
|
operation = new AcceptOperation(this, acceptedSocket);
|
|
acceptPending = true;
|
|
BeginAcceptPendingLocked();
|
|
DWORD received = 0;
|
|
auto result = acceptEx(
|
|
listener,
|
|
acceptedSocket,
|
|
operation->addresses,
|
|
0,
|
|
sizeof(SOCKADDR_IN) + 16,
|
|
sizeof(SOCKADDR_IN) + 16,
|
|
&received,
|
|
&operation->native.overlapped
|
|
);
|
|
if (!result)
|
|
{
|
|
auto error = WSAGetLastError();
|
|
if (error != WSA_IO_PENDING)
|
|
{
|
|
immediateError = error;
|
|
acceptPending = false;
|
|
EndPendingAcceptLocked();
|
|
}
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (immediateError != ERROR_SUCCESS)
|
|
{
|
|
if (acceptedSocket != INVALID_SOCKET)
|
|
{
|
|
closesocket(acceptedSocket);
|
|
}
|
|
delete operation;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void EndPendingAcceptLocked()
|
|
{
|
|
if (--pendingAccepts == 0)
|
|
{
|
|
eventAcceptDrained.Signal();
|
|
}
|
|
}
|
|
|
|
void QueueUnexpectedStop()
|
|
{
|
|
bool queue = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (started && !stopping && !unexpectedStopQueued)
|
|
{
|
|
unexpectedStopQueued = true;
|
|
queue = true;
|
|
}
|
|
}
|
|
if (!queue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto self = this;
|
|
runtime->QueueCallback(Func<void()>([self]()
|
|
{
|
|
IAsyncSocketServerCallback* installed = nullptr;
|
|
CS_LOCK(self->lockState)
|
|
{
|
|
if (self->started && !self->stopping && !self->unexpectedStopNotified)
|
|
{
|
|
self->unexpectedStopNotified = true;
|
|
installed = self->callback;
|
|
}
|
|
}
|
|
if (installed)
|
|
{
|
|
try
|
|
{
|
|
installed->OnServerStopped();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
self->Stop();
|
|
}));
|
|
}
|
|
|
|
void CompleteAccept(AcceptOperation* operation, DWORD error)
|
|
{
|
|
bool running = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
acceptPending = false;
|
|
running = started && !stopping;
|
|
}
|
|
|
|
auto acceptedSocket = operation->acceptedSocket;
|
|
if (!running || error != ERROR_SUCCESS)
|
|
{
|
|
closesocket(acceptedSocket);
|
|
if (running)
|
|
{
|
|
if (!PostAccept())
|
|
{
|
|
QueueUnexpectedStop();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (setsockopt(acceptedSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (CHAR*)&listener, sizeof(listener)) == SOCKET_ERROR || !runtime->Associate(acceptedSocket))
|
|
{
|
|
closesocket(acceptedSocket);
|
|
if (!PostAccept())
|
|
{
|
|
QueueUnexpectedStop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto state = Ptr(new ConnectionState(runtime.Obj(), acceptedSocket));
|
|
auto connection = Ptr(new AsyncSocketConnection(state));
|
|
bool retain = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (started && !stopping)
|
|
{
|
|
connections.Add(connection);
|
|
retain = true;
|
|
}
|
|
}
|
|
|
|
if (!PostAccept())
|
|
{
|
|
QueueUnexpectedStop();
|
|
}
|
|
if (!retain)
|
|
{
|
|
connection->Stop();
|
|
return;
|
|
}
|
|
|
|
auto self = this;
|
|
runtime->QueueCallback(Func<void()>([self, connection]()
|
|
{
|
|
bool invoke = false;
|
|
IAsyncSocketServerCallback* installed = nullptr;
|
|
CS_LOCK(self->lockState)
|
|
{
|
|
invoke = self->started && !self->stopping;
|
|
if (invoke)
|
|
{
|
|
installed = self->callback;
|
|
}
|
|
}
|
|
if (!invoke)
|
|
{
|
|
connection->Stop();
|
|
return;
|
|
}
|
|
|
|
WaitForClientResult result = WaitForClientResult::Reject;
|
|
try
|
|
{
|
|
result = installed->OnClientConnected(connection.Obj());
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (result == WaitForClientResult::Reject)
|
|
{
|
|
CS_LOCK(self->lockState)
|
|
{
|
|
self->connections.Remove(connection.Obj());
|
|
}
|
|
connection->Stop();
|
|
}
|
|
}));
|
|
}
|
|
|
|
public:
|
|
Impl(vint _port)
|
|
: port(_port)
|
|
, runtime(Ptr(new IocpRuntime))
|
|
{
|
|
CHECK_ERROR(eventAcceptDrained.CreateManualUnsignal(true), L"AsyncSocketServer failed to create its accept drain event.");
|
|
CHECK_ERROR(eventStartFinished.CreateManualUnsignal(true), L"AsyncSocketServer failed to create its startup drain event.");
|
|
CHECK_ERROR(eventStopped.CreateManualUnsignal(false), L"AsyncSocketServer failed to create its stop event.");
|
|
}
|
|
|
|
~Impl()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
vint GetPort()
|
|
{
|
|
return port;
|
|
}
|
|
|
|
void Start(IAsyncSocketServerCallback* _callback)
|
|
{
|
|
CHECK_ERROR(_callback != nullptr, L"AsyncSocketServer::Start requires a callback.");
|
|
bool begin = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!startCalled && !stopping)
|
|
{
|
|
startCalled = true;
|
|
starting = true;
|
|
startingThreadId = Thread::GetCurrentThreadId();
|
|
eventStartFinished.Unsignal();
|
|
begin = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(begin, L"AsyncSocketServer::Start can only be called once.");
|
|
StartScope startScope(this);
|
|
SOCKET createdListener = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED);
|
|
if (createdListener == INVALID_SOCKET)
|
|
{
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to create its listener socket.");
|
|
}
|
|
|
|
BOOL exclusive = TRUE;
|
|
if (setsockopt(createdListener, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (CHAR*)&exclusive, sizeof(exclusive)) == SOCKET_ERROR)
|
|
{
|
|
closesocket(createdListener);
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to apply SO_EXCLUSIVEADDRUSE.");
|
|
}
|
|
|
|
SOCKADDR_IN address = {};
|
|
address.sin_family = AF_INET;
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
address.sin_port = htons((u_short)port);
|
|
if (bind(createdListener, (SOCKADDR*)&address, sizeof(address)) == SOCKET_ERROR)
|
|
{
|
|
auto error = WSAGetLastError();
|
|
closesocket(createdListener);
|
|
auto failure = error == WSAEADDRINUSE || error == WSAEACCES ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other;
|
|
throw AsyncSocketServerStartException(failure, SocketErrorMessage(L"AsyncSocketServer bind", error));
|
|
}
|
|
if (listen(createdListener, SOMAXCONN) == SOCKET_ERROR)
|
|
{
|
|
auto error = WSAGetLastError();
|
|
closesocket(createdListener);
|
|
auto failure = error == WSAEADDRINUSE || error == WSAEACCES ? AsyncSocketServerStartFailure::AddressInUse : AsyncSocketServerStartFailure::Other;
|
|
throw AsyncSocketServerStartException(failure, SocketErrorMessage(L"AsyncSocketServer listen", error));
|
|
}
|
|
if (!runtime->Associate(createdListener))
|
|
{
|
|
closesocket(createdListener);
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to associate its listener with the IO completion port.");
|
|
}
|
|
|
|
LPFN_ACCEPTEX loadedAcceptEx = nullptr;
|
|
GUID acceptExGuid = WSAID_ACCEPTEX;
|
|
DWORD transferred = 0;
|
|
if (WSAIoctl(
|
|
createdListener,
|
|
SIO_GET_EXTENSION_FUNCTION_POINTER,
|
|
&acceptExGuid,
|
|
sizeof(acceptExGuid),
|
|
&loadedAcceptEx,
|
|
sizeof(loadedAcceptEx),
|
|
&transferred,
|
|
nullptr,
|
|
nullptr
|
|
) == SOCKET_ERROR)
|
|
{
|
|
closesocket(createdListener);
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to load AcceptEx.");
|
|
}
|
|
|
|
bool canStart = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping)
|
|
{
|
|
listener = createdListener;
|
|
acceptEx = loadedAcceptEx;
|
|
callback = _callback;
|
|
started = true;
|
|
canStart = true;
|
|
}
|
|
}
|
|
if (!canStart)
|
|
{
|
|
closesocket(createdListener);
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer was stopped during startup.");
|
|
}
|
|
if (!PostAccept())
|
|
{
|
|
Stop();
|
|
throw AsyncSocketServerStartException(AsyncSocketServerStartFailure::Other, L"AsyncSocketServer failed to post AcceptEx.");
|
|
}
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
SOCKET closingListener = INVALID_SOCKET;
|
|
bool first = false;
|
|
bool waitForStart = false;
|
|
bool selfStarting = false;
|
|
auto selfWorker = currentCallbackRuntime == runtime.Obj() || currentCompletionRuntime == runtime.Obj();
|
|
auto currentThreadId = Thread::GetCurrentThreadId();
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopping)
|
|
{
|
|
stopping = true;
|
|
started = false;
|
|
closingListener = listener;
|
|
listener = INVALID_SOCKET;
|
|
first = true;
|
|
}
|
|
selfStarting = starting && startingThreadId == currentThreadId;
|
|
waitForStart = starting && !selfStarting;
|
|
}
|
|
if (waitForStart)
|
|
{
|
|
eventStartFinished.Wait();
|
|
}
|
|
if (!first)
|
|
{
|
|
if (selfStarting)
|
|
{
|
|
return;
|
|
}
|
|
// A runtime callback must not wait for the caller that is draining it.
|
|
if (!selfWorker)
|
|
{
|
|
eventStopped.Wait();
|
|
// A callback-worker caller requests runtime exit but cannot join itself.
|
|
// An external repeated Stop completes that deferred finalization here.
|
|
runtime->Stop();
|
|
CS_LOCK(lockState)
|
|
{
|
|
callback = nullptr;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (closingListener != INVALID_SOCKET)
|
|
{
|
|
closesocket(closingListener);
|
|
}
|
|
eventAcceptDrained.Wait();
|
|
|
|
List<Ptr<AsyncSocketConnection>> stoppingConnections;
|
|
CS_LOCK(lockState)
|
|
{
|
|
for (auto connection : connections)
|
|
{
|
|
stoppingConnections.Add(connection);
|
|
}
|
|
connections.Clear();
|
|
}
|
|
for (auto connection : stoppingConnections)
|
|
{
|
|
connection->Stop();
|
|
}
|
|
|
|
runtime->Stop();
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!selfWorker)
|
|
{
|
|
callback = nullptr;
|
|
}
|
|
stopped = true;
|
|
}
|
|
eventStopped.Signal();
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
bool result = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
result = stopped;
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
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<IocpRuntime> runtime;
|
|
Ptr<ConnectionState> state;
|
|
Ptr<AsyncSocketConnection> connection;
|
|
SpinLock lockStop;
|
|
bool stopped = false;
|
|
|
|
public:
|
|
Impl(vint _port)
|
|
: port(_port)
|
|
, runtime(Ptr(new IocpRuntime))
|
|
, state(Ptr(new ConnectionState(runtime.Obj(), true, _port)))
|
|
, connection(Ptr(new AsyncSocketConnection(state)))
|
|
{
|
|
}
|
|
|
|
~Impl()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
vint GetPort()
|
|
{
|
|
return port;
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
bool first = false;
|
|
SPIN_LOCK(lockStop)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
stopped = true;
|
|
first = true;
|
|
}
|
|
}
|
|
if (first)
|
|
{
|
|
connection->Stop();
|
|
state->CloseRetryTimer();
|
|
runtime->Stop();
|
|
}
|
|
}
|
|
|
|
IAsyncSocketConnection* GetConnection()
|
|
{
|
|
return connection.Obj();
|
|
}
|
|
|
|
void WaitForServer()
|
|
{
|
|
state->WaitForServer();
|
|
}
|
|
|
|
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 windows_socket::AsyncSocketServer(port));
|
|
}
|
|
|
|
Ptr<IAsyncSocketClient> CreateDefaultAsyncSocketClient(vint port)
|
|
{
|
|
return Ptr(new windows_socket::AsyncSocketClient(port));
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\HTTPCLIENT.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process::windows_http
|
|
{
|
|
|
|
/***********************************************************************
|
|
HttpClient (Reading)
|
|
***********************************************************************/
|
|
|
|
void HttpClient::RaiseLocalError(WString errorMessage, bool fatal)
|
|
{
|
|
if (callback)
|
|
{
|
|
callback->OnLocalError(errorMessage, fatal);
|
|
}
|
|
if (fatal)
|
|
{
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
bool HttpClient::IsStopping()
|
|
{
|
|
bool result = false;
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
result = state == State::Stopping;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void HttpClient::BeginReadingLoopUnsafe()
|
|
{
|
|
SendHttpRequest(HttpRequestType::Request, urlRequest, WString::Empty);
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpClient (WaitForServer)
|
|
***********************************************************************/
|
|
|
|
INetworkProtocolConnection* HttpClient::GetConnection()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
void HttpClient::CompleteConnectRequest(const WString& response, const WString& error)
|
|
{
|
|
SPIN_LOCK(lockConnectResult)
|
|
{
|
|
connectResponse = response;
|
|
connectError = error;
|
|
connectCompleted = true;
|
|
}
|
|
eventWaitForServer.Signal();
|
|
}
|
|
|
|
void HttpClient::WaitForServer()
|
|
{
|
|
{
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
if (state == State::Stopping) return;
|
|
CHECK_ERROR(state == State::Ready, L"WaitForServer can only be called once.");
|
|
state = State::WaitForServerConnection;
|
|
}
|
|
}
|
|
|
|
{
|
|
SPIN_LOCK(lockConnectResult)
|
|
{
|
|
connectCompleted = false;
|
|
connectResponse = WString::Empty;
|
|
connectError = WString::Empty;
|
|
}
|
|
}
|
|
|
|
eventWaitForServer.Unsignal();
|
|
if (!SendHttpRequest(HttpRequestType::Connect, urlConnect, WString::Empty))
|
|
{
|
|
return;
|
|
}
|
|
|
|
eventWaitForServer.Wait();
|
|
if (IsStopping()) return;
|
|
|
|
WString body;
|
|
WString error;
|
|
bool completed = false;
|
|
{
|
|
SPIN_LOCK(lockConnectResult)
|
|
{
|
|
body = connectResponse;
|
|
error = connectError;
|
|
completed = connectCompleted;
|
|
}
|
|
}
|
|
|
|
CHECK_ERROR(completed, L"/Connect did not complete.");
|
|
CHECK_ERROR(error == WString::Empty, L"/Connect failed.");
|
|
|
|
vint separatorIndex = body.IndexOf(L';');
|
|
CHECK_ERROR(separatorIndex != -1, L"/Connect response body is not in the correct format: requestUrl;responseUrl.");
|
|
urlRequest = baseUrl + body.Left(separatorIndex);
|
|
urlResponse = baseUrl + body.Right(body.Length() - separatorIndex - 1);
|
|
{
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
if (state == State::Stopping) return;
|
|
state = State::Running;
|
|
}
|
|
}
|
|
|
|
if (callback)
|
|
{
|
|
callback->OnConnected();
|
|
}
|
|
}
|
|
|
|
ClientStatus HttpClient::GetStatus()
|
|
{
|
|
ClientStatus result = ClientStatus::Disconnected;
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
switch (state)
|
|
{
|
|
case State::Ready:
|
|
result = ClientStatus::Ready;
|
|
break;
|
|
case State::WaitForServerConnection:
|
|
result = ClientStatus::WaitingForServer;
|
|
break;
|
|
case State::Running:
|
|
result = ClientStatus::Connected;
|
|
break;
|
|
default:
|
|
result = ClientStatus::Disconnected;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpClient (Writing)
|
|
***********************************************************************/
|
|
|
|
bool HttpClient::SendHttpRequest(HttpRequestType requestType, const WString& url, const WString& body, vint attempt)
|
|
{
|
|
Ptr<HttpClientApi> api;
|
|
{
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
if (state == State::Stopping) return false;
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
CHECK_ERROR(state == State::WaitForServerConnection, L"/Connect can only be called when client is waiting for the server.");
|
|
break;
|
|
case HttpRequestType::Request:
|
|
CHECK_ERROR(state == State::Running, L"/Request can only be called when client is running.");
|
|
break;
|
|
case HttpRequestType::Response:
|
|
CHECK_ERROR(state == State::Running, L"/Response can only be called when client is running.");
|
|
break;
|
|
}
|
|
api = httpClientApi;
|
|
}
|
|
}
|
|
|
|
if (!api) return false;
|
|
|
|
HttpRequest encodedBody;
|
|
if (requestType == HttpRequestType::Response)
|
|
{
|
|
encodedBody.SetBodyUtf8(body);
|
|
}
|
|
|
|
HttpRequest request;
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
request = CreateHttpNetworkProtocolConnectRequest(url);
|
|
break;
|
|
case HttpRequestType::Request:
|
|
request = CreateHttpNetworkProtocolReceiveRequest(url);
|
|
request.receiveTimeout = 0;
|
|
break;
|
|
case HttpRequestType::Response:
|
|
request = CreateHttpNetworkProtocolSendRequest(url, encodedBody.body);
|
|
request.keepAliveOnStop = true;
|
|
break;
|
|
}
|
|
|
|
api->HttpQuery(request, [this, requestType, body, attempt](Variant<HttpResponse, HttpError> result)
|
|
{
|
|
OnHttpRequestCompleted(requestType, body, attempt, std::move(result));
|
|
});
|
|
return true;
|
|
}
|
|
|
|
void HttpClient::OnHttpRequestFailed(HttpRequestType requestType, const WString& body, vint attempt, const WString& errorMessage)
|
|
{
|
|
if (IsStopping()) return;
|
|
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
{
|
|
bool fatal = attempt >= HttpRequestMaxAttempts;
|
|
RaiseLocalError(errorMessage, fatal);
|
|
if (!fatal && !IsStopping())
|
|
{
|
|
SendHttpRequest(HttpRequestType::Connect, urlConnect, WString::Empty, attempt + 1);
|
|
}
|
|
}
|
|
break;
|
|
case HttpRequestType::Request:
|
|
SendHttpRequest(HttpRequestType::Request, urlRequest, WString::Empty, attempt + 1);
|
|
break;
|
|
case HttpRequestType::Response:
|
|
{
|
|
bool fatal = attempt >= HttpRequestMaxAttempts;
|
|
RaiseLocalError(errorMessage, fatal);
|
|
if (!fatal && !IsStopping())
|
|
{
|
|
SendHttpRequest(HttpRequestType::Response, urlResponse, body, attempt + 1);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void HttpClient::OnHttpRequestCompleted(HttpRequestType requestType, WString body, vint attempt, Variant<HttpResponse, HttpError> result)
|
|
{
|
|
if (auto error = result.TryGet<HttpError>())
|
|
{
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Connect failed: " + error->message);
|
|
break;
|
|
case HttpRequestType::Request:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Request failed: " + error->message);
|
|
break;
|
|
case HttpRequestType::Response:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Response failed: " + error->message);
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto&& response = result.Get<HttpResponse>();
|
|
if (response.statusCode != 200)
|
|
{
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
OnHttpRequestFailed(requestType, body, attempt, WString::Unmanaged(L"/Connect returned status code: ") + itow(response.statusCode) + L".");
|
|
break;
|
|
case HttpRequestType::Request:
|
|
OnHttpRequestFailed(requestType, body, attempt, WString::Unmanaged(L"/Request returned status code: ") + itow(response.statusCode) + L", another renderer may have connected to the core.");
|
|
break;
|
|
case HttpRequestType::Response:
|
|
OnHttpRequestFailed(requestType, body, attempt, WString::Unmanaged(L"/Response returned status code: ") + itow(response.statusCode) + L", another renderer may have connected to the core.");
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (response.contentType != HttpNetworkProtocolContentType)
|
|
{
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Connect response did not return content type: application/json; charset=utf8.");
|
|
break;
|
|
case HttpRequestType::Request:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Request response did not return content type: application/json; charset=utf8.");
|
|
break;
|
|
case HttpRequestType::Response:
|
|
OnHttpRequestFailed(requestType, body, attempt, L"/Response response did not return content type: application/json; charset=utf8.");
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto responseBody = response.GetBodyUtf8();
|
|
switch (requestType)
|
|
{
|
|
case HttpRequestType::Connect:
|
|
CompleteConnectRequest(responseBody, WString::Empty);
|
|
break;
|
|
case HttpRequestType::Request:
|
|
if (!IsStopping())
|
|
{
|
|
BeginReadingLoopUnsafe();
|
|
if (responseBody.Length() > 0 && callback)
|
|
{
|
|
callback->OnReadString(responseBody);
|
|
}
|
|
}
|
|
break;
|
|
case HttpRequestType::Response:
|
|
if (!IsStopping() && responseBody.Length() > 0 && callback)
|
|
{
|
|
callback->OnReadString(responseBody);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void HttpClient::SendString(const WString& str)
|
|
{
|
|
SendHttpRequest(HttpRequestType::Response, urlResponse, str);
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpClient
|
|
***********************************************************************/
|
|
|
|
HttpClient::HttpClient(const WString _baseUrl, vint port)
|
|
: baseUrl(_baseUrl)
|
|
{
|
|
CHECK_ERROR(eventWaitForServer.CreateAutoUnsignal(false), L"HttpClient initialization failed on eventWaitForServer.CreateAutoUnsignal.");
|
|
|
|
httpClientApi = Ptr(new HttpClientApi(L"localhost", port));
|
|
urlConnect = baseUrl + HttpServerUrl_Connect;
|
|
}
|
|
|
|
HttpClient::~HttpClient()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
void HttpClient::InstallCallback(INetworkProtocolCallback* _callback)
|
|
{
|
|
CHECK_ERROR(!callback || !_callback, L"HttpClient::InstallCallback only accepts one callback at a time.");
|
|
callback = _callback;
|
|
if (!callback) return;
|
|
callback->OnInstalled(this);
|
|
}
|
|
|
|
void HttpClient::Stop()
|
|
{
|
|
Ptr<HttpClientApi> stoppingApi;
|
|
bool notifyDisconnected = false;
|
|
{
|
|
SPIN_LOCK(lockState)
|
|
{
|
|
if (httpClientApi)
|
|
{
|
|
state = State::Stopping;
|
|
stoppingApi = httpClientApi;
|
|
httpClientApi = nullptr;
|
|
notifyDisconnected = true;
|
|
}
|
|
else
|
|
{
|
|
state = State::Stopping;
|
|
}
|
|
}
|
|
}
|
|
|
|
eventWaitForServer.Signal();
|
|
if (stoppingApi)
|
|
{
|
|
stoppingApi->Stop();
|
|
}
|
|
|
|
if (notifyDisconnected && callback)
|
|
{
|
|
callback->OnDisconnected();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\HTTPCLIENTAPI.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
#pragma comment(lib, "WinHttp.lib")
|
|
|
|
namespace vl::inter_process::windows_http
|
|
{
|
|
|
|
using namespace vl::collections;
|
|
|
|
/***********************************************************************
|
|
HttpClientApi
|
|
***********************************************************************/
|
|
|
|
HttpError HttpClientApi::MakeError(const WString& operation, DWORD errorCode)
|
|
{
|
|
HttpError error;
|
|
error.operation = operation;
|
|
error.errorCode = errorCode;
|
|
error.message = operation + L" failed with Windows error " + itow((vint)errorCode) + L".";
|
|
return error;
|
|
}
|
|
|
|
bool HttpClientApi::IsStopping()
|
|
{
|
|
bool result = false;
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
result = stopping;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void HttpClientApi::BeginPendingCallback()
|
|
{
|
|
if (pendingCallbacks++ == 0)
|
|
{
|
|
eventPendingCallbacks.Unsignal();
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::EndPendingCallback()
|
|
{
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
if (--pendingCallbacks == 0)
|
|
{
|
|
eventPendingCallbacks.Signal();
|
|
}
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::AttachRequestUnsafe(Ptr<HttpRequestContext> context)
|
|
{
|
|
activeRequests.Add(context);
|
|
}
|
|
|
|
void HttpClientApi::RemoveRequestUnsafe(Ptr<HttpRequestContext> context)
|
|
{
|
|
for (vint index = 0; index < activeRequests.Count(); index++)
|
|
{
|
|
if (activeRequests[index] == context)
|
|
{
|
|
activeRequests.RemoveAt(index);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::CloseRequest(Ptr<HttpRequestContext> context)
|
|
{
|
|
HINTERNET httpRequest = NULL;
|
|
SPIN_LOCK(context->lockContext)
|
|
{
|
|
if (!context->closing)
|
|
{
|
|
context->closing = true;
|
|
httpRequest = context->httpRequest;
|
|
}
|
|
}
|
|
if (httpRequest)
|
|
{
|
|
WinHttpCloseHandle(httpRequest);
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::OnRequestHandleClosing(Ptr<HttpRequestContext> context)
|
|
{
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
RemoveRequestUnsafe(context);
|
|
}
|
|
EndPendingCallback();
|
|
}
|
|
|
|
void HttpClientApi::CompleteRequest(Ptr<HttpRequestContext> context, HttpResponse&& response)
|
|
{
|
|
Func<void(Variant<HttpResponse, HttpError>)> callback;
|
|
bool stoppingNow = IsStopping();
|
|
bool invokeCallback = false;
|
|
SPIN_LOCK(context->lockContext)
|
|
{
|
|
if (!context->completed)
|
|
{
|
|
context->completed = true;
|
|
callback = context->callback;
|
|
invokeCallback = !stoppingNow;
|
|
}
|
|
}
|
|
|
|
if (invokeCallback && callback)
|
|
{
|
|
CloseRequest(context);
|
|
callback(Variant<HttpResponse, HttpError>(std::move(response)));
|
|
}
|
|
else
|
|
{
|
|
CloseRequest(context);
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::CompleteRequest(Ptr<HttpRequestContext> context, HttpError&& error)
|
|
{
|
|
Func<void(Variant<HttpResponse, HttpError>)> callback;
|
|
bool stoppingNow = IsStopping();
|
|
bool invokeCallback = false;
|
|
SPIN_LOCK(context->lockContext)
|
|
{
|
|
if (!context->completed)
|
|
{
|
|
context->completed = true;
|
|
callback = context->callback;
|
|
invokeCallback = !stoppingNow;
|
|
}
|
|
}
|
|
|
|
if (invokeCallback && callback)
|
|
{
|
|
CloseRequest(context);
|
|
callback(Variant<HttpResponse, HttpError>(std::move(error)));
|
|
}
|
|
else
|
|
{
|
|
CloseRequest(context);
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::CompleteRequestWithLastError(Ptr<HttpRequestContext> context, const WString& operation, DWORD errorCode)
|
|
{
|
|
if (errorCode == ERROR_INVALID_HANDLE && IsStopping() && !context->keepAliveOnStop)
|
|
{
|
|
return;
|
|
}
|
|
CompleteRequest(context, MakeError(operation, errorCode));
|
|
}
|
|
|
|
void CALLBACK HttpClientApi::HttpStatusCallback(HINTERNET httpRequest, DWORD_PTR contextValue, DWORD status, LPVOID statusInformation, DWORD statusInformationLength)
|
|
{
|
|
if (!contextValue) return;
|
|
auto contextPtr = reinterpret_cast<Ptr<HttpClientApi::HttpRequestContext>*>(contextValue);
|
|
auto context = *contextPtr;
|
|
auto self = context->api;
|
|
|
|
switch (status)
|
|
{
|
|
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
|
|
{
|
|
if (self->IsStopping() && !context->keepAliveOnStop) return;
|
|
|
|
BOOL httpResult = WinHttpReceiveResponse(httpRequest, NULL);
|
|
DWORD lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpReceiveResponse", lastError);
|
|
}
|
|
}
|
|
break;
|
|
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
|
|
{
|
|
if (self->IsStopping() && !context->keepAliveOnStop) return;
|
|
|
|
DWORD statusCode = 0;
|
|
DWORD dwordLength = sizeof(DWORD);
|
|
BOOL httpResult = WinHttpQueryHeaders(
|
|
httpRequest,
|
|
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
|
WINHTTP_HEADER_NAME_BY_INDEX,
|
|
&statusCode,
|
|
&dwordLength,
|
|
WINHTTP_NO_HEADER_INDEX);
|
|
DWORD lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryHeaders(status)", lastError);
|
|
return;
|
|
}
|
|
context->response.statusCode = statusCode;
|
|
|
|
DWORD headerLength = 0;
|
|
httpResult = WinHttpQueryHeaders(
|
|
httpRequest,
|
|
WINHTTP_QUERY_CONTENT_TYPE,
|
|
WINHTTP_HEADER_NAME_BY_INDEX,
|
|
NULL,
|
|
&headerLength,
|
|
WINHTTP_NO_HEADER_INDEX);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE && lastError == ERROR_INSUFFICIENT_BUFFER)
|
|
{
|
|
Array<wchar_t> headerBuffer(headerLength / sizeof(wchar_t) + 1);
|
|
ZeroMemory(&headerBuffer[0], headerBuffer.Count() * sizeof(wchar_t));
|
|
httpResult = WinHttpQueryHeaders(
|
|
httpRequest,
|
|
WINHTTP_QUERY_CONTENT_TYPE,
|
|
WINHTTP_HEADER_NAME_BY_INDEX,
|
|
&headerBuffer[0],
|
|
&headerLength,
|
|
WINHTTP_NO_HEADER_INDEX);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryHeaders(content-type)", lastError);
|
|
return;
|
|
}
|
|
context->response.contentType = &headerBuffer[0];
|
|
}
|
|
else if (httpResult == FALSE && lastError != ERROR_WINHTTP_HEADER_NOT_FOUND)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryHeaders(content-type)", lastError);
|
|
return;
|
|
}
|
|
|
|
headerLength = 0;
|
|
httpResult = WinHttpQueryHeaders(
|
|
httpRequest,
|
|
WINHTTP_QUERY_SET_COOKIE,
|
|
WINHTTP_HEADER_NAME_BY_INDEX,
|
|
NULL,
|
|
&headerLength,
|
|
WINHTTP_NO_HEADER_INDEX);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE && lastError == ERROR_INSUFFICIENT_BUFFER)
|
|
{
|
|
Array<wchar_t> headerBuffer(headerLength / sizeof(wchar_t) + 1);
|
|
ZeroMemory(&headerBuffer[0], headerBuffer.Count() * sizeof(wchar_t));
|
|
httpResult = WinHttpQueryHeaders(
|
|
httpRequest,
|
|
WINHTTP_QUERY_SET_COOKIE,
|
|
WINHTTP_HEADER_NAME_BY_INDEX,
|
|
&headerBuffer[0],
|
|
&headerLength,
|
|
WINHTTP_NO_HEADER_INDEX);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryHeaders(cookie)", lastError);
|
|
return;
|
|
}
|
|
context->response.cookie = &headerBuffer[0];
|
|
}
|
|
else if (httpResult == FALSE && lastError != ERROR_WINHTTP_HEADER_NOT_FOUND)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryHeaders(cookie)", lastError);
|
|
return;
|
|
}
|
|
|
|
context->bodyBufferWriting = 0;
|
|
httpResult = WinHttpQueryDataAvailable(httpRequest, NULL);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryDataAvailable", lastError);
|
|
}
|
|
}
|
|
break;
|
|
case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
|
|
{
|
|
if (self->IsStopping() && !context->keepAliveOnStop) return;
|
|
CHECK_ERROR(statusInformationLength == sizeof(DWORD), L"WinHttpQueryDataAvailable returned an unexpected payload.");
|
|
DWORD dataAvailable = *(PDWORD)statusInformation;
|
|
|
|
if (dataAvailable == 0)
|
|
{
|
|
context->response.body.Resize(context->bodyBufferWriting);
|
|
self->CompleteRequest(context, std::move(context->response));
|
|
return;
|
|
}
|
|
|
|
context->bodyBufferWritingAvailable = dataAvailable;
|
|
DWORD bufferSize = context->bodyBufferWriting + dataAvailable + 1;
|
|
if (context->response.body.Count() < (vint)bufferSize)
|
|
{
|
|
context->response.body.Resize((bufferSize + HttpRespondBodyStep - 1) / HttpRespondBodyStep * HttpRespondBodyStep);
|
|
}
|
|
|
|
BOOL httpResult = WinHttpReadData(
|
|
httpRequest,
|
|
&context->response.body[context->bodyBufferWriting],
|
|
dataAvailable,
|
|
NULL);
|
|
DWORD lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpReadData", lastError);
|
|
}
|
|
}
|
|
break;
|
|
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
|
|
{
|
|
if (self->IsStopping() && !context->keepAliveOnStop) return;
|
|
if (context->bodyBufferWritingAvailable != statusInformationLength)
|
|
{
|
|
self->CompleteRequest(context, MakeError(L"WinHttpReadData", ERROR_INVALID_DATA));
|
|
return;
|
|
}
|
|
context->bodyBufferWriting += context->bodyBufferWritingAvailable;
|
|
|
|
BOOL httpResult = WinHttpQueryDataAvailable(httpRequest, NULL);
|
|
DWORD lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
self->CompleteRequestWithLastError(context, L"WinHttpQueryDataAvailable", lastError);
|
|
}
|
|
}
|
|
break;
|
|
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
|
|
{
|
|
if (self->IsStopping() && !context->keepAliveOnStop) return;
|
|
|
|
auto asyncResult = reinterpret_cast<WINHTTP_ASYNC_RESULT*>(statusInformation);
|
|
DWORD errorCode = asyncResult ? asyncResult->dwError : ERROR_WINHTTP_INTERNAL_ERROR;
|
|
HttpError error = MakeError(L"WinHTTP async request", errorCode);
|
|
if (asyncResult)
|
|
{
|
|
error.message += L" Operation code: " + itow((vint)asyncResult->dwResult) + L".";
|
|
}
|
|
self->CompleteRequest(context, std::move(error));
|
|
}
|
|
break;
|
|
case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
|
|
self->OnRequestHandleClosing(context);
|
|
delete contextPtr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
HttpClientApi::HttpClientApi(const WString& _server, vint _port)
|
|
: server(_server)
|
|
, port(_port)
|
|
{
|
|
CHECK_ERROR(eventPendingCallbacks.CreateManualUnsignal(true), L"HttpClientApi initialization failed on eventPendingCallbacks.CreateManualUnsignal.");
|
|
|
|
httpSession = WinHttpOpen(
|
|
L"vl::inter_process::windows_http::HttpClientApi",
|
|
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
|
WINHTTP_NO_PROXY_NAME,
|
|
WINHTTP_NO_PROXY_BYPASS,
|
|
WINHTTP_FLAG_ASYNC);
|
|
CHECK_ERROR(httpSession != NULL, L"WinHttpOpen failed.");
|
|
|
|
#ifdef WINHTTP_OPTION_IPV6_FAST_FALLBACK
|
|
// This option is unavailable before Windows 10 1903. Keep the default address fallback when unsupported.
|
|
BOOL ipv6FastFallback = TRUE;
|
|
WinHttpSetOption(httpSession, WINHTTP_OPTION_IPV6_FAST_FALLBACK, &ipv6FastFallback, sizeof(ipv6FastFallback));
|
|
#endif
|
|
|
|
httpConnection = WinHttpConnect(
|
|
httpSession,
|
|
server.Buffer(),
|
|
(INTERNET_PORT)port,
|
|
0);
|
|
CHECK_ERROR(httpConnection != NULL, L"WinHttpConnect failed.");
|
|
}
|
|
|
|
HttpClientApi::~HttpClientApi()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
void HttpClientApi::HttpQuery(const HttpRequest& request, Func<void(Variant<HttpResponse, HttpError>)> callback)
|
|
{
|
|
bool rejected = false;
|
|
{
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
if (stopping)
|
|
{
|
|
rejected = true;
|
|
}
|
|
else
|
|
{
|
|
BeginPendingCallback();
|
|
}
|
|
}
|
|
}
|
|
if (rejected)
|
|
{
|
|
if (callback)
|
|
{
|
|
callback(Variant<HttpResponse, HttpError>(MakeError(L"HttpClientApi::HttpQuery", ERROR_OPERATION_ABORTED)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
BOOL httpResult = FALSE;
|
|
DWORD lastError = 0;
|
|
List<LPCWSTR> acceptTypes;
|
|
for (vint i = 0; i < request.acceptTypes.Count(); i++)
|
|
{
|
|
acceptTypes.Add(request.acceptTypes.Get(i).Buffer());
|
|
}
|
|
acceptTypes.Add(nullptr);
|
|
|
|
auto method = request.method == WString::Empty ? WString::Unmanaged(L"GET") : request.method;
|
|
auto httpRequest = WinHttpOpenRequest(
|
|
httpConnection,
|
|
method.Buffer(),
|
|
request.query.Buffer(),
|
|
NULL,
|
|
WINHTTP_NO_REFERER,
|
|
&acceptTypes[0],
|
|
(request.secure ? WINHTTP_FLAG_SECURE : 0) | WINHTTP_FLAG_REFRESH);
|
|
lastError = GetLastError();
|
|
if (httpRequest == NULL)
|
|
{
|
|
EndPendingCallback();
|
|
if (callback)
|
|
{
|
|
callback(Variant<HttpResponse, HttpError>(MakeError(L"WinHttpOpenRequest", lastError)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
httpResult = WinHttpSetTimeouts(
|
|
httpRequest,
|
|
(int)request.resolveTimeout,
|
|
(int)request.connectTimeout,
|
|
(int)request.sendTimeout,
|
|
(int)request.receiveTimeout);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
EndPendingCallback();
|
|
WinHttpCloseHandle(httpRequest);
|
|
if (callback)
|
|
{
|
|
callback(Variant<HttpResponse, HttpError>(MakeError(L"WinHttpSetTimeouts", lastError)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto contextPtr = new Ptr<HttpRequestContext>(new HttpRequestContext);
|
|
auto context = *contextPtr;
|
|
context->api = this;
|
|
context->httpRequest = httpRequest;
|
|
context->callback = callback;
|
|
context->keepAliveOnStop = request.keepAliveOnStop;
|
|
|
|
if (request.body.Count() > 0)
|
|
{
|
|
context->requestBody.Resize(request.body.Count());
|
|
memcpy(&context->requestBody[0], &request.body.Get(0), request.body.Count());
|
|
}
|
|
|
|
auto failBeforeCallbackInstalled = [&](const WString& operation, DWORD errorCode)
|
|
{
|
|
WinHttpCloseHandle(httpRequest);
|
|
delete contextPtr;
|
|
EndPendingCallback();
|
|
if (callback)
|
|
{
|
|
callback(Variant<HttpResponse, HttpError>(MakeError(operation, errorCode)));
|
|
}
|
|
};
|
|
|
|
DWORD_PTR contextValue = reinterpret_cast<DWORD_PTR>(contextPtr);
|
|
httpResult = WinHttpSetOption(
|
|
httpRequest,
|
|
WINHTTP_OPTION_CONTEXT_VALUE,
|
|
&contextValue,
|
|
sizeof(contextValue));
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
failBeforeCallbackInstalled(L"WinHttpSetOption(WINHTTP_OPTION_CONTEXT_VALUE)", lastError);
|
|
return;
|
|
}
|
|
|
|
if (request.username != WString::Empty && request.password != WString::Empty)
|
|
{
|
|
httpResult = WinHttpSetCredentials(
|
|
httpRequest,
|
|
WINHTTP_AUTH_TARGET_SERVER,
|
|
WINHTTP_AUTH_SCHEME_BASIC,
|
|
request.username.Buffer(),
|
|
request.password.Buffer(),
|
|
NULL);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
failBeforeCallbackInstalled(L"WinHttpSetCredentials", lastError);
|
|
return;
|
|
}
|
|
}
|
|
if (request.contentType != WString::Empty)
|
|
{
|
|
httpResult = WinHttpAddRequestHeaders(
|
|
httpRequest,
|
|
(L"Content-Type: " + request.contentType).Buffer(),
|
|
-1,
|
|
WINHTTP_ADDREQ_FLAG_REPLACE | WINHTTP_ADDREQ_FLAG_ADD);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
failBeforeCallbackInstalled(L"WinHttpAddRequestHeaders(content-type)", lastError);
|
|
return;
|
|
}
|
|
}
|
|
if (request.cookie != WString::Empty)
|
|
{
|
|
httpResult = WinHttpAddRequestHeaders(
|
|
httpRequest,
|
|
(L"Cookie: " + request.cookie).Buffer(),
|
|
-1,
|
|
WINHTTP_ADDREQ_FLAG_REPLACE | WINHTTP_ADDREQ_FLAG_ADD);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
failBeforeCallbackInstalled(L"WinHttpAddRequestHeaders(cookie)", lastError);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (vint i = 0; i < request.extraHeaders.Count(); i++)
|
|
{
|
|
WString key = request.extraHeaders.Keys()[i];
|
|
WString value = request.extraHeaders.Values().Get(i);
|
|
httpResult = WinHttpAddRequestHeaders(
|
|
httpRequest,
|
|
(key + L": " + value).Buffer(),
|
|
-1,
|
|
WINHTTP_ADDREQ_FLAG_REPLACE | WINHTTP_ADDREQ_FLAG_ADD);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
failBeforeCallbackInstalled(L"WinHttpAddRequestHeaders(extra)", lastError);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool failedBeforeSend = false;
|
|
WString failedOperation;
|
|
DWORD failedError = 0;
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
if (stopping)
|
|
{
|
|
failedBeforeSend = true;
|
|
failedOperation = L"HttpClientApi::HttpQuery";
|
|
failedError = ERROR_OPERATION_ABORTED;
|
|
}
|
|
else
|
|
{
|
|
auto previousCallback = WinHttpSetStatusCallback(
|
|
httpRequest,
|
|
HttpStatusCallback,
|
|
WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS,
|
|
NULL);
|
|
lastError = GetLastError();
|
|
if (previousCallback == WINHTTP_INVALID_STATUS_CALLBACK)
|
|
{
|
|
failedBeforeSend = true;
|
|
failedOperation = L"WinHttpSetStatusCallback";
|
|
failedError = lastError;
|
|
}
|
|
else
|
|
{
|
|
AttachRequestUnsafe(context);
|
|
}
|
|
}
|
|
}
|
|
if (failedBeforeSend)
|
|
{
|
|
failBeforeCallbackInstalled(failedOperation, failedError);
|
|
return;
|
|
}
|
|
|
|
DWORD requestBodyLength = (DWORD)context->requestBody.Count();
|
|
LPVOID requestBodyBuffer = requestBodyLength == 0 ? WINHTTP_NO_REQUEST_DATA : (LPVOID)&context->requestBody[0];
|
|
httpResult = WinHttpSendRequest(
|
|
httpRequest,
|
|
WINHTTP_NO_ADDITIONAL_HEADERS,
|
|
0,
|
|
requestBodyBuffer,
|
|
requestBodyLength,
|
|
requestBodyLength,
|
|
contextValue);
|
|
lastError = GetLastError();
|
|
if (httpResult == FALSE)
|
|
{
|
|
CompleteRequestWithLastError(context, L"WinHttpSendRequest", lastError);
|
|
}
|
|
}
|
|
|
|
void HttpClientApi::Stop()
|
|
{
|
|
if (httpSession == NULL) return;
|
|
|
|
List<HINTERNET> stoppingRequests;
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
stopping = true;
|
|
for (auto&& context : activeRequests)
|
|
{
|
|
HINTERNET httpRequest = NULL;
|
|
SPIN_LOCK(context->lockContext)
|
|
{
|
|
if (!context->keepAliveOnStop && !context->closing)
|
|
{
|
|
context->closing = true;
|
|
httpRequest = context->httpRequest;
|
|
}
|
|
}
|
|
if (httpRequest)
|
|
{
|
|
stoppingRequests.Add(httpRequest);
|
|
}
|
|
}
|
|
}
|
|
for (auto httpRequest : stoppingRequests)
|
|
{
|
|
WinHttpCloseHandle(httpRequest);
|
|
}
|
|
|
|
eventPendingCallbacks.Wait();
|
|
|
|
WinHttpCloseHandle(httpConnection);
|
|
WinHttpSetStatusCallback(
|
|
httpSession,
|
|
NULL,
|
|
WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS,
|
|
NULL);
|
|
WinHttpCloseHandle(httpSession);
|
|
|
|
SPIN_LOCK(lockActiveRequests)
|
|
{
|
|
activeRequests.Clear();
|
|
}
|
|
|
|
httpConnection = NULL;
|
|
httpSession = NULL;
|
|
}
|
|
|
|
WString HttpClientApi::UrlEncodeQuery(const WString& query)
|
|
{
|
|
return HttpUrlEncodeQuery(query);
|
|
}
|
|
|
|
WString HttpClientApi::UrlDecodeQuery(const WString& query)
|
|
{
|
|
return HttpUrlDecodeQuery(query);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\HTTPSERVER.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process::windows_http
|
|
{
|
|
|
|
using namespace vl::collections;
|
|
|
|
/***********************************************************************
|
|
HttpServerConnection
|
|
***********************************************************************/
|
|
|
|
void HttpServerConnection::OnCancelCurrentHttpRequestForPendingRequest()
|
|
{
|
|
if (httpPendingRequestId != HTTP_NULL_ID)
|
|
{
|
|
if (!server)
|
|
{
|
|
httpPendingRequestId = HTTP_NULL_ID;
|
|
return;
|
|
}
|
|
ULONG result = HttpCancelHttpRequest(
|
|
server->GetHttpRequestQueue(),
|
|
httpPendingRequestId,
|
|
NULL);
|
|
CHECK_ERROR(
|
|
result == NO_ERROR || result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED,
|
|
L"HttpCancelHttpRequest failed for canceling outdated /Request.");
|
|
httpPendingRequestId = HTTP_NULL_ID;
|
|
}
|
|
}
|
|
|
|
void HttpServerConnection::OnNewHttpRequestForPendingRequest(HTTP_REQUEST_ID httpRequestId)
|
|
{
|
|
OnCancelCurrentHttpRequestForPendingRequest();
|
|
httpPendingRequestId = httpRequestId;
|
|
if (pendingRequestsToSend.Count() > 0)
|
|
{
|
|
auto pendingRequest = pendingRequestsToSend[0];
|
|
pendingRequestsToSend.RemoveAt(0);
|
|
HttpServerApi::SendResponseUtf8(server->GetHttpRequestQueue(), httpPendingRequestId, pendingRequest);
|
|
httpPendingRequestId = HTTP_NULL_ID;
|
|
}
|
|
}
|
|
|
|
WString HttpServerConnection::SubmitResponse(PHTTP_REQUEST pRequest)
|
|
{
|
|
auto body = server->GetUtf8Body(pRequest).Value();
|
|
|
|
SPIN_LOCK(pendingRequestLock)
|
|
{
|
|
submittingResponse = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
SPIN_LOCK(lockQueuedStrings)
|
|
{
|
|
if (callback)
|
|
{
|
|
callback->OnReadString(body);
|
|
}
|
|
else
|
|
{
|
|
queuedStrings.Add(body);
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
SPIN_LOCK(pendingRequestLock)
|
|
{
|
|
submittingResponse = false;
|
|
responsesToSubmit.Clear();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
WString responseToClient;
|
|
SPIN_LOCK(pendingRequestLock)
|
|
{
|
|
submittingResponse = false;
|
|
if (responsesToSubmit.Count() > 0)
|
|
{
|
|
responseToClient = responsesToSubmit[0];
|
|
responsesToSubmit.RemoveAt(0);
|
|
while (responsesToSubmit.Count() > 0)
|
|
{
|
|
pendingRequestsToSend.Add(responsesToSubmit[0]);
|
|
responsesToSubmit.RemoveAt(0);
|
|
}
|
|
}
|
|
else if (pendingRequestsToSend.Count() > 0)
|
|
{
|
|
responseToClient = pendingRequestsToSend[0];
|
|
pendingRequestsToSend.RemoveAt(0);
|
|
}
|
|
}
|
|
return responseToClient;
|
|
}
|
|
|
|
void HttpServerConnection::InstallCallback(INetworkProtocolCallback* _callback)
|
|
{
|
|
CHECK_ERROR(!callback || !_callback, L"HttpServerConnection::InstallCallback only accepts one callback at a time.");
|
|
if (_callback)
|
|
{
|
|
_callback->OnInstalled(this);
|
|
}
|
|
|
|
List<WString> strings;
|
|
SPIN_LOCK(lockQueuedStrings)
|
|
{
|
|
callback = _callback;
|
|
if (!callback) return;
|
|
strings = std::move(queuedStrings);
|
|
}
|
|
for (const auto& str : strings)
|
|
{
|
|
_callback->OnReadString(str);
|
|
}
|
|
}
|
|
|
|
void HttpServerConnection::BeginReadingLoopUnsafe()
|
|
{
|
|
// Do nothing, HttpServer automatically handles this.
|
|
}
|
|
|
|
void HttpServerConnection::SendString(const WString& str)
|
|
{
|
|
SPIN_LOCK(pendingRequestLock)
|
|
{
|
|
if (submittingResponse)
|
|
{
|
|
responsesToSubmit.Add(str);
|
|
}
|
|
else if (httpPendingRequestId != HTTP_NULL_ID)
|
|
{
|
|
ULONG result = HttpServerApi::SendResponse(server->GetHttpRequestQueue(), httpPendingRequestId, { 200, L"OK", str, HttpNetworkProtocolContentType });
|
|
if (result == NO_ERROR)
|
|
{
|
|
httpPendingRequestId = HTTP_NULL_ID;
|
|
}
|
|
else if (result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED)
|
|
{
|
|
httpPendingRequestId = HTTP_NULL_ID;
|
|
pendingRequestsToSend.Add(str);
|
|
}
|
|
else
|
|
{
|
|
CHECK_FAIL(L"HttpSendHttpResponse failed for responding /Request.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pendingRequestsToSend.Add(str);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HttpServerConnection::Stop()
|
|
{
|
|
auto holding = Ptr(this);
|
|
if (server)
|
|
{
|
|
SPIN_LOCK(server->lockConnections)
|
|
{
|
|
server->connections.Remove(guid);
|
|
}
|
|
SPIN_LOCK(pendingRequestLock)
|
|
{
|
|
OnCancelCurrentHttpRequestForPendingRequest();
|
|
}
|
|
if (callback)
|
|
{
|
|
callback->OnDisconnected();
|
|
}
|
|
}
|
|
}
|
|
|
|
WString HttpServerConnection::GenerateNewGuid()
|
|
{
|
|
RPC_STATUS status = -1;
|
|
UUID guid;
|
|
status = UuidCreate(&guid);
|
|
CHECK_ERROR(status == RPC_S_OK, L"UuidCreate failed.");
|
|
|
|
RPC_WSTR guidString = nullptr;
|
|
status = UuidToString(&guid, &guidString);
|
|
CHECK_ERROR(status == RPC_S_OK, L"UuidToString failed.");
|
|
|
|
WString result = guidString;
|
|
status = RpcStringFree(&guidString);
|
|
CHECK_ERROR(status == RPC_S_OK, L"RpcStringFree failed.");
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpServer (HttpServerApi)
|
|
***********************************************************************/
|
|
|
|
void HttpServer::OnHttpRequestReceived(PHTTP_REQUEST pRequest)
|
|
{
|
|
bool isValidRequest = wcsncmp(pRequest->CookedUrl.pAbsPath, urlRequestPrefix.Buffer(), urlRequestPrefix.Length()) == 0;
|
|
bool isValidResponse = wcsncmp(pRequest->CookedUrl.pAbsPath, urlResponsePrefix.Buffer(), urlResponsePrefix.Length()) == 0;
|
|
|
|
auto FindExistingConnection = [=, this](const WString& guid)->Ptr<HttpServerConnection>
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
vint index = connections.Keys().IndexOf(guid);
|
|
if (index == -1)
|
|
{
|
|
HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 404, L"Unknown connection guid" });
|
|
}
|
|
else
|
|
{
|
|
return connections.Values()[index];
|
|
}
|
|
}
|
|
return {};
|
|
};
|
|
|
|
if (pRequest->Verb == HttpVerbGET && pRequest->CookedUrl.pAbsPath == urlConnect)
|
|
{
|
|
auto newGuid = HttpServerConnection::GenerateNewGuid();
|
|
auto connection = Ptr(new HttpServerConnection);
|
|
connection->server = this;
|
|
connection->guid = newGuid;
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
connections.Add(newGuid, connection);
|
|
}
|
|
auto result = OnClientConnected(connection.Obj());
|
|
if (result == WaitForClientResult::Reject)
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
connections.Remove(newGuid);
|
|
}
|
|
connection->server = nullptr;
|
|
HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 404, L"Connection rejected" });
|
|
}
|
|
else
|
|
{
|
|
auto completeUrlRequest = WString::Unmanaged(HttpServerUrl_Request) + L"/" + newGuid;
|
|
auto completeUrlResponse = WString::Unmanaged(HttpServerUrl_Response) + L"/" + newGuid;
|
|
HttpServerApi::SendResponseUtf8(GetHttpRequestQueue(), pRequest->RequestId, CreateHttpNetworkProtocolConnectBody(completeUrlRequest, completeUrlResponse));
|
|
}
|
|
}
|
|
else if (pRequest->Verb == HttpVerbPOST && isValidRequest)
|
|
{
|
|
auto guid = WString::Unmanaged(pRequest->CookedUrl.pAbsPath + urlRequestPrefix.Length());
|
|
if (auto connection = FindExistingConnection(guid))
|
|
{
|
|
SPIN_LOCK(connection->pendingRequestLock)
|
|
{
|
|
connection->OnNewHttpRequestForPendingRequest(pRequest->RequestId);
|
|
}
|
|
}
|
|
}
|
|
else if (pRequest->Verb == HttpVerbPOST && isValidResponse)
|
|
{
|
|
auto guid = WString::Unmanaged(pRequest->CookedUrl.pAbsPath + urlResponsePrefix.Length());
|
|
if (auto connection = FindExistingConnection(guid))
|
|
{
|
|
auto responseToClient = connection->SubmitResponse(pRequest);
|
|
auto result = HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 200, L"OK", responseToClient, HttpNetworkProtocolContentType });
|
|
CHECK_ERROR(
|
|
result == NO_ERROR || result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED,
|
|
L"HttpSendHttpResponse failed for responding /Response."
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
HttpServerApi::SendResponse(GetHttpRequestQueue(), pRequest->RequestId, { 404, L"Unknown URL" });
|
|
}
|
|
}
|
|
|
|
void HttpServer::OnHttpServerStopping()
|
|
{
|
|
List<Ptr<HttpServerConnection>> stoppingConnections;
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
for (auto connection : connections.Values())
|
|
{
|
|
stoppingConnections.Add(connection);
|
|
}
|
|
connections.Clear();
|
|
}
|
|
for (auto connection : stoppingConnections)
|
|
{
|
|
SPIN_LOCK(connection->pendingRequestLock)
|
|
{
|
|
connection->OnCancelCurrentHttpRequestForPendingRequest();
|
|
}
|
|
connection->server = nullptr;
|
|
}
|
|
for (auto connection : stoppingConnections)
|
|
{
|
|
if (connection->callback)
|
|
{
|
|
connection->callback->OnDisconnected();
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpServer
|
|
***********************************************************************/
|
|
|
|
HttpServer::HttpServer(const WString _baseUrl, vint port)
|
|
: HttpServerApi(WString::Unmanaged(L"http://localhost:") + itow(port) + _baseUrl + L"/", true)
|
|
, baseUrl(_baseUrl)
|
|
{
|
|
urlConnect = baseUrl + HttpServerUrl_Connect;
|
|
urlRequestPrefix = baseUrl + HttpServerUrl_Request + L"/";
|
|
urlResponsePrefix = baseUrl + HttpServerUrl_Response + L"/";
|
|
}
|
|
|
|
HttpServer::~HttpServer()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
WaitForClientResult HttpServer::OnClientConnected(INetworkProtocolConnection* connection)
|
|
{
|
|
return WaitForClientResult::Accept;
|
|
}
|
|
|
|
void HttpServer::Start()
|
|
{
|
|
HttpServerApi::Start();
|
|
}
|
|
|
|
void HttpServer::Stop()
|
|
{
|
|
HttpServerApi::Stop();
|
|
}
|
|
|
|
bool HttpServer::IsStopped()
|
|
{
|
|
return HttpServerApi::IsStopped();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\HTTPSERVERAPI.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
#pragma comment(lib, "Httpapi.lib")
|
|
|
|
namespace vl::inter_process::windows_http
|
|
{
|
|
|
|
using namespace vl::collections;
|
|
|
|
/***********************************************************************
|
|
HttpServerApi (ListenToHttpRequest)
|
|
***********************************************************************/
|
|
|
|
void HttpServerApi::OnHttpConnectionBrokenUnsafe()
|
|
{
|
|
if (state == State::Running)
|
|
{
|
|
state = State::Stopping;
|
|
OnHttpServerStopping();
|
|
}
|
|
}
|
|
|
|
void HttpServerApi::OnHttpRequestReceivedUnsafe(PHTTP_REQUEST pRequest)
|
|
{
|
|
if (state == State::Stopping)
|
|
{
|
|
SendResponse(httpRequestQueue, pRequest->RequestId, { 404, L"Server is stopping" });
|
|
return;
|
|
}
|
|
|
|
if (respondToOptions && pRequest->Verb == HttpVerbOPTIONS)
|
|
{
|
|
SendOptionsResponse(httpRequestQueue, pRequest->RequestId);
|
|
}
|
|
else
|
|
{
|
|
OnHttpRequestReceived(pRequest);
|
|
}
|
|
ListenToHttpRequest();
|
|
}
|
|
|
|
ULONG HttpServerApi::ListenToHttpRequest_Init(OVERLAPPED* overlapped)
|
|
{
|
|
ZeroMemory(&bufferRequest[0], bufferRequest.Count());
|
|
|
|
ULONG result = HttpReceiveHttpRequest(
|
|
httpRequestQueue,
|
|
HTTP_NULL_ID,
|
|
0,
|
|
(PHTTP_REQUEST)&bufferRequest[0],
|
|
(ULONG)bufferRequest.Count(),
|
|
NULL,
|
|
overlapped);
|
|
|
|
return result;
|
|
}
|
|
|
|
ULONG HttpServerApi::ListenToHttpRequest_InitMoreData(ULONG* bytesReturned)
|
|
{
|
|
HTTP_REQUEST_ID httpRequestIdReading = ((PHTTP_REQUEST)&bufferRequest[0])->RequestId;
|
|
ZeroMemory(&bufferRequest[0], bufferRequest.Count());
|
|
|
|
ULONG result = HttpReceiveHttpRequest(
|
|
httpRequestQueue,
|
|
httpRequestIdReading,
|
|
0,
|
|
(PHTTP_REQUEST)&bufferRequest[0],
|
|
(ULONG)bufferRequest.Count(),
|
|
bytesReturned,
|
|
NULL);
|
|
|
|
return result;
|
|
}
|
|
|
|
ULONG HttpServerApi::ListenToHttpRequest_OverlappedMoreData(vint expectedBufferSize)
|
|
{
|
|
HTTP_REQUEST_ID httpRequestIdReading = ((PHTTP_REQUEST)&bufferRequest[0])->RequestId;
|
|
bufferRequest.Resize(expectedBufferSize);
|
|
ZeroMemory(&bufferRequest[0], bufferRequest.Count());
|
|
|
|
ULONG bytesReturned = 0;
|
|
ULONG result = HttpReceiveHttpRequest(
|
|
httpRequestQueue,
|
|
httpRequestIdReading,
|
|
0,
|
|
(PHTTP_REQUEST)&bufferRequest[0],
|
|
(ULONG)bufferRequest.Count(),
|
|
&bytesReturned,
|
|
NULL);
|
|
|
|
return result;
|
|
}
|
|
|
|
void HttpServerApi::ListenToHttpRequest()
|
|
{
|
|
if (state == State::Stopping) return;
|
|
|
|
ResetEvent(hEventRequest);
|
|
ZeroMemory(&overlappedRequest, sizeof(overlappedRequest));
|
|
overlappedRequest.hEvent = hEventRequest;
|
|
|
|
ZeroMemory(&bufferRequest[0], sizeof(HTTP_REQUEST));
|
|
PHTTP_REQUEST pRequest = (PHTTP_REQUEST)&bufferRequest[0];
|
|
|
|
ULONG result = ListenToHttpRequest_Init(&overlappedRequest);
|
|
if (result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED)
|
|
{
|
|
OnHttpConnectionBrokenUnsafe();
|
|
return;
|
|
}
|
|
|
|
if (result == NO_ERROR)
|
|
{
|
|
OnHttpRequestReceivedUnsafe(pRequest);
|
|
return;
|
|
}
|
|
|
|
if (result == ERROR_MORE_DATA)
|
|
{
|
|
ULONG bytesReturned = 0;
|
|
result = ListenToHttpRequest_InitMoreData(&bytesReturned);
|
|
if (result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED)
|
|
{
|
|
OnHttpConnectionBrokenUnsafe();
|
|
return;
|
|
}
|
|
CHECK_ERROR(result == ERROR_MORE_DATA, L"HttpReceiveHttpRequest(#1) failed on unexpected result.");
|
|
|
|
result = ListenToHttpRequest_OverlappedMoreData((vint)bytesReturned);
|
|
if (result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED)
|
|
{
|
|
OnHttpConnectionBrokenUnsafe();
|
|
return;
|
|
}
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpReceiveHttpRequest(#2) failed on unexpected result.");
|
|
|
|
PHTTP_REQUEST pRequest = (PHTTP_REQUEST)&bufferRequest[0];
|
|
OnHttpRequestReceivedUnsafe(pRequest);
|
|
return;
|
|
}
|
|
|
|
CHECK_ERROR(result == ERROR_IO_PENDING, L"HttpReceiveHttpRequest(#3) failed on unexpected result.");
|
|
|
|
BOOL waitResult = RegisterWaitForSingleObject(
|
|
&hWaitHandleRequest,
|
|
hEventRequest,
|
|
[](PVOID lpParameter, BOOLEAN TimerOrWaitFired)
|
|
{
|
|
auto self = (HttpServerApi*)lpParameter;
|
|
struct PendingCallbackScope
|
|
{
|
|
HttpServerApi* server;
|
|
|
|
PendingCallbackScope(HttpServerApi* _server)
|
|
: server(_server)
|
|
{
|
|
server->BeginPendingCallback();
|
|
}
|
|
|
|
~PendingCallbackScope()
|
|
{
|
|
server->EndPendingCallback();
|
|
}
|
|
} pendingCallbackScope(self);
|
|
|
|
auto waitHandle = std::atomic_ref<HANDLE>(self->hWaitHandleRequest).exchange(INVALID_HANDLE_VALUE);
|
|
if (waitHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
UnregisterWait(waitHandle);
|
|
}
|
|
|
|
DWORD read = 0;
|
|
BOOL result = GetOverlappedResult(self->httpRequestQueue, &self->overlappedRequest, &read, FALSE);
|
|
if (result == TRUE)
|
|
{
|
|
PHTTP_REQUEST pRequest = (PHTTP_REQUEST)&self->bufferRequest[0];
|
|
self->OnHttpRequestReceivedUnsafe(pRequest);
|
|
}
|
|
else
|
|
{
|
|
DWORD error = GetLastError();
|
|
if (error == ERROR_CONNECTION_INVALID || error == ERROR_OPERATION_ABORTED)
|
|
{
|
|
self->OnHttpConnectionBrokenUnsafe();
|
|
return;
|
|
}
|
|
CHECK_ERROR(error == ERROR_MORE_DATA, L"GetOverlappedResult(#4) failed on unexpected GetLastError.");
|
|
CHECK_ERROR(self->bufferRequest.Count() < (vint)read, L"GetOverlappedResult(#5) failed on unexpected read size.");
|
|
|
|
ULONG result = self->ListenToHttpRequest_OverlappedMoreData((vint)read);
|
|
if (result == ERROR_CONNECTION_INVALID || result == ERROR_OPERATION_ABORTED)
|
|
{
|
|
self->OnHttpConnectionBrokenUnsafe();
|
|
return;
|
|
}
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpReceiveHttpRequest(#6) failed on unexpected result.");
|
|
|
|
PHTTP_REQUEST pRequest = (PHTTP_REQUEST)&self->bufferRequest[0];
|
|
self->OnHttpRequestReceivedUnsafe(pRequest);
|
|
}
|
|
},
|
|
this,
|
|
INFINITE,
|
|
WT_EXECUTEONLYONCE);
|
|
CHECK_ERROR(waitResult, L"RegisterWaitForSingleObject failed for HttpReceiveHttpRequest.");
|
|
if (state == State::Stopping)
|
|
{
|
|
auto waitHandle = std::atomic_ref<HANDLE>(hWaitHandleRequest).exchange(INVALID_HANDLE_VALUE);
|
|
if (waitHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
UnregisterWaitEx(waitHandle, INVALID_HANDLE_VALUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HttpServerApi::BeginPendingCallback()
|
|
{
|
|
if (pendingCallbacks++ == 0)
|
|
{
|
|
eventPendingCallbacks.Unsignal();
|
|
}
|
|
}
|
|
|
|
void HttpServerApi::EndPendingCallback()
|
|
{
|
|
if (--pendingCallbacks == 0)
|
|
{
|
|
eventPendingCallbacks.Signal();
|
|
}
|
|
}
|
|
|
|
void HttpServerApi::OnHttpServerStopping()
|
|
{
|
|
}
|
|
|
|
HANDLE HttpServerApi::GetHttpRequestQueue() const
|
|
{
|
|
return httpRequestQueue;
|
|
}
|
|
|
|
Nullable<WString> HttpServerApi::GetUtf8Body(PHTTP_REQUEST pRequest)
|
|
{
|
|
ULONG bodyLength = 0;
|
|
ULONG bodyReceived = 0;
|
|
{
|
|
auto& headerContentType = pRequest->Headers.KnownHeaders[HttpHeaderContentType];
|
|
CHECK_ERROR(headerContentType.pRawValue != NULL, L"/Response missing Content-Type header.");
|
|
CHECK_ERROR(
|
|
strncmp((const char*)headerContentType.pRawValue, "application/json; charset=utf8", headerContentType.RawValueLength) == 0,
|
|
L"/Response Content-Type header must be \"application/json; charset=utf8\".");
|
|
}
|
|
{
|
|
auto& headerContentLength = pRequest->Headers.KnownHeaders[HttpHeaderContentLength];
|
|
CHECK_ERROR(headerContentLength.pRawValue != NULL, L"/Response missing Content-Type header.");
|
|
bodyLength = (ULONG)atoi(headerContentLength.pRawValue);
|
|
}
|
|
CHECK_ERROR(bodyLength > 0, L"/Response must contain body data.");
|
|
|
|
Array<char8_t> bodyBuffer(bodyLength + 1);
|
|
ZeroMemory(&bodyBuffer[0], bodyBuffer.Count() * sizeof(char8_t));
|
|
ULONG bodyWritten = 0;
|
|
for (USHORT i = 0; i < pRequest->EntityChunkCount; i++)
|
|
{
|
|
auto& chunk = pRequest->pEntityChunks[i];
|
|
CHECK_ERROR(chunk.DataChunkType == HttpDataChunkFromMemory, L"/Response contains an unsupported body chunk.");
|
|
auto chunkLength = chunk.FromMemory.BufferLength;
|
|
CHECK_ERROR(bodyWritten + chunkLength <= bodyLength, L"/Response body is longer than Content-Length.");
|
|
memcpy(&bodyBuffer[bodyWritten], chunk.FromMemory.pBuffer, chunkLength);
|
|
bodyWritten += chunkLength;
|
|
}
|
|
if (pRequest->Flags & HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS)
|
|
{
|
|
ULONG result = NO_ERROR;
|
|
result = HttpReceiveRequestEntityBody(
|
|
httpRequestQueue,
|
|
pRequest->RequestId,
|
|
HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER,
|
|
&bodyBuffer[bodyWritten],
|
|
bodyLength - bodyWritten,
|
|
&bodyReceived,
|
|
NULL);
|
|
CHECK_ERROR(result == NO_ERROR || result == ERROR_HANDLE_EOF, L"HttpReceiveRequestEntityBody.");
|
|
bodyWritten += bodyReceived;
|
|
}
|
|
CHECK_ERROR(bodyWritten == bodyLength, L"/Response body is shorter than Content-Length.");
|
|
|
|
U8String bodyUtf8 = U8String::Unmanaged(&bodyBuffer[0]);
|
|
return u8tow(bodyUtf8);
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpServerApi (Writing)
|
|
***********************************************************************/
|
|
|
|
void HttpServerApi::SendOptionsResponse(HANDLE httpRequestQueue, HTTP_REQUEST_ID requestId)
|
|
{
|
|
ULONG bytesSent = 0;
|
|
HTTP_RESPONSE httpResponse;
|
|
ZeroMemory(&httpResponse, sizeof(httpResponse));
|
|
|
|
httpResponse.StatusCode = 200;
|
|
|
|
static const char headerACAOName[] = "Access-Control-Allow-Origin";
|
|
static const char headerACAMName[] = "Access-Control-Allow-Methods";
|
|
static const char headerACAMValue[] = "GET, POST, OPTIONS";
|
|
static const char headerACAHName[] = "Access-Control-Allow-Headers";
|
|
static const char headerACAHValue[] = "Content-Type";
|
|
static HTTP_UNKNOWN_HEADER unknownHeaders[] = { {
|
|
sizeof(headerACAOName) - 1,
|
|
1,
|
|
headerACAOName,
|
|
"*"
|
|
},{
|
|
sizeof(headerACAMName) - 1,
|
|
sizeof(headerACAMValue) - 1,
|
|
headerACAMName,
|
|
headerACAMValue
|
|
},{
|
|
sizeof(headerACAHName) - 1,
|
|
sizeof(headerACAHValue) - 1,
|
|
headerACAHName,
|
|
headerACAHValue
|
|
} };
|
|
httpResponse.Headers.UnknownHeaderCount = sizeof(unknownHeaders) / sizeof(HTTP_UNKNOWN_HEADER);
|
|
httpResponse.Headers.pUnknownHeaders = unknownHeaders;
|
|
|
|
ULONG result = NO_ERROR;
|
|
result = HttpSendHttpResponse(
|
|
httpRequestQueue,
|
|
requestId,
|
|
0,
|
|
&httpResponse,
|
|
NULL,
|
|
&bytesSent,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
NULL);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpSendHttpResponse failed (OPTIONS).");
|
|
}
|
|
|
|
ULONG HttpServerApi::SendResponse(HANDLE httpRequestQueue, HTTP_REQUEST_ID requestId, const HttpServerResponse& response)
|
|
{
|
|
ULONG bytesSent = 0;
|
|
HTTP_RESPONSE httpResponse;
|
|
HTTP_DATA_CHUNK httpResponseBody;
|
|
ZeroMemory(&httpResponse, sizeof(httpResponse));
|
|
ZeroMemory(&httpResponseBody, sizeof(httpResponseBody));
|
|
|
|
httpResponse.StatusCode = (USHORT)response.statusCode;
|
|
|
|
U8String reasonUtf8;
|
|
if (response.reason != WString::Empty)
|
|
{
|
|
reasonUtf8 = wtou8(response.reason);
|
|
httpResponse.pReason = (PCSTR)reasonUtf8.Buffer();
|
|
httpResponse.ReasonLength = (USHORT)reasonUtf8.Length();
|
|
}
|
|
|
|
U8String bodyUtf8;
|
|
if (response.body != WString::Empty)
|
|
{
|
|
bodyUtf8 = wtou8(response.body);
|
|
httpResponse.EntityChunkCount = 1;
|
|
httpResponse.pEntityChunks = &httpResponseBody;
|
|
httpResponseBody.DataChunkType = HttpDataChunkFromMemory;
|
|
httpResponseBody.FromMemory.pBuffer = (PVOID)bodyUtf8.Buffer();
|
|
httpResponseBody.FromMemory.BufferLength = (ULONG)bodyUtf8.Length();
|
|
}
|
|
|
|
U8String contentTypeUtf8;
|
|
if (response.contentType != WString::Empty)
|
|
{
|
|
contentTypeUtf8 = wtou8(response.contentType);
|
|
httpResponse.Headers.KnownHeaders[HttpHeaderContentType].pRawValue = (PCSTR)contentTypeUtf8.Buffer();
|
|
httpResponse.Headers.KnownHeaders[HttpHeaderContentType].RawValueLength = (USHORT)contentTypeUtf8.Length();
|
|
}
|
|
|
|
static const char headerACAOName[] = "Access-Control-Allow-Origin";
|
|
static HTTP_UNKNOWN_HEADER unknownHeaders[] = { {
|
|
sizeof(headerACAOName) - 1,
|
|
1,
|
|
headerACAOName,
|
|
"*"
|
|
} };
|
|
httpResponse.Headers.UnknownHeaderCount = sizeof(unknownHeaders) / sizeof(HTTP_UNKNOWN_HEADER);
|
|
httpResponse.Headers.pUnknownHeaders = unknownHeaders;
|
|
|
|
ULONG result = NO_ERROR;
|
|
result = HttpSendHttpResponse(
|
|
httpRequestQueue,
|
|
requestId,
|
|
0,
|
|
&httpResponse,
|
|
NULL,
|
|
&bytesSent,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
NULL);
|
|
return result;
|
|
}
|
|
|
|
void HttpServerApi::SendResponseUtf8(HANDLE httpRequestQueue, HTTP_REQUEST_ID requestId, WString body)
|
|
{
|
|
auto result = SendResponse(httpRequestQueue, requestId, { 200, WString::Unmanaged(L"OK"), body, HttpNetworkProtocolContentType });
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpSendHttpResponse failed for responding UTF-8 body.");
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpServerApi
|
|
***********************************************************************/
|
|
|
|
HttpServerApi::HttpServerApi(const WString& _urlPrefix, bool _respondToOptions)
|
|
: bufferRequest(HttpRequestBufferInitSize)
|
|
, urlPrefix(_urlPrefix)
|
|
, respondToOptions(_respondToOptions)
|
|
{
|
|
hEventRequest = CreateEvent(NULL, TRUE, TRUE, NULL);
|
|
CHECK_ERROR(hEventRequest != NULL, L"HttpServerApi initialization failed on CreateEvent(hEventRequest).");
|
|
CHECK_ERROR(eventPendingCallbacks.CreateManualUnsignal(true), L"HttpServerApi initialization failed on eventPendingCallbacks.CreateManualUnsignal.");
|
|
|
|
ULONG result = NO_ERROR;
|
|
|
|
result = HttpInitialize(
|
|
HTTPAPI_VERSION_2,
|
|
HTTP_INITIALIZE_SERVER,
|
|
NULL);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpInitialize failed.");
|
|
|
|
result = HttpCreateRequestQueue(
|
|
HTTPAPI_VERSION_2,
|
|
NULL,
|
|
NULL,
|
|
0,
|
|
&httpRequestQueue);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpCreateRequestQueue failed.");
|
|
|
|
result = HttpCreateServerSession(
|
|
HTTPAPI_VERSION_2,
|
|
&httpSessionId,
|
|
0);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpCreateServerSession failed.");
|
|
|
|
result = HttpCreateUrlGroup(
|
|
httpSessionId,
|
|
&httpUrlGroupId,
|
|
0);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpCreateUrlGroup failed.");
|
|
|
|
result = HttpAddUrlToUrlGroup(
|
|
httpUrlGroupId,
|
|
urlPrefix.Buffer(),
|
|
0,
|
|
0);
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpAddUrlToUrlGroup failed.");
|
|
|
|
HTTP_BINDING_INFO bindingInfo;
|
|
ZeroMemory(&bindingInfo, sizeof(bindingInfo));
|
|
bindingInfo.Flags.Present = 1;
|
|
bindingInfo.RequestQueueHandle = httpRequestQueue;
|
|
|
|
result = HttpSetUrlGroupProperty(
|
|
httpUrlGroupId,
|
|
HttpServerBindingProperty,
|
|
&bindingInfo,
|
|
sizeof(bindingInfo));
|
|
CHECK_ERROR(result == NO_ERROR, L"HttpSetUrlGroupProperty failed (HttpServerBindingProperty).");
|
|
}
|
|
|
|
HttpServerApi::~HttpServerApi()
|
|
{
|
|
Stop();
|
|
CloseHandle(hEventRequest);
|
|
}
|
|
|
|
void HttpServerApi::Start()
|
|
{
|
|
CHECK_ERROR(state == State::Ready, L"HttpServerApi can only be started once.");
|
|
state = State::Running;
|
|
ListenToHttpRequest();
|
|
}
|
|
|
|
void HttpServerApi::Stop()
|
|
{
|
|
state = State::Stopping;
|
|
auto waitHandle = std::atomic_ref<HANDLE>(hWaitHandleRequest).exchange(INVALID_HANDLE_VALUE);
|
|
if (waitHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
UnregisterWaitEx(waitHandle, INVALID_HANDLE_VALUE);
|
|
}
|
|
eventPendingCallbacks.Wait();
|
|
|
|
OnHttpServerStopping();
|
|
|
|
if (httpRequestQueue != INVALID_HANDLE_VALUE)
|
|
{
|
|
HttpCloseUrlGroup(httpUrlGroupId);
|
|
HttpCloseServerSession(httpSessionId);
|
|
HttpCloseRequestQueue(httpRequestQueue);
|
|
httpRequestQueue = INVALID_HANDLE_VALUE;
|
|
httpUrlGroupId = HTTP_NULL_ID;
|
|
httpSessionId = HTTP_NULL_ID;
|
|
|
|
HttpTerminate(
|
|
HTTP_INITIALIZE_SERVER,
|
|
NULL);
|
|
}
|
|
}
|
|
|
|
bool HttpServerApi::IsStopped()
|
|
{
|
|
return state == State::Stopping;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\NAMEDPIPE.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process::named_pipe
|
|
{
|
|
|
|
using namespace vl::console;
|
|
using namespace vl::collections;
|
|
|
|
class NamedPipeConnection::ReadWaitContext
|
|
{
|
|
public:
|
|
NamedPipeConnection* connection = nullptr;
|
|
HANDLE hWaitHandle = INVALID_HANDLE_VALUE;
|
|
EventObject eventRegistrationFinished;
|
|
atomic_vint callbackStarted = 0;
|
|
|
|
ReadWaitContext()
|
|
{
|
|
CHECK_ERROR(eventRegistrationFinished.CreateManualUnsignal(false), L"ReadFile failed on eventRegistrationFinished.CreateManualUnsignal.");
|
|
}
|
|
};
|
|
|
|
class NamedPipeServer::PendingConnection::ConnectWaitContext
|
|
{
|
|
public:
|
|
Ptr<PendingConnection> pendingConnection;
|
|
HANDLE hWaitHandle = INVALID_HANDLE_VALUE;
|
|
EventObject eventRegistrationFinished;
|
|
atomic_vint callbackStarted = 0;
|
|
|
|
ConnectWaitContext(Ptr<PendingConnection> _pendingConnection)
|
|
: pendingConnection(_pendingConnection)
|
|
{
|
|
CHECK_ERROR(eventRegistrationFinished.CreateManualUnsignal(false), L"ConnectNamedPipe failed on eventRegistrationFinished.CreateManualUnsignal.");
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
NamedPipeConnection (Reading)
|
|
***********************************************************************/
|
|
|
|
void NamedPipeConnection::BeginReadingUnsafe()
|
|
{
|
|
if (firstRead)
|
|
{
|
|
streamReadFile.SeekFromBegin(0);
|
|
}
|
|
}
|
|
|
|
void NamedPipeConnection::SubmitReadBufferUnsafe(vint bytes)
|
|
{
|
|
streamReadFile.Write(&bufferReadFile[0], bytes);
|
|
}
|
|
|
|
void NamedPipeConnection::EndReadingUnsafe()
|
|
{
|
|
vint32_t position = (vint32_t)streamReadFile.Position();
|
|
streamReadFile.SeekFromBegin(0);
|
|
CHECK_ERROR(position >= 2 * sizeof(vint32_t), L"ReadFile failed on incomplete message.");
|
|
|
|
vint32_t bytes = 0;
|
|
vint consumed = 0;
|
|
consumed = streamReadFile.Read(&bytes, sizeof(bytes));
|
|
CHECK_ERROR(consumed == sizeof(bytes), L"ReadFile failed on incomplete message.");
|
|
|
|
if (bytes > position - (vint32_t)sizeof(bytes))
|
|
{
|
|
firstRead = false;
|
|
streamReadFile.SeekFromBegin(position);
|
|
return;
|
|
}
|
|
CHECK_ERROR(bytes == position - sizeof(bytes), L"ReadFile failed on corrupted message.");
|
|
firstRead = true;
|
|
|
|
Array<wchar_t> strBuffer;
|
|
auto ReadSingleString = [&]()
|
|
{
|
|
vint32_t length = 0;
|
|
consumed = streamReadFile.Read(&length, sizeof(length));
|
|
CHECK_ERROR(consumed == sizeof(length) && streamReadFile.Position() <= position, L"ReadFile failed on incomplete message.");
|
|
|
|
if (length == 0)
|
|
{
|
|
return WString::Empty;
|
|
}
|
|
else
|
|
{
|
|
strBuffer.Resize(length);
|
|
consumed = streamReadFile.Read(&strBuffer[0], length * sizeof(wchar_t));
|
|
CHECK_ERROR(consumed == length * sizeof(wchar_t) && streamReadFile.Position() <= position, L"ReadFile failed on incomplete message.");
|
|
return WString::CopyFrom(&strBuffer[0], length);
|
|
}
|
|
};
|
|
|
|
callback->OnReadString(ReadSingleString());
|
|
|
|
CHECK_ERROR(streamReadFile.Position() == position, L"ReadFile failed on incomplete message.");
|
|
}
|
|
|
|
void NamedPipeConnection::BeginReadingLoopUnsafe()
|
|
{
|
|
if (stopped) return;
|
|
RESTART_LOOP:
|
|
{
|
|
BeginReadingUnsafe();
|
|
ResetEvent(hEventReadFile);
|
|
ZeroMemory(&overlappedReadFile, sizeof(overlappedReadFile));
|
|
overlappedReadFile.hEvent = hEventReadFile;
|
|
DWORD read = 0;
|
|
BOOL result = ReadFile(hPipe, &bufferReadFile[0], sizeof(BYTE) * MaxMessageSize, &read, &overlappedReadFile);
|
|
|
|
if (result == TRUE)
|
|
{
|
|
SubmitReadBufferUnsafe((vint)read);
|
|
EndReadingUnsafe();
|
|
goto RESTART_LOOP;
|
|
}
|
|
|
|
DWORD error = GetLastError();
|
|
if (error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
OnDisconnected();
|
|
}
|
|
return;
|
|
}
|
|
if (error == ERROR_INVALID_HANDLE)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
OnLocalError(L"ReadFile failed because the named pipe was closed.");
|
|
OnDisconnected();
|
|
}
|
|
return;
|
|
}
|
|
CHECK_ERROR(error == ERROR_MORE_DATA || error == ERROR_IO_PENDING, L"ReadFile failed on unexpected GetLastError.");
|
|
|
|
auto context = new ReadWaitContext;
|
|
context->connection = this;
|
|
BeginPendingCallback();
|
|
|
|
ReadWaitContext* expectedContext = nullptr;
|
|
if (stopped || !readWaitContext.compare_exchange_strong(expectedContext, context))
|
|
{
|
|
EndPendingCallback();
|
|
delete context;
|
|
return;
|
|
}
|
|
|
|
BOOL waitResult = RegisterWaitForSingleObject(
|
|
&context->hWaitHandle,
|
|
hEventReadFile,
|
|
[](PVOID lpParameter, BOOLEAN TimerOrWaitFired)
|
|
{
|
|
auto context = (ReadWaitContext*)lpParameter;
|
|
context->callbackStarted = 1;
|
|
|
|
auto self = context->connection;
|
|
ReadWaitContext* expectedContext = context;
|
|
bool ownsContext = self->readWaitContext.compare_exchange_strong(expectedContext, nullptr);
|
|
|
|
auto finalize = [=]()
|
|
{
|
|
if (ownsContext)
|
|
{
|
|
context->eventRegistrationFinished.Wait();
|
|
UnregisterWait(context->hWaitHandle);
|
|
}
|
|
self->EndPendingCallback();
|
|
if (ownsContext)
|
|
{
|
|
delete context;
|
|
}
|
|
};
|
|
|
|
DWORD read = 0;
|
|
BOOL result = GetOverlappedResult(self->hPipe, &self->overlappedReadFile, &read, FALSE);
|
|
if (result == TRUE)
|
|
{
|
|
self->SubmitReadBufferUnsafe((vint)read);
|
|
self->EndReadingUnsafe();
|
|
}
|
|
else
|
|
{
|
|
DWORD error = GetLastError();
|
|
if (error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA)
|
|
{
|
|
if (!self->stopped)
|
|
{
|
|
self->OnDisconnected();
|
|
}
|
|
finalize();
|
|
return;
|
|
}
|
|
if (error == ERROR_OPERATION_ABORTED || error == ERROR_INVALID_HANDLE)
|
|
{
|
|
if (!self->stopped)
|
|
{
|
|
self->OnLocalError(L"GetOverlappedResult(ReadFile) failed because the named pipe was closed.");
|
|
self->OnDisconnected();
|
|
}
|
|
finalize();
|
|
return;
|
|
}
|
|
CHECK_ERROR(error == ERROR_MORE_DATA, L"GetOverlappedResult(ReadFile) failed on unexpected GetLastError.");
|
|
self->SubmitReadBufferUnsafe((vint)read);
|
|
}
|
|
if (!self->stopped)
|
|
{
|
|
self->BeginReadingLoopUnsafe();
|
|
}
|
|
finalize();
|
|
},
|
|
context,
|
|
INFINITE,
|
|
WT_EXECUTEONLYONCE);
|
|
|
|
context->eventRegistrationFinished.Signal();
|
|
if (!waitResult)
|
|
{
|
|
expectedContext = context;
|
|
if (readWaitContext.compare_exchange_strong(expectedContext, nullptr))
|
|
{
|
|
EndPendingCallback();
|
|
delete context;
|
|
CHECK_FAIL(L"RegisterWaitForSingleObject failed for ReadFile.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NamedPipeConnection (Writing)
|
|
***********************************************************************/
|
|
|
|
vint32_t NamedPipeConnection::WriteInt32ToStream(vint32_t number)
|
|
{
|
|
return (vint32_t)streamWriteFile.Write(&number, sizeof(number));
|
|
}
|
|
|
|
vint32_t NamedPipeConnection::WriteStringToStream(const WString& str)
|
|
{
|
|
vint32_t bytes = 0;
|
|
vint32_t count = (vint32_t)str.Length();
|
|
bytes += (vint32_t)streamWriteFile.Write(&count, sizeof(count));
|
|
if (count > 0)
|
|
{
|
|
bytes += (vint32_t)streamWriteFile.Write((void*)str.Buffer(), sizeof(wchar_t) * str.Length());
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
void NamedPipeConnection::BeginSendStream()
|
|
{
|
|
vint32_t bytes = 0;
|
|
streamWriteFile.SeekFromBegin(0);
|
|
streamWriteFile.Write(&bytes, sizeof(bytes));
|
|
}
|
|
|
|
void NamedPipeConnection::EndSendStream(vint32_t bytes)
|
|
{
|
|
streamWriteFile.SeekFromBegin(0);
|
|
WriteInt32ToStream(bytes);
|
|
|
|
vint32_t length = bytes + sizeof(bytes);
|
|
for (vint i = 0; i < (length + MaxMessageSize - 1) / MaxMessageSize; i++)
|
|
{
|
|
vint offset = i * MaxMessageSize;
|
|
vint bytesToSend = length >= (i + 1) * MaxMessageSize ? MaxMessageSize : length % MaxMessageSize;
|
|
auto buffer = (LPCVOID)((char*)streamWriteFile.GetInternalBuffer() + offset);
|
|
|
|
ResetEvent(hEventWriteFile);
|
|
ZeroMemory(&overlappedWriteFile, sizeof(overlappedWriteFile));
|
|
overlappedWriteFile.hEvent = hEventWriteFile;
|
|
DWORD written = 0;
|
|
BOOL result = WriteFile(hPipe, buffer, (DWORD)bytesToSend, NULL, &overlappedWriteFile);
|
|
if (result == FALSE)
|
|
{
|
|
auto error = GetLastError();
|
|
if (error == ERROR_BROKEN_PIPE || error == ERROR_INVALID_HANDLE)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
OnLocalError(L"WriteFile failed because the named pipe was closed.");
|
|
}
|
|
OnDisconnected();
|
|
return;
|
|
}
|
|
CHECK_ERROR(error == ERROR_IO_PENDING, L"WriteFile failed on unexpected GetLastError.");
|
|
WaitForSingleObject(hEventWriteFile, INFINITE);
|
|
result = GetOverlappedResult(hPipe, &overlappedWriteFile, &written, FALSE);
|
|
if (result == FALSE)
|
|
{
|
|
error = GetLastError();
|
|
if (error == ERROR_BROKEN_PIPE || error == ERROR_INVALID_HANDLE || error == ERROR_OPERATION_ABORTED)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
OnLocalError(L"GetOverlappedResult(WriteFile) failed because the named pipe was closed.");
|
|
}
|
|
OnDisconnected();
|
|
return;
|
|
}
|
|
CHECK_FAIL(L"GetOverlappedResult(WriteFile) failed on unexpected GetLastError.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
written = (DWORD)bytesToSend;
|
|
}
|
|
CHECK_ERROR(written == (DWORD)bytesToSend, L"WriteFile failed to write all data.");
|
|
}
|
|
}
|
|
|
|
void NamedPipeConnection::SendString(const WString& str)
|
|
{
|
|
if (stopped) return;
|
|
SPIN_LOCK(lockWrite)
|
|
{
|
|
vint32_t bytes = 0;
|
|
BeginSendStream();
|
|
bytes += WriteStringToStream(str);
|
|
EndSendStream(bytes);
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NamedPipeConnection
|
|
***********************************************************************/
|
|
|
|
void NamedPipeConnection::OnLocalError(const WString& errorMessage)
|
|
{
|
|
if (callback)
|
|
{
|
|
callback->OnLocalError(errorMessage, true);
|
|
}
|
|
}
|
|
|
|
void NamedPipeConnection::OnDisconnected()
|
|
{
|
|
if (callback)
|
|
{
|
|
callback->OnDisconnected();
|
|
}
|
|
auto owningServer = server;
|
|
if (owningServer && pendingCallbacks == 0)
|
|
{
|
|
SPIN_LOCK(owningServer->lockConnections)
|
|
{
|
|
if (server == owningServer)
|
|
{
|
|
owningServer->connections.Remove(this);
|
|
server = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NamedPipeConnection::NamedPipeConnection(HANDLE _hPipe)
|
|
: bufferReadFile(MaxMessageSize)
|
|
{
|
|
hPipe = _hPipe;
|
|
|
|
hEventReadFile = CreateEvent(NULL, TRUE, TRUE, NULL);
|
|
CHECK_ERROR(hEventReadFile != NULL, L"NamedPipeConnection initialization failed on CreateEvent(hEventReadFile).");
|
|
|
|
hEventWriteFile = CreateEvent(NULL, TRUE, TRUE, NULL);
|
|
CHECK_ERROR(hEventWriteFile != NULL, L"NamedPipeConnection initialization failed on CreateEvent(hEventWriteFile).");
|
|
|
|
CHECK_ERROR(eventPendingCallbacks.CreateManualUnsignal(true), L"NamedPipeConnection initialization failed on eventPendingCallbacks.CreateManualUnsignal.");
|
|
}
|
|
|
|
NamedPipeConnection::~NamedPipeConnection()
|
|
{
|
|
Stop();
|
|
CloseHandle(hEventReadFile);
|
|
CloseHandle(hEventWriteFile);
|
|
}
|
|
|
|
void NamedPipeConnection::InstallCallback(INetworkProtocolCallback* _callback)
|
|
{
|
|
CHECK_ERROR(!callback || !_callback, L"NamedPipeConnection::InstallCallback only accepts one callback at a time.");
|
|
callback = _callback;
|
|
if (!callback) return;
|
|
callback->OnInstalled(this);
|
|
}
|
|
|
|
void NamedPipeConnection::Stop()
|
|
{
|
|
stopped = 1;
|
|
SPIN_LOCK(lockWrite)
|
|
{
|
|
if (hPipe != INVALID_HANDLE_VALUE)
|
|
{
|
|
CancelIoEx(hPipe, NULL);
|
|
}
|
|
}
|
|
|
|
ReadWaitContext* context = readWaitContext.exchange(nullptr);
|
|
if (context)
|
|
{
|
|
context->eventRegistrationFinished.Wait();
|
|
if (context->hWaitHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
UnregisterWaitEx(context->hWaitHandle, INVALID_HANDLE_VALUE);
|
|
}
|
|
if (context->callbackStarted == 0)
|
|
{
|
|
EndPendingCallback();
|
|
}
|
|
delete context;
|
|
}
|
|
eventPendingCallbacks.Wait();
|
|
|
|
SPIN_LOCK(lockWrite)
|
|
{
|
|
if (hPipe != INVALID_HANDLE_VALUE)
|
|
{
|
|
CloseHandle(hPipe);
|
|
hPipe = INVALID_HANDLE_VALUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NamedPipeConnection::BeginPendingCallback()
|
|
{
|
|
if (pendingCallbacks++ == 0)
|
|
{
|
|
eventPendingCallbacks.Unsignal();
|
|
}
|
|
}
|
|
|
|
void NamedPipeConnection::EndPendingCallback()
|
|
{
|
|
if (--pendingCallbacks == 0)
|
|
{
|
|
eventPendingCallbacks.Signal();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NamedPipeServer::PendingConnection
|
|
***********************************************************************/
|
|
|
|
NamedPipeServer::PendingConnection::PendingConnection(NamedPipeServer* _server, Ptr<NamedPipeConnection> _connection)
|
|
: server(_server)
|
|
, connection(_connection)
|
|
{
|
|
ZeroMemory(&overlappedConnect, sizeof(overlappedConnect));
|
|
hEventConnect = CreateEvent(NULL, TRUE, FALSE, NULL);
|
|
CHECK_ERROR(hEventConnect != NULL, L"ConnectNamedPipe failed on CreateEvent.");
|
|
overlappedConnect.hEvent = hEventConnect;
|
|
CHECK_ERROR(eventPendingCallbacks.CreateManualUnsignal(true), L"ConnectNamedPipe failed on eventPendingCallbacks.CreateManualUnsignal.");
|
|
}
|
|
|
|
NamedPipeServer::PendingConnection::~PendingConnection()
|
|
{
|
|
Stop();
|
|
CloseHandle(hEventConnect);
|
|
}
|
|
|
|
void NamedPipeServer::PendingConnection::Stop()
|
|
{
|
|
server = nullptr;
|
|
ConnectWaitContext* context = connectWaitContext.exchange(nullptr);
|
|
if (context)
|
|
{
|
|
context->eventRegistrationFinished.Wait();
|
|
if (context->hWaitHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
UnregisterWaitEx(context->hWaitHandle, INVALID_HANDLE_VALUE);
|
|
}
|
|
if (context->callbackStarted == 0)
|
|
{
|
|
EndPendingCallback();
|
|
}
|
|
delete context;
|
|
}
|
|
eventPendingCallbacks.Wait();
|
|
if (connection)
|
|
{
|
|
connection->Stop();
|
|
connection = nullptr;
|
|
}
|
|
}
|
|
|
|
void NamedPipeServer::PendingConnection::BeginPendingCallback()
|
|
{
|
|
if (pendingCallbacks++ == 0)
|
|
{
|
|
eventPendingCallbacks.Unsignal();
|
|
}
|
|
}
|
|
|
|
void NamedPipeServer::PendingConnection::EndPendingCallback()
|
|
{
|
|
if (--pendingCallbacks == 0)
|
|
{
|
|
eventPendingCallbacks.Signal();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NamedPipeServer
|
|
***********************************************************************/
|
|
|
|
HANDLE NamedPipeServer::ServerCreatePipe(const WString& pipeName)
|
|
{
|
|
HANDLE hPipe = CreateNamedPipe(
|
|
(L"\\\\.\\pipe\\" + pipeName).Buffer(),
|
|
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_REJECT_REMOTE_CLIENTS,
|
|
PIPE_UNLIMITED_INSTANCES,
|
|
65536,
|
|
65536,
|
|
6000,
|
|
NULL);
|
|
CHECK_ERROR(hPipe != INVALID_HANDLE_VALUE, L"CreateNamedPipe failed.");
|
|
return hPipe;
|
|
}
|
|
|
|
NamedPipeServer::NamedPipeServer(const WString& _pipeName)
|
|
: pipeName(_pipeName)
|
|
{
|
|
}
|
|
|
|
NamedPipeServer::~NamedPipeServer()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
void NamedPipeServer::BeginListening()
|
|
{
|
|
Ptr<PendingConnection> pendingConnection;
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
if (!started || stopped)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto connection = Ptr(new NamedPipeConnection(ServerCreatePipe(pipeName)));
|
|
pendingConnection = Ptr(new PendingConnection(this, connection));
|
|
|
|
BOOL result = ConnectNamedPipe(connection->hPipe, &pendingConnection->overlappedConnect);
|
|
DWORD error = result ? ERROR_SUCCESS : GetLastError();
|
|
switch (error)
|
|
{
|
|
case ERROR_SUCCESS:
|
|
case ERROR_PIPE_CONNECTED:
|
|
CompletePendingConnection(pendingConnection, true);
|
|
break;
|
|
case ERROR_IO_PENDING:
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
if (stopped)
|
|
{
|
|
pendingConnection->Stop();
|
|
return;
|
|
}
|
|
pendingConnections.Add(pendingConnection);
|
|
auto context = new PendingConnection::ConnectWaitContext(pendingConnection);
|
|
pendingConnection->BeginPendingCallback();
|
|
PendingConnection::ConnectWaitContext* expectedContext = nullptr;
|
|
CHECK_ERROR(pendingConnection->connectWaitContext.compare_exchange_strong(expectedContext, context), L"ConnectNamedPipe found an existing wait context.");
|
|
BOOL waitResult = RegisterWaitForSingleObject(
|
|
&context->hWaitHandle,
|
|
pendingConnection->hEventConnect,
|
|
[](PVOID lpParameter, BOOLEAN TimerOrWaitFired)
|
|
{
|
|
auto context = (PendingConnection::ConnectWaitContext*)lpParameter;
|
|
context->callbackStarted = 1;
|
|
|
|
auto pendingConnection = context->pendingConnection;
|
|
PendingConnection::ConnectWaitContext* expectedContext = context;
|
|
bool ownsContext = pendingConnection->connectWaitContext.compare_exchange_strong(expectedContext, nullptr);
|
|
|
|
auto finalize = [=]()
|
|
{
|
|
if (ownsContext)
|
|
{
|
|
context->eventRegistrationFinished.Wait();
|
|
UnregisterWait(context->hWaitHandle);
|
|
}
|
|
pendingConnection->EndPendingCallback();
|
|
if (ownsContext)
|
|
{
|
|
delete context;
|
|
}
|
|
};
|
|
|
|
DWORD transferred = 0;
|
|
BOOL result = GetOverlappedResult(pendingConnection->connection->hPipe, &pendingConnection->overlappedConnect, &transferred, FALSE);
|
|
if (result == TRUE)
|
|
{
|
|
if (pendingConnection->server)
|
|
{
|
|
pendingConnection->server->CompletePendingConnection(pendingConnection.Obj(), true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto error = GetLastError();
|
|
if (error == ERROR_OPERATION_ABORTED || error == ERROR_INVALID_HANDLE || error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA)
|
|
{
|
|
if (pendingConnection->server)
|
|
{
|
|
pendingConnection->server->CompletePendingConnection(pendingConnection.Obj(), false);
|
|
}
|
|
finalize();
|
|
return;
|
|
}
|
|
CHECK_FAIL(L"GetOverlappedResult(ConnectNamedPipe) failed on unexpected GetLastError.");
|
|
}
|
|
finalize();
|
|
},
|
|
context,
|
|
INFINITE,
|
|
WT_EXECUTEONLYONCE);
|
|
|
|
context->eventRegistrationFinished.Signal();
|
|
if (!waitResult)
|
|
{
|
|
expectedContext = context;
|
|
if (pendingConnection->connectWaitContext.compare_exchange_strong(expectedContext, nullptr))
|
|
{
|
|
pendingConnection->EndPendingCallback();
|
|
delete context;
|
|
CHECK_FAIL(L"RegisterWaitForSingleObject failed for ConnectNamedPipe.");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
CHECK_FAIL(L"ConnectNamedPipe failed on unexpected GetLastError.");
|
|
}
|
|
}
|
|
|
|
void NamedPipeServer::CompletePendingConnection(Ptr<PendingConnection> pendingConnection, bool connected)
|
|
{
|
|
auto connection = pendingConnection->connection;
|
|
bool shouldListen = false;
|
|
bool shouldNotify = false;
|
|
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
pendingConnections.Remove(pendingConnection.Obj());
|
|
if (started && !stopped)
|
|
{
|
|
shouldListen = true;
|
|
if (connected)
|
|
{
|
|
connection->server = this;
|
|
connections.Add(connection);
|
|
shouldNotify = true;
|
|
pendingConnection->connection = nullptr;
|
|
pendingConnection->server = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldListen)
|
|
{
|
|
BeginListening();
|
|
}
|
|
|
|
if (shouldNotify)
|
|
{
|
|
auto result = OnClientConnected(connection.Obj());
|
|
if (result == WaitForClientResult::Reject)
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
connections.Remove(connection.Obj());
|
|
}
|
|
connection->server = nullptr;
|
|
connection->Stop();
|
|
}
|
|
}
|
|
else if (connection)
|
|
{
|
|
connection->Stop();
|
|
}
|
|
}
|
|
|
|
void NamedPipeServer::CompletePendingConnection(PendingConnection* pendingConnection, bool connected)
|
|
{
|
|
Ptr<PendingConnection> holding;
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
for (vint i = 0; i < pendingConnections.Count(); i++)
|
|
{
|
|
if (pendingConnections[i].Obj() == pendingConnection)
|
|
{
|
|
holding = pendingConnections[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (holding)
|
|
{
|
|
CompletePendingConnection(holding, connected);
|
|
}
|
|
}
|
|
|
|
WaitForClientResult NamedPipeServer::OnClientConnected(INetworkProtocolConnection* connection)
|
|
{
|
|
return WaitForClientResult::Accept;
|
|
}
|
|
|
|
void NamedPipeServer::Start()
|
|
{
|
|
{
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
CHECK_ERROR(!started, L"NamedPipeServer has already started.");
|
|
CHECK_ERROR(!stopped, L"NamedPipeServer has stopped.");
|
|
started = true;
|
|
}
|
|
}
|
|
BeginListening();
|
|
}
|
|
|
|
void NamedPipeServer::Stop()
|
|
{
|
|
List<Ptr<NamedPipeConnection>> stoppingConnections;
|
|
List<Ptr<PendingConnection>> stoppingPendingConnections;
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
if (!stopped)
|
|
{
|
|
started = false;
|
|
stopped = true;
|
|
for (auto connection : connections)
|
|
{
|
|
connection->server = nullptr;
|
|
stoppingConnections.Add(connection);
|
|
}
|
|
for (auto pendingConnection : pendingConnections)
|
|
{
|
|
stoppingPendingConnections.Add(pendingConnection);
|
|
}
|
|
connections.Clear();
|
|
pendingConnections.Clear();
|
|
}
|
|
}
|
|
|
|
for (auto pendingConnection : stoppingPendingConnections)
|
|
{
|
|
pendingConnection->Stop();
|
|
}
|
|
for (auto connection : stoppingConnections)
|
|
{
|
|
connection->Stop();
|
|
}
|
|
}
|
|
|
|
bool NamedPipeServer::IsStopped()
|
|
{
|
|
bool result = false;
|
|
SPIN_LOCK(lockConnections)
|
|
{
|
|
result = stopped;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
NamedPipeClient
|
|
***********************************************************************/
|
|
|
|
HANDLE NamedPipeClient::ClientCreatePipe(const WString& pipeName)
|
|
{
|
|
auto fullPipeName = L"\\\\.\\pipe\\" + pipeName;
|
|
auto start = GetTickCount64();
|
|
while (true)
|
|
{
|
|
HANDLE hPipe = CreateFile(
|
|
fullPipeName.Buffer(),
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
0,
|
|
NULL,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_OVERLAPPED,
|
|
NULL);
|
|
if (hPipe != INVALID_HANDLE_VALUE)
|
|
{
|
|
return hPipe;
|
|
}
|
|
|
|
auto error = GetLastError();
|
|
CHECK_ERROR(error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY, L"CreateFile failed.");
|
|
CHECK_ERROR(GetTickCount64() - start < 6000, L"CreateFile failed because the pipe server did not become available.");
|
|
if (error == ERROR_PIPE_BUSY)
|
|
{
|
|
WaitNamedPipe(fullPipeName.Buffer(), 100);
|
|
}
|
|
else
|
|
{
|
|
Thread::Sleep(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
NamedPipeClient::NamedPipeClient(const WString& _pipeName)
|
|
: NamedPipeConnection(ClientCreatePipe(_pipeName))
|
|
{
|
|
}
|
|
|
|
NamedPipeClient::~NamedPipeClient()
|
|
{
|
|
}
|
|
|
|
INetworkProtocolConnection* NamedPipeClient::GetConnection()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
void NamedPipeClient::WaitForServer()
|
|
{
|
|
status = ClientStatus::WaitingForServer;
|
|
DWORD dwPipeMode = PIPE_READMODE_MESSAGE;
|
|
BOOL bSucceeded = SetNamedPipeHandleState(
|
|
hPipe,
|
|
&dwPipeMode,
|
|
NULL,
|
|
NULL);
|
|
CHECK_ERROR(bSucceeded, L"SetNamedPipeHandleState failed.");
|
|
status = ClientStatus::Connected;
|
|
if (callback)
|
|
{
|
|
callback->OnConnected();
|
|
}
|
|
}
|
|
|
|
ClientStatus NamedPipeClient::GetStatus()
|
|
{
|
|
return status;
|
|
}
|
|
|
|
void NamedPipeClient::Stop()
|
|
{
|
|
NamedPipeConnection::Stop();
|
|
status = ClientStatus::Disconnected;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\WINDOWS\NETWORKPROTOCOL.WINDOWS.CPP
|
|
***********************************************************************/
|
|
|
|
#pragma comment(lib, "httpapi.lib")
|
|
#pragma comment(lib, "rpcrt4.lib")
|
|
|
|
/***********************************************************************
|
|
.\TUI\TUI.WINDOWS.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#define _WINSOCKAPI_
|
|
#include <exception>
|
|
|
|
#ifndef VCZH_MSVC
|
|
static_assert(false, "Do not build this file for non-Windows applications.");
|
|
#endif
|
|
|
|
using namespace vl;
|
|
using namespace vl::collections;
|
|
|
|
namespace vl
|
|
{
|
|
namespace console
|
|
{
|
|
vint TUI::MeasureChar(char32_t code)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::console::TUI::MeasureChar(char32_t)#"
|
|
if (!tui_internal::IsScalar(code)) return 0;
|
|
|
|
wchar_t text[2];
|
|
auto length = 1;
|
|
if (code <= 0xFFFF)
|
|
{
|
|
text[0] = (wchar_t)code;
|
|
}
|
|
else
|
|
{
|
|
code -= 0x10000;
|
|
text[0] = (wchar_t)(0xD800 + (code >> 10));
|
|
text[1] = (wchar_t)(0xDC00 + (code & 0x3FF));
|
|
length = 2;
|
|
}
|
|
|
|
WORD ctype1[2] = {};
|
|
WORD ctype3[2] = {};
|
|
CHECK_ERROR(
|
|
GetStringTypeW(CT_CTYPE1, text, length, ctype1) &&
|
|
GetStringTypeW(CT_CTYPE3, text, length, ctype3),
|
|
ERROR_MESSAGE_PREFIX L"Failed to query character types."
|
|
);
|
|
if (ctype1[0] & C1_CNTRL) return 0;
|
|
if (ctype3[0] & (C3_NONSPACING | C3_DIACRITIC | C3_VOWELMARK)) return 0;
|
|
if (ctype3[0] & C3_HALFWIDTH) return 1;
|
|
if (length == 2 || (ctype3[0] & (C3_FULLWIDTH | C3_IDEOGRAPH | C3_HIRAGANA | C3_KATAKANA))) return 2;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
return 1;
|
|
}
|
|
|
|
namespace tui_internal
|
|
{
|
|
void WriteConsoleAll(HANDLE handle, const wchar_t* text, vint length)
|
|
{
|
|
vint written = 0;
|
|
while (written < length)
|
|
{
|
|
DWORD count = 0;
|
|
CHECK_ERROR(WriteConsoleW(handle, text + written, (DWORD)(length - written), &count, nullptr), L"vl::console::TUI Windows backend failed to write terminal output.");
|
|
CHECK_ERROR(count > 0, L"vl::console::TUI Windows backend made no progress while writing terminal output.");
|
|
written += count;
|
|
}
|
|
}
|
|
|
|
vint AnsiToWindowsColor(vint color)
|
|
{
|
|
return (color & 8) | ((color & 1) << 2) | (color & 2) | ((color & 4) >> 2);
|
|
}
|
|
|
|
class WindowsTuiBackend : public unittest::ITuiBackend
|
|
{
|
|
private:
|
|
HANDLE inputHandle = INVALID_HANDLE_VALUE;
|
|
HANDLE originalOutputHandle = INVALID_HANDLE_VALUE;
|
|
HANDLE outputHandle = INVALID_HANDLE_VALUE;
|
|
HANDLE classicOutputHandle = INVALID_HANDLE_VALUE;
|
|
DWORD inputMode = 0;
|
|
DWORD outputMode = 0;
|
|
CONSOLE_CURSOR_INFO cursorInfo = {};
|
|
CONSOLE_SCREEN_BUFFER_INFOEX screenInfo = {};
|
|
CONSOLE_SCREEN_BUFFER_INFO originalGeometry = {};
|
|
List<unittest::TuiBackendEvent> pendingEvents;
|
|
DWORD mouseButtons = 0;
|
|
vint viewportWidth = 0;
|
|
vint viewportHeight = 0;
|
|
bool started = false;
|
|
bool inputModeChanged = false;
|
|
bool outputModeChanged = false;
|
|
bool cursorInfoSaved = false;
|
|
bool geometrySaved = false;
|
|
bool usingVt = false;
|
|
|
|
void SetConsoleGeometry(HANDLE handle, COORD bufferSize, SMALL_RECT window)
|
|
{
|
|
CONSOLE_SCREEN_BUFFER_INFO current = {};
|
|
CHECK_ERROR(GetConsoleScreenBufferInfo(handle, ¤t), L"vl::console::TUI Windows backend failed to query console geometry.");
|
|
auto currentWidth = (SHORT)(current.srWindow.Right - current.srWindow.Left + 1);
|
|
auto currentHeight = (SHORT)(current.srWindow.Bottom - current.srWindow.Top + 1);
|
|
auto temporaryWidth = currentWidth < bufferSize.X ? currentWidth : bufferSize.X;
|
|
auto temporaryHeight = currentHeight < bufferSize.Y ? currentHeight : bufferSize.Y;
|
|
SMALL_RECT temporary = { 0, 0, (SHORT)(temporaryWidth - 1), (SHORT)(temporaryHeight - 1) };
|
|
if (current.srWindow.Left != temporary.Left ||
|
|
current.srWindow.Top != temporary.Top ||
|
|
current.srWindow.Right != temporary.Right ||
|
|
current.srWindow.Bottom != temporary.Bottom)
|
|
{
|
|
CHECK_ERROR(SetConsoleWindowInfo(handle, TRUE, &temporary), L"vl::console::TUI Windows backend failed to prepare the console window for resizing.");
|
|
}
|
|
if (current.dwSize.X != bufferSize.X || current.dwSize.Y != bufferSize.Y)
|
|
{
|
|
CHECK_ERROR(SetConsoleScreenBufferSize(handle, bufferSize), L"vl::console::TUI Windows backend failed to resize the console screen buffer.");
|
|
}
|
|
if (temporary.Left != window.Left ||
|
|
temporary.Top != window.Top ||
|
|
temporary.Right != window.Right ||
|
|
temporary.Bottom != window.Bottom)
|
|
{
|
|
CHECK_ERROR(SetConsoleWindowInfo(handle, TRUE, &window), L"vl::console::TUI Windows backend failed to resize the console window.");
|
|
}
|
|
}
|
|
|
|
void QueueResize(vint width, vint height)
|
|
{
|
|
unittest::TuiBackendEvent event;
|
|
event.type = unittest::TuiBackendEventType::Resize;
|
|
event.width = width;
|
|
event.height = height;
|
|
pendingEvents.Add(event);
|
|
}
|
|
|
|
void SynchronizeViewport(bool queueEvent)
|
|
{
|
|
CONSOLE_SCREEN_BUFFER_INFO info = {};
|
|
CHECK_ERROR(GetConsoleScreenBufferInfo(outputHandle, &info), L"vl::console::TUI Windows backend failed to query the console viewport.");
|
|
auto width = (vint)(info.srWindow.Right - info.srWindow.Left + 1);
|
|
auto height = (vint)(info.srWindow.Bottom - info.srWindow.Top + 1);
|
|
CHECK_ERROR(width > 0 && height > 0, L"vl::console::TUI Windows backend received an invalid console viewport.");
|
|
auto changed = width != viewportWidth || height != viewportHeight;
|
|
COORD bufferSize = { (SHORT)width, (SHORT)height };
|
|
SMALL_RECT window = { 0, 0, (SHORT)(width - 1), (SHORT)(height - 1) };
|
|
if (info.dwSize.X != bufferSize.X ||
|
|
info.dwSize.Y != bufferSize.Y ||
|
|
info.srWindow.Left != 0 ||
|
|
info.srWindow.Top != 0)
|
|
{
|
|
SetConsoleGeometry(outputHandle, bufferSize, window);
|
|
}
|
|
viewportWidth = width;
|
|
viewportHeight = height;
|
|
if (queueEvent && changed)
|
|
{
|
|
QueueResize(width, height);
|
|
}
|
|
}
|
|
|
|
TuiMouseInfo GetMouseInfo(const MOUSE_EVENT_RECORD& record)
|
|
{
|
|
CONSOLE_SCREEN_BUFFER_INFO info = {};
|
|
CHECK_ERROR(GetConsoleScreenBufferInfo(outputHandle, &info), L"vl::console::TUI Windows backend failed to query the console viewport.");
|
|
TuiMouseInfo result;
|
|
result.x = record.dwMousePosition.X - info.srWindow.Left;
|
|
result.y = record.dwMousePosition.Y - info.srWindow.Top;
|
|
result.ctrl = (record.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) != 0;
|
|
result.shift = (record.dwControlKeyState & SHIFT_PRESSED) != 0;
|
|
result.alt = (record.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) != 0;
|
|
result.left = (record.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) != 0;
|
|
result.middle = (record.dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) != 0;
|
|
result.right = (record.dwButtonState & RIGHTMOST_BUTTON_PRESSED) != 0;
|
|
return result;
|
|
}
|
|
|
|
TuiKeyInfo GetKeyInfo(const KEY_EVENT_RECORD& record)
|
|
{
|
|
TuiKeyInfo result;
|
|
result.ctrl = (record.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) != 0;
|
|
result.shift = (record.dwControlKeyState & SHIFT_PRESSED) != 0;
|
|
result.alt = (record.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) != 0;
|
|
result.capslock = (record.dwControlKeyState & CAPSLOCK_ON) != 0;
|
|
result.autoRepeatKeyDown = record.bKeyDown && record.wRepeatCount > 1;
|
|
return result;
|
|
}
|
|
|
|
void QueueChar(wchar_t code, const KEY_EVENT_RECORD& record)
|
|
{
|
|
unittest::TuiBackendEvent event;
|
|
event.type = unittest::TuiBackendEventType::Char;
|
|
event.charInfo.code = code;
|
|
event.charInfo.ctrl = (record.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) != 0;
|
|
event.charInfo.shift = (record.dwControlKeyState & SHIFT_PRESSED) != 0;
|
|
event.charInfo.alt = (record.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) != 0;
|
|
event.charInfo.capslock = (record.dwControlKeyState & CAPSLOCK_ON) != 0;
|
|
pendingEvents.Add(event);
|
|
}
|
|
|
|
void DecodeKey(const KEY_EVENT_RECORD& record)
|
|
{
|
|
unittest::TuiBackendEvent keyEvent;
|
|
keyEvent.type = record.bKeyDown ? unittest::TuiBackendEventType::KeyDown : unittest::TuiBackendEventType::KeyUp;
|
|
keyEvent.keyInfo = GetKeyInfo(record);
|
|
pendingEvents.Add(keyEvent);
|
|
|
|
if (!record.bKeyDown || record.uChar.UnicodeChar == 0) return;
|
|
auto codeUnit = record.uChar.UnicodeChar;
|
|
auto repeat = record.wRepeatCount == 0 ? 1 : record.wRepeatCount;
|
|
for (vint i = 0; i < repeat; i++)
|
|
{
|
|
QueueChar(codeUnit, record);
|
|
}
|
|
}
|
|
|
|
void QueueMouseButton(unittest::TuiBackendEventType type, TuiMouseButton button, const TuiMouseInfo& info)
|
|
{
|
|
unittest::TuiBackendEvent event;
|
|
event.type = type;
|
|
event.mouseButton = button;
|
|
event.mouseInfo = info;
|
|
pendingEvents.Add(event);
|
|
}
|
|
|
|
void DecodeMouse(const MOUSE_EVENT_RECORD& record)
|
|
{
|
|
auto info = GetMouseInfo(record);
|
|
if (record.dwEventFlags == MOUSE_MOVED)
|
|
{
|
|
unittest::TuiBackendEvent event;
|
|
event.type = unittest::TuiBackendEventType::MouseMove;
|
|
event.mouseInfo = info;
|
|
pendingEvents.Add(event);
|
|
}
|
|
else if (record.dwEventFlags == MOUSE_WHEELED || record.dwEventFlags == MOUSE_HWHEELED)
|
|
{
|
|
unittest::TuiBackendEvent event;
|
|
event.type = record.dwEventFlags == MOUSE_WHEELED ? unittest::TuiBackendEventType::MouseVerticalWheel : unittest::TuiBackendEventType::MouseHorizontalWheel;
|
|
event.mouseInfo = info;
|
|
event.mouseInfo.wheel = (SHORT)HIWORD(record.dwButtonState);
|
|
pendingEvents.Add(event);
|
|
}
|
|
else if (record.dwEventFlags == DOUBLE_CLICK)
|
|
{
|
|
auto button = (record.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) ? TuiMouseButton::Left
|
|
: (record.dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) ? TuiMouseButton::Middle
|
|
: TuiMouseButton::Right;
|
|
QueueMouseButton(unittest::TuiBackendEventType::MouseDoubleClick, button, info);
|
|
}
|
|
else if (record.dwEventFlags == 0)
|
|
{
|
|
struct ButtonMapping
|
|
{
|
|
DWORD mask;
|
|
TuiMouseButton button;
|
|
};
|
|
ButtonMapping mappings[] =
|
|
{
|
|
{ FROM_LEFT_1ST_BUTTON_PRESSED, TuiMouseButton::Left },
|
|
{ FROM_LEFT_2ND_BUTTON_PRESSED, TuiMouseButton::Middle },
|
|
{ RIGHTMOST_BUTTON_PRESSED, TuiMouseButton::Right },
|
|
};
|
|
for (auto mapping : mappings)
|
|
{
|
|
auto before = (mouseButtons & mapping.mask) != 0;
|
|
auto after = (record.dwButtonState & mapping.mask) != 0;
|
|
if (before != after)
|
|
{
|
|
QueueMouseButton(after ? unittest::TuiBackendEventType::MouseDown : unittest::TuiBackendEventType::MouseUp, mapping.button, info);
|
|
}
|
|
}
|
|
}
|
|
mouseButtons = record.dwButtonState;
|
|
}
|
|
|
|
void DecodeRecord(const INPUT_RECORD& record)
|
|
{
|
|
switch (record.EventType)
|
|
{
|
|
case KEY_EVENT:
|
|
DecodeKey(record.Event.KeyEvent);
|
|
break;
|
|
case MOUSE_EVENT:
|
|
DecodeMouse(record.Event.MouseEvent);
|
|
break;
|
|
case WINDOW_BUFFER_SIZE_EVENT:
|
|
SynchronizeViewport(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AppendColor(WString& output, TuiColor foreground, TuiColor background, TuiColorMode colorMode)
|
|
{
|
|
if (colorMode == TuiColorMode::TrueColor)
|
|
{
|
|
output += L"\x1B[38;2;" + itow(foreground.r) + L";" + itow(foreground.g) + L";" + itow(foreground.b)
|
|
+ L";48;2;" + itow(background.r) + L";" + itow(background.g) + L";" + itow(background.b) + L"m";
|
|
}
|
|
else
|
|
{
|
|
TuiColor custom16[16];
|
|
const TuiColor* custom = nullptr;
|
|
if (colorMode == TuiColorMode::Color16 && screenInfo.cbSize == sizeof(screenInfo))
|
|
{
|
|
for (vint i = 0; i < 16; i++)
|
|
{
|
|
auto color = screenInfo.ColorTable[AnsiToWindowsColor(i)];
|
|
custom16[i] = { GetRValue(color), GetGValue(color), GetBValue(color) };
|
|
}
|
|
custom = custom16;
|
|
}
|
|
auto foregroundIndex = QuantizeColor(foreground, colorMode, custom);
|
|
auto backgroundIndex = QuantizeColor(background, colorMode, custom);
|
|
if (colorMode == TuiColorMode::Color256)
|
|
{
|
|
output += L"\x1B[38;5;" + itow(foregroundIndex) + L";48;5;" + itow(backgroundIndex) + L"m";
|
|
}
|
|
else
|
|
{
|
|
auto foregroundCode = foregroundIndex < 8 ? 30 + foregroundIndex : 90 + foregroundIndex - 8;
|
|
auto backgroundCode = backgroundIndex < 8 ? 40 + backgroundIndex : 100 + backgroundIndex - 8;
|
|
output += L"\x1B[" + itow(foregroundCode) + L";" + itow(backgroundCode) + L"m";
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
TuiColorMode Start(const TuiStartOptions& options) override
|
|
{
|
|
CHECK_ERROR(!started, L"vl::console::TUI Windows backend is already active.");
|
|
inputHandle = GetStdHandle(STD_INPUT_HANDLE);
|
|
originalOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
outputHandle = originalOutputHandle;
|
|
CHECK_ERROR(inputHandle != INVALID_HANDLE_VALUE && originalOutputHandle != INVALID_HANDLE_VALUE, L"vl::console::TUI requires valid console handles.");
|
|
CHECK_ERROR(GetConsoleMode(inputHandle, &inputMode) && GetConsoleMode(originalOutputHandle, &outputMode), L"vl::console::TUI requires interactive console handles.");
|
|
cursorInfoSaved = GetConsoleCursorInfo(originalOutputHandle, &cursorInfo) != 0;
|
|
screenInfo = {};
|
|
screenInfo.cbSize = sizeof(screenInfo);
|
|
if (!GetConsoleScreenBufferInfoEx(originalOutputHandle, &screenInfo))
|
|
{
|
|
screenInfo.cbSize = 0;
|
|
}
|
|
CHECK_ERROR(GetConsoleScreenBufferInfo(originalOutputHandle, &originalGeometry), L"vl::console::TUI failed to save the original console geometry.");
|
|
geometrySaved = true;
|
|
started = true;
|
|
|
|
try
|
|
{
|
|
auto newInputMode = inputMode;
|
|
newInputMode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
newInputMode |= ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT;
|
|
CHECK_ERROR(SetConsoleMode(inputHandle, newInputMode), L"vl::console::TUI failed to activate console input mode.");
|
|
inputModeChanged = true;
|
|
|
|
auto newOutputMode = outputMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
usingVt = SetConsoleMode(originalOutputHandle, newOutputMode) != 0;
|
|
if (usingVt)
|
|
{
|
|
outputModeChanged = true;
|
|
const wchar_t sequence[] = L"\x1B[?1049h\x1B[?25l";
|
|
WriteConsoleAll(outputHandle, sequence, sizeof(sequence) / sizeof(*sequence) - 1);
|
|
SynchronizeViewport(false);
|
|
if (options.colorMode == TuiColorMode::Auto) return TuiColorMode::TrueColor;
|
|
return options.colorMode;
|
|
}
|
|
|
|
classicOutputHandle = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr);
|
|
CHECK_ERROR(classicOutputHandle != INVALID_HANDLE_VALUE, L"vl::console::TUI failed to create an alternate console screen buffer.");
|
|
auto width = (SHORT)(originalGeometry.srWindow.Right - originalGeometry.srWindow.Left + 1);
|
|
auto height = (SHORT)(originalGeometry.srWindow.Bottom - originalGeometry.srWindow.Top + 1);
|
|
SetConsoleGeometry(classicOutputHandle, { width, height }, { 0, 0, (SHORT)(width - 1), (SHORT)(height - 1) });
|
|
CHECK_ERROR(SetConsoleActiveScreenBuffer(classicOutputHandle), L"vl::console::TUI failed to activate the alternate console screen buffer.");
|
|
outputHandle = classicOutputHandle;
|
|
viewportWidth = width;
|
|
viewportHeight = height;
|
|
return TuiColorMode::Color16;
|
|
}
|
|
catch (...)
|
|
{
|
|
auto exception = std::current_exception();
|
|
try
|
|
{
|
|
Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
std::rethrow_exception(exception);
|
|
}
|
|
}
|
|
|
|
void Stop() override
|
|
{
|
|
if (!started) return;
|
|
auto restored = true;
|
|
if (usingVt)
|
|
{
|
|
auto width = originalGeometry.srWindow.Right - originalGeometry.srWindow.Left + 1;
|
|
auto height = originalGeometry.srWindow.Bottom - originalGeometry.srWindow.Top + 1;
|
|
auto sequence = WString::Unmanaged(L"\x1B[0m\x1B[8;")
|
|
+ itow(height) + L";" + itow(width) + L"t\x1B[?1049l\x1B[?25h";
|
|
try
|
|
{
|
|
WriteConsoleAll(outputHandle, sequence.Buffer(), sequence.Length());
|
|
auto deadline = GetTickCount64() + 250;
|
|
vint stableCount = 0;
|
|
while (true)
|
|
{
|
|
CONSOLE_SCREEN_BUFFER_INFO info = {};
|
|
CHECK_ERROR(GetConsoleScreenBufferInfo(outputHandle, &info), L"vl::console::TUI Windows backend failed to wait for terminal restoration.");
|
|
auto currentWidth = info.srWindow.Right - info.srWindow.Left + 1;
|
|
auto currentHeight = info.srWindow.Bottom - info.srWindow.Top + 1;
|
|
if (currentWidth == width && currentHeight == height)
|
|
{
|
|
if (++stableCount == 10) break;
|
|
}
|
|
else
|
|
{
|
|
stableCount = 0;
|
|
}
|
|
if (GetTickCount64() >= deadline) break;
|
|
::Sleep(1);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
restored = false;
|
|
}
|
|
}
|
|
if (classicOutputHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
if (!SetConsoleActiveScreenBuffer(originalOutputHandle)) restored = false;
|
|
if (!CloseHandle(classicOutputHandle)) restored = false;
|
|
classicOutputHandle = INVALID_HANDLE_VALUE;
|
|
}
|
|
if (geometrySaved)
|
|
{
|
|
try
|
|
{
|
|
SetConsoleGeometry(originalOutputHandle, originalGeometry.dwSize, originalGeometry.srWindow);
|
|
}
|
|
catch (...)
|
|
{
|
|
restored = false;
|
|
}
|
|
}
|
|
if (cursorInfoSaved && !SetConsoleCursorInfo(originalOutputHandle, &cursorInfo)) restored = false;
|
|
if (outputModeChanged && !SetConsoleMode(originalOutputHandle, outputMode)) restored = false;
|
|
if (inputModeChanged && !SetConsoleMode(inputHandle, inputMode)) restored = false;
|
|
pendingEvents.Clear();
|
|
mouseButtons = 0;
|
|
viewportWidth = 0;
|
|
viewportHeight = 0;
|
|
outputHandle = INVALID_HANDLE_VALUE;
|
|
originalOutputHandle = INVALID_HANDLE_VALUE;
|
|
inputHandle = INVALID_HANDLE_VALUE;
|
|
usingVt = false;
|
|
inputModeChanged = false;
|
|
outputModeChanged = false;
|
|
cursorInfoSaved = false;
|
|
geometrySaved = false;
|
|
started = false;
|
|
CHECK_ERROR(restored, L"vl::console::TUI Windows backend failed to restore the original console state.");
|
|
}
|
|
|
|
bool TryGetConsoleSize(vint& width, vint& height) override
|
|
{
|
|
auto handle = outputHandle != INVALID_HANDLE_VALUE ? outputHandle : GetStdHandle(STD_OUTPUT_HANDLE);
|
|
CONSOLE_SCREEN_BUFFER_INFO info = {};
|
|
if (handle == INVALID_HANDLE_VALUE || !GetConsoleScreenBufferInfo(handle, &info)) return false;
|
|
width = info.srWindow.Right - info.srWindow.Left + 1;
|
|
height = info.srWindow.Bottom - info.srWindow.Top + 1;
|
|
return width > 0 && height > 0;
|
|
}
|
|
|
|
vuint64_t GetMonotonicTime() override
|
|
{
|
|
return GetTickCount64();
|
|
}
|
|
|
|
bool ReadEvent(vint milliseconds, unittest::TuiBackendEvent& event) override
|
|
{
|
|
if (pendingEvents.Count() == 0)
|
|
{
|
|
SynchronizeViewport(true);
|
|
}
|
|
if (pendingEvents.Count() == 0)
|
|
{
|
|
auto result = WaitForSingleObject(inputHandle, milliseconds < 0 ? INFINITE : (DWORD)milliseconds);
|
|
if (result == WAIT_TIMEOUT)
|
|
{
|
|
SynchronizeViewport(true);
|
|
if (pendingEvents.Count() == 0) return false;
|
|
}
|
|
else
|
|
{
|
|
CHECK_ERROR(result == WAIT_OBJECT_0, L"vl::console::TUI Windows backend failed while waiting for console input.");
|
|
INPUT_RECORD records[32];
|
|
DWORD count = 0;
|
|
CHECK_ERROR(ReadConsoleInputW(inputHandle, records, sizeof(records) / sizeof(*records), &count), L"vl::console::TUI Windows backend failed to read console input.");
|
|
for (DWORD i = 0; i < count; i++) DecodeRecord(records[i]);
|
|
SynchronizeViewport(true);
|
|
}
|
|
}
|
|
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
|
|
{
|
|
if (usingVt)
|
|
{
|
|
WString output;
|
|
TuiColor lastForeground;
|
|
TuiColor lastBackground;
|
|
bool hasLastColor = false;
|
|
for (vint y = 0; y < height; y++)
|
|
{
|
|
output += L"\x1B[" + itow(y + 1) + L";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();
|
|
if (code == 0) output += L" ";
|
|
else output += u32tow(U32String::CopyFrom(&code, 1));
|
|
}
|
|
}
|
|
output += L"\x1B[0m";
|
|
WriteConsoleAll(outputHandle, output.Buffer(), output.Length());
|
|
}
|
|
else
|
|
{
|
|
Array<CHAR_INFO> output(width * height);
|
|
TuiColor custom16[16];
|
|
for (vint i = 0; i < 16; i++)
|
|
{
|
|
auto color = screenInfo.ColorTable[AnsiToWindowsColor(i)];
|
|
custom16[i] = { GetRValue(color), GetGValue(color), GetBValue(color) };
|
|
}
|
|
for (vint i = 0; i < width * height; i++)
|
|
{
|
|
auto& pixel = buffer[i];
|
|
auto code = pixel.GetChar32();
|
|
auto measured = code == 0 ? 1 : TUI::MeasureChar(code);
|
|
output[i].Char.UnicodeChar = code == 0 ? L' ' : measured == 2 || code > 0xFFFF ? L'?' : (wchar_t)code;
|
|
if (pixel.glyph == TuiPixelGlyph::WideCharContinuation) output[i].Char.UnicodeChar = L' ';
|
|
auto foreground = AnsiToWindowsColor(QuantizeColor(pixel.foregroundColor, TuiColorMode::Color16, custom16));
|
|
auto background = AnsiToWindowsColor(QuantizeColor(pixel.backgroundColor, TuiColorMode::Color16, custom16));
|
|
output[i].Attributes = (WORD)(foreground | (background << 4));
|
|
}
|
|
COORD size = { (SHORT)width, (SHORT)height };
|
|
COORD origin = {};
|
|
SMALL_RECT area = { 0, 0, (SHORT)(width - 1), (SHORT)(height - 1) };
|
|
CHECK_ERROR(WriteConsoleOutputW(outputHandle, &output[0], size, origin, &area), L"vl::console::TUI Windows backend failed to render the classic console buffer.");
|
|
}
|
|
}
|
|
};
|
|
|
|
Ptr<unittest::ITuiBackend> CreateTuiBackend()
|
|
{
|
|
return Ptr(new WindowsTuiBackend);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|