Measure AppContainer disk quota overshoot
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <userenv.h>
|
||||
#include <sddl.h>
|
||||
#include <aclapi.h>
|
||||
#include <jobapi2.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
@@ -378,6 +379,51 @@ static bool IsSmokeResult(const std::wstring& path) {
|
||||
return valid && sawDot;
|
||||
}
|
||||
|
||||
static constexpr ULONGLONG kMaxAppContainerDataBytes = 128ull * 1024 * 1024;
|
||||
|
||||
static bool MeasureDirectoryBytes(
|
||||
const std::wstring& root, ULONGLONG& totalBytes, unsigned depth = 0
|
||||
) {
|
||||
if (depth > 48) return false;
|
||||
const DWORD rootAttributes = GetFileAttributesW(root.c_str());
|
||||
if (rootAttributes == INVALID_FILE_ATTRIBUTES ||
|
||||
!(rootAttributes & FILE_ATTRIBUTE_DIRECTORY) ||
|
||||
(rootAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) return false;
|
||||
WIN32_FIND_DATAW entry{};
|
||||
HANDLE search = FindFirstFileW((root + L"\\*").c_str(), &entry);
|
||||
if (search == INVALID_HANDLE_VALUE) return false;
|
||||
bool success = true;
|
||||
do {
|
||||
if (wcscmp(entry.cFileName, L".") == 0 || wcscmp(entry.cFileName, L"..") == 0) continue;
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
const std::wstring path = root + L"\\" + entry.cFileName;
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
success = MeasureDirectoryBytes(path, totalBytes, depth + 1);
|
||||
} else {
|
||||
WIN32_FILE_ATTRIBUTE_DATA attributes{};
|
||||
if (!GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &attributes) ||
|
||||
(attributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
const ULONGLONG size = (static_cast<ULONGLONG>(attributes.nFileSizeHigh) << 32) |
|
||||
attributes.nFileSizeLow;
|
||||
if (size > (~static_cast<ULONGLONG>(0)) - totalBytes) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
totalBytes += size;
|
||||
}
|
||||
if (!success) break;
|
||||
} while (FindNextFileW(search, &entry));
|
||||
const DWORD finalError = GetLastError();
|
||||
FindClose(search);
|
||||
return success && finalError == ERROR_NO_MORE_FILES;
|
||||
}
|
||||
|
||||
static DWORD RunContainedExe(
|
||||
PSID appContainerSid,
|
||||
const std::wstring& application,
|
||||
@@ -386,12 +432,26 @@ static DWORD RunContainedExe(
|
||||
const std::wstring& cwd,
|
||||
HANDLE job,
|
||||
std::string* capturedOutput = nullptr,
|
||||
bool* outputTruncated = nullptr
|
||||
bool* outputTruncated = nullptr,
|
||||
const std::wstring* quotaRoot = nullptr,
|
||||
bool* quotaExceeded = nullptr,
|
||||
ULONGLONG* quotaObservedBytes = nullptr
|
||||
) {
|
||||
constexpr size_t kMaxCapturedOutput = 64 * 1024;
|
||||
const bool capture = capturedOutput != nullptr;
|
||||
if (capturedOutput) capturedOutput->clear();
|
||||
if (outputTruncated) *outputTruncated = false;
|
||||
if (quotaExceeded) *quotaExceeded = false;
|
||||
if (quotaObservedBytes) *quotaObservedBytes = 0;
|
||||
if (quotaRoot) {
|
||||
ULONGLONG currentBytes = 0;
|
||||
const bool measured = MeasureDirectoryBytes(*quotaRoot, currentBytes);
|
||||
if (quotaObservedBytes) *quotaObservedBytes = currentBytes;
|
||||
if (!measured || currentBytes > kMaxAppContainerDataBytes) {
|
||||
if (quotaExceeded) *quotaExceeded = true;
|
||||
return ERROR_DISK_FULL;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE pipeRead = nullptr;
|
||||
HANDLE pipeWrite = nullptr;
|
||||
@@ -533,6 +593,7 @@ static DWORD RunContainedExe(
|
||||
DWORD exitCode = ERROR_TIMEOUT;
|
||||
if (capture) {
|
||||
const ULONGLONG deadline = GetTickCount64() + 30000;
|
||||
ULONGLONG nextQuotaCheck = GetTickCount64() + 25;
|
||||
bool processFinished = false;
|
||||
bool pipeFinished = false;
|
||||
while (!pipeFinished || !processFinished) {
|
||||
@@ -562,6 +623,19 @@ static DWORD RunContainedExe(
|
||||
processFinished = true;
|
||||
TerminateJobObject(job, ERROR_SUCCESS);
|
||||
}
|
||||
if (!processFinished && quotaRoot && GetTickCount64() >= nextQuotaCheck) {
|
||||
ULONGLONG currentBytes = 0;
|
||||
const bool measured = MeasureDirectoryBytes(*quotaRoot, currentBytes);
|
||||
if (quotaObservedBytes) *quotaObservedBytes = currentBytes;
|
||||
if (!measured || currentBytes > kMaxAppContainerDataBytes) {
|
||||
if (quotaExceeded) *quotaExceeded = true;
|
||||
TerminateJobObject(job, ERROR_DISK_FULL);
|
||||
exitCode = ERROR_DISK_FULL;
|
||||
processFinished = WaitForSingleObject(process.hProcess, 5000) == WAIT_OBJECT_0;
|
||||
pipeFinished = false;
|
||||
}
|
||||
nextQuotaCheck = GetTickCount64() + 25;
|
||||
}
|
||||
if (!processFinished && GetTickCount64() >= deadline) {
|
||||
TerminateJobObject(job, ERROR_TIMEOUT);
|
||||
exitCode = ERROR_TIMEOUT;
|
||||
@@ -653,6 +727,13 @@ int wmain() {
|
||||
limits.JobMemoryLimit = 512ull * 1024 * 1024;
|
||||
bool jobReady = job && SetInformationJobObject(
|
||||
job, JobObjectExtendedLimitInformation, &limits, sizeof(limits));
|
||||
JOBOBJECT_IO_RATE_CONTROL_INFORMATION ioLimits{};
|
||||
ioLimits.MaxBandwidth = 8ll * 1024 * 1024;
|
||||
ioLimits.ControlFlags = JOB_OBJECT_IO_RATE_CONTROL_ENABLE;
|
||||
DWORD jobIoControlResult = jobReady
|
||||
? SetIoRateControlInformationJobObject(job, &ioLimits)
|
||||
: ERROR_INVALID_HANDLE;
|
||||
DWORD jobIoControlError = jobIoControlResult == 0 ? GetLastError() : ERROR_SUCCESS;
|
||||
|
||||
auto environment = SafeEnvironmentBlock();
|
||||
if (environment.empty()) return 15;
|
||||
@@ -756,6 +837,19 @@ int wmain() {
|
||||
L" --noproxy \"*\" --max-time 4 http://127.0.0.1:8100/health -o NUL",
|
||||
environment.data(), cwd, job)
|
||||
: ERROR_INVALID_HANDLE;
|
||||
std::wstring diskFillScriptPath;
|
||||
bool diskFillScriptReady = stagedInputCopied && ResolvePythonEntry(
|
||||
projectSnapshotPath, L"scripts/appcontainer_disk_fill_smoke.py", diskFillScriptPath);
|
||||
std::string diskFillOutput;
|
||||
bool diskFillOutputTruncated = false;
|
||||
bool diskQuotaExceeded = false;
|
||||
ULONGLONG diskQuotaObservedBytes = 0;
|
||||
DWORD diskFillRunResult = jobReady && pythonRuntimeCopied && diskFillScriptReady
|
||||
? RunContainedExe(appContainerSid, pythonExecutable,
|
||||
L"-s " + QuoteArg(diskFillScriptPath), environment.data(), cwd, job,
|
||||
&diskFillOutput, &diskFillOutputTruncated, &appDataPath,
|
||||
&diskQuotaExceeded, &diskQuotaObservedBytes)
|
||||
: ERROR_INVALID_HANDLE;
|
||||
|
||||
DWORD attrs = GetFileAttributesW(writePath.c_str());
|
||||
DWORD attrsError = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : ERROR_SUCCESS;
|
||||
@@ -828,6 +922,12 @@ int wmain() {
|
||||
overflowTruncated ? L"true" : L"false");
|
||||
wprintf(L"curl_version_exit=%lu\ncurl_local_health_exit=%lu\n",
|
||||
curlVersionResult, curlNetworkResult);
|
||||
wprintf(L"disk_fill_script_valid=%s\ndisk_fill_exit=%lu\ndisk_quota_limit_bytes=%llu\ndisk_quota_observed_bytes=%llu\ndisk_quota_exceeded=%s\n",
|
||||
diskFillScriptReady ? L"true" : L"false", diskFillRunResult,
|
||||
kMaxAppContainerDataBytes, diskQuotaObservedBytes,
|
||||
diskQuotaExceeded ? L"true" : L"false");
|
||||
wprintf(L"job_io_rate_control_set=%s\njob_io_rate_control_error=%lu\n",
|
||||
jobIoControlResult != 0 ? L"true" : L"false", jobIoControlError);
|
||||
if (!jobReady || shellResult != 0 || readResult == 0 || writeResult == 0 ||
|
||||
!hostSecretPreserved || !writeWasBlocked || !workspaceFolderReady ||
|
||||
allowedWriteResult != 0 || !allowedWorkspaceWriteVisible ||
|
||||
@@ -836,6 +936,8 @@ int wmain() {
|
||||
!pythonVersionVisible ||
|
||||
overflowRunResult != 0 || overflowOutput.size() != 64 * 1024 ||
|
||||
!overflowTruncated ||
|
||||
!curlCopied || curlVersionResult != 0 || curlNetworkResult == 0) return 20;
|
||||
!curlCopied || curlVersionResult != 0 || curlNetworkResult == 0 ||
|
||||
!diskFillScriptReady || diskFillRunResult != ERROR_DISK_FULL ||
|
||||
!diskQuotaExceeded || diskQuotaObservedBytes <= kMaxAppContainerDataBytes) return 20;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user