功能:完善通用云管工作台、审计与诊断能力
This commit is contained in:
1 parent
5ac692724d
commit
9f05aa20ea
225 files changed
+14266
-141
No files matched your search
@@ -0,0 +1,50 @@
|
||||
/* Fake NVIDIA entrypoints for exercising the real preload adapter's ABI.
|
||||
* No GPU or proprietary library is used. Compile with vendor/NvFBC.h.
|
||||
*/
|
||||
#include "NvFBC.h"
|
||||
extern void *dlsym(void *, const char *);
|
||||
static int test_case, captured_cursor, output_id, sampling;
|
||||
static NVFBCSTATUS NVFBCAPI mock_capture(const NVFBC_SESSION_HANDLE handle,
|
||||
NVFBC_CREATE_CAPTURE_SESSION_PARAMS *p) {
|
||||
(void)handle;
|
||||
captured_cursor = p->bWithCursor;
|
||||
output_id = p->dwOutputId;
|
||||
sampling = p->dwSamplingRateMs;
|
||||
return test_case == 6 ? NVFBC_ERR_INTERNAL : NVFBC_SUCCESS;
|
||||
}
|
||||
static NVFBCSTATUS NVFBCAPI mock_status(const NVFBC_SESSION_HANDLE handle,
|
||||
NVFBC_GET_STATUS_PARAMS *p) {
|
||||
(void)handle;
|
||||
p->bIsCapturePossible = test_case == 4 ? NVFBC_FALSE : NVFBC_TRUE;
|
||||
p->screenSize = (NVFBC_SIZE){test_case == 3 ? 1920 : 2560, 1440};
|
||||
p->bXRandRAvailable = NVFBC_TRUE;
|
||||
p->dwOutputNum = test_case == 5 ? 2 : 0;
|
||||
return NVFBC_SUCCESS;
|
||||
}
|
||||
NVFBCSTATUS NVFBCAPI NvFBCCreateInstance(NVFBC_API_FUNCTION_LIST *f) {
|
||||
f->nvFBCCreateCaptureSession = mock_capture;
|
||||
f->nvFBCGetStatus = mock_status;
|
||||
return NVFBC_SUCCESS;
|
||||
}
|
||||
int run_test(int which, int leased, int headless) {
|
||||
test_case = which;
|
||||
PNVFBCCREATEINSTANCE create = (PNVFBCCREATEINSTANCE)dlsym((void *)0, "NvFBCCreateInstance");
|
||||
if (!create) return 1;
|
||||
NVFBC_API_FUNCTION_LIST f = {.dwVersion=NVFBC_VERSION};
|
||||
if (create(&f) != NVFBC_SUCCESS) return 2;
|
||||
NVFBC_CREATE_CAPTURE_SESSION_PARAMS p = {
|
||||
.dwVersion = which == 7 ? 0 : NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER,
|
||||
.bWithCursor = NVFBC_TRUE, .dwOutputId = 73, .dwSamplingRateMs=16
|
||||
};
|
||||
NVFBCSTATUS status = f.nvFBCCreateCaptureSession(1, &p);
|
||||
if (status != (which == 6 ? NVFBC_ERR_INTERNAL : NVFBC_SUCCESS)) return 3;
|
||||
if (captured_cursor != (leased ? NVFBC_FALSE : NVFBC_TRUE)) return 4;
|
||||
if (p.bWithCursor != NVFBC_TRUE || p.dwOutputId != 73 || output_id != 73 || sampling != 16) return 5;
|
||||
NVFBC_GET_STATUS_PARAMS s = {.dwVersion=which == 8 ? 0 : NVFBC_GET_STATUS_PARAMS_VER};
|
||||
if (f.nvFBCGetStatus(1, &s) != NVFBC_SUCCESS) return 6;
|
||||
int enumerate_root = headless && which != 3 && which != 4 && which != 5;
|
||||
if (enumerate_root) {
|
||||
if (s.dwOutputNum != 1 || s.bXRandRAvailable || s.outputs[0].trackedBox.w != 2560 || s.outputs[0].trackedBox.h != 1440) return 7;
|
||||
} else if (s.dwOutputNum != (which == 5 ? 2U : 0U) || !s.bXRandRAvailable) return 8;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Run against built shared objects on Linux, including abort-on-ABI-change cases.
|
||||
|
||||
Usage: python3 run_nvfbc_adapter.py /absolute/adapter.so /absolute/stub.so
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import resource
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
||||
library = ctypes.CDLL(sys.argv[2], mode=ctypes.RTLD_GLOBAL)
|
||||
library.run_test.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
|
||||
library.run_test.restype = ctypes.c_int
|
||||
raise SystemExit(library.run_test(*(int(v) for v in sys.argv[3:])))
|
||||
|
||||
adapter, stub = [str(Path(p).resolve(strict=True)) for p in sys.argv[1:]]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for case, leased, headless in [(0, 0, 0), (1, 1, 0), (2, 1, 1),
|
||||
(2, 0, 1), (3, 1, 1), (4, 1, 1),
|
||||
(5, 1, 1), (6, 1, 1), (7, 1, 1), (8, 1, 1)]:
|
||||
env = dict(os.environ, LD_PRELOAD=adapter,
|
||||
VDI_CURSOR_RECEIPT=str(Path(directory) / 'receipt'))
|
||||
env.pop('VDI_CURSOR_LEASE', None)
|
||||
env.pop('VDI_NVFBC_HEADLESS', None)
|
||||
if leased:
|
||||
env['VDI_CURSOR_LEASE'] = 'c' * 32
|
||||
if headless:
|
||||
env['VDI_NVFBC_HEADLESS'] = '1'
|
||||
result = subprocess.run([sys.executable, __file__, adapter, stub,
|
||||
str(case), str(leased), str(headless)], env=env)
|
||||
expected = -signal.SIGABRT if case in (7, 8) else 0
|
||||
assert result.returncode == expected, (case, leased, headless, result.returncode)
|
||||
if leased or headless:
|
||||
receipt = (Path(directory) / 'receipt').read_text()
|
||||
if case == 6:
|
||||
assert receipt == 'nvfbc-ready-v1\n', receipt
|
||||
elif case not in (7, 8):
|
||||
assert receipt == 'nvfbc-v1\n', receipt
|
||||
print('PASS', case, 'lease', leased, 'headless', headless, flush=True)
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Linux host regressions: reject incomplete capture manifests and stale readiness."""
|
||||
import importlib.util
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith('linux'), 'host requires Linux flock')
|
||||
class HostLeaseTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
configured_source = os.environ.get('VDI_NATIVE_CURSOR_HOST')
|
||||
source = Path(configured_source) if configured_source else (
|
||||
Path(__file__).resolve().parents[2] / 'terminal/native-cursor/host.py')
|
||||
spec = importlib.util.spec_from_file_location('cursor_host', source)
|
||||
cls.host = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cls.host)
|
||||
|
||||
def test_display_restart_cannot_reuse_previous_ready_or_receipt(self):
|
||||
h = self.host
|
||||
lease = 'a' * 32
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
run = Path(directory)
|
||||
(run / 'ready').write_text('b' * 32)
|
||||
(run / 'receipt').write_text('nvfbc-v1\n')
|
||||
calls = []
|
||||
|
||||
def systemctl(*args):
|
||||
calls.append(args)
|
||||
if args == ('start', h.GUARD):
|
||||
self.assertFalse((run / 'ready').exists())
|
||||
self.assertFalse((run / 'receipt').exists())
|
||||
self.assertEqual((run / 'lease').read_text(), lease)
|
||||
(run / 'ready').write_text(lease)
|
||||
return ''
|
||||
|
||||
cursor = types.SimpleNamespace(size=(2560, 1440), read=lambda: {})
|
||||
hello = json.dumps(dict(version=1, lease=lease, size=[2560, 1440]))
|
||||
stdin = types.SimpleNamespace(buffer=io.BytesIO((hello + '\nstop\n').encode()))
|
||||
stdout = io.StringIO()
|
||||
with patch.object(h, 'RUN', run), patch.object(h, 'systemctl', systemctl), \
|
||||
patch.object(h, 'XCursor', lambda: cursor), patch.object(h.sys, 'stdin', stdin), \
|
||||
patch.object(h.sys, 'stdout', stdout), \
|
||||
patch.object(h.select, 'select', lambda *args: ([stdin], [], [])):
|
||||
h.stream()
|
||||
self.assertEqual(json.loads(stdout.getvalue())['lease'], lease)
|
||||
self.assertEqual(calls, [('stop', h.GUARD), ('start', h.GUARD), ('stop', h.GUARD)])
|
||||
self.assertFalse((run / 'lease').exists())
|
||||
|
||||
def test_incomplete_manifest_rejected_before_stopping_capture(self):
|
||||
h = self.host
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / 'lease').write_text('a' * 32)
|
||||
(root / 'manifest.json').write_text(json.dumps({'sunshine': '0' * 64}))
|
||||
with patch.object(h, 'RUN', root), patch.object(h, 'ROOT', root), \
|
||||
patch.object(h, 'systemctl') as systemctl:
|
||||
with self.assertRaisesRegex(RuntimeError, 'incomplete pinned capture manifest'):
|
||||
h.guard()
|
||||
systemctl.assert_not_called()
|
||||
|
||||
def test_software_sprite_guard_stops_capture_before_unhide_on_failure(self):
|
||||
h = self.host
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / 'lease').write_text('a' * 32)
|
||||
manifest = {}
|
||||
for name in ('sunshine', 'capture-no-cursor.so'):
|
||||
(root / name).write_bytes(name.encode())
|
||||
manifest[name] = hashlib.sha256(name.encode()).hexdigest()
|
||||
(root / 'manifest.json').write_text(json.dumps(manifest))
|
||||
(root / 'hide-server-cursor').touch()
|
||||
events = []
|
||||
|
||||
class CursorOwner:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def hide_server_cursor(self):
|
||||
events.append('hide')
|
||||
|
||||
def __exit__(self, *_):
|
||||
events.append('restore')
|
||||
|
||||
def capture(lease):
|
||||
events.append('capture')
|
||||
raise RuntimeError('producer failed')
|
||||
|
||||
with patch.object(h, 'ROOT', root), patch.object(h, 'RUN', root), \
|
||||
patch.object(h, 'XCursor', CursorOwner), \
|
||||
patch.object(h, 'guard_capture', capture), \
|
||||
patch.object(h, 'systemctl', lambda *args: events.append(args)):
|
||||
with self.assertRaisesRegex(RuntimeError, 'producer failed'):
|
||||
h.guard()
|
||||
self.assertEqual(events, ['hide', 'capture', ('stop', h.SERVICE), 'restore'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,70 @@
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'gpu_hook', Path(__file__).resolve().parents[1] / 'pve-shared-gpu-hook.py')
|
||||
hook = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(hook)
|
||||
|
||||
|
||||
class SharedGPUHookTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.directory.cleanup)
|
||||
root = Path(self.directory.name)
|
||||
(root / 'boot').write_text('test-boot')
|
||||
for name, value in [('STATE_DIR', root), ('BOOT_ID', root / 'boot')]:
|
||||
p = patch.object(hook, name, value)
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
def test_running_guest_or_busy_device_never_resets(self):
|
||||
for failing_guard in ('stopped', 'unused_gpu'):
|
||||
with self.subTest(guard=failing_guard), patch.object(hook, 'stopped'), \
|
||||
patch.object(hook, 'unused_gpu'), patch.object(hook, 'reenumerate') as reset:
|
||||
getattr(hook, failing_guard).side_effect = RuntimeError('busy')
|
||||
with self.assertRaises(RuntimeError):
|
||||
hook.handle(102, 'pre-start')
|
||||
reset.assert_not_called()
|
||||
self.assertFalse((hook.STATE_DIR / 'reservation.json').exists())
|
||||
|
||||
def test_second_start_blocked_until_first_guest_stops(self):
|
||||
with patch.object(hook, 'stopped'), patch.object(hook, 'unused_gpu'), \
|
||||
patch.object(hook, 'reenumerate') as reset:
|
||||
hook.handle(102, 'pre-start')
|
||||
with self.assertRaises(RuntimeError):
|
||||
hook.handle(100, 'pre-start')
|
||||
self.assertEqual(reset.call_count, 1)
|
||||
hook.handle(100, 'post-stop')
|
||||
self.assertTrue((hook.STATE_DIR / 'reservation.json').exists())
|
||||
hook.handle(102, 'post-start')
|
||||
hook.handle(102, 'post-stop')
|
||||
hook.handle(100, 'pre-start')
|
||||
self.assertEqual(reset.call_count, 2)
|
||||
|
||||
def test_failed_reset_stays_reserved_and_is_not_replayed(self):
|
||||
with patch.object(hook, 'stopped'), patch.object(hook, 'unused_gpu'), \
|
||||
patch.object(hook, 'reenumerate', side_effect=RuntimeError('device missing')) as reset:
|
||||
with self.assertRaises(RuntimeError):
|
||||
hook.handle(102, 'pre-start')
|
||||
hook.handle(102, 'post-stop')
|
||||
state = json.loads((hook.STATE_DIR / 'reservation.json').read_text())
|
||||
self.assertEqual(state['phase'], 'failed')
|
||||
with self.assertRaises(RuntimeError):
|
||||
hook.handle(102, 'pre-start')
|
||||
self.assertEqual(reset.call_count, 1)
|
||||
|
||||
def test_post_start_cannot_claim_another_guest_reservation(self):
|
||||
with patch.object(hook, 'stopped'), patch.object(hook, 'unused_gpu'), \
|
||||
patch.object(hook, 'reenumerate'):
|
||||
hook.handle(102, 'pre-start')
|
||||
with self.assertRaises(RuntimeError):
|
||||
hook.handle(100, 'post-start')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user