Add single transction with ChatGPT5.2
Christian Immanuel · 2026-07-05 11:48 · d3a1519c2be6b19ebe8cf3439943d010212e0467
depscollector | 235 ++++++++++++++++++++++++++++++++++++----------------------
1 file changed, 146 insertions(+), 89 deletions(-)
diff --git a/depscollector b/depscollector
index 8e0522f..5a968ad 100755
--- a/depscollector
+++ b/depscollector
@@ -2,6 +2,7 @@
set -euo pipefail
DB_PATH_DEFAULT="/var/lib/depscollector/depscollector.db"
+
db_path="$DB_PATH_DEFAULT"
update=0
verbose=0
@@ -30,24 +31,78 @@ Options:
Behavior:
If no -b DIR is given, it reuses directories from the 'sources' table;
if still empty, it falls back to /usr/bin.
-
-Examples:
- depscollector -b /usr/bin
- depscollector -u -b /usr/bin -b /usr/sbin
- depscollector -u -d /var/lib/depscollector/depscollector.db
EOF
}
-init_db() {
+# --------------------- sqlite single-connection ---------------------
+# We run a persistent sqlite3 process and talk to it via stdin/stdout.
+# For SELECTs, we append a unique marker row and read until we see it.
+declare -a SQLCOPROC=()
+
+sql_start() {
mkdir -p "$(dirname "$db_path")"
- sqlite3 "$db_path" <<'EOF'
-PRAGMA foreign_keys = ON;
+ coproc SQLCOPROC { sqlite3 "$db_path" -batch; }
+
+ # Configure session
+ sql_send ".bail on"
+ sql_send ".echo off"
+ sql_send ".headers off"
+ sql_send ".mode list"
+ sql_send ".separator $'\t'"
+ sql_send "PRAGMA foreign_keys = ON;"
+}
+
+sql_stop() {
+ # Politely ask sqlite to quit; ignore errors during shutdown.
+ { sql_send ".quit"; } 2>/dev/null || true
+ { wait "${SQLCOPROC_PID:-0}"; } 2>/dev/null || true
+}
+
+sql_send() {
+ # Send exactly one line/statement (sqlite3 accepts multi-stmt lines too).
+ printf '%s\n' "$1" >&"${SQLCOPROC[1]}"
+}
+
+sql_exec() {
+ # Execute SQL that produces no output (or output we don't care about).
+ # We still add a marker SELECT to ensure sqlite processes it before we continue.
+ local sql="$1"
+ local mark="__EXEC_DONE__${RANDOM}${RANDOM}__"
+
+ sql_send "$sql"
+ sql_send "SELECT '$mark';"
+
+ local line
+ while IFS= read -r line <&"${SQLCOPROC[0]}"; do
+ [[ "$line" == "$mark" ]] && break
+ done
+}
+
+sql_query_lines() {
+ # Print result lines of a SELECT to stdout.
+ local sql="$1"
+ local mark="__END__${RANDOM}${RANDOM}__"
+
+ sql_send "$sql"
+ sql_send "SELECT '$mark';"
+
+ local line
+ while IFS= read -r line <&"${SQLCOPROC[0]}"; do
+ [[ "$line" == "$mark" ]] && break
+ printf '%s\n' "$line"
+ done
+}
+
+# --------------------- DB init ---------------------
+init_db() {
+ sql_exec "
CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dir TEXT UNIQUE NOT NULL
-);
+);"
+ sql_exec "
CREATE TABLE IF NOT EXISTS binaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
@@ -56,62 +111,86 @@ CREATE TABLE IF NOT EXISTS binaries (
owner_group TEXT,
source_id INTEGER NOT NULL,
FOREIGN KEY (source_id) REFERENCES sources(id)
-);
+);"
+ sql_exec "
CREATE TABLE IF NOT EXISTS libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
owner_user TEXT,
owner_group TEXT
-);
+);"
+ sql_exec "
CREATE TABLE IF NOT EXISTS binary_dependencies (
binary_id INTEGER NOT NULL,
library_id INTEGER NOT NULL,
PRIMARY KEY (binary_id, library_id),
FOREIGN KEY (binary_id) REFERENCES binaries(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE
-);
-EOF
+);"
}
+# --------------------- scan helpers ---------------------
get_owner() { stat -c '%U:%G' "$1"; }
-find_library_path() {
- local lib="$1"
- ldconfig -p | awk -v lib="$lib" '$1 == lib { print $NF; exit }'
+is_elf() { file -b "$1" 2>/dev/null | grep -q 'ELF'; }
+
+collect_bins_from_dirs() {
+ local -a all=()
+ local dir
+
+ for dir in "${bin_dirs[@]}"; do
+ [[ -d "$dir" ]] || { log "Skipping non-dir: $dir"; continue; }
+ while IFS= read -r -d '' f; do
+ all+=("$f")
+ done < <(find "$dir" -type f -perm -111 -print0 2>/dev/null)
+ done
+
+ printf "%s\n" "${all[@]}" | sort -u
}
-insert_source_get_id() {
- local dir="$1"
- local dir_esc; dir_esc="$(sql_escape "$dir")"
+# One-time ldconfig cache: libname -> full path
+declare -A LDCACHE=()
+build_ldcache() {
+ local name path
+ while IFS= read -r line; do
+ # Example line:
+ # libz.so.1 (libc6,x86-64) => /lib/libz.so.1
+ name="$(awk '{print $1}' <<<"$line")"
+ path="$(awk '{print $NF}' <<<"$line")"
+ [[ -n "$name" && -n "$path" ]] || continue
+ # Keep first hit
+ [[ -n "${LDCACHE[$name]+x}" ]] || LDCACHE["$name"]="$path"
+ done < <(ldconfig -p 2>/dev/null || true)
+}
- sqlite3 "$db_path" <<EOF
-INSERT OR IGNORE INTO sources (dir) VALUES ('$dir_esc');
-EOF
- sqlite3 "$db_path" "SELECT id FROM sources WHERE dir = '$dir_esc';"
+find_library_path() {
+ local lib="$1"
+ printf '%s' "${LDCACHE[$lib]:-}"
}
+# --------------------- DB operations ---------------------
insert_binary() {
local path="$1"
- local name owner_user owner_group source_dir source_id
+ local name owner_user owner_group source_dir
name="$(basename "$path")"
IFS=: read -r owner_user owner_group < <(get_owner "$path")
source_dir="$(dirname "$path")"
- source_id="$(insert_source_get_id "$source_dir")"
- [[ -n "$source_id" ]] || die "Could not resolve source_id for $source_dir"
- local path_esc name_esc ou_esc og_esc
- path_esc="$(sql_escape "$path")"
- name_esc="$(sql_escape "$name")"
- ou_esc="$(sql_escape "$owner_user")"
- og_esc="$(sql_escape "$owner_group")"
+ local p n ou og sd
+ p="$(sql_escape "$path")"
+ n="$(sql_escape "$name")"
+ ou="$(sql_escape "$owner_user")"
+ og="$(sql_escape "$owner_group")"
+ sd="$(sql_escape "$source_dir")"
- sqlite3 "$db_path" <<EOF
+ sql_exec "INSERT OR IGNORE INTO sources (dir) VALUES ('$sd');"
+ sql_exec "
INSERT OR IGNORE INTO binaries (path, name, owner_user, owner_group, source_id)
-VALUES ('$path_esc', '$name_esc', '$ou_esc', '$og_esc', $source_id);
-EOF
+VALUES ('$p', '$n', '$ou', '$og', (SELECT id FROM sources WHERE dir = '$sd'));
+"
}
insert_library() {
@@ -122,48 +201,40 @@ insert_library() {
name="$(basename "$lib_path")"
IFS=: read -r owner_user owner_group < <(get_owner "$lib_path")
- local p_esc n_esc ou_esc og_esc
- p_esc="$(sql_escape "$lib_path")"
- n_esc="$(sql_escape "$name")"
- ou_esc="$(sql_escape "$owner_user")"
- og_esc="$(sql_escape "$owner_group")"
+ local p n ou og
+ p="$(sql_escape "$lib_path")"
+ n="$(sql_escape "$name")"
+ ou="$(sql_escape "$owner_user")"
+ og="$(sql_escape "$owner_group")"
- sqlite3 "$db_path" <<EOF
+ sql_exec "
INSERT OR IGNORE INTO libraries (path, name, owner_user, owner_group)
-VALUES ('$p_esc', '$n_esc', '$ou_esc', '$og_esc');
-EOF
+VALUES ('$p', '$n', '$ou', '$og');
+"
}
link_binary_to_library() {
local binary_path="$1"
local lib_path="$2"
- local b_esc l_esc
- b_esc="$(sql_escape "$binary_path")"
- l_esc="$(sql_escape "$lib_path")"
+ local bp lp
+ bp="$(sql_escape "$binary_path")"
+ lp="$(sql_escape "$lib_path")"
- sqlite3 "$db_path" <<EOF
+ sql_exec "
INSERT OR IGNORE INTO binary_dependencies (binary_id, library_id)
SELECT b.id, l.id
-FROM binaries b, libraries l
-WHERE b.path = '$b_esc'
- AND l.path = '$l_esc';
-EOF
-}
-
-is_elf() {
- # Fast-ish ELF check (works for most cases)
- file -b "$1" 2>/dev/null | grep -q 'ELF'
+FROM binaries b
+JOIN libraries l
+WHERE b.path = '$bp' AND l.path = '$lp';
+"
}
process_binary() {
local binary="$1"
-
insert_binary "$binary"
- if ! is_elf "$binary"; then
- return 0
- fi
+ is_elf "$binary" || return 0
local libs lib lib_path
libs="$(readelf -d "$binary" 2>/dev/null | awk '/NEEDED/ { gsub(/
@@ -182,50 +253,32 @@ $$
done <<<"$libs"
}
-collect_bins_from_dirs() {
- local -a all=()
- local dir
- for dir in "${bin_dirs[@]}"; do
- [[ -d "$dir" ]] || { log "Skipping non-dir: $dir"; continue; }
- # NUL-safe
- while IFS= read -r -d '' f; do
- all+=("$f")
- done < <(find "$dir" -type f -perm -111 -print0 2>/dev/null)
- done
-
- # unique
- printf "%s\n" "${all[@]}" | sort -u
-}
-
remove_stale_entries() {
- # Build current list
mapfile -t current_bins < <(collect_bins_from_dirs)
-
- # DB list
- mapfile -t db_bins < <(sqlite3 "$db_path" "SELECT path FROM binaries ORDER BY path;")
+ mapfile -t db_bins < <(sql_query_lines "SELECT path FROM binaries ORDER BY path;")
local tmp_current tmp_db
tmp_current="$(mktemp)"
tmp_db="$(mktemp)"
+
printf "%s\n" "${current_bins[@]}" | sort >"$tmp_current"
printf "%s\n" "${db_bins[@]}" | sort >"$tmp_db"
- # Remove stale
while IFS= read -r stale_bin; do
[[ -n "$stale_bin" ]] || continue
- local s_esc; s_esc="$(sql_escape "$stale_bin")"
- sqlite3 "$db_path" "DELETE FROM binaries WHERE path = '$s_esc';"
+ local s; s="$(sql_escape "$stale_bin")"
+ sql_exec "DELETE FROM binaries WHERE path = '$s';"
done < <(comm -23 "$tmp_db" "$tmp_current")
rm -f "$tmp_current" "$tmp_db"
- # Cleanup orphan libs
- sqlite3 "$db_path" <<'EOF'
+ sql_exec "
DELETE FROM libraries
WHERE id NOT IN (SELECT library_id FROM binary_dependencies);
-EOF
+"
}
+# --------------------- main ---------------------
main() {
need_cmd sqlite3
need_cmd find
@@ -248,10 +301,14 @@ main() {
esac
done
+ sql_start
+ trap sql_stop EXIT
+
init_db
+ build_ldcache
if [[ ${#bin_dirs[@]} -eq 0 ]]; then
- mapfile -t bin_dirs < <(sqlite3 "$db_path" "SELECT DISTINCT dir FROM sources ORDER BY dir;")
+ mapfile -t bin_dirs < <(sql_query_lines "SELECT DISTINCT dir FROM sources ORDER BY dir;")
fi
if [[ ${#bin_dirs[@]} -eq 0 ]]; then
bin_dirs=("/usr/bin")
@@ -260,15 +317,15 @@ main() {
log "DB: $db_path"
log "Dirs: ${bin_dirs[*]}"
- # Transaction makes it much faster.
- sqlite3 "$db_path" "BEGIN;"
+ sql_exec "BEGIN;"
if (( update )); then
remove_stale_entries
mapfile -t all_bins < <(collect_bins_from_dirs)
- mapfile -t tracked < <(sqlite3 "$db_path" "SELECT path FROM binaries ORDER BY path;")
+ mapfile -t tracked < <(sql_query_lines "SELECT path FROM binaries ORDER BY path;")
+ local tmp_all tmp_tracked
tmp_all="$(mktemp)"
tmp_tracked="$(mktemp)"
printf "%s\n" "${all_bins[@]}" | sort >"$tmp_all"
@@ -287,7 +344,7 @@ main() {
done < <(collect_bins_from_dirs)
fi
- sqlite3 "$db_path" "COMMIT;"
+ sql_exec "COMMIT;"
}
main "$@"