diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..11b690a
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,42 @@
+name: Lint
+
+on:
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ lint:
+ strategy:
+ fail-fast: false
+ matrix:
+ linter: [
+ {"name": "tests-flake8", "format": "flake8", "cwd": ".", "cmd": "flake8 tests"},
+ {"name": "tests-mypy", "format": "mypy", "cwd": ".", "cmd": "mypy tests"},
+ {"name": "tests-pylint", "format": "pylint-json", "cwd": ".", "cmd": "pylint --load-plugins pylint_pytest $(Get-ChildItem -Filter *.py -Recurse tests)"},
+ {"name": "eslint", "format": "eslint-unix", "cwd": "bugalua", "cmd": "node_modules\\.bin\\eslint.ps1 --format=unix ."},
+ ]
+ name: ${{ matrix.linter.name }}
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ - name: Install Python dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r tests\dev-requirements.txt
+ python -m pip install git+https://github.com/bugale/Bugalintly.git@bugalintly
+ - name: Install Node.JS dependencies
+ run: |
+ cd bugalua
+ npm install .
+ - name: Lint
+ run: |
+ cd ${{ matrix.linter.cwd }}
+ ${{ matrix.linter.cmd }} > lint.log
+ $exitcode = $LASTEXITCODE
+ type lint.log | Lintly --log --no-request-changes --no-review-body --base-dir .. --format=${{ matrix.linter.format }} --comment-tag=${{ matrix.linter.name }}
+ exit $exitcode
+ env:
+ LINTLY_API_KEY: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..16c7669
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,29 @@
+name: Test
+
+on:
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+ - name: Install Windows SDK 22621
+ run: |
+ Invoke-WebRequest -UseBasicParsing -Uri 'https://go.microsoft.com/fwlink/p/?linkid=2196241' -OutFile 'winsdksetup.exe'
+ Start-Process -Wait '.\winsdksetup.exe' '/features OptionId.WindowsDesktopDebuggers /quiet /norestart'
+ Remove-Item -Force 'winsdksetup.exe'
+ - name: Install Bugalua
+ run: |
+ .\Install.ps1
+ - name: Install test dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -r tests\requirements.txt
+ - name: Test
+ run: |
+ pytest tests
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5a94566
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+**\__pycache__
+**\package-lock.json
+**\node_modules
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..72fb5b2
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "python.linting.enabled": true,
+ "python.linting.flake8Enabled": true,
+ "python.linting.flake8Args": [],
+ "python.linting.mypyEnabled": true,
+ "python.linting.mypyArgs": ["--follow-imports=silent"],
+ "python.linting.pylintEnabled": true,
+ "python.linting.pylintArgs": ["--load-plugins", "pylint_pytest"],
+ "python.testing.pytestEnabled": true,
+ "eslint.enable": true,
+ "files.associations": {
+ "*.yaml": "home-assistant"
+ },
+ "editor.tabSize": 4,
+ "[javascript]": {
+ "editor.tabSize": 2
+ }
+}
\ No newline at end of file
diff --git a/Install.ps1 b/Install.ps1
new file mode 100644
index 0000000..5e10d05
--- /dev/null
+++ b/Install.ps1
@@ -0,0 +1,5 @@
+param ($windbgPath='C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe')
+(Get-Content $PSScriptRoot\Manifest\config.xml) -Replace [regex]::escape('C:\the\path\to\this\repo'), "$PSScriptRoot" | Set-Content $PSScriptRoot\Manifest\config.xml
+$TempFile = New-TemporaryFile
+".settings load $PSScriptRoot\Manifest\config.xml`n.settings save`nqq`n" | Out-File -Encoding ASCII $TempFile
+& "$windbgPath" "-c" "$<$TempFile" "C:\Windows\system32\cmd.exe"
diff --git a/Manifest/Manifest.1.xml b/Manifest/Manifest.1.xml
new file mode 100644
index 0000000..c401590
--- /dev/null
+++ b/Manifest/Manifest.1.xml
@@ -0,0 +1,24 @@
+
+
+
+ Bugalua
+ 1.0.0.0
+ Lua debugging extension
+
+
+
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Manifest/ManifestVersion.txt b/Manifest/ManifestVersion.txt
new file mode 100644
index 0000000..d04da7a
--- /dev/null
+++ b/Manifest/ManifestVersion.txt
@@ -0,0 +1,3 @@
+1
+1.0.0.0
+1
\ No newline at end of file
diff --git a/Manifest/config.xml b/Manifest/config.xml
new file mode 100644
index 0000000..7df73f1
--- /dev/null
+++ b/Manifest/config.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bugalua/bugalua.js b/bugalua/bugalua.js
new file mode 100644
index 0000000..559c51c
--- /dev/null
+++ b/bugalua/bugalua.js
@@ -0,0 +1,194 @@
+'use strict';
+
+const console = { log: (...args) => host.diagnostics.debugLog(...args, '\n') };
+
+function getLuaState(luaAddr) {
+ // #region General Types
+ const ptrsize = host.currentSession.Attributes.Machine.PointerSize;
+ const voidType = { size: 0, name: 'void', from(addr) { return addr; } };
+
+ function int(size, name, signed = false) {
+ return {
+ size,
+ name,
+ from(addr) { return parseInt(host.memory.readMemoryValues(addr, 1, size, signed)[0]); },
+ };
+ }
+ const [uint8, uint32] = [int(1, 'uint8'), int(4, 'uint32')];
+ const sizeT = int(ptrsize, 'size_t');
+
+ function ptr(type) {
+ return {
+ size: ptrsize,
+ name: `${type.name}*`,
+ from(addr) {
+ return {
+ addr: sizeT.from(addr),
+ type: type.name,
+ get value() { return type.from(sizeT.from(addr)); },
+ toString() { return `[${type.name}* at 0x${addr.toString(16)}]`; },
+ };
+ },
+ };
+ }
+ function ptr32(type) {
+ return {
+ size: 4,
+ name: `${type.name}*`,
+ from(addr) {
+ return {
+ addr: uint32.from(addr),
+ type: type.name,
+ get value() { return type.from(uint32.from(addr)); },
+ toString() { return `[${type.name}* at 0x${addr.toString(16)}]`; },
+ };
+ },
+ };
+ }
+
+ function array(type, length) {
+ class Array {
+ constructor(addr) {
+ Object.defineProperties(this, {
+ length: { value: length, writable: false },
+ [Symbol.iterator]: {
+ value: () => {
+ let index = 0;
+
+ return {
+ next: () => ({
+ done: index >= this.length,
+ value: type.from(addr + (index++) * type.size),
+ }),
+ };
+ },
+ },
+ });
+ }
+ }
+ return { size: length * type.size, name: (`${type.name}[${length}]`), from(addr) { return new Array(addr); } };
+ }
+
+ function struct(name, fieldsArr) {
+ let offset = 0;
+ const fields = [];
+ fieldsArr.forEach(([type, fieldName]) => {
+ fields.push({ type, fieldName, offset });
+ offset += type.size;
+ });
+ class Struct {
+ constructor(addr) {
+ fields.forEach((field) => {
+ Object.defineProperty(this, field.fieldName, {
+ get() {
+ return field.type.from(addr + field.offset);
+ },
+ });
+ });
+ this.toString = () => `[struct ${name} at 0x${addr.toString(16)}]`;
+ }
+ }
+ return { size: offset, name, from(addr) { return new Struct(addr); } };
+ }
+
+ function defer(fn, name) {
+ return { get size() { return fn().size; }, name, get from() { return fn().from; } };
+ }
+ // #endregion General Types
+
+ // #region LuaJIT 2.0.5 Types
+ const MSize = uint32;
+ const MRef = ptr32;
+ const GCRef = ptr32;
+
+ const luaJit205GCState = struct('GC_State', [
+ [MSize, 'total'],
+ [MSize, 'threshold'],
+ [uint8, 'currentwhite'],
+ [uint8, 'state'],
+ [uint8, 'nocdatafin'],
+ [uint8, 'unused2'],
+ [MSize, 'sweepstr'],
+ [GCRef(voidType), 'root'],
+ [MRef(voidType), 'sweep'],
+ [GCRef(voidType), 'gray'],
+ [GCRef(voidType), 'grayagain'],
+ [GCRef(voidType), 'weak'],
+ [GCRef(voidType), 'mmudata'],
+ [MSize, 'stepmul'],
+ [MSize, 'debt'],
+ [MSize, 'estimate'],
+ [MSize, 'pause'],
+ ]);
+
+ const luaJit205GlobalState = struct('global_State', [
+ [ptr(GCRef(voidType)), 'strhash'],
+ [MSize, 'strmask'],
+ [MSize, 'strnum'],
+ [ptr(voidType), 'allocf'],
+ [ptr(voidType), 'allocd'],
+ [luaJit205GCState, 'gc'],
+ /* Incomplete */
+ ]);
+
+ const luaJit205LuaState = struct('lua_State', [
+ [GCRef(voidType), 'nextgc'],
+ [uint8, 'marked'],
+ [uint8, 'gct'],
+ [uint8, 'dummy_ffid'],
+ [uint8, 'status'],
+ [MRef(luaJit205GlobalState), 'glref'],
+ /* Incomplete */
+ ]);
+ // #endregion LuaJIT 2.0.5 Types
+
+ // #region Frontend
+ class SizeBytes {
+ constructor(bytes) {
+ this.bytes = bytes;
+ }
+
+ toString() {
+ if (this.bytes <= 512) {
+ return `${this.bytes.toLocaleString()} B`;
+ }
+
+ const unitsArr = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ let number = this.bytes / 1024;
+ let units = 0;
+ while (number > 512) {
+ number /= 1024;
+ units += 1;
+ }
+ return `${number.toFixed(2)} ${unitsArr[units]} (${this.bytes.toLocaleString()} B)`;
+ }
+ }
+
+ class LuaState {
+ constructor(addr) {
+ this.L = luaJit205LuaState.from(addr);
+ }
+
+ get address() {
+ return this.L.addr;
+ }
+
+ get MemoryUsage() {
+ return new SizeBytes(this.L.glref.value.gc.total);
+ }
+ }
+ // #endregion Frontend
+
+ return new LuaState(luaAddr);
+}
+
+function initializeScript() {
+ return [
+ new host.functionAlias((addr) => getLuaState(addr), 'lua'),
+ ];
+}
+
+const exports = [
+ console,
+ initializeScript,
+];
diff --git a/bugalua/package.json b/bugalua/package.json
new file mode 100644
index 0000000..5cb381c
--- /dev/null
+++ b/bugalua/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "bugalua",
+ "version": "0.0.1",
+ "main": "bugalua.js",
+ "devDependencies": {
+ "eslint": "*",
+ "eslint-config-airbnb": "*"
+ },
+ "eslintConfig": {
+ "extends": [ "airbnb" ],
+ "rules": {
+ "strict": "off",
+ "linebreak-style": "off",
+ "max-classes-per-file": "off",
+ "no-unused-vars": ["error", { "varsIgnorePattern": "^exports$" }],
+ "new-cap": "off",
+ "no-plusplus": "off",
+ "radix": "off"
+ },
+ "globals": { "host": "readonly" }
+ }
+}
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..0fe1aa5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,21 @@
+[flake8]
+max-line-length = 160
+
+[mypy]
+namespace_packages = True
+strict = True
+show_error_codes = True
+show_column_numbers = True
+
+[mypy-cffi,minidump.*]
+ignore_missing_imports = True
+
+[tool:pytest]
+log_cli = 1
+log_cli_level = DEBUG
+filterwarnings = error
+
+[pylint.config]
+max-line-length = 160
+output-format = json
+disable = missing-module-docstring,missing-class-docstring,missing-function-docstring,broad-except
diff --git a/tests/binaries/luajit-2.0.5-Win-x64/header.h b/tests/binaries/luajit-2.0.5-Win-x64/header.h
new file mode 100644
index 0000000..03cf3c9
--- /dev/null
+++ b/tests/binaries/luajit-2.0.5-Win-x64/header.h
@@ -0,0 +1,214 @@
+/* option for multiple returns in `lua_pcall' and `lua_call' */
+#define LUA_MULTRET -1
+
+
+/*
+** pseudo-indices
+*/
+#define LUA_REGISTRYINDEX -10000
+#define LUA_ENVIRONINDEX -10001
+#define LUA_GLOBALSINDEX -10002
+
+
+/* thread status; 0 is OK */
+#define LUA_YIELD 1
+#define LUA_ERRRUN 2
+#define LUA_ERRSYNTAX 3
+#define LUA_ERRMEM 4
+#define LUA_ERRERR 5
+
+
+typedef struct lua_State lua_State;
+
+typedef int (*lua_CFunction) (lua_State *L);
+
+
+/*
+** functions that read/write blocks when loading/dumping Lua chunks
+*/
+typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz);
+
+typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud);
+
+
+/*
+** prototype for memory-allocation functions
+*/
+typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);
+
+
+/*
+** basic types
+*/
+#define LUA_TNONE -1
+
+#define LUA_TNIL 0
+#define LUA_TBOOLEAN 1
+#define LUA_TLIGHTUSERDATA 2
+#define LUA_TNUMBER 3
+#define LUA_TSTRING 4
+#define LUA_TTABLE 5
+#define LUA_TFUNCTION 6
+#define LUA_TUSERDATA 7
+#define LUA_TTHREAD 8
+
+
+
+/* minimum Lua stack available to a C function */
+#define LUA_MINSTACK 20
+
+
+/* type of numbers in Lua */
+typedef double lua_Number;
+
+
+/* type for integer functions */
+typedef size_t lua_Integer;
+
+
+
+/*
+** state manipulation
+*/
+extern lua_State *(lua_newstate) (lua_Alloc f, void *ud);
+extern void (lua_close) (lua_State *L);
+extern lua_State *(lua_newthread) (lua_State *L);
+
+extern lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf);
+
+
+/*
+** basic stack manipulation
+*/
+extern int (lua_gettop) (lua_State *L);
+extern void (lua_settop) (lua_State *L, int idx);
+extern void (lua_pushvalue) (lua_State *L, int idx);
+extern void (lua_remove) (lua_State *L, int idx);
+extern void (lua_insert) (lua_State *L, int idx);
+extern void (lua_replace) (lua_State *L, int idx);
+extern int (lua_checkstack) (lua_State *L, int sz);
+
+extern void (lua_xmove) (lua_State *from, lua_State *to, int n);
+
+
+/*
+** access functions (stack -> C)
+*/
+
+extern int (lua_isnumber) (lua_State *L, int idx);
+extern int (lua_isstring) (lua_State *L, int idx);
+extern int (lua_iscfunction) (lua_State *L, int idx);
+extern int (lua_isuserdata) (lua_State *L, int idx);
+extern int (lua_type) (lua_State *L, int idx);
+extern const char *(lua_typename) (lua_State *L, int tp);
+
+extern int (lua_equal) (lua_State *L, int idx1, int idx2);
+extern int (lua_rawequal) (lua_State *L, int idx1, int idx2);
+extern int (lua_lessthan) (lua_State *L, int idx1, int idx2);
+
+extern lua_Number (lua_tonumber) (lua_State *L, int idx);
+extern lua_Integer (lua_tointeger) (lua_State *L, int idx);
+extern int (lua_toboolean) (lua_State *L, int idx);
+extern const char *(lua_tolstring) (lua_State *L, int idx, size_t *len);
+extern size_t (lua_objlen) (lua_State *L, int idx);
+extern lua_CFunction (lua_tocfunction) (lua_State *L, int idx);
+extern void *(lua_touserdata) (lua_State *L, int idx);
+extern lua_State *(lua_tothread) (lua_State *L, int idx);
+extern const void *(lua_topointer) (lua_State *L, int idx);
+
+
+/*
+** push functions (C -> stack)
+*/
+extern void (lua_pushnil) (lua_State *L);
+extern void (lua_pushnumber) (lua_State *L, lua_Number n);
+extern void (lua_pushinteger) (lua_State *L, lua_Integer n);
+extern void (lua_pushlstring) (lua_State *L, const char *s, size_t l);
+extern void (lua_pushstring) (lua_State *L, const char *s);
+extern const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...);
+extern void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
+extern void (lua_pushboolean) (lua_State *L, int b);
+extern void (lua_pushlightuserdata) (lua_State *L, void *p);
+extern int (lua_pushthread) (lua_State *L);
+
+
+/*
+** get functions (Lua -> stack)
+*/
+extern void (lua_gettable) (lua_State *L, int idx);
+extern void (lua_getfield) (lua_State *L, int idx, const char *k);
+extern void (lua_rawget) (lua_State *L, int idx);
+extern void (lua_rawgeti) (lua_State *L, int idx, int n);
+extern void (lua_createtable) (lua_State *L, int narr, int nrec);
+extern void *(lua_newuserdata) (lua_State *L, size_t sz);
+extern int (lua_getmetatable) (lua_State *L, int objindex);
+extern void (lua_getfenv) (lua_State *L, int idx);
+
+
+/*
+** set functions (stack -> Lua)
+*/
+extern void (lua_settable) (lua_State *L, int idx);
+extern void (lua_setfield) (lua_State *L, int idx, const char *k);
+extern void (lua_rawset) (lua_State *L, int idx);
+extern void (lua_rawseti) (lua_State *L, int idx, int n);
+extern int (lua_setmetatable) (lua_State *L, int objindex);
+extern int (lua_setfenv) (lua_State *L, int idx);
+
+
+/*
+** `load' and `call' functions (load and run Lua code)
+*/
+extern void (lua_call) (lua_State *L, int nargs, int nresults);
+extern int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc);
+extern int (lua_cpcall) (lua_State *L, lua_CFunction func, void *ud);
+extern int (lua_load) (lua_State *L, lua_Reader reader, void *dt,
+ const char *chunkname);
+
+extern int (lua_dump) (lua_State *L, lua_Writer writer, void *data);
+
+
+/*
+** coroutine functions
+*/
+extern int (lua_yield) (lua_State *L, int nresults);
+extern int (lua_resume) (lua_State *L, int narg);
+extern int (lua_status) (lua_State *L);
+
+/*
+** garbage-collection function and options
+*/
+
+#define LUA_GCSTOP 0
+#define LUA_GCRESTART 1
+#define LUA_GCCOLLECT 2
+#define LUA_GCCOUNT 3
+#define LUA_GCCOUNTB 4
+#define LUA_GCSTEP 5
+#define LUA_GCSETPAUSE 6
+#define LUA_GCSETSTEPMUL 7
+
+extern int (lua_gc) (lua_State *L, int what, int data);
+
+
+/*
+** miscellaneous functions
+*/
+
+extern int (lua_error) (lua_State *L);
+
+extern int (lua_next) (lua_State *L, int idx);
+
+extern void (lua_concat) (lua_State *L, int n);
+
+extern lua_Alloc (lua_getallocf) (lua_State *L, void **ud);
+extern void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);
+
+
+/*
+** auxillary functions
+*/
+
+extern lua_State *(luaL_newstate) (void);
+extern void luaL_openlibs(lua_State *L);
+extern int luaL_loadstring(lua_State *L, const char *s);
diff --git a/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll b/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll
new file mode 100644
index 0000000..c0cde4e
Binary files /dev/null and b/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll differ
diff --git a/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb b/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb
new file mode 100644
index 0000000..bf5fc3a
Binary files /dev/null and b/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb differ
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..779cdb3
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,35 @@
+from typing import Any
+
+from cffi import FFI
+import pytest
+
+
+from infra import lua_loaders
+
+
+@pytest.fixture
+def _ffi_and_lua() -> tuple[FFI, Any]:
+ return lua_loaders.luajit_205()
+
+
+@pytest.fixture
+def ffi(_ffi_and_lua: tuple[FFI, Any]) -> FFI:
+ return _ffi_and_lua[0]
+
+
+@pytest.fixture
+def lua(_ffi_and_lua: tuple[FFI, Any]) -> Any:
+ return _ffi_and_lua[1]
+
+
+@pytest.fixture
+def lua_state(lua: Any) -> Any:
+ state = lua.luaL_newstate()
+ lua.luaL_openlibs(state)
+ yield state
+ lua.lua_close(state)
+
+
+@pytest.fixture
+def lua_state_addr(ffi: FFI, lua_state: Any) -> int:
+ return int(ffi.cast('size_t', lua_state))
diff --git a/tests/dev-requirements.txt b/tests/dev-requirements.txt
new file mode 100644
index 0000000..fcb39b6
--- /dev/null
+++ b/tests/dev-requirements.txt
@@ -0,0 +1,5 @@
+pylint
+pylint-pytest
+mypy
+flake8
+-r requirements.txt
\ No newline at end of file
diff --git a/tests/infra/__init__.py b/tests/infra/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/infra/dumps.py b/tests/infra/dumps.py
new file mode 100644
index 0000000..f4a5eb6
--- /dev/null
+++ b/tests/infra/dumps.py
@@ -0,0 +1,72 @@
+import os
+import tempfile
+import subprocess
+from typing import Any
+import logging
+
+from cffi import FFI
+from minidump.utils import createminidump
+
+logger = logging.getLogger('tests')
+
+
+def take_dump(lua: Any, ffi: FFI, lua_state: Any, code: str) -> str:
+ """ Runs the given lua code, exposing a `dump()` global function. Returns the name of the dump file generated """
+ with tempfile.NamedTemporaryFile(suffix='.dmp', delete=False) as dump_file:
+ filename = dump_file.name
+ dumps_taken = 0
+
+ def dump(_: Any) -> int:
+ nonlocal dumps_taken
+ logger.debug('Taking a dump')
+ try:
+ if dumps_taken == 0:
+ createminidump.create_dump(pid=os.getpid(), output_filename=filename, mindumptype=(
+ createminidump.MINIDUMP_TYPE.MiniDumpWithFullMemory |
+ createminidump.MINIDUMP_TYPE.MiniDumpIgnoreInaccessibleMemory |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithUnloadedModules |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithProcessThreadData |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithFullMemoryInfo |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithThreadInfo |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithHandleData |
+ createminidump.MINIDUMP_TYPE.MiniDumpWithTokenInformation
+ ))
+ dumps_taken += 1
+ except Exception:
+ logger.exception('Failed taking dump')
+ return 0
+
+ callback = ffi.callback('lua_CFunction', dump)
+ lua.lua_pushstring(lua_state, b'dump')
+ lua.lua_pushcclosure(lua_state, callback, 0)
+ lua.lua_settable(lua_state, lua.LUA_GLOBALSINDEX)
+ lua.luaL_loadstring(lua_state, code.encode('utf-8'))
+ if lua.lua_pcall(lua_state, 0, 0, 0) != 0:
+ error_message = ffi.string(lua.lua_tolstring(lua_state, -1, ffi.NULL)).decode('utf-8')
+ lua.lua_remove(lua_state, -1)
+ raise Exception(f'Error running Lua code {error_message}')
+ assert dumps_taken == 1, f'Expected exactly one dump to be requested, {dumps_taken} were requested'
+ assert os.path.isfile(filename), 'No dump was created'
+ logger.debug('Successfully took dump at: %s', filename)
+ return filename
+
+
+def run_windbg_commands(dump: str, commands: str) -> str:
+ """ Opens the given dump file in WinDbg, runs the given commands, and returns the output """
+ token = '***WINDBGTOKEN***'
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False) as commands_file:
+ commands_file.write(f'''
+!lua
+.echo "{token}"
+{commands}
+.echo "{token}"
+qq
+''')
+ logger.debug('Running windbg commands: %s', commands)
+ proc = subprocess.run([r'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe', '-z', dump, '-c', f'$<{commands_file.name}'],
+ check=False, capture_output=True, encoding='utf-8')
+ # Token appears 4 times (twice inside the command echo, and twice as its output)
+ output = proc.stdout.split(token)[2].strip() # Take the middle part
+ output = '\n'.join(output.split('\n')[:-1]) # Remove the last line (which is the beginning of the token echo command)
+ logger.debug('windbg commands: %s returned: %s', commands, output)
+ return output
diff --git a/tests/infra/lua_loaders.py b/tests/infra/lua_loaders.py
new file mode 100644
index 0000000..f3ac2e5
--- /dev/null
+++ b/tests/infra/lua_loaders.py
@@ -0,0 +1,13 @@
+import os
+from typing import Any
+
+from cffi import FFI
+
+
+def luajit_205() -> tuple[FFI, Any]:
+ """ LuaJit 2.0.5 FFI object """
+ ffi = FFI()
+ directory = os.path.join(os.path.dirname(__file__), '..', 'binaries', 'luajit-2.0.5-Win-x64')
+ with open(os.path.join(directory, 'header.h'), encoding='utf-8') as header:
+ ffi.cdef(header.read())
+ return ffi, ffi.dlopen(os.path.join(directory, 'lua51.dll'))
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..141ed57
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,3 @@
+pytest
+cffi
+minidump
\ No newline at end of file
diff --git a/tests/test_memory_usage.py b/tests/test_memory_usage.py
new file mode 100644
index 0000000..6f284f2
--- /dev/null
+++ b/tests/test_memory_usage.py
@@ -0,0 +1,28 @@
+from typing import Any
+import logging
+
+from cffi import FFI
+import pytest
+
+from infra import dumps
+
+logger = logging.getLogger('tests')
+
+
+@pytest.mark.parametrize('usage', [0, 1024, 133337, 1024 * 512, 1024 * 1024 * 512])
+def test_memory_usage(ffi: FFI, lua: Any, lua_state: Any, lua_state_addr: int, usage: int) -> None:
+ """ Test """
+ lua.lua_newuserdata(lua_state, usage)
+ dump = dumps.take_dump(lua=lua, ffi=ffi, lua_state=lua_state, code='''
+ gcusage = collectgarbage("count")
+ dump()
+ ''')
+ lua.lua_pushstring(lua_state, b'gcusage')
+ lua.lua_gettable(lua_state, lua.LUA_GLOBALSINDEX)
+ gcusage = lua.lua_tonumber(lua_state, -1)
+ lua.lua_remove(lua_state, -1)
+ assert (gcusage * 1024) >= usage, 'GC usage is smaller than expected'
+
+ output = dumps.run_windbg_commands(dump=dump, commands=f'dx @$scriptContents.getLuaState({lua_state_addr}).MemoryUsage.bytes')
+ memory_used = int(output.split(':')[2].strip(), 0)
+ assert memory_used == gcusage * 1024, 'Memory usage is not as expected'