Skip to content

remove ctags temporary files when ctags is killed - #33

Merged
keegancsmith merged 1 commit into
sourcegraph:mainfrom
clemlesne:fix/private-tmpdir
Oct 28, 2026
Merged

keegancsmith merged 1 commit into
sourcegraph:mainfrom
clemlesne:fix/private-tmpdir

Conversation

@clemlesne

Copy link
Copy Markdown
Contributor

Fixes #29.

In interactive mode, Universal Ctags writes each reply to $TMPDIR/tags.XXXXXX and removes it once it has printed it. When the process is killed during a request, the file stays behind. go-ctags kills ctags in Close, and Zoekt calls Close when a Parse takes more than a minute, so the files that stay are the large ones.

On Sourcegraph this lands on the index volume: zoekt-sourcegraph-indexserver sets TMPDIR to <index>/.indexserver.tmp and only empties it when it restarts. Timeouts do happen there (zoekt#1095), and that directory has filled up before (zoekt#646). I found this by reproduction, not from a user report.

This PR gives each ctags process its own directory:

  • New creates it with os.MkdirTemp("", "go-ctags-") and passes it to ctags as TMPDIR (and TMP, which ctags reads on Windows).
  • Close removes it after Wait, so a killed ctags no longer leaves anything behind. Every error path in New removes it too.
  • In interactive mode the reply file is the only file ctags creates, so nothing else lands there.

No API change. Independent of #32: ctags.go merges cleanly with it, and ctags_test.go only conflicts because both add imports and tests at the end of the file (keep both).

Two side effects:

  • If $TMPDIR is unusable, New now fails with creating temporary directory for <bin>: …. Before, ctags started and then crashed on its first request.
  • The directory must live as long as the parser: ctags reads TMPDIR on every request and crashes if it is gone. Zoekt's parsers live for one shard, so this only matters for a long-idle parser on a host whose tmp cleaner deletes old directories.

What it does not fix: if the calling process itself is killed (for example zoekt-git-index on INDEXING_TIMEOUT or OOM), Close never runs. A busy ctags then still leaves its file, as today, and an idle parser now leaves an empty go-ctags-* directory. The prefix makes those easy to sweep. Ctags has no signal handlers, so sending SIGTERM before SIGKILL would not help. The complete fix would be upstream: unlinking the file right after mkstemp.

Test Plan:

  • New TestCloseRemovesTempDir: real ctags with TMPDIR pointed at an empty test directory; kill ctags while it writes its reply, then Close; the test directory is empty again. It fails if Close does not remove the directory, or if ctags does not get the private TMPDIR.
  • New TestNewFailureLeavesNoTempDir: when ctags cannot start, New returns an error and leaves the test directory empty.
  • go test -race ./... passes on ctags 6.2.1 and on 6.0.0.2783f009 (the CI build, darwin). ./gen.sh leaves no diff.
  • The reproduction program from Killing ctags leaves its temporary reply file in $TMPDIR #29 leaves tags.Ufwjwu: 68947968 bytes on main and nothing on this branch.
  • zoekt c0b97d2 with a replace to this branch: go test ./internal/ctags/... ./index/... ./cmd/zoekt-index/... passes.

🤖 Generated with Claude Code

In interactive mode Universal Ctags writes each reply to
$TMPDIR/tags.XXXXXX and removes it once printed. When the process is
killed during a request, the file stays behind. go-ctags kills ctags in
Close, and Zoekt calls Close when a Parse takes more than a minute, so
the leftovers are the largest replies. On Sourcegraph they land on the
index volume (the indexserver points TMPDIR there) until the indexserver
restarts.

New now creates a private directory with os.MkdirTemp and passes it to
ctags as TMPDIR, and as TMP, which ctags reads on Windows. Close removes
it after Wait, once ctags can no longer write to it, and every error path
in New removes it too. The reply file is the only file ctags creates in
interactive mode, so nothing else ends up there.

Two consequences for maintainers:

- ctags reads TMPDIR again for every request and crashes if the
  directory is gone, so it must live as long as the process.
- If $TMPDIR is unusable, New now fails with "creating temporary
  directory for <bin>: ..." instead of ctags crashing on its first
  request.

Not covered: if the calling process itself is killed, Close never runs;
a busy ctags still leaves its file, and an idle parser leaves an empty
go-ctags-* directory. ctags has no signal handlers, so SIGTERM before
SIGKILL would not help.

The tests point TMPDIR at a test directory and check that it ends up
empty: after killing ctags mid-reply and closing the parser, and after
New fails to start ctags.

Fixes sourcegraph#29

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@keegancsmith keegancsmith left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@keegancsmith
keegancsmith merged commit 9c41d9e into sourcegraph:main Oct 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Killing ctags leaves its temporary reply file in $TMPDIR

2 participants