diff --git a/src/pack.ts b/src/pack.ts index ad73357e..c497340d 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -330,6 +330,18 @@ export class Pack } this[PROCESSING] = true + // The head is the only job that is ever piped, and piping is the + // only thing that frees a job slot. Never gate it on the job + // limit, or a full read-ahead table deadlocks the whole pack + // (hardlinks parked behind a pending head, issue #460). + while (this[CURRENT]) { + const head = this[CURRENT] + this[PROCESSJOB](head) + if (!head.ignore) { + break + } + this[QUEUE].shift() + } for ( let w = this[QUEUE].head; !!w && this[JOBS] < this.jobs; diff --git a/test/create.ts b/test/create.ts index ee634344..495396ff 100644 --- a/test/create.ts +++ b/test/create.ts @@ -363,3 +363,29 @@ t.test('transform a filename', async t => { ).concat() t.equal(data.subarray(0, 'bloorg.md'.length).toString(), 'bloorg.md') }) + +// https://github.com/isaacs/node-tar/issues/460 +t.test('hardlinks across subdirs do not hang create({file})', async t => { + const cwd = t.testdir({}) + for (let i = 0; i < 20; i++) { + const sub = path.join(cwd, `dir${String(i).padStart(3, '0')}`) + fs.mkdirSync(sub) + fs.writeFileSync(path.join(sub, 'file.txt'), `content-${i}`) + } + const src = path.join(cwd, 'dir000', 'hardlink-source.txt') + fs.writeFileSync(src, 'I am the link target') + for (let i = 1; i < 15; i++) { + fs.linkSync( + src, + path.join(cwd, `dir${String(i).padStart(3, '0')}`, 'hardlink.txt'), + ) + } + const file = path.join(cwd, 'out.tar') + await Promise.race([ + c({ file, cwd }, ['.']), + new Promise((_, rej) => + setTimeout(() => rej(new Error('create hung')), 10000), + ), + ]) + t.ok(fs.statSync(file).size > 1024, 'wrote archive bytes') +}) diff --git a/test/pack.js b/test/pack.js index e03708d9..d84602fb 100644 --- a/test/pack.js +++ b/test/pack.js @@ -1959,3 +1959,65 @@ t.test('avoid permanent link deferral', async t => { new Set(['pkgB/index.js', 'pkgB/foo.js', 'pkgB/dist/index.js']), ) }) + +// https://github.com/isaacs/node-tar/issues/460 +// Hardlinks in later subdirs fill the job table while the head is +// still pending. PROCESS used to skip the head once JOBS hit the +// limit, so the pack never piped and never ended. +const hardlinkTree = dir => { + for (let i = 0; i < 20; i++) { + const sub = path.join(dir, `dir${String(i).padStart(3, '0')}`) + fs.mkdirSync(sub) + fs.writeFileSync(path.join(sub, 'file.txt'), `content-${i}`) + } + const src = path.join(dir, 'dir000', 'hardlink-source.txt') + fs.writeFileSync(src, 'I am the link target') + for (let i = 1; i < 15; i++) { + fs.linkSync( + src, + path.join(dir, `dir${String(i).padStart(3, '0')}`, 'hardlink.txt'), + ) + } +} + +t.test( + 'hardlinks across subdirs do not deadlock the pack head', + async t => { + const dir = t.testdir({}) + hardlinkTree(dir) + const p = new Pack({ cwd: dir }) + const out = [] + p.on('data', d => out.push(d)) + const finished = new Promise((res, rej) => { + p.on('end', res) + p.on('error', rej) + }) + p.add('.').end() + await Promise.race([ + finished, + new Promise((_, rej) => + setTimeout(() => rej(new Error('pack hung')), 10000), + ), + ]) + const data = Buffer.concat(out) + t.ok(data.length > 1024, 'wrote archive bytes') + const { files, links } = await new Promise(res => { + let files = 0 + let links = 0 + const parser = new Parser({ + onReadEntry(entry) { + if (entry.type === 'File') { + files++ + } else if (entry.type === 'Link') { + links++ + } + entry.resume() + }, + }) + parser.on('end', () => res({ files, links })) + parser.end(data) + }) + t.equal(files, 21, 'source file plus one file.txt per dir') + t.equal(links, 14, 'hardlinks packed as Link entries') + }, +)