+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * Copyright (C) 2015-2016 EfficiOS Inc.
+ * Copyright (C) 2015-2016 Alexandre Montplaisir <alexmonthy@efficios.com>
+ * Copyright (C) 2013 David Goulet <dgoulet@efficios.com>
+ */
+
+package org.lttng.ust.agent.client;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.management.ManagementFactory;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.lttng.ust.agent.utils.LttngUstAgentLogger;
+
+/**
+ * Client for agents to connect to a local session daemon, using a TCP socket.
+ *
+ * @author David Goulet
+ */
+public class LttngTcpSessiondClient implements Runnable {
+
+ private static final String SESSION_HOST = "127.0.0.1";
+ private static final String ROOT_PORT_FILE = "/var/run/lttng/agent.port";
+ private static final String USER_PORT_FILE = "/.lttng/agent.port";
+ private static final Charset PORT_FILE_ENCODING = Charset.forName("UTF-8");
+
+ private static final int PROTOCOL_MAJOR_VERSION = 2;
+ private static final int PROTOCOL_MINOR_VERSION = 0;
+
+ /** Command header from the session deamon. */
+ private final CountDownLatch registrationLatch = new CountDownLatch(1);
+
+ private Socket sessiondSock;
+ private volatile boolean quit = false;
+
+ private DataInputStream inFromSessiond;
+ private DataOutputStream outToSessiond;
+
+ private final ILttngTcpClientListener logAgent;
+ private final int domainValue;
+ private final boolean isRoot;
+
+ /**
+ * Constructor
+ *
+ * @param logAgent
+ * The listener this client will operate on, typically an LTTng
+ * agent.
+ * @param domainValue
+ * The integer to send to the session daemon representing the
+ * tracing domain to handle.
+ * @param isRoot
+ * True if this client should connect to the root session daemon,
+ * false if it should connect to the user one.
+ */
+ public LttngTcpSessiondClient(ILttngTcpClientListener logAgent, int domainValue, boolean isRoot) {
+ this.logAgent = logAgent;
+ this.domainValue = domainValue;
+ this.isRoot = isRoot;
+ }
+
+ /**
+ * Wait until this client has successfully established a connection to its
+ * target session daemon.
+ *
+ * @param seconds
+ * A timeout in seconds after which this method will return
+ * anyway.
+ * @return True if the the client actually established the connection, false
+ * if we returned because the timeout has elapsed or the thread was
+ * interrupted.
+ */
+ public boolean waitForConnection(int seconds) {
+ try {
+ return registrationLatch.await(seconds, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void run() {
+ for (;;) {
+ if (this.quit) {
+ break;
+ }
+
+ try {
+
+ /*
+ * Connect to the session daemon before anything else.
+ */
+ log("Connecting to sessiond");
+ connectToSessiond();
+
+ /*
+ * Register to the session daemon as the Java component of the
+ * UST application.
+ */
+ log("Registering to sessiond");
+ registerToSessiond();
+
+ /*
+ * Block on socket receive and wait for command from the
+ * session daemon. This will return if and only if there is a
+ * fatal error or the socket closes.
+ */
+ log("Waiting on sessiond commands...");
+ handleSessiondCmd();
+ } catch (UnknownHostException uhe) {
+ uhe.printStackTrace();
+ /*
+ * Terminate agent thread.
+ */
+ close();
+ } catch (IOException ioe) {
+ /*
+ * I/O exception may have been triggered by a session daemon
+ * closing the socket. Close our own socket and
+ * retry connecting after a delay.
+ */
+ try {
+ if (this.sessiondSock != null) {
+ this.sessiondSock.close();
+ }
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ /*
+ * Retry immediately if sleep is interrupted.
+ */
+ } catch (IOException closeioe) {
+ closeioe.printStackTrace();
+ /*
+ * Terminate agent thread.
+ */
+ close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispose this client and close any socket connection it may hold.
+ */
+ public void close() {
+ log("Closing client");
+ this.quit = true;
+
+ try {
+ if (this.sessiondSock != null) {
+ this.sessiondSock.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void connectToSessiond() throws IOException {
+ int rootPort = getPortFromFile(ROOT_PORT_FILE);
+ int userPort = getPortFromFile(getHomePath() + USER_PORT_FILE);
+
+ /*
+ * Check for the edge case of both files existing but pointing to the
+ * same port. In this case, let the root client handle it.
+ */
+ if ((rootPort != 0) && (rootPort == userPort) && (!isRoot)) {
+ log("User and root config files both point to port " + rootPort +
+ ". Letting the root client handle it.");
+ throw new IOException();
+ }
+
+ int portToUse = (isRoot ? rootPort : userPort);
+
+ if (portToUse == 0) {
+ /* No session daemon available. Stop and retry later. */
+ throw new IOException();
+ }
+
+ this.sessiondSock = new Socket(SESSION_HOST, portToUse);
+ this.inFromSessiond = new DataInputStream(sessiondSock.getInputStream());
+ this.outToSessiond = new DataOutputStream(sessiondSock.getOutputStream());
+ }
+
+ private static String getHomePath() {
+ /*
+ * The environment variable LTTNG_HOME overrides HOME if
+ * defined.
+ */
+ String homePath = System.getenv("LTTNG_HOME");
+
+ if (homePath == null) {
+ homePath = System.getProperty("user.home");
+ }
+ return homePath;
+ }
+
+ /**
+ * Read port number from file created by the session daemon.
+ *
+ * @return port value if found else 0.
+ */
+ private static int getPortFromFile(String path) throws IOException {
+ BufferedReader br = null;
+
+ try {
+ br = new BufferedReader(new InputStreamReader(new FileInputStream(path), PORT_FILE_ENCODING));
+ String line = br.readLine();
+ if (line == null) {
+ /* File exists but is empty. */
+ return 0;
+ }
+
+ int port = Integer.parseInt(line, 10);
+ if (port < 0 || port > 65535) {
+ /* Invalid value. Ignore. */
+ port = 0;
+ }
+ return port;
+
+ } catch (NumberFormatException e) {
+ /* File contained something that was not a number. */
+ return 0;
+ } catch (FileNotFoundException e) {
+ /* No port available. */
+ return 0;
+ } finally {
+ if (br != null) {
+ br.close();
+ }
+ }
+ }
+
+ private void registerToSessiond() throws IOException {
+ byte data[] = new byte[16];
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
+
+ buf.putInt(domainValue);
+ buf.putInt(Integer.parseInt(pid));
+ buf.putInt(PROTOCOL_MAJOR_VERSION);
+ buf.putInt(PROTOCOL_MINOR_VERSION);
+ this.outToSessiond.write(data, 0, data.length);
+ this.outToSessiond.flush();
+ }
+
+ /**
+ * Handle session command from the session daemon.
+ */
+ private void handleSessiondCmd() throws IOException {
+ /* Data read from the socket */
+ byte inputData[] = null;
+ /* Reply data written to the socket, sent to the sessiond */
+ LttngAgentResponse response;
+
+ while (true) {
+ /* Get header from session daemon. */
+ SessiondCommandHeader cmdHeader = recvHeader();
+
+ if (cmdHeader.getDataSize() > 0) {
+ inputData = recvPayload(cmdHeader);
+ }
+
+ switch (cmdHeader.getCommandType()) {
+ case CMD_REG_DONE:
+ {
+ /*
+ * Countdown the registration latch, meaning registration is
+ * done and we can proceed to continue tracing.
+ */
+ registrationLatch.countDown();
+ /*
+ * We don't send any reply to the registration done command.
+ * This just marks the end of the initial session setup.
+ */
+ log("Registration done");
+ continue;
+ }
+ case CMD_LIST:
+ {
+ SessiondCommand listLoggerCmd = new SessiondListLoggersCommand();
+ response = listLoggerCmd.execute(logAgent);
+ log("Received list loggers command");
+ break;
+ }
+ case CMD_EVENT_ENABLE:
+ {
+ if (inputData == null) {
+ /* Invalid command */
+ response = LttngAgentResponse.FAILURE_RESPONSE;
+ break;
+ }
+ SessiondCommand enableEventCmd = new SessiondEnableEventCommand(inputData);
+ response = enableEventCmd.execute(logAgent);
+ log("Received enable event command: " + enableEventCmd.toString());
+ break;
+ }
+ case CMD_EVENT_DISABLE:
+ {
+ if (inputData == null) {
+ /* Invalid command */
+ response = LttngAgentResponse.FAILURE_RESPONSE;
+ break;
+ }
+ SessiondCommand disableEventCmd = new SessiondDisableEventCommand(inputData);
+ response = disableEventCmd.execute(logAgent);
+ log("Received disable event command: " + disableEventCmd.toString());
+ break;
+ }
+ case CMD_APP_CTX_ENABLE:
+ {
+ if (inputData == null) {
+ /* This commands expects a payload, invalid command */
+ response = LttngAgentResponse.FAILURE_RESPONSE;
+ break;
+ }
+ SessiondCommand enableAppCtxCmd = new SessiondEnableAppContextCommand(inputData);
+ response = enableAppCtxCmd.execute(logAgent);
+ log("Received enable app-context command");
+ break;
+ }
+ case CMD_APP_CTX_DISABLE:
+ {
+ if (inputData == null) {
+ /* This commands expects a payload, invalid command */
+ response = LttngAgentResponse.FAILURE_RESPONSE;
+ break;
+ }
+ SessiondCommand disableAppCtxCmd = new SessiondDisableAppContextCommand(inputData);
+ response = disableAppCtxCmd.execute(logAgent);
+ log("Received disable app-context command");
+ break;
+ }
+ default:
+ {
+ /* Unknown command, send empty reply */
+ response = null;
+ log("Received unknown command, ignoring");
+ break;
+ }
+ }
+
+ /* Send response to the session daemon. */
+ byte[] responseData;
+ if (response == null) {
+ responseData = new byte[4];
+ ByteBuffer buf = ByteBuffer.wrap(responseData);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ } else {
+ log("Sending response: " + response.toString());
+ responseData = response.getBytes();
+ }
+ this.outToSessiond.write(responseData, 0, responseData.length);
+ this.outToSessiond.flush();
+ }
+ }
+
+ /**
+ * Receive header data from the session daemon using the LTTng command
+ * static buffer of the right size.
+ */
+ private SessiondCommandHeader recvHeader() throws IOException {
+ byte data[] = new byte[SessiondCommandHeader.HEADER_SIZE];
+ int bytesLeft = data.length;
+ int bytesOffset = 0;
+
+ while (bytesLeft > 0) {
+ int bytesRead = this.inFromSessiond.read(data, bytesOffset, bytesLeft);
+
+ if (bytesRead < 0) {
+ throw new IOException();
+ }
+ bytesLeft -= bytesRead;
+ bytesOffset += bytesRead;
+ }
+ return new SessiondCommandHeader(data);
+ }
+
+ /**
+ * Receive payload from the session daemon. This MUST be done after a
+ * recvHeader() so the header value of a command are known.
+ *
+ * The caller SHOULD use isPayload() before which returns true if a payload
+ * is expected after the header.
+ */
+ private byte[] recvPayload(SessiondCommandHeader headerCmd) throws IOException {
+ byte payload[] = new byte[(int) headerCmd.getDataSize()];
+ int bytesLeft = payload.length;
+ int bytesOffset = 0;
+
+ /* Failsafe check so we don't waste our time reading 0 bytes. */
+ if (bytesLeft == 0) {
+ return null;
+ }
+
+ while (bytesLeft > 0) {
+ int bytesRead = inFromSessiond.read(payload, bytesOffset, bytesLeft);
+
+ if (bytesRead < 0) {
+ throw new IOException();
+ }
+ bytesLeft -= bytesRead;
+ bytesOffset += bytesRead;
+ }
+ return payload;
+ }
+
+ /**
+ * Wrapper for this class's logging, adds the connection's characteristics
+ * to help differentiate between multiple TCP clients.
+ */
+ private void log(String message) {
+ LttngUstAgentLogger.log(getClass(),
+ "(root=" + isRoot + ", domain=" + domainValue + ") " + message);
+ }
+}