Fix various lua low level access bugs (#575)

### 1 Access key was not added properly
First commit fixes this 

### 2 Lua engine was getting killed when it waits for async function
runLua function gets run in two different paths (i.e. runLua for lua
edit triggers and another one for onOuput handling) However, when one of
invocations waits for another promise from js side (i.e. low level
function) to finish, it gives chance for next trigger mode to run which
would call `if(luaEngine){ luaEngine.global.close() }` that will close
lua engine for the invocation that was waiting for promise which is
problematic.

Second commit fixes this by having a pool of engine for each trigger
mode having single mutex that prevents running same engine at the same
time while allowing different tigger modes to run parallely

### 3 Break statements were missing making every trigger to run for
input event.
This was causing insane lagging and buggy behaviours. Third commit fixes
this.
This commit is contained in:
kwaroran
2024-07-17 16:44:17 +09:00
committed by GitHub
2 changed files with 498 additions and 384 deletions

88
src/ts/mutex.ts Normal file
View File

@@ -0,0 +1,88 @@
/**
* A lock for synchronizing async operations.
* Use this to protect a critical section
* from getting modified by multiple async operations
* at the same time.
*/
export class Mutex {
/**
* When multiple operations attempt to acquire the lock,
* this queue remembers the order of operations.
*/
private _queue: {
resolve: (release: ReleaseFunction) => void
}[] = []
private _isLocked = false
/**
* Wait until the lock is acquired.
* @returns A function that releases the acquired lock.
*/
acquire() {
return new Promise<ReleaseFunction>((resolve) => {
this._queue.push({resolve})
this._dispatch()
});
}
/**
* Enqueue a function to be run serially.
*
* This ensures no other functions will start running
* until `callback` finishes running.
* @param callback Function to be run exclusively.
* @returns The return value of `callback`.
*/
async runExclusive<T>(callback: () => Promise<T>) {
const release = await this.acquire()
try {
return await callback()
} finally {
release()
}
}
/**
* Check the availability of the resource
* and provide access to the next operation in the queue.
*
* _dispatch is called whenever availability changes,
* such as after lock acquire request or lock release.
*/
private _dispatch() {
if (this._isLocked) {
// The resource is still locked.
// Wait until next time.
return
}
const nextEntry = this._queue.shift()
if (!nextEntry) {
// There is nothing in the queue.
// Do nothing until next dispatch.
return
}
// The resource is available.
this._isLocked = true
// and give access to the next operation
// in the queue.
nextEntry.resolve(this._buildRelease())
}
/**
* Build a release function for each operation
* so that it can release the lock after
* the operation is complete.
*/
private _buildRelease(): ReleaseFunction {
return () => {
// Each release function make
// the resource available again
this._isLocked = false
// and call dispatch.
this._dispatch()
}
}
}
type ReleaseFunction = () => void

View File

@@ -11,14 +11,21 @@ import type { OpenAIChat } from ".";
import { requestChatData } from "./request";
import { v4 } from "uuid";
import { getModuleTriggers } from "./modules";
import { Mutex } from "../mutex";
let luaFactory:LuaFactory
let luaEngine:LuaEngine
let lastCode = ''
let LuaSafeIds = new Set<string>()
let LuaEditDisplayIds = new Set<string>()
let LuaLowLevelIds = new Set<string>()
interface LuaEngineState {
code: string;
engine: LuaEngine;
mutex: Mutex;
}
let LuaEngines = new Map<string, LuaEngineState>()
export async function runLua(code:string, arg:{
char?:character|groupChat|simpleCharacterArgument,
chat?:Chat
@@ -37,14 +44,26 @@ export async function runLua(code:string, arg:{
let stopSending = false
let lowLevelAccess = arg.lowLevelAccess ?? false
if(!luaEngine || lastCode !== code){
if(luaEngine){
luaEngine.global.close()
}
if(!luaFactory){
makeLuaFactory()
await makeLuaFactory()
}
luaEngine = await luaFactory.createEngine({injectObjects: true})
let luaEngineState = LuaEngines.get(mode)
let wasEmpty = false
if (!luaEngineState) {
luaEngineState = {
code,
engine: await luaFactory.createEngine({injectObjects: true}),
mutex: new Mutex()
}
LuaEngines.set(mode, luaEngineState)
wasEmpty = true
}
return await luaEngineState.mutex.runExclusive(async () => {
if (wasEmpty || code !== luaEngineState.code) {
if (!wasEmpty)
luaEngineState.engine.global.close()
luaEngineState.engine = await luaFactory.createEngine({injectObjects: true})
const luaEngine = luaEngineState.engine
luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => {
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
return
@@ -380,7 +399,7 @@ export async function runLua(code:string, arg:{
})
await luaEngine.doString(luaCodeWarper(code))
lastCode = code
luaEngineState.code = code
}
let accessKey = v4()
if(mode === 'editDisplay'){
@@ -389,10 +408,11 @@ export async function runLua(code:string, arg:{
else{
LuaSafeIds.add(accessKey)
if(lowLevelAccess){
LuaLowLevelIds.add(v4())
LuaLowLevelIds.add(accessKey)
}
}
let res:any
const luaEngine = luaEngineState.engine
try {
switch(mode){
case 'input':{
@@ -400,18 +420,21 @@ export async function runLua(code:string, arg:{
if(func){
res = await func(accessKey)
}
break
}
case 'output':{
const func = luaEngine.global.get('onOutput')
if(func){
res = await func(accessKey)
}
break
}
case 'start':{
const func = luaEngine.global.get('onStart')
if(func){
res = await func(accessKey)
}
break
}
case 'editRequest':
case 'editDisplay':
@@ -422,12 +445,14 @@ export async function runLua(code:string, arg:{
res = await func(mode, accessKey, JSON.stringify(data))
res = JSON.parse(res)
}
break
}
default:{
const func = luaEngine.global.get(mode)
if(func){
res = await func(accessKey)
}
break
}
}
if(res === false){
@@ -443,6 +468,7 @@ export async function runLua(code:string, arg:{
return {
stopSending, chat, res
}
})
}
async function makeLuaFactory(){