| 197 | // it is left for the next run. When the file shrank below the stored offset |
| 198 | // (truncation or rotation) the offset resets to 0. |
| 199 | function readNewChunk(filePath, storedOffset) { |
| 200 | if (!fs.existsSync(filePath)) { |
| 201 | return { lines: [], nextOffset: 0, reset: storedOffset > 0 }; |
| 202 | } |
| 203 | |
| 204 | const size = fs.statSync(filePath).size; |
| 205 | let offset = storedOffset; |
| 206 | let reset = false; |
| 207 | if (size < offset) { |
| 208 | offset = 0; |
| 209 | reset = true; |
| 210 | } |
| 211 | if (size === offset) { |
| 212 | return { lines: [], nextOffset: offset, reset }; |
| 213 | } |
| 214 | |
| 215 | const buffer = Buffer.alloc(size - offset); |
| 216 | const fd = fs.openSync(filePath, 'r'); |
| 217 | try { |
| 218 | fs.readSync(fd, buffer, 0, buffer.length, offset); |
| 219 | } finally { |
| 220 | fs.closeSync(fd); |
| 221 | } |
| 222 | |
| 223 | const chunk = buffer.toString('utf8'); |
| 224 | const segments = chunk.split('\n'); |
| 225 | let rawLines; |
| 226 | let consumedBytes; |
| 227 | |
| 228 | if (chunk.endsWith('\n')) { |
| 229 | rawLines = segments.slice(0, -1); |
| 230 | consumedBytes = buffer.length; |
| 231 | } else { |
| 232 | const tail = segments[segments.length - 1]; |
| 233 | let tailParses = false; |
| 234 | try { |
| 235 | JSON.parse(tail); |
| 236 | tailParses = true; |
| 237 | } catch (error) { |
| 238 | tailParses = false; |
| 239 | } |
| 240 | if (tailParses) { |
| 241 | rawLines = segments; |
| 242 | consumedBytes = buffer.length; |
| 243 | } else { |
| 244 | rawLines = segments.slice(0, -1); |
| 245 | consumedBytes = buffer.length - Buffer.byteLength(tail, 'utf8'); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | const lines = rawLines |
| 250 | .map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line)) |
| 251 | .filter((line) => line.trim() !== ''); |
| 252 | |
| 253 | return { lines, nextOffset: offset + consumedBytes, reset }; |
| 254 | } |
| 255 | |
| 256 | function loadState(stateFile) { |