diff --git a/liquibase-core/src/main/java/liquibase/change/core/ExecuteShellCommandChange.java b/liquibase-core/src/main/java/liquibase/change/core/ExecuteShellCommandChange.java index 194c0caf8e6..1ad59961007 100644 --- a/liquibase-core/src/main/java/liquibase/change/core/ExecuteShellCommandChange.java +++ b/liquibase-core/src/main/java/liquibase/change/core/ExecuteShellCommandChange.java @@ -42,7 +42,7 @@ public class ExecuteShellCommandChange extends AbstractChange { protected List finalCommandArray; private String executable; private List os; - private List args = new ArrayList(); + private final List args = new ArrayList<>(); private String timeout; private static final Pattern TIMEOUT_PATTERN = Pattern.compile("^\\s*(\\d+)\\s*([sSmMhH]?)\\s*$"); private static final Long SECS_IN_MILLIS = 1000L; @@ -190,8 +190,8 @@ protected void executeCommand(Database database) throws Exception { int returnCode = 0; try { //output both stdout and stderr data from proc to stdout of this process - StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), errorStream); - StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), inputStream); + StreamGobbler errorGobbler = createErrorGobbler(p.getErrorStream(), errorStream); + StreamGobbler outputGobbler = createErrorGobbler(p.getInputStream(), inputStream); errorGobbler.start(); outputGobbler.start(); @@ -225,6 +225,10 @@ protected void executeCommand(Database database) throws Exception { processResult(returnCode, errorStreamOut, infoStreamOut, database); } + protected StreamGobbler createErrorGobbler(InputStream processStream, OutputStream outputStream) { + return new StreamGobbler(processStream, outputStream, Thread.currentThread()); + } + /** * Max bytes to copy from output to {@link #processResult(int, String, String, Database)}. If null, process all output. * @return @@ -238,10 +242,8 @@ protected Integer getMaxStreamGobblerOutput() { *

* Creates a scheduled task to destroy the process in given timeout milliseconds. * This killer task will be cancelled if the process returns before the timeout value. - * - * @param process + * @param process * @param timeoutInMillis waits for specified timeoutInMillis before destroying the process. - * It will wait indefinitely if timeoutInMillis is 0. */ @java.lang.SuppressWarnings("squid:S2142") private int waitForOrKill(final Process process, final long timeoutInMillis) throws TimeoutException { @@ -273,6 +275,11 @@ public void run() { } } catch (InterruptedException ignore) { // check again + if (timedOut.get()) { + timer.cancel(); + String timeoutStr = timeout != null ? timeout : timeoutInMillis + " ms"; + throw new TimeoutException("Process timed out (" + timeoutStr + ")"); + } } } @@ -368,22 +375,24 @@ protected void customLoadLogic(ParsedNode parsedNode, ResourceAccessor resourceA } } } - private class StreamGobbler extends Thread { + + public class StreamGobbler extends Thread { private static final int THREAD_SLEEP_MILLIS = 100; private final OutputStream outputStream; private InputStream processStream; boolean loggedTruncated = false; long copiedSize = 0; + private final Thread parentThread; - private StreamGobbler(InputStream processStream, ByteArrayOutputStream outputStream) { + public StreamGobbler(InputStream processStream, OutputStream outputStream, Thread parentThread) { this.processStream = processStream; this.outputStream = outputStream; + this.parentThread = parentThread; } @Override public void run() { - try { - BufferedInputStream bufferedInputStream = new BufferedInputStream(processStream); + try (BufferedInputStream bufferedInputStream = new BufferedInputStream(processStream)) { while (processStream != null) { if (bufferedInputStream.available() > 0) { copy(bufferedInputStream, outputStream); @@ -396,7 +405,10 @@ public void run() { } } } catch (IOException ioe) { - ioe.printStackTrace(); + Scope.getCurrentScope().getLog(ExecuteShellCommandChange.class).warning(ioe.getMessage()); + if (parentThread != null) { + parentThread.interrupt(); + } } } diff --git a/liquibase-core/src/main/java/liquibase/changelog/ChangeLogIterator.java b/liquibase-core/src/main/java/liquibase/changelog/ChangeLogIterator.java index d6364240782..4d6b426b94c 100644 --- a/liquibase-core/src/main/java/liquibase/changelog/ChangeLogIterator.java +++ b/liquibase-core/src/main/java/liquibase/changelog/ChangeLogIterator.java @@ -1,13 +1,11 @@ package liquibase.changelog; -import liquibase.ContextExpression; -import liquibase.Labels; -import liquibase.RuntimeEnvironment; -import liquibase.Scope; +import liquibase.*; import liquibase.changelog.filter.ChangeSetFilter; import liquibase.changelog.filter.ChangeSetFilterResult; import liquibase.changelog.visitor.ChangeSetVisitor; import liquibase.changelog.visitor.SkippedChangeSetVisitor; +import liquibase.configuration.LiquibaseConfiguration; import liquibase.exception.LiquibaseException; import liquibase.exception.UnexpectedLiquibaseException; import liquibase.exception.ValidationErrors; @@ -137,7 +135,8 @@ private void validateChangeSetExecutor(ChangeSet changeSet, RuntimeEnvironment e if (changeSet.getRunWith() == null) { return; } - String executorName = changeSet.getRunWith(); + String executorName = ChangeSet.lookupExecutor(changeSet.getRunWith()); + Executor executor; try { executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor(executorName, env.getTargetDatabase()); diff --git a/liquibase-core/src/main/java/liquibase/changelog/ChangeSet.java b/liquibase-core/src/main/java/liquibase/changelog/ChangeSet.java index 44dfab8a5eb..7bbc6331ae0 100644 --- a/liquibase-core/src/main/java/liquibase/changelog/ChangeSet.java +++ b/liquibase-core/src/main/java/liquibase/changelog/ChangeSet.java @@ -8,6 +8,7 @@ import liquibase.change.core.EmptyChange; import liquibase.change.core.RawSQLChange; import liquibase.changelog.visitor.ChangeExecListener; +import liquibase.configuration.LiquibaseConfiguration; import liquibase.database.Database; import liquibase.database.DatabaseList; import liquibase.database.ObjectQuotingStrategy; @@ -719,7 +720,8 @@ private Executor setupCustomExecutorIfNecessary(Database database) { if (getRunWith() == null || originalExecutor instanceof LoggingExecutor) { return originalExecutor; } - Executor customExecutor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor(getRunWith(), database); + String executorName = ChangeSet.lookupExecutor(getRunWith()); + Executor customExecutor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor(executorName, database); Scope.getCurrentScope().getSingleton(ExecutorService.class).setExecutor("jdbc", database, customExecutor); List changes = getChanges(); for (Change change : changes) { @@ -735,6 +737,24 @@ private Executor setupCustomExecutorIfNecessary(Database database) { return originalExecutor; } + public static String lookupExecutor(String executorName) { + if (StringUtil.isEmpty(executorName)) { + return null; + } + String key = "liquibase." + executorName + ".executor"; + String replacementExecutorName = + (String)Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class).getCurrentConfiguredValue(null, null, key).getValue(); + if (replacementExecutorName != null) { + Scope.getCurrentScope().getLog(ChangeSet.class).info("Mapped '" + executorName + "' to executor '" + replacementExecutorName + "'"); + return replacementExecutorName; + } else if (executorName.equalsIgnoreCase("native")) { + String message = "Unable to locate an executor for 'runWith=native'. You must specify a valid executor name."; + Scope.getCurrentScope().getLog(ChangeSet.class).warning(message); + Scope.getCurrentScope().getUI().sendErrorMessage("WARNING: " + message); + } + return executorName; + } + public void rollback(Database database) throws RollbackFailedException { rollback(database, null); } diff --git a/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlcmd.conf b/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlcmd.conf new file mode 100644 index 00000000000..1f9d48ba450 --- /dev/null +++ b/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlcmd.conf @@ -0,0 +1,55 @@ +#### _ _ _ _ _____ +## | | (_) (_) | | __ \ +## | | _ __ _ _ _ _| |__ __ _ ___ ___ | |__) | __ ___ +## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ | ___/ '__/ _ \ +## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ | | | | | (_) | +## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| |_| |_| \___/ +## | | +## |_| +## +## The liquibase.sqlcmd.conf file stores properties which are used during the +## execution of the Microsoft SQLCMD tool. +## Learn more: https://www.liquibase.org/documentation/config_properties.html +#### +#### +## Note about relative and absolute paths: +## The liquibase.sqlcmd.path must be a valid path to the SQLCMD executable. +## The liquibase.sqlcmd.timeout value can be one of: +## -1 - disable the timeout +## Any integer value > 0 (measured in seconds) +## +#### + +# The full path to the SQLCMD executable. +# Sample Linux path +# liquibase.sqlcmd.path=/opt/mssql-tools/bin/sqlcmd +# Sample Windows path +# liquibase.sqlcmd.path="C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\170\\Tools\\Binn\\SQLCMD.EXE" + +# A valid timeout value for the execution of the SQLCMD tool +liquibase.sqlcmd.timeout=-1 + +# Flag to indicate whether or not to keep the temporary SQL file after execution of SQLCMD. +# True = keep False = delete (default) +liquibase.sqlcmd.keep.temp=true + +# OPTIONAL Flag to designate the location to store temporary SQL file after execution of SQLCMD. +# Liquibase will attempt to use path exactly as entered, so please ensure it complies with your OS requirements. +# liquibase.sqlcmd.keep.temp.path= + +# OPTIONAL Flag to designate the name of temporary SQL file after execution of SQLCMD. +# Liquibase will attempt to use the name exactly as entered, so please ensure it complies with your OS requirements. +# liquibase.sqlcmd.keep.temp.name= + +# OPTIONAL Args to pass directly to SQLCMD. +# Learn about SQLCMD args at https:// +# Note: The delimiter for args is a space eg:" " and not "," or ";" separated. +# liquibase.sqlcmd.args= + +# OPTIONAL Path to a log file for the SQLCMD output +# liquibase.sqlcmd.logFile= +# + +# OPTIONAL Name of a custom executor to use instead of SQLCMD +# The Executor must be on the Liquibase classpath +# liquibase.sqlcmd.executor= diff --git a/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlplus.conf b/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlplus.conf index f6afd2cbcb6..9a748db4eda 100644 --- a/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlplus.conf +++ b/liquibase-core/src/main/resources/liquibase/examples/sql/liquibase.sqlplus.conf @@ -21,9 +21,9 @@ #### # The full path to the SQLPLUS executable. -# Sample linux path +# Sample Linux path # liquibase.sqlplus.path=/apps/app/12.2.0.1.0/oracle/product/12.2.0.1.0/client_1/bin/sqlplus -# Sample windows path +# Sample Windows path # liquibase.sqlplus.path=c:\\oracle\\product\\11.2.0\\client_1\\bin\\sqlplus.exe # A valid timeout value for the execution of the SQLPLUS tool @@ -45,3 +45,7 @@ liquibase.sqlplus.keep.temp=true # Learn about SQLPLUS args at https://docs.oracle.com/cd/B10501_01/server.920/a90842/ch4.htm # Note: The delimiter for args is a space eg:" " and not "," or ";" separated. # liquibase.sqlplus.args= + +# OPTIONAL Name of a custom executor to use instead of SQLPLUS +# The Executor must be on the Liquibase classpath +# liquibase.sqlplus.executor= diff --git a/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlcmd.conf b/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlcmd.conf new file mode 100644 index 00000000000..d27bb25ffa3 --- /dev/null +++ b/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlcmd.conf @@ -0,0 +1,54 @@ +#### _ _ _ _ _____ +## | | (_) (_) | | __ \ +## | | _ __ _ _ _ _| |__ __ _ ___ ___ | |__) | __ ___ +## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ | ___/ '__/ _ \ +## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ | | | | | (_) | +## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| |_| |_| \___/ +## | | +## |_| +## +## The liquibase.sqlcmd.conf file stores properties which are used during the +## execution of the Microsoft SQLCMD tool. +## Learn more: https://www.liquibase.org/documentation/config_properties.html +#### +#### +## Note about relative and absolute paths: +## The liquibase.sqlcmd.path must be a valid path to the SQLCMD executable. +## The liquibase.sqlcmd.timeout value can be one of: +## -1 - disable the timeout +## Any integer value > 0 (measured in seconds) +## +#### + +# The full path to the SQLCMD executable. +# Sample Linux path +# liquibase.sqlcmd.path=/opt/mssql-tools/bin/sqlcmd +# Sample Windows path +# liquibase.sqlcmd.path="C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\170\\Tools\\Binn\\SQLCMD.EXE" + +# A valid timeout value for the execution of the SQLCMD tool +liquibase.sqlcmd.timeout=-1 + +# Flag to indicate whether or not to keep the temporary SQL file after execution of SQLCMD. +# True = keep False = delete (default) +liquibase.sqlcmd.keep.temp=true + +# OPTIONAL Flag to designate the location to store temporary SQL file after execution of SQLCMD. +# Liquibase will attempt to use path exactly as entered, so please ensure it complies with your OS requirements. +# liquibase.sqlcmd.keep.temp.path= + +# OPTIONAL Flag to designate the name of temporary SQL file after execution of SQLCMD. +# Liquibase will attempt to use the name exactly as entered, so please ensure it complies with your OS requirements. +# liquibase.sqlcmd.keep.temp.name= + +# OPTIONAL Args to pass directly to SQLCMD. +# Learn about SQLCMD args at https:// +# Note: The delimiter for args is a space eg:" " and not "," or ";" separated. +# liquibase.sqlcmd.args= + +# OPTIONAL Path to a log file for the SQLCMD output +# liquibase.sqlcmd.logFile= + +# OPTIONAL Name of a custom executor to use instead of SQLCMD +# The Executor must be on the Liquibase classpath +# liquibase.sqlcmd.executor= diff --git a/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlplus.conf b/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlplus.conf index f6afd2cbcb6..281ea92c7ea 100644 --- a/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlplus.conf +++ b/liquibase-core/src/main/resources/liquibase/examples/xml/liquibase.sqlplus.conf @@ -21,9 +21,9 @@ #### # The full path to the SQLPLUS executable. -# Sample linux path +# Sample Linux path # liquibase.sqlplus.path=/apps/app/12.2.0.1.0/oracle/product/12.2.0.1.0/client_1/bin/sqlplus -# Sample windows path +# Sample Windows path # liquibase.sqlplus.path=c:\\oracle\\product\\11.2.0\\client_1\\bin\\sqlplus.exe # A valid timeout value for the execution of the SQLPLUS tool @@ -45,3 +45,7 @@ liquibase.sqlplus.keep.temp=true # Learn about SQLPLUS args at https://docs.oracle.com/cd/B10501_01/server.920/a90842/ch4.htm # Note: The delimiter for args is a space eg:" " and not "," or ";" separated. # liquibase.sqlplus.args= + +# OPTIONAL Name of a custom executor to use instead of SQLPLUS +# The Executor must be on the Liquibase classpath +# liquibase.sqlplus.executor= diff --git a/liquibase-extension-examples/src/main/resources/META-INF/services/liquibase.executor.Executor b/liquibase-extension-examples/src/main/resources/META-INF/services/liquibase.executor.Executor new file mode 100644 index 00000000000..bbb80864b19 --- /dev/null +++ b/liquibase-extension-examples/src/main/resources/META-INF/services/liquibase.executor.Executor @@ -0,0 +1 @@ +liquibase.executor.jvm.ExampleExecutor \ No newline at end of file