Merge branch 'feat/wrapper-interface'
This commit is contained in:
commit
b69fc6854e
@ -4,6 +4,7 @@ import java.util.*;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path ;
|
import java.nio.file.Path ;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
|
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
|
||||||
@ -40,6 +41,7 @@ public class jrunner {
|
|||||||
String nl = "\n";
|
String nl = "\n";
|
||||||
Boolean queryMode = false;
|
Boolean queryMode = false;
|
||||||
String outputFormat = "csv";
|
String outputFormat = "csv";
|
||||||
|
String passFilePath = "";
|
||||||
String msg = "";
|
String msg = "";
|
||||||
Connection scon = null;
|
Connection scon = null;
|
||||||
Connection dcon = null;
|
Connection dcon = null;
|
||||||
@ -66,10 +68,15 @@ public class jrunner {
|
|||||||
msg = msg + nl + "-t trim text";
|
msg = msg + nl + "-t trim text";
|
||||||
msg = msg + nl + "-c clear target table";
|
msg = msg + nl + "-c clear target table";
|
||||||
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
||||||
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
|
msg = msg + nl + "-f output format (csv, tsv, json) - default: csv";
|
||||||
|
msg = msg + nl + "--passfile path to the connection alias file - default: ~/.jrunnerpass";
|
||||||
|
msg = msg + nl + "--strict exit 1 on failure (default exits 0, for legacy callers)";
|
||||||
msg = msg + nl + "--help info";
|
msg = msg + nl + "--help info";
|
||||||
msg = msg + nl + "";
|
msg = msg + nl + "";
|
||||||
msg = msg + nl + "~/.jrunnerpass format:";
|
msg = msg + nl + "Prefer -sc/-dc aliases over -scp/-dcp: a password given on the";
|
||||||
|
msg = msg + nl + "command line is visible to any local user via 'ps'.";
|
||||||
|
msg = msg + nl + "";
|
||||||
|
msg = msg + nl + "passfile format:";
|
||||||
msg = msg + nl + " [alias]";
|
msg = msg + nl + " [alias]";
|
||||||
msg = msg + nl + " url=jdbc:...";
|
msg = msg + nl + " url=jdbc:...";
|
||||||
msg = msg + nl + " user=username";
|
msg = msg + nl + " user=username";
|
||||||
@ -77,6 +84,12 @@ public class jrunner {
|
|||||||
|
|
||||||
//---------------------------------------parse args into variables-------------------------------------------------
|
//---------------------------------------parse args into variables-------------------------------------------------
|
||||||
|
|
||||||
|
// Pre-scan: -sq can fail while the loop is still running, and it must
|
||||||
|
// honour --strict regardless of where the flag sits in argv.
|
||||||
|
for (String a : args) {
|
||||||
|
if (a.equals("--strict")) strictExit = true;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < args.length; i = i +1 ){
|
for (int i = 0; i < args.length; i = i +1 ){
|
||||||
switch (args[i]) {
|
switch (args[i]) {
|
||||||
//source connection alias
|
//source connection alias
|
||||||
@ -120,7 +133,7 @@ public class jrunner {
|
|||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
//System.out.println(nl + "error reasing source sql file: " + printStackTrace());
|
//System.out.println(nl + "error reasing source sql file: " + printStackTrace());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -140,6 +153,13 @@ public class jrunner {
|
|||||||
case "-f":
|
case "-f":
|
||||||
outputFormat = args[i+1].toLowerCase();
|
outputFormat = args[i+1].toLowerCase();
|
||||||
break;
|
break;
|
||||||
|
//alias file location (the service user has no home directory)
|
||||||
|
case "--passfile":
|
||||||
|
passFilePath = args[i+1];
|
||||||
|
break;
|
||||||
|
//exit non-zero on failure (handled in the pre-scan above)
|
||||||
|
case "--strict":
|
||||||
|
break;
|
||||||
case "-v":
|
case "-v":
|
||||||
System.out.println(msg);
|
System.out.println(msg);
|
||||||
return;
|
return;
|
||||||
@ -163,13 +183,16 @@ public class jrunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve connection aliases from ~/.jrunnerpass
|
// Resolve connection aliases from the passfile
|
||||||
if (!scAlias.isEmpty() || !dcAlias.isEmpty()) {
|
if (!scAlias.isEmpty() || !dcAlias.isEmpty()) {
|
||||||
Map<String, String[]> connections = loadPassFile();
|
Path resolvedPassFile = passFilePath.isEmpty()
|
||||||
|
? Paths.get(System.getProperty("user.home"), ".jrunnerpass")
|
||||||
|
: Paths.get(passFilePath);
|
||||||
|
Map<String, String[]> connections = loadPassFile(resolvedPassFile);
|
||||||
if (!scAlias.isEmpty()) {
|
if (!scAlias.isEmpty()) {
|
||||||
String[] sc = connections.get(scAlias);
|
String[] sc = connections.get(scAlias);
|
||||||
if (sc == null) {
|
if (sc == null) {
|
||||||
System.err.println("Error: source alias '" + scAlias + "' not found in ~/.jrunnerpass");
|
System.err.println("Error: source alias '" + scAlias + "' not found in " + resolvedPassFile);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
if (scu.isEmpty()) scu = sc[0];
|
if (scu.isEmpty()) scu = sc[0];
|
||||||
@ -179,7 +202,7 @@ public class jrunner {
|
|||||||
if (!dcAlias.isEmpty()) {
|
if (!dcAlias.isEmpty()) {
|
||||||
String[] dc = connections.get(dcAlias);
|
String[] dc = connections.get(dcAlias);
|
||||||
if (dc == null) {
|
if (dc == null) {
|
||||||
System.err.println("Error: destination alias '" + dcAlias + "' not found in ~/.jrunnerpass");
|
System.err.println("Error: destination alias '" + dcAlias + "' not found in " + resolvedPassFile);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
if (dcu.isEmpty()) dcu = dc[0];
|
if (dcu.isEmpty()) dcu = dc[0];
|
||||||
@ -210,7 +233,7 @@ public class jrunner {
|
|||||||
Class.forName("com.ibm.as400.access.AS400JDBCDriver");
|
Class.forName("com.ibm.as400.access.AS400JDBCDriver");
|
||||||
} catch (ClassNotFoundException cnf) {
|
} catch (ClassNotFoundException cnf) {
|
||||||
System.out.println("The AS400 JDBC driver did not load");
|
System.out.println("The AS400 JDBC driver did not load");
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
//-------------------------------------------establish connections-------------------------------------------------
|
//-------------------------------------------establish connections-------------------------------------------------
|
||||||
@ -232,7 +255,7 @@ public class jrunner {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
System.out.println("issue connecting to source:");
|
System.out.println("issue connecting to source:");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
if (!queryMode) {
|
if (!queryMode) {
|
||||||
System.out.println(" ✅ source database");
|
System.out.println(" ✅ source database");
|
||||||
@ -244,7 +267,7 @@ public class jrunner {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
System.out.println("issue connecting to destination:");
|
System.out.println("issue connecting to destination:");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
System.out.println(" ✅ destination database");
|
System.out.println(" ✅ destination database");
|
||||||
}
|
}
|
||||||
@ -253,18 +276,46 @@ public class jrunner {
|
|||||||
stmt = scon.createStatement();
|
stmt = scon.createStatement();
|
||||||
stmt.setFetchSize(10000);
|
stmt.setFetchSize(10000);
|
||||||
tsStart = Timestamp.from(Instant.now());
|
tsStart = Timestamp.from(Instant.now());
|
||||||
|
summaryStart = tsStart;
|
||||||
if (!queryMode) {
|
if (!queryMode) {
|
||||||
System.out.println(tsStart);
|
System.out.println(tsStart);
|
||||||
}
|
}
|
||||||
rs = stmt.executeQuery(sq);
|
// execute() rather than executeQuery(): a statement that returns no
|
||||||
//while (rs.next()) {
|
// result set (DDL, INSERT, DELETE, TRUNCATE, MERGE) is a normal,
|
||||||
// System.out.println(rs.getString("x"));
|
// successful outcome, but executeQuery() raises for it — Postgres
|
||||||
//}
|
// "No results were returned by the query", SQL Server "The
|
||||||
|
// statement did not return a result set". Callers had to allowlist
|
||||||
|
// those messages to tell a real failure from a working TRUNCATE,
|
||||||
|
// which made a non-zero exit code impossible to adopt.
|
||||||
|
boolean hasResultSet = stmt.execute(sq);
|
||||||
|
// A stored procedure may report update counts before opening its
|
||||||
|
// cursor, and /opt/sync uses CALL statements as migration sources
|
||||||
|
// (rlarp.QUOTE_REBUILD and friends). Walk past any update counts to
|
||||||
|
// the first real result set instead of concluding there is none —
|
||||||
|
// executeQuery() would simply have thrown here. The last count is
|
||||||
|
// retained so a plain DML statement can still report rows affected.
|
||||||
|
int updated = -1;
|
||||||
|
while (!hasResultSet) {
|
||||||
|
int uc = stmt.getUpdateCount();
|
||||||
|
if (uc == -1) break; // no further results of any kind
|
||||||
|
updated = uc;
|
||||||
|
hasResultSet = stmt.getMoreResults();
|
||||||
|
}
|
||||||
|
if (!hasResultSet) {
|
||||||
|
if (!queryMode) {
|
||||||
|
System.out.println("------------no result set---------------------------------");
|
||||||
|
System.out.println("rows affected: " + updated);
|
||||||
|
}
|
||||||
|
emitSummary("ok", updated >= 0 ? updated : -1, summaryElapsedMs());
|
||||||
|
closeQuietly(scon, dcon, queryMode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rs = stmt.getResultSet();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
System.out.println("issue retrieving rows from source:");
|
System.out.println("issue retrieving rows from source:");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------build meta---------------------------------------------------------------
|
//---------------------------------------build meta---------------------------------------------------------------
|
||||||
try {
|
try {
|
||||||
@ -277,7 +328,7 @@ public class jrunner {
|
|||||||
dtn = new String[cols + 1];
|
dtn = new String[cols + 1];
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
for (int i = 1; i <= cols; i++){
|
for (int i = 1; i <= cols; i++){
|
||||||
@ -288,7 +339,7 @@ public class jrunner {
|
|||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
//-------------------------clear the target table if requeted----------------------------------------------------
|
//-------------------------clear the target table if requeted----------------------------------------------------
|
||||||
if (!queryMode && clear) {
|
if (!queryMode && clear) {
|
||||||
@ -301,7 +352,7 @@ public class jrunner {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.out.println(sql);
|
System.out.println(sql);
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (queryMode) {
|
if (queryMode) {
|
||||||
@ -310,7 +361,7 @@ public class jrunner {
|
|||||||
outputQueryResults(rs, cols, dtn, outputFormat);
|
outputQueryResults(rs, cols, dtn, outputFormat);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:sqlserver:")) {
|
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:sqlserver:")) {
|
||||||
//-------------------------------bulk copy (SQL Server dest)-------------------------------------------------
|
//-------------------------------bulk copy (SQL Server dest)-------------------------------------------------
|
||||||
@ -332,9 +383,10 @@ public class jrunner {
|
|||||||
// onto the last progress tick; the trailing " rows written" makes
|
// onto the last progress tick; the trailing " rows written" makes
|
||||||
// it parseable.
|
// it parseable.
|
||||||
System.out.print("\r" + src.rowsWritten());
|
System.out.print("\r" + src.rowsWritten());
|
||||||
|
summaryRows = src.rowsWritten();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:postgresql:")) {
|
} else if (bulk && dcu.toLowerCase().startsWith("jdbc:postgresql:")) {
|
||||||
//-------------------------------bulk copy (COPY, Postgres dest)--------------------------------------------
|
//-------------------------------bulk copy (COPY, Postgres dest)--------------------------------------------
|
||||||
@ -374,9 +426,10 @@ public class jrunner {
|
|||||||
}
|
}
|
||||||
cin.endCopy();
|
cin.endCopy();
|
||||||
System.out.print("\r" + rows);
|
System.out.print("\r" + rows);
|
||||||
|
summaryRows = rows;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println("------------row count-------------------------------------");
|
System.out.println("------------row count-------------------------------------");
|
||||||
@ -510,7 +563,7 @@ public class jrunner {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.out.println(sql);
|
System.out.println(sql);
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
sql = "";
|
sql = "";
|
||||||
}
|
}
|
||||||
@ -522,15 +575,16 @@ public class jrunner {
|
|||||||
stmtd = dcon.createStatement();
|
stmtd = dcon.createStatement();
|
||||||
stmtd.executeUpdate(sql);
|
stmtd.executeUpdate(sql);
|
||||||
System.out.print("\r" + t);
|
System.out.print("\r" + t);
|
||||||
|
summaryRows = t;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.out.println(sql);
|
System.out.println(sql);
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.exit(0);
|
die();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//System.out.println(sql);
|
//System.out.println(sql);
|
||||||
@ -550,6 +604,7 @@ public class jrunner {
|
|||||||
tsEnd = Timestamp.from(Instant.now());
|
tsEnd = Timestamp.from(Instant.now());
|
||||||
System.out.println(tsStart);
|
System.out.println(tsStart);
|
||||||
System.out.println(tsEnd);
|
System.out.println(tsEnd);
|
||||||
|
emitSummary("ok", summaryRows, summaryElapsedMs());
|
||||||
}
|
}
|
||||||
//long time = Duration.between(tsStart, tsEnd).toMillis();
|
//long time = Duration.between(tsStart, tsEnd).toMillis();
|
||||||
//System.out.println("time elapsed: " + time);
|
//System.out.println("time elapsed: " + time);
|
||||||
@ -693,14 +748,85 @@ public class jrunner {
|
|||||||
case "tsv":
|
case "tsv":
|
||||||
outputTSV(rs, cols);
|
outputTSV(rs, cols);
|
||||||
break;
|
break;
|
||||||
case "table":
|
|
||||||
case "json":
|
case "json":
|
||||||
|
outputJSON(rs, cols);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
outputCSV(rs, cols);
|
outputCSV(rs, cols);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON output. Two things CSV cannot express, both of which callers need:
|
||||||
|
//
|
||||||
|
// * NULL vs empty string. outputCSV writes an empty field for both, so
|
||||||
|
// everything downstream — the merge, reconcile, the wizard — sees them
|
||||||
|
// as identical. Here NULL is JSON null and '' is "".
|
||||||
|
// * Column types. Without them the wizard's SQL-entry mode has to default
|
||||||
|
// every destination column to text, because a CSV stream carries no
|
||||||
|
// type metadata for the caller to map.
|
||||||
|
//
|
||||||
|
// Values are emitted as JSON strings rather than JSON numbers: a DECIMAL
|
||||||
|
// rendered as a JSON number would go through a float and lose exactness,
|
||||||
|
// and callers already treat query output as text. null is the one non-string.
|
||||||
|
//
|
||||||
|
// Written incrementally rather than assembled in memory, so a large result
|
||||||
|
// set streams the same way CSV does while still being one valid document.
|
||||||
|
private static void outputJSON(ResultSet rs, int cols) throws SQLException {
|
||||||
|
ResultSetMetaData md = rs.getMetaData();
|
||||||
|
StringBuilder head = new StringBuilder("{\"columns\":[");
|
||||||
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
if (i > 1) head.append(",");
|
||||||
|
head.append("{\"name\":\"").append(escapeJSON(md.getColumnName(i))).append("\"");
|
||||||
|
head.append(",\"type\":\"").append(escapeJSON(md.getColumnTypeName(i))).append("\"");
|
||||||
|
head.append(",\"precision\":").append(md.getPrecision(i));
|
||||||
|
head.append(",\"scale\":").append(md.getScale(i));
|
||||||
|
head.append("}");
|
||||||
|
}
|
||||||
|
head.append("],\"rows\":[");
|
||||||
|
System.out.print(head);
|
||||||
|
|
||||||
|
boolean firstRow = true;
|
||||||
|
while (rs.next()) {
|
||||||
|
if (!firstRow) System.out.print(",");
|
||||||
|
firstRow = false;
|
||||||
|
System.out.print("[");
|
||||||
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
if (i > 1) System.out.print(",");
|
||||||
|
String value = rs.getString(i);
|
||||||
|
if (rs.wasNull() || value == null) {
|
||||||
|
System.out.print("null");
|
||||||
|
} else {
|
||||||
|
System.out.print("\"" + escapeJSON(value) + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.print("]");
|
||||||
|
}
|
||||||
|
System.out.println("]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeJSON(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(value.length() + 16);
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '"': sb.append("\\\""); break;
|
||||||
|
case '\\': sb.append("\\\\"); break;
|
||||||
|
case '\n': sb.append("\\n"); break;
|
||||||
|
case '\r': sb.append("\\r"); break;
|
||||||
|
case '\t': sb.append("\\t"); break;
|
||||||
|
case '\b': sb.append("\\b"); break;
|
||||||
|
case '\f': sb.append("\\f"); break;
|
||||||
|
default:
|
||||||
|
// Other control characters are illegal raw in JSON strings.
|
||||||
|
if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
else sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private static void outputCSV(ResultSet rs, int cols) throws SQLException {
|
private static void outputCSV(ResultSet rs, int cols) throws SQLException {
|
||||||
// Print header row
|
// Print header row
|
||||||
for (int i = 1; i <= cols; i++) {
|
for (int i = 1; i <= cols; i++) {
|
||||||
@ -754,23 +880,80 @@ public class jrunner {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Historically every error path did printStackTrace() then System.exit(0),
|
||||||
|
// so a failed run was indistinguishable from a successful one to any
|
||||||
|
// caller checking $?. 112 /opt/sync scripts run under `set -e` against
|
||||||
|
// that behaviour, so flipping it unconditionally would change them all at
|
||||||
|
// once; --strict makes the correct exit code opt-in until they're audited.
|
||||||
|
private static boolean strictExit = false;
|
||||||
|
|
||||||
|
private static void die() {
|
||||||
|
emitSummary("error", summaryRows, summaryElapsedMs());
|
||||||
|
System.exit(strictExit ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row count for the summary. Migration has three write paths (SQLServer
|
||||||
|
// bulk copy, Postgres COPY, batched INSERT) each with its own counter, so
|
||||||
|
// whichever runs records the total here.
|
||||||
|
private static long summaryRows = -1;
|
||||||
|
private static Timestamp summaryStart = null;
|
||||||
|
|
||||||
|
private static long summaryElapsedMs() {
|
||||||
|
if (summaryStart == null) return -1;
|
||||||
|
return Duration.between(summaryStart.toInstant(), Instant.now()).toMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single machine-readable line for the calling process.
|
||||||
|
//
|
||||||
|
// Deliberately on stderr: migration-mode stdout carries human-readable
|
||||||
|
// progress that pipekit streams to its live log, and query-mode stdout must
|
||||||
|
// stay pure CSV, so anything structured on stdout would corrupt one or the
|
||||||
|
// other. stderr is already captured in full by the caller.
|
||||||
|
//
|
||||||
|
// Replaces having to regex a row count out of prose — the count previously
|
||||||
|
// reached the caller only because a \r progress tick and a trailing
|
||||||
|
// " rows written" happened to render on the same line.
|
||||||
|
private static void emitSummary(String status, long rows, long ms) {
|
||||||
|
StringBuilder sb = new StringBuilder("@summary {\"status\":\"");
|
||||||
|
sb.append(status).append("\"");
|
||||||
|
if (rows >= 0) sb.append(",\"rows\":").append(rows);
|
||||||
|
if (ms >= 0) sb.append(",\"ms\":").append(ms);
|
||||||
|
sb.append("}");
|
||||||
|
System.err.println(sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort connection close for the early return taken when a statement
|
||||||
|
// produced no result set. A failure here cannot invalidate work that the
|
||||||
|
// server already committed, so it is reported but not fatal.
|
||||||
|
private static void closeQuietly(Connection scon, Connection dcon, boolean queryMode) {
|
||||||
|
try {
|
||||||
|
if (scon != null) scon.close();
|
||||||
|
if (!queryMode && dcon != null) dcon.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("issue closing connections: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String escapeTSV(String value) {
|
private static String escapeTSV(String value) {
|
||||||
return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " ");
|
return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads ~/.jrunnerpass and returns a map of alias -> {url, user, pass}
|
// Loads a passfile and returns a map of alias -> {url, user, pass}
|
||||||
// Format:
|
// Format:
|
||||||
// [alias]
|
// [alias]
|
||||||
// url=jdbc:...
|
// url=jdbc:...
|
||||||
// user=username
|
// user=username
|
||||||
// pass=password
|
// pass=password
|
||||||
private static Map<String, String[]> loadPassFile() {
|
//
|
||||||
|
// Defaults to ~/.jrunnerpass; --passfile overrides it, which is what lets a
|
||||||
|
// service account with no home directory keep its passwords off argv.
|
||||||
|
private static Map<String, String[]> loadPassFile(Path passFile) {
|
||||||
Map<String, String[]> connections = new LinkedHashMap<>();
|
Map<String, String[]> connections = new LinkedHashMap<>();
|
||||||
Path passFile = Paths.get(System.getProperty("user.home"), ".jrunnerpass");
|
|
||||||
if (!Files.exists(passFile)) {
|
if (!Files.exists(passFile)) {
|
||||||
System.err.println("Error: ~/.jrunnerpass not found");
|
System.err.println("Error: passfile not found: " + passFile);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
warnIfGroupOrWorldReadable(passFile);
|
||||||
try {
|
try {
|
||||||
String currentAlias = null;
|
String currentAlias = null;
|
||||||
String url = "", user = "", pass = "";
|
String url = "", user = "", pass = "";
|
||||||
@ -795,9 +978,23 @@ public class jrunner {
|
|||||||
connections.put(currentAlias, new String[]{url, user, pass});
|
connections.put(currentAlias, new String[]{url, user, pass});
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Error reading ~/.jrunnerpass: " + e.getMessage());
|
System.err.println("Error reading passfile " + passFile + ": " + e.getMessage());
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
return connections;
|
return connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The passfile holds plaintext passwords, so a too-permissive mode defeats
|
||||||
|
// the point of moving them off argv. Warn rather than refuse — the file may
|
||||||
|
// be deliberately group-readable by a service group.
|
||||||
|
private static void warnIfGroupOrWorldReadable(Path passFile) {
|
||||||
|
try {
|
||||||
|
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(passFile);
|
||||||
|
if (perms.contains(PosixFilePermission.OTHERS_READ)) {
|
||||||
|
System.err.println("Warning: " + passFile + " is world-readable; chmod 600 it");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Non-POSIX filesystem, or permissions unreadable — not worth failing over.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user