Skip to content

[Duplicate Code] streamFromContainer and tailFile duplicate identical signal-handler + readline loop #2481

@github-actions

Description

@github-actions

Duplicate Code Opportunity

Summary

  • Pattern: streamFromContainer and tailFile in src/logs/log-streamer.ts both contain an identical 14-line block that: (1) attaches SIGINT/SIGTERM cleanup handlers, (2) runs a readline interface over a child process stdout, (3) silently swallows SIGTERM errors, and (4) removes the cleanup handlers in a finally block.
  • Locations: src/logs/log-streamer.ts:75–107 (streamFromContainer) and src/logs/log-streamer.ts:165–198 (tailFile)
  • Impact: Signal-handler boilerplate is copy-pasted; a future bug (e.g. leaked handlers on re-throw) must be fixed in both places

Evidence

// streamFromContainer (lines ~78–107) — identical to tailFile (lines ~168–198)
const cleanup = () => { proc.kill('SIGTERM'); };
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
try {
  if (proc.stdout) {
    const rl = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
    for await (const line of rl) {
      processLine(line, formatter, parse, withPid);
    }
  }
  await proc;
} catch (error) {
  if (error instanceof Error && 'signal' in error && error.signal === 'SIGTERM') return;
  throw error;
} finally {
  process.off('SIGINT', cleanup);
  process.off('SIGTERM', cleanup);
}

Suggested Refactoring

Extract into a private helper:

async function runWithSignalHandling(
  proc: ExecaChildProcess,
  formatter: LogFormatter,
  parse: boolean,
  withPid: boolean
): Promise<void> {
  const cleanup = () => proc.kill('SIGTERM');
  process.on('SIGINT', cleanup);
  process.on('SIGTERM', cleanup);
  try {
    if (proc.stdout) {
      const rl = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
      for await (const line of rl) processLine(line, formatter, parse, withPid);
    }
    await proc;
  } catch (error) {
    if (error instanceof Error && 'signal' in error && error.signal === 'SIGTERM') return;
    throw error;
  } finally {
    process.off('SIGINT', cleanup);
    process.off('SIGTERM', cleanup);
  }
}

Both streamFromContainer and tailFile then reduce to a single await runWithSignalHandling(proc, ...) call.

Affected Files

  • src/logs/log-streamer.ts — lines 54–107 (streamFromContainer) and 154–198 (tailFile)

Effort Estimate

Low


Detected by Duplicate Code Detector workflow. Run date: 2026-05-04

Generated by Duplicate Code Detector · ● 878.7K ·

  • expires on Jun 3, 2026, 1:14 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions