/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cinchapi.concourse.server.plugin;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.io.output.TeeOutputStream;
import com.cinchapi.common.base.CheckedExceptions;
import com.cinchapi.common.io.Files;
import com.cinchapi.common.logging.Logger;
import com.cinchapi.concourse.server.plugin.io.InterProcessCommunication;
import com.cinchapi.concourse.server.plugin.io.MessageQueue;
import com.cinchapi.concourse.server.plugin.io.PluginSerializer;
import com.cinchapi.concourse.thrift.AccessToken;
import com.cinchapi.concourse.util.ConcurrentMaps;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
/**
* A {@link Plugin} extends the functionality of Concourse Server.
* <p>
* Each class that extends this one may define methods that can be dynamically
* invoked using the
* {@link com.cinchapi.concourse.Concourse#invokePlugin(String, String, Object...)
* invokePlugin} method.
* </p>
*
* @author Jeff Nelson
*/
public abstract class Plugin {
/**
* The {@link AccessToken} that the plugin should use when making non-user
* (i.e. service) requests to Concourse Server.
*/
public final static AccessToken SERVICE_TOKEN;
/**
* The name of the dynamic property that is passed to the plugin's JVM to
* instruct it as to where the plugin's home is located.
*/
protected final static String PLUGIN_HOME_JVM_PROPERTY = "com.cinchapi.concourse.plugin.home";
/**
* The name of the dynamic property that is passed to the plugin's JVM to
* instruct it as to what {@link AccessToken} to use for service-based
* server requests.
*/
protected final static String PLUGIN_SERVICE_TOKEN_JVM_PROPERTY = "com.cinchapi.concourse.plugin.token";
static {
// Read the service token from the system properties
String encoded = System.getProperty(PLUGIN_SERVICE_TOKEN_JVM_PROPERTY);
if(encoded != null) {
byte[] decoded = BaseEncoding.base32Hex().decode(encoded);
ByteBuffer bytes = ByteBuffer.wrap(decoded);
SERVICE_TOKEN = new AccessToken(bytes);
}
else {
SERVICE_TOKEN = null;
}
}
/**
* The communication channel for messages that come from Concourse Server,
*/
protected final InterProcessCommunication fromServer;
/**
* A {@link Logger} for plugin operations.
*/
protected final Logger log;
/**
* A reference to the local Concourse Server {@link ConcourseRuntime
* runtime} to which this plugin is registered.
*/
protected final ConcourseRuntime runtime;
/**
* Responsible for taking arbitrary objects and turning them into binary so
* they can be sent across the wire.
*/
protected final PluginSerializer serializer = new PluginSerializer();
/**
* The communication channel for messages that are sent by this
* {@link Plugin} to Concourse Server.
*/
private final InterProcessCommunication fromPlugin;
/**
* Upstream response from Concourse Server in response to requests made via
* {@link ConcourseRuntime}.
*/
private final ConcurrentMap<AccessToken, RemoteMethodResponse> fromServerResponses;
/**
* A boolean that tracks whether the ready state has been set.
*/
private boolean inReadyState = false;
/**
* Construct a new instance.
*
* @param fromServer the location where Concourse Server places messages to
* be consumed by the Plugin
* @param fromPlugin the location where the Plugin places messages to be
* consumed by Concourse Server
*/
public Plugin(String fromServer, String fromPlugin) {
this.runtime = ConcourseRuntime.getRuntime();
this.fromServer = new MessageQueue(fromServer);
this.fromPlugin = new MessageQueue(fromPlugin);
this.fromServerResponses = Maps
.<AccessToken, RemoteMethodResponse> newConcurrentMap();
Path logDir = Paths.get(System.getProperty(PLUGIN_HOME_JVM_PROPERTY)
+ File.separator + "log");
logDir.toFile().mkdirs();
this.log = Logger.builder().name(this.getClass().getName())
.level(getConfig().getLogLevel()).directory(logDir.toString())
.build();
// Redirect System.out and System.err to a console.log file
Path consoleLog = logDir.resolve("console.log");
try {
File consoleLogFile = consoleLog.toFile();
consoleLogFile.createNewFile();
FileOutputStream fos = new FileOutputStream(consoleLogFile);
TeeOutputStream out = new TeeOutputStream(System.out, fos);
TeeOutputStream err = new TeeOutputStream(System.err, fos);
PrintStream consoleOut = new PrintStream(out, true);
PrintStream consoleErr = new PrintStream(err, true);
System.setOut(consoleOut);
System.setErr(consoleErr);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
consoleOut.close();
consoleErr.close();
}));
}
catch (IOException e) {
throw CheckedExceptions.throwAsRuntimeException(e);
}
}
/**
* Return a {@link BackgroundInformation} instance that has plugin-related
* attributes that are needed for making background requests to the upstream
* service.
*
* @return the Plugin's {@link BackgroundInformation}.
*/
public BackgroundInformation backgroundInformation() {
return new BackgroundInformation();
}
/**
* Start the plugin and process requests until instructed to
* {@link Instruction#STOP stop}.
*/
public void run() {
setReadyState();
log.info("Running plugin {}", this.getClass());
ByteBuffer data;
while ((data = fromServer.read()) != null) {
RemoteMessage message = serializer.deserialize(data);
if(message.type() == RemoteMessage.Type.REQUEST) {
RemoteMethodRequest request = (RemoteMethodRequest) message;
log.debug("Received REQUEST from Concourse Server: {}",
message);
Thread worker = new RemoteInvocationThread(request, fromPlugin,
this, false, fromServerResponses);
worker.setUncaughtExceptionHandler((thread, throwable) -> {
log.error(
"While processing request '{}', the following "
+ "non-recoverable error occurred:",
request, throwable);
});
worker.start();
}
else if(message.type() == RemoteMessage.Type.RESPONSE) {
RemoteMethodResponse response = (RemoteMethodResponse) message;
log.debug("Received RESPONSE from Concourse Server: {}",
response);
ConcurrentMaps.putAndSignal(fromServerResponses, response.creds,
response);
}
else if(message.type() == RemoteMessage.Type.STOP) { // STOP
log.info("Stopping plugin {}", this.getClass());
break;
}
else {
// Ignore the message...
continue;
}
}
}
/**
* Return the {@link PluginConfiguration preferences} for this plugin.
* <p>
* The plugin should override this class if the
* {@link StandardPluginConfiguration} is insufficient.
* </p>
*
* @return the {@link PluginConfiguration preferences} for the plugin
*/
protected PluginConfiguration getConfig() {
return new StandardPluginConfiguration();
}
/**
* Signal that the plugin is ready for operations.
*/
private void setReadyState() {
if(!inReadyState) {
try {
File ready = Files
.getHashedFilePath(System
.getProperty(PLUGIN_SERVICE_TOKEN_JVM_PROPERTY))
.toFile();
ready.getParentFile().mkdirs();
ready.createNewFile();
inReadyState = true;
}
catch (IOException e) {}
}
}
/**
* A wrapper class for all the information needed to perform background
* requests in this Plugin and its related classes.
*
* @author Jeff Nelson
*/
@Immutable
public class BackgroundInformation {
private BackgroundInformation() {/* no-op */}
/**
* Return the {@link InterProcessCommunication} channel that the Plugin
* and its related classes use for outgoing messages to the upstream
* service.
*
* @return the outgoing channel
*/
public InterProcessCommunication outgoing() {
return fromPlugin;
}
/**
* Return the queue of responses from the upstream service.
*
* @return the response queue
*/
public ConcurrentMap<AccessToken, RemoteMethodResponse> responses() {
return fromServerResponses;
}
}
}