view src/main/java/org/monetdb/merovingian/Control.java @ 970:f90d811e97eb default tip

Adjust getTableTypes() test for new table type: LOCAL TEMPORARY VIEW, added in 11.53.4 (Mar2025-SP1)
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 03 Apr 2025 15:01:33 +0200 (3 days ago)
parents ff075ed5ce81
children
line wrap: on
line source
/*
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2024, 2025 MonetDB Foundation;
 * Copyright August 2008 - 2023 MonetDB B.V.;
 * Copyright 1997 - July 2008 CWI.
 */

package org.monetdb.merovingian;

import org.monetdb.mcl.net.MapiSocket;
import org.monetdb.mcl.io.*;
import org.monetdb.mcl.MCLException;
import org.monetdb.mcl.parser.MCLParseException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

/**
 * A Control class to perform operations on a remote merovingian
 * instance, using the TCP control protocol.
 *
 * This class implements the protocol specific bits to perform all
 * possible actions against a merovingian server that has remote control
 * facilities enabled.
 *
 * In the merovingian world, other merovingians in the vicinity are
 * known to each merovingian, allowing to perform cluster wide actions.
 * The implementation taken in this class is to require one known
 * merovingian to get insight in the entire network.  Note that
 * connecting to a merovingian requires a passphrase that is likely to
 * be different for each merovingian.
 *
 * @author Fabian Groffen
 * @version 1.0
 */
public class Control {
	/** The host to connect to */
	private final String host;
	/** The port to connect to */
	private final int port;
	/** The passphrase to use when connecting */
	private final String passphrase;
	/** The file we should write MapiSocket debuglog to */
	private String debug;
	private int soTimeout = -1; /* -1 means not initialized */


	/**
	 * Constructs a new Control object.
	 *
	 * @param host - IP address or DNS
	 * @param port - port number
	 * @param passphrase - phrase used to pass authorisation
	 * @throws IllegalArgumentException if host, port or passphrase are
	 * null or &lt;= 0
	 */
	public Control(String host, int port, String passphrase)
		throws IllegalArgumentException
	{
		this.host = host;
		this.port = port;
		this.passphrase = passphrase;
	}
	
	/**
	 * Sets the socket timeout.
	 *
	 * @param timeout (in milliseconds) Use -1 to unset timeout (and use default)
	 */
	public void setSoTimeout(int timeout) {
		soTimeout = timeout;
	}

	/**
	 * Instructs to write a MCL protocol debug log to the given file.
	 * This affects any newly performed command, and can be changed
	 * in between commands.  Passing null to this method disables the
	 * debug log.
	 *
	 * @param filename the filename to write debug information to, or null
	 */
	public void setDebug(String filename) {
		this.debug = filename;
	}

	private String controlHash(String pass, String salt) {
		long ho;
		long h = 0;

		/* use a very simple hash function designed for a single int val
		 * (hash buckets), we can make this more interesting if necessary in
		 * the future.
		 * http://www.cs.hmc.edu/~geoff/classes/hmc.cs070.200101/homework10/hashfuncs.html */

		for (int i = 0; i < pass.length(); i++) {
			ho = h & 0xF8000000L;
			h <<= 5;
			h &= 0xFFFFFFFFL;
			h ^= ho >>> 27;
			h ^= (int)(pass.charAt(i));
		}

		for (int i = 0; i < salt.length(); i++) {
			ho = h & 0xF8000000L;
			h <<= 5;
			h &= 0xFFFFFFFFL;
			h ^= ho >> 27;
			h ^= (int)(salt.charAt(i));
		}

		return(Long.toString(h));
	}

	final static private String RESPONSE_OK = "OK";

	private List<String> sendCommand(
			String database, String command, boolean hasOutput)
		throws MerovingianException, IOException
	{
		BufferedMCLReader min;
		BufferedMCLWriter mout;
		MapiSocket ms = new MapiSocket();
		ms.setDatabase("merovingian");
		ms.setLanguage("control");
		if (soTimeout != -1)
			ms.setSoTimeout(soTimeout);
		if (debug != null)
			ms.debug(debug);
		try {
			ms.connect(host, port, "monetdb", passphrase);
			min = ms.getReader();
			mout = ms.getWriter();
		} catch (MCLParseException e) {
			throw new MerovingianException(e.getMessage());
		} catch (MCLException e) {
			throw new MerovingianException(e.getMessage());
		} catch (AssertionError e) { // mcl panics
			ms.close();

			// Try old protocol instead
			Socket s;
			PrintStream out;
			BufferedReader in;
			s = new Socket(host, port);
			out = new PrintStream(s.getOutputStream());
			in = new BufferedReader(
					new InputStreamReader(s.getInputStream()));
			try {
				/* login ritual, step 1: get challenge from server */
				String response = in.readLine();
				if (response == null)
					throw new MerovingianException("server closed the connection");

				if (!response.startsWith("merovingian:1:") &&
						!response.startsWith("merovingian:2:"))
					throw new MerovingianException("unsupported merovingian server");

				String[] tokens = response.split(":");
				if (tokens.length < 3)
					throw new MerovingianException("did not understand merovingian server");
				String version = tokens[1];
				String token = tokens[2];

				response = controlHash(passphrase, token);
				if (version.equals("1")) {
					out.print(response + "\n");
				} else if (version.equals("2")) {
					// we only support control mode for now
					out.print(response + ":control\n");
				}

				response = in.readLine();
				if (response == null) {
					throw new MerovingianException("server closed the connection");
				}

				if (!response.substring(1).equals(RESPONSE_OK)) {
					throw new MerovingianException(response);
				}

				/* send command, form is simple: "<db> <cmd>\n" */
				out.print(database + " " + command + "\n");

				/* Response has the first line either "OK\n" or an error
				 * message.  In case of a command with output, the data will
				 * follow the first line */
				response = in.readLine();
				if (response == null) {
					throw new MerovingianException("server closed the connection");
				}
				if (!response.substring(1).equals(RESPONSE_OK)) {
					throw new MerovingianException(response);
				}

				if (!hasOutput)
					return null;

				ArrayList<String> l = new ArrayList<String>();
				while ((response = in.readLine()) != null) {
					l.add(response);
				}
				return l;
			} finally {
				in.close();
				out.close();
				s.close();
			}
		}

		mout.writeLine(database + " " + command + "\n");
		ArrayList<String> l = new ArrayList<String>();
		min.advance();
		String tmpLine = min.getLine();
		switch (min.getLineType()) {
		case ERROR:
			throw new MerovingianException(tmpLine.substring(6));
		case RESULT:
			if (!tmpLine.substring(1).equals(RESPONSE_OK))
				throw new MerovingianException(tmpLine);
			break;
		default:
			throw new MerovingianException("unexpected line: " + tmpLine);
		}


		lineloop:
		while (true) {
			min.advance();
			switch (min.getLineType()) {
				case PROMPT:
				break lineloop;
			case RESULT:
				l.add(tmpLine.substring(1));
				continue lineloop;
			default:
				throw new MerovingianException("unexpected line: " + tmpLine);
			}
		}

		ms.close();
		return l;
	}

	public void start(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "start", false);
	}

	public boolean isStopped(String database)
		throws MerovingianException, IOException
	{
		switch (getStatus(database).getState()) {
		case SABdbInactive:
		case SABdbCrashed:
		case SABdbIllegal:
			return true;
		case SABdbStarting:
		case SABdbRunning:
			return false;
		default:
			throw new IllegalStateException();
		}
	}
	
	public void stop(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "stop", false);
	}

	public void kill(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "kill", false);
	}

	public void create(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "create", false);
	}

	public void destroy(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "destroy", false);
	}

	public void lock(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "lock", false);
	}

	public void release(String database)
		throws MerovingianException, IOException
	{
		sendCommand(database, "release", false);
	}

	public void rename(String database, String newname)
		throws MerovingianException, IOException
	{
		if (newname == null)
			newname = ""; /* force error from merovingian */

		sendCommand(database, "name=" + newname, false);
	}

	/**
	 * Sets property for database to value.  If value is null, the
	 * property is unset, and its inherited value becomes active again.
	 *
	 * @param database the target database
	 * @param property the property to set value for
	 * @param value the value to set
	 * @throws MerovingianException if performing the command failed at
	 *         the merovingian side
	 * @throws IOException if connecting to or communicating with
	 *         merovingian failed
	 */
	public void setProperty(String database, String property, String value)
		throws MerovingianException, IOException
	{
		/* inherit: set to empty string */
		if (value == null)
			value = "";

		sendCommand(database, property + "=" + value, false);
	}

	public void inheritProperty(String database, String property)
		throws MerovingianException, IOException
	{
		setProperty(database, property, null);
	}

	public Properties getProperties(String database)
		throws MerovingianException, IOException
	{
		Properties ret = new Properties();
		List<String> response = sendCommand(database, "get", true);
		for (String responseLine : response) {
			if (responseLine.startsWith("#"))
				continue;
			int pos = responseLine.indexOf("=");
			if (pos > 0) {
				ret.setProperty(
						responseLine.substring(0, pos),
						responseLine.substring(pos + 1, responseLine.length()));
			}
		}
		return ret;
	}

	public Properties getDefaultProperties()
		throws MerovingianException, IOException
	{
		return(getProperties("#defaults"));
	}

	public SabaothDB getStatus(String database)
		throws MerovingianException, IOException
	{
		List<String> response = sendCommand(database, "status", true);
		if (response.isEmpty())
			throw new MerovingianException("communication error");
		return new SabaothDB(response.get(0));
	}

	/**
	 * Test whether a specific database exists.
	 *
	 * @param database name of database
	 * @return true, iff database already exists.
	 * @throws MerovingianException if performing the command failed at
	 *         the merovingian side
	 * @throws IOException if connecting to or communicating with
	 *         merovingian failed
	 */
	public boolean exists(String database)
		throws MerovingianException, IOException
	{
		List<SabaothDB> all = getAllStatuses();
		for (SabaothDB db : all) {
			if (db.getName().equals(database)) {
				return true;
			}
		}
		return false;
	}

	public List<SabaothDB> getAllStatuses()
		throws MerovingianException, IOException
	{
		List<SabaothDB> l = new ArrayList<SabaothDB>();
		List<String> response = sendCommand("#all", "status", true);
		try {
			for (String responseLine : response) {
				l.add(new SabaothDB(responseLine));
			}
		} catch (IllegalArgumentException e) {
			throw new MerovingianException(e.getMessage());
		}
		return Collections.unmodifiableList(l);
	}

	public List<URI> getAllNeighbours()
		throws MerovingianException, IOException
	{
		List<URI> l = new ArrayList<URI>();
		List<String> response = sendCommand("anelosimus", "eximius", true);
		try {
			for (String responseLine : response) {
				// format is <db>\t<uri>
				String[] parts = responseLine.split("\t", 2);
				if (parts.length != 2)
					throw new MerovingianException("invalid entry: " +
							responseLine);
				if (parts[0].equals("*")) {
					l.add(new URI(parts[1]));
				} else {
					l.add(new URI(parts[1] + parts[0]));
				}
			}
		} catch (URISyntaxException e) {
			throw new MerovingianException(e.getMessage());
		}
		return Collections.unmodifiableList(l);
	}
}