mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(whatsapp): await write stream finish before returning encFilePath (#65896)
* fix(whatsapp): await write stream finish in encryptedStream to fix race-condition ENOENT crash * fix(whatsapp): ship Baileys media hotfix on npm installs * fix(whatsapp): keep Baileys hotfix postinstall best-effort * fix(whatsapp): harden Baileys postinstall temp writes * fix(whatsapp): preserve Baileys hotfix file mode --------- Co-authored-by: termtek <termtek@ubuntu.tail2b72cd.ts.net>
This commit is contained in:
@@ -6,8 +6,21 @@
|
||||
// plugin deps from the workspace root, so stale plugin-local node_modules must
|
||||
// not linger under extensions/* and shadow the root graph.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
closeSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolveNpmRunner } from "./npm-runner.mjs";
|
||||
|
||||
@@ -17,6 +30,34 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions");
|
||||
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const BAILEYS_MEDIA_FILE = join(
|
||||
"node_modules",
|
||||
"@whiskeysockets",
|
||||
"baileys",
|
||||
"lib",
|
||||
"Utils",
|
||||
"messages-media.js",
|
||||
);
|
||||
const BAILEYS_MEDIA_HOTFIX_NEEDLE = [
|
||||
" encFileWriteStream.write(mac);",
|
||||
" encFileWriteStream.end();",
|
||||
" originalFileStream?.end?.();",
|
||||
" stream.destroy();",
|
||||
" logger?.debug('encrypted data successfully');",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_HOTFIX_REPLACEMENT = [
|
||||
" encFileWriteStream.write(mac);",
|
||||
" const encFinishPromise = once(encFileWriteStream, 'finish');",
|
||||
" const originalFinishPromise = originalFileStream ? once(originalFileStream, 'finish') : Promise.resolve();",
|
||||
" encFileWriteStream.end();",
|
||||
" originalFileStream?.end?.();",
|
||||
" stream.destroy();",
|
||||
" await Promise.all([encFinishPromise, originalFinishPromise]);",
|
||||
" logger?.debug('encrypted data successfully');",
|
||||
].join("\n");
|
||||
const BAILEYS_MEDIA_ONCE_IMPORT_RE = /import\s+\{\s*once\s*\}\s+from\s+['"]events['"]/u;
|
||||
const BAILEYS_MEDIA_ASYNC_CONTEXT_RE =
|
||||
/async\s+function\s+encryptedStream|encryptedStream\s*=\s*async/u;
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||
@@ -152,6 +193,121 @@ export function createNestedNpmInstallEnv(env = process.env) {
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
export function applyBaileysEncryptedStreamFinishHotfix(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
const readFile = params.readFileSync ?? readFileSync;
|
||||
const resolveRealPath = params.realpathSync ?? realpathSync;
|
||||
const chmodFile = params.chmodSync ?? chmodSync;
|
||||
const openFile = params.openSync ?? openSync;
|
||||
const closeFile = params.closeSync ?? closeSync;
|
||||
const renameFile = params.renameSync ?? renameSync;
|
||||
const removePath = params.rmSync ?? rmSync;
|
||||
const createTempPath =
|
||||
params.createTempPath ??
|
||||
((unsafeTargetPath) =>
|
||||
join(
|
||||
dirname(unsafeTargetPath),
|
||||
`.${basename(unsafeTargetPath)}.openclaw-hotfix-${randomUUID()}`,
|
||||
));
|
||||
const writeFile =
|
||||
params.writeFileSync ?? ((filePath, value) => writeFileSync(filePath, value, "utf8"));
|
||||
const targetPath = join(packageRoot, BAILEYS_MEDIA_FILE);
|
||||
const nodeModulesRoot = join(packageRoot, "node_modules");
|
||||
|
||||
function validateTargetPath() {
|
||||
if (!pathExists(targetPath)) {
|
||||
return { ok: false, reason: "missing" };
|
||||
}
|
||||
|
||||
const targetStats = pathLstat(targetPath);
|
||||
if (!targetStats.isFile() || targetStats.isSymbolicLink()) {
|
||||
return { ok: false, reason: "unsafe_target", targetPath };
|
||||
}
|
||||
|
||||
const nodeModulesRootReal = resolveRealPath(nodeModulesRoot);
|
||||
const targetPathReal = resolveRealPath(targetPath);
|
||||
const relativeTargetPath = relative(nodeModulesRootReal, targetPathReal);
|
||||
if (relativeTargetPath.startsWith("..") || isAbsolute(relativeTargetPath)) {
|
||||
return { ok: false, reason: "path_escape", targetPath };
|
||||
}
|
||||
|
||||
return { ok: true, targetPathReal, mode: targetStats.mode & 0o777 };
|
||||
}
|
||||
|
||||
try {
|
||||
const initialTargetValidation = validateTargetPath();
|
||||
if (!initialTargetValidation.ok) {
|
||||
return { applied: false, reason: initialTargetValidation.reason, targetPath };
|
||||
}
|
||||
|
||||
const currentText = readFile(targetPath, "utf8");
|
||||
if (currentText.includes(BAILEYS_MEDIA_HOTFIX_REPLACEMENT)) {
|
||||
return { applied: false, reason: "already_patched" };
|
||||
}
|
||||
if (!currentText.includes(BAILEYS_MEDIA_HOTFIX_NEEDLE)) {
|
||||
return { applied: false, reason: "unexpected_content" };
|
||||
}
|
||||
if (!BAILEYS_MEDIA_ONCE_IMPORT_RE.test(currentText)) {
|
||||
return { applied: false, reason: "missing_once_import", targetPath };
|
||||
}
|
||||
if (!BAILEYS_MEDIA_ASYNC_CONTEXT_RE.test(currentText)) {
|
||||
return { applied: false, reason: "not_async_context", targetPath };
|
||||
}
|
||||
|
||||
const patchedText = currentText.replace(
|
||||
BAILEYS_MEDIA_HOTFIX_NEEDLE,
|
||||
BAILEYS_MEDIA_HOTFIX_REPLACEMENT,
|
||||
);
|
||||
const tempPath = createTempPath(targetPath);
|
||||
const tempFd = openFile(tempPath, "wx", initialTargetValidation.mode);
|
||||
let tempFdClosed = false;
|
||||
try {
|
||||
writeFile(tempFd, patchedText, "utf8");
|
||||
closeFile(tempFd);
|
||||
tempFdClosed = true;
|
||||
const finalTargetValidation = validateTargetPath();
|
||||
if (!finalTargetValidation.ok) {
|
||||
return { applied: false, reason: finalTargetValidation.reason, targetPath };
|
||||
}
|
||||
renameFile(tempPath, targetPath);
|
||||
chmodFile(targetPath, initialTargetValidation.mode);
|
||||
} finally {
|
||||
if (!tempFdClosed) {
|
||||
try {
|
||||
closeFile(tempFd);
|
||||
} catch {
|
||||
// ignore failed-open cleanup
|
||||
}
|
||||
}
|
||||
removePath(tempPath, { force: true });
|
||||
}
|
||||
return { applied: true, reason: "patched", targetPath };
|
||||
} catch (error) {
|
||||
return {
|
||||
applied: false,
|
||||
reason: "error",
|
||||
targetPath,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function applyBundledPluginRuntimeHotfixes(params = {}) {
|
||||
const log = params.log ?? console;
|
||||
const baileysResult = applyBaileysEncryptedStreamFinishHotfix(params);
|
||||
if (baileysResult.applied) {
|
||||
log.log("[postinstall] patched @whiskeysockets/baileys encryptedStream flush ordering");
|
||||
return;
|
||||
}
|
||||
if (baileysResult.reason !== "missing" && baileysResult.reason !== "already_patched") {
|
||||
log.warn(
|
||||
`[postinstall] could not patch @whiskeysockets/baileys encryptedStream: ${baileysResult.reason}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSourceCheckoutRoot(params) {
|
||||
const pathExists = params.existsSync ?? existsSync;
|
||||
return (
|
||||
@@ -216,6 +372,13 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
} catch (e) {
|
||||
log.warn(`[postinstall] could not prune bundled plugin source node_modules: ${String(e)}`);
|
||||
}
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -245,6 +408,13 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
.map((dep) => `${dep.name}@${dep.version}`);
|
||||
|
||||
if (missingSpecs.length === 0) {
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,6 +454,14 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
// Non-fatal: gateway will surface the missing dep via doctor.
|
||||
log.warn(`[postinstall] could not install bundled plugin deps: ${String(e)}`);
|
||||
}
|
||||
|
||||
applyBundledPluginRuntimeHotfixes({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
|
||||
Reference in New Issue
Block a user