cclaw-cli 0.48.17 → 0.48.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content/node-hooks.js +43 -15
- package/dist/fs-utils.js +56 -24
- package/package.json +1 -1
|
@@ -118,26 +118,54 @@ async function writeFileAtomic(filePath, content, options = {}) {
|
|
|
118
118
|
"." + path.basename(filePath) + ".tmp-" + process.pid + "-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8)
|
|
119
119
|
);
|
|
120
120
|
await fs.writeFile(tempPath, content, { encoding: "utf8" });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} finally {
|
|
132
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
133
|
-
}
|
|
121
|
+
// Windows' fs.rename can fail transiently with EPERM/EBUSY/EACCES when the
|
|
122
|
+
// destination file is held open by another process (antivirus, indexer,
|
|
123
|
+
// or a sibling hook invocation racing on the same file). Retry with tiny
|
|
124
|
+
// backoff before falling back to copyFile.
|
|
125
|
+
const renameRetryableCodes = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
126
|
+
let attempt = 0;
|
|
127
|
+
const maxAttempts = 6;
|
|
128
|
+
while (true) {
|
|
129
|
+
try {
|
|
130
|
+
await fs.rename(tempPath, filePath);
|
|
134
131
|
if (options.mode !== undefined) {
|
|
135
132
|
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
136
133
|
}
|
|
137
134
|
return;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
137
|
+
if (code === "EXDEV") {
|
|
138
|
+
try {
|
|
139
|
+
await fs.copyFile(tempPath, filePath);
|
|
140
|
+
} finally {
|
|
141
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
142
|
+
}
|
|
143
|
+
if (options.mode !== undefined) {
|
|
144
|
+
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (renameRetryableCodes.has(code) && attempt < maxAttempts) {
|
|
149
|
+
attempt += 1;
|
|
150
|
+
await hookSleep(10 * attempt + Math.floor(Math.random() * 10));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (renameRetryableCodes.has(code)) {
|
|
154
|
+
// Last-resort fallback: copy-then-unlink. Not atomic, but the
|
|
155
|
+
// directory lock around this call already serializes writers.
|
|
156
|
+
try {
|
|
157
|
+
await fs.copyFile(tempPath, filePath);
|
|
158
|
+
if (options.mode !== undefined) {
|
|
159
|
+
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
} finally {
|
|
163
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
167
|
+
throw error;
|
|
138
168
|
}
|
|
139
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
140
|
-
throw error;
|
|
141
169
|
}
|
|
142
170
|
}
|
|
143
171
|
|
package/dist/fs-utils.js
CHANGED
|
@@ -75,35 +75,67 @@ export async function writeFileSafe(filePath, content, options = {}) {
|
|
|
75
75
|
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
76
76
|
const targetMode = options.mode;
|
|
77
77
|
await fs.writeFile(tempPath, content, { encoding: "utf8", ...(targetMode !== undefined ? { mode: targetMode } : {}) });
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// On Windows, `fs.rename` can fail transiently with EPERM / EBUSY / EACCES
|
|
79
|
+
// when the target file is briefly held open by another process (antivirus,
|
|
80
|
+
// search indexer, or a concurrent cclaw hook). Retry with small backoff
|
|
81
|
+
// before falling back to a non-atomic copy + unlink.
|
|
82
|
+
const renameRetryableCodes = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
83
|
+
let attempt = 0;
|
|
84
|
+
const maxAttempts = 6;
|
|
85
|
+
// eslint-disable-next-line no-constant-condition
|
|
86
|
+
while (true) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.rename(tempPath, filePath);
|
|
89
|
+
if (targetMode !== undefined) {
|
|
90
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
catch (error) {
|
|
95
|
+
const code = error?.code;
|
|
96
|
+
// `rename` fails with EXDEV when the temp file and target live on
|
|
97
|
+
// different filesystems (container bind mounts, tmpfs + rootfs,
|
|
98
|
+
// cross-volume setups). Fall back to copy + unlink so atomic writes
|
|
99
|
+
// still work — copyFile is not fully atomic but is the best we can
|
|
100
|
+
// do across devices, and we remove the temp even if copy fails.
|
|
101
|
+
if (code === "EXDEV") {
|
|
102
|
+
try {
|
|
103
|
+
await fs.copyFile(tempPath, filePath);
|
|
104
|
+
if (targetMode !== undefined) {
|
|
105
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
96
110
|
}
|
|
111
|
+
return;
|
|
97
112
|
}
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
if (code !== undefined && renameRetryableCodes.has(code) && attempt < maxAttempts) {
|
|
114
|
+
attempt += 1;
|
|
115
|
+
const waitMs = 10 * attempt + Math.floor(Math.random() * 10);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
117
|
+
continue;
|
|
100
118
|
}
|
|
101
|
-
|
|
119
|
+
if (code !== undefined && renameRetryableCodes.has(code)) {
|
|
120
|
+
// Last-resort fallback on Windows: copy-then-unlink. Not atomic,
|
|
121
|
+
// but the caller is expected to have serialized via a directory
|
|
122
|
+
// lock so this only loses atomicity under extreme contention.
|
|
123
|
+
try {
|
|
124
|
+
await fs.copyFile(tempPath, filePath);
|
|
125
|
+
if (targetMode !== undefined) {
|
|
126
|
+
await fs.chmod(filePath, targetMode).catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Other errors: try to clean up the temp to avoid littering the
|
|
135
|
+
// directory with orphaned `.tmp-<pid>-*` files, then rethrow.
|
|
136
|
+
await fs.unlink(tempPath).catch(() => undefined);
|
|
137
|
+
throw error;
|
|
102
138
|
}
|
|
103
|
-
// Other errors: try to clean up the temp to avoid littering the
|
|
104
|
-
// directory with orphaned `.tmp-<pid>-*` files, then rethrow.
|
|
105
|
-
await fs.unlink(tempPath).catch(() => undefined);
|
|
106
|
-
throw error;
|
|
107
139
|
}
|
|
108
140
|
}
|
|
109
141
|
export async function exists(filePath) {
|