Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions test/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
62 changes: 62 additions & 0 deletions test/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
},
)