Skip to content
This repository was archived by the owner on Jul 6, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ ext {
argparse4jVersion = '0.7.0'
junitVersion = '4.12'
evaluatorVersion = '3.5.4'
neo4jJavaDriverVersion = '4.0.0-rc1'
neo4jJavaDriverVersion = '4.0.0-rc2'
findbugsVersion = '3.0.0'
jansiVersion = '1.13'
jlineVersion = '2.14.6'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.ServiceUnavailableException;
import org.neo4j.driver.exceptions.TransientException;
import org.neo4j.shell.cli.CliArgs;
import org.neo4j.shell.commands.CommandHelper;
import org.neo4j.shell.exception.CommandException;
Expand All @@ -27,9 +28,11 @@
import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -102,6 +105,16 @@ private void ensureUser() throws Exception {
}
}

private void ensureDefaultDatabaseStarted() throws Exception {
CliArgs cliArgs = new CliArgs();
cliArgs.setUsername("neo4j", "");
cliArgs.setPassword("neo", "");
cliArgs.setDatabase("system");
ShellAndConnection sac = getShell(cliArgs);
main.connectMaybeInteractively(sac.shell, sac.connectionConfig, true, false);
sac.shell.execute("START DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
}

@Test
public void promptsOnWrongAuthenticationIfInteractive() throws Exception {
// when
Expand Down Expand Up @@ -239,7 +252,7 @@ public void wrongPortWithNeo4j() throws Exception
ConnectionConfig connectionConfig = sac.connectionConfig;

exception.expect( ServiceUnavailableException.class );
exception.expectMessage( "Unable to connect to database, ensure the database is running and that there is a working network connection to it" );
// The error message here may be subject to change and is not stable across versions so let us not assert on it
main.connectMaybeInteractively( shell, connectionConfig, true, true );
}

Expand Down Expand Up @@ -374,6 +387,173 @@ public void shouldFailIfInputFileDoesntExistInteractively() throws Exception {
shell.execute( ":source what.cypher" );
}

@Test
public void doesNotStartWhenDefaultDatabaseUnavailableIfInteractive() throws Exception {
shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell));
inputBuffer.put(String.format("neo4j%nneo%n").getBytes());

assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

// when
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// Multiple databases are only available from 4.0
assumeTrue( majorVersion( shell.getServerVersion() ) >= 4 );

// then
// should be connected
assertTrue(shell.isConnected());
// should have prompted and set the username and password
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
assertEquals(expectedLoginOutput, baos.toString());
assertEquals("neo4j", connectionConfig.username());
assertEquals("neo", connectionConfig.password());

// Stop the default database
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);

try {
shell.disconnect();

// Should get exception that database is unavailable when trying to connect
exception.expect(TransientException.class);
exception.expectMessage("Database 'neo4j' is unavailable");
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// then
assertFalse(shell.isConnected());
} finally {
// Start the default database again
ensureDefaultDatabaseStarted();
}
}

@Test
public void startsAgainstSystemDatabaseWhenDefaultDatabaseUnavailableIfInteractive() throws Exception {
shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell));

assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

// when
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// Multiple databases are only available from 4.0
assumeTrue( majorVersion( shell.getServerVersion() ) >= 4 );

// then
// should be connected
assertTrue(shell.isConnected());
// should have prompted and set the username and password
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
assertEquals(expectedLoginOutput, baos.toString());
assertEquals("neo4j", connectionConfig.username());
assertEquals("neo", connectionConfig.password());

// Stop the default database
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);

try {
shell.disconnect();

// Connect to system database
CliArgs cliArgs = new CliArgs();
cliArgs.setUsername("neo4j", "");
cliArgs.setPassword("neo", "");
cliArgs.setDatabase("system");
ShellAndConnection sac = getShell(cliArgs);
// Use the new shell and connection config from here on
shell = sac.shell;
connectionConfig = sac.connectionConfig;
main.connectMaybeInteractively(shell, connectionConfig, true, false);

// then
assertTrue(shell.isConnected());
} finally {
// Start the default database again
ensureDefaultDatabaseStarted();
}
}

@Test
public void switchingToUnavailableDatabaseIfInteractive() throws Exception {
shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell));
inputBuffer.put(String.format("neo4j%nneo%n").getBytes());

assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

// when
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// Multiple databases are only available from 4.0
assumeTrue(majorVersion( shell.getServerVersion() ) >= 4);

// then
// should be connected
assertTrue(shell.isConnected());
// should have prompted and set the username and password
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
assertEquals(expectedLoginOutput, baos.toString());
assertEquals("neo4j", connectionConfig.username());
assertEquals("neo", connectionConfig.password());

// Stop the default database
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);

try {
// Should get exception that database is unavailable when trying to connect
exception.expect(TransientException.class);
exception.expectMessage("Database 'neo4j' is unavailable");
shell.execute(":use " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
} finally {
// Start the default database again
ensureDefaultDatabaseStarted();
}
}

@Test
public void switchingToUnavailableDefaultDatabaseIfInteractive() throws Exception {
shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell));
inputBuffer.put(String.format("neo4j%nneo%n").getBytes());

assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

// when
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// Multiple databases are only available from 4.0
assumeTrue(majorVersion( shell.getServerVersion() ) >= 4);

// then
// should be connected
assertTrue(shell.isConnected());
// should have prompted and set the username and password
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
assertEquals(expectedLoginOutput, baos.toString());
assertEquals("neo4j", connectionConfig.username());
assertEquals("neo", connectionConfig.password());

// Stop the default database
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);

try {
// Should get exception that database is unavailable when trying to connect
exception.expect(TransientException.class);
exception.expectMessage("Database 'neo4j' is unavailable");
shell.execute(":use");
} finally {
// Start the default database again
ensureDefaultDatabaseStarted();
}
}

private String executeFileNonInteractively(String filename) throws Exception {
return executeFileNonInteractively(filename, mock(Logger.class));
}
Expand Down
29 changes: 25 additions & 4 deletions cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.neo4j.shell;

import org.neo4j.driver.exceptions.DiscoveryException;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.exceptions.ServiceUnavailableException;
import org.neo4j.shell.commands.Command;
import org.neo4j.shell.commands.CommandExecutable;
import org.neo4j.shell.commands.CommandHelper;
Expand Down Expand Up @@ -98,7 +100,7 @@ private void executeCypher(@Nonnull final String cypher) throws CommandException
});
lastNeo4jErrorCode = null;
} catch (Neo4jException e) {
lastNeo4jErrorCode = e.code();
lastNeo4jErrorCode = getErrorCode(e);
throw e;
}
}
Expand Down Expand Up @@ -160,7 +162,7 @@ public Optional<List<BoltResult>> commitTransaction() throws CommandException {
lastNeo4jErrorCode = null;
return results;
} catch (Neo4jException e) {
lastNeo4jErrorCode = e.code();
lastNeo4jErrorCode = getErrorCode(e);
throw e;
}
}
Expand All @@ -175,7 +177,7 @@ public boolean isTransactionOpen() {
return boltStateHandler.isTransactionOpen();
}

void setCommandHelper(@Nonnull CommandHelper commandHelper) {
public void setCommandHelper(@Nonnull CommandHelper commandHelper) {
this.commandHelper = commandHelper;
}

Expand All @@ -195,7 +197,7 @@ public void setActiveDatabase(String databaseName) throws CommandException
boltStateHandler.setActiveDatabase(databaseName);
lastNeo4jErrorCode = null;
} catch (Neo4jException e) {
lastNeo4jErrorCode = e.code();
lastNeo4jErrorCode = getErrorCode(e);
throw e;
}
}
Expand Down Expand Up @@ -230,4 +232,23 @@ public void changePassword(@Nonnull ConnectionConfig connectionConfig) {
public void disconnect() {
boltStateHandler.disconnect();
}

private String getErrorCode(Neo4jException e) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels a little brittle but I don't have a better idea.

Neo4jException statusException = e;

// If we encountered a later suppressed Neo4jException we use that as the basis for the status instead
Throwable[] suppressed = e.getSuppressed();
for (Throwable s : suppressed) {
if (s instanceof Neo4jException) {
statusException = (Neo4jException) s;
break;
}
}

if (statusException instanceof ServiceUnavailableException || statusException instanceof DiscoveryException) {
// Treat this the same way as a DatabaseUnavailable error for now.
return DATABASE_UNAVAILABLE_ERROR_CODE;
}
return statusException.code();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public class InteractiveShellRunner implements ShellRunner, SignalHandler {
private final static String TRANSACTION_PROMPT = "# ";
private final static String USERNAME_DB_DELIMITER = "@";
private final static int ONELINE_PROMPT_MAX_LENGTH = 50;
private static final String UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT = "<default_database>";
private static final String DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT = "[UNAVAILABLE]";
static final String UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT = "<default_database>";
static final String DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT = "[UNAVAILABLE]";

// Need to know if we are currently executing when catch Ctrl-C, needs to be atomic due to
// being called from different thread
Expand Down Expand Up @@ -166,14 +166,11 @@ AnsiFormattedText updateAndGetPrompt() {
}

String databaseName = databaseManager.getActualDatabaseAsReportedByServer();
if (databaseName == null) {
if (databaseName == null || ABSENT_DB_NAME.equals(databaseName)) {
// We have failed to get a successful response from the connection ping query
// Build the prompt from the db name as set by the user + a suffix indicating that we are in a disconnected state
String dbNameSetByUser = databaseManager.getActiveDatabaseAsSetByUser();
databaseName = ABSENT_DB_NAME.equals(dbNameSetByUser)? UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT : dbNameSetByUser;
} else if (ABSENT_DB_NAME.equals(databaseName)) {
// The driver did not give us a database name in the response from the connection ping query
databaseName = UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT;
}

String errorSuffix = getErrorPrompt(executer.lastNeo4jErrorCode());
Expand Down
21 changes: 18 additions & 3 deletions cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiConsole;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.exceptions.DiscoveryException;
import org.neo4j.driver.exceptions.ServiceUnavailableException;
import org.neo4j.shell.cli.Format;
import org.neo4j.shell.exception.AnsiFormattedException;

Expand Down Expand Up @@ -132,10 +134,23 @@ String getFormattedMessage(@Nonnull final Throwable e) {
.append("\nor as environment variable(s), NEO4J_USERNAME, and NEO4J_PASSWORD respectively.")
.append("\nSee --help for more info.");
} else {
if (e.getMessage() != null) {
msg = msg.append(e.getMessage());
Throwable cause = e;

// Get the suppressed root cause of ServiceUnavailableExceptions
if (e instanceof ServiceUnavailableException) {
Throwable[] suppressed = e.getSuppressed();
for (Throwable s : suppressed) {
if (s instanceof DiscoveryException ) {
cause = getRootCause(s);
break;
}
}
}

if (cause.getMessage() != null) {
msg = msg.append(cause.getMessage());
} else {
msg = msg.append(e.getClass().getSimpleName());
msg = msg.append(cause.getClass().getSimpleName());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void setActiveDatabase(String databaseName) throws CommandException
try {
reconnect(true);
}
catch (ClientException e2) {
catch (Exception e2) {
e.addSuppressed(e2);
}
}
Expand Down Expand Up @@ -174,9 +174,6 @@ private void reconnect() {
}

private void reconnect(boolean keepBookmark) {
// This will already throw an exception if there is no connectivity
driver.verifyConnectivity();

SessionConfig.Builder builder = SessionConfig.builder();
builder.withDefaultAccessMode(AccessMode.WRITE);
if (!ABSENT_DB_NAME.equals(activeDatabaseNameAsSetByUser)) {
Expand Down Expand Up @@ -268,9 +265,6 @@ public void changePassword(@Nonnull ConnectionConfig connectionConfig) {
try {
driver = getDriver(connectionConfig, authToken);

// This will already throw an exception if there is no connectivity
driver.verifyConnectivity();

SessionConfig.Builder builder = SessionConfig.builder()
.withDefaultAccessMode(AccessMode.WRITE)
.withDatabase(SYSTEM_DB_NAME);
Expand Down
Loading