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:
Frank Yang
2026-04-13 23:11:52 +08:00
committed by GitHub
parent 67593a8108
commit 4ecc8c0d0e
3 changed files with 408 additions and 2 deletions

View File

@@ -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) {