Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 257x 257x 257x 257x 257x 257x 257x 257x 257x 30630x 30630x 22178x 2030x 2028x 2028x 2028x 2028x 2028x 2028x 2028x 20114x 20114x 20114x 18084x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2030x 2028x 2028x 2028x 2x 2x 2x 4549x 1971x 1971x 1971x 1971x 29x 8x 27x 27x 1446x 136x 136x 136x 136x 136x 136x 136x 1446x 136x | /**
* SQLite database connection manager for CLI configuration storage.
* Uses the sqlite.ts adapter which wraps node:sqlite's DatabaseSync
* with a bun:sqlite-compatible API surface.
*/
import { chmodSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import { getEnv } from "../env.js";
const _require = createRequire(import.meta.url);
import { migrateFromJson } from "./migration.js";
import { initSchema, runMigrations } from "./schema.js";
import { Database } from "./sqlite.js";
export const CONFIG_DIR_ENV_VAR = "SENTRY_CONFIG_DIR";
const DEFAULT_CONFIG_DIR_NAME = ".sentry";
const DB_FILENAME = "cli.db";
/** 7-day TTL for cache entries (milliseconds) */
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
/** Probability of running cleanup on write operations */
const CLEANUP_PROBABILITY = 0.1;
/** Traced database wrapper (returned by getDatabase) */
let db: Database | null = null;
/** Raw database without tracing (used for repair operations) */
let rawDb: Database | null = null;
let dbOpenedPath: string | null = null;
export function getConfigDir(): string {
const { homedir } = _require("node:os");
return (
getEnv()[CONFIG_DIR_ENV_VAR] || join(homedir(), DEFAULT_CONFIG_DIR_NAME)
);
}
export function getDbPath(): string {
return join(getConfigDir(), DB_FILENAME);
}
function ensureConfigDir(): void {
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
}
function setDbPermissions(): void {
const dbPath = getDbPath();
try {
chmodSync(dbPath, 0o600);
// WAL mode creates -wal and -shm files that may contain sensitive data
// Chmod them too if they exist (they may not exist on first run)
try {
chmodSync(`${dbPath}-wal`, 0o600);
} catch {
// File may not exist yet
}
try {
chmodSync(`${dbPath}-shm`, 0o600);
} catch {
// File may not exist yet
}
} catch {
// Windows doesn't support chmod
}
}
/** Get or initialize the database connection. */
export function getDatabase(): Database {
const dbPath = getDbPath();
// Auto-invalidate if config directory changed (for tests)
Iif (db && dbOpenedPath !== dbPath) {
db.close();
db = null;
rawDb = null;
dbOpenedPath = null;
}
if (db) {
return db;
}
ensureConfigDir();
rawDb = new Database(dbPath);
try {
// 5000ms busy_timeout prevents SQLITE_BUSY errors during concurrent CLI access.
// When multiple CLI instances run simultaneously (e.g., parallel terminals, CI jobs),
// SQLite needs time to acquire locks. WAL mode allows concurrent reads, but writers
// must wait. Without sufficient timeout, concurrent processes fail immediately.
// Set busy_timeout FIRST - before WAL mode - to handle lock contention during init.
rawDb.exec("PRAGMA busy_timeout = 5000");
rawDb.exec("PRAGMA journal_mode = WAL");
rawDb.exec("PRAGMA foreign_keys = ON");
rawDb.exec("PRAGMA synchronous = NORMAL");
setDbPermissions();
initSchema(rawDb);
runMigrations(rawDb);
migrateFromJson(rawDb);
// Wrap with tracing proxy for automatic query instrumentation.
// Lazy-require telemetry to avoid top-level import of @sentry/node-core (~85ms).
// Shell completions set SENTRY_CLI_NO_TELEMETRY=1 to skip this entirely.
if (getEnv().SENTRY_CLI_NO_TELEMETRY === "1") {
db = rawDb;
} else E{
const { createTracedDatabase } = _require("../telemetry.js") as {
createTracedDatabase: (d: Database) => Database;
};
db = createTracedDatabase(rawDb);
}
dbOpenedPath = dbPath;
return db;
} catch (error) {
// Clean up on initialization failure to prevent connection leak
rawDb.close();
rawDb = null;
throw error;
}
}
/** Close the database connection (used for testing). */
export function closeDatabase(): void {
if (db) {
db.close();
db = null;
rawDb = null;
dbOpenedPath = null;
}
}
/**
* Get the raw (unwrapped) database connection.
* Used for repair operations to avoid triggering the traced wrapper's
* auto-repair logic (which would cause infinite loops).
*/
export function getRawDatabase(): Database {
if (!rawDb) {
// Ensure database is initialized
getDatabase();
}
// After getDatabase() call, rawDb is guaranteed to be set
Iif (!rawDb) {
throw new Error("Database initialization failed");
}
return rawDb;
}
function shouldRunCleanup(): boolean {
return Math.random() < CLEANUP_PROBABILITY;
}
function cleanupExpiredCaches(): void {
const database = getDatabase();
const expiryTime = Date.now() - CACHE_TTL_MS;
const now = Date.now();
database
.query("DELETE FROM project_cache WHERE last_accessed < ?")
.run(expiryTime);
database
.query("DELETE FROM dsn_cache WHERE last_accessed < ?")
.run(expiryTime);
database
.query("DELETE FROM project_aliases WHERE last_accessed < ?")
.run(expiryTime);
// project_root_cache uses ttl_expires_at instead of last_accessed
database
.query("DELETE FROM project_root_cache WHERE ttl_expires_at < ?")
.run(now);
}
export function maybeCleanupCaches(): void {
if (shouldRunCleanup()) {
cleanupExpiredCaches();
}
}
|