├── samples ├── asset-package │ ├── foo.txt │ └── package.json ├── test-with-parent │ ├── test2.txt │ └── package.json └── testpackage │ ├── test.txt │ └── package.json ├── src ├── main │ ├── resources │ │ ├── playpen-p3.bat │ │ ├── playpen-cli.bat │ │ ├── packages.json │ │ ├── playpen-local.bat │ │ ├── playpen-network.bat │ │ ├── keystore.json │ │ ├── network.json │ │ ├── local.json │ │ ├── playpen-p3.sh │ │ ├── playpen-cli.sh │ │ ├── playpen-local.sh │ │ ├── playpen-network.sh │ │ ├── logging-cli.xml │ │ ├── logging-p3.xml │ │ ├── logging-local.xml │ │ └── logging-network.xml │ ├── java │ │ └── io │ │ │ └── playpen │ │ │ └── core │ │ │ ├── p3 │ │ │ ├── ExecutionType.java │ │ │ ├── IPackageStep.java │ │ │ ├── IPackageResolver.java │ │ │ ├── PackageException.java │ │ │ ├── PackageContext.java │ │ │ ├── step │ │ │ │ ├── ExpandStep.java │ │ │ │ ├── ExpandAssetsStep.java │ │ │ │ ├── CopyStep.java │ │ │ │ ├── PipeStep.java │ │ │ │ ├── StringTemplateStep.java │ │ │ │ └── ExecuteStep.java │ │ │ ├── resolver │ │ │ │ ├── InMemoryCacheResolver.java │ │ │ │ ├── PromotedResolver.java │ │ │ │ └── LocalRepositoryResolver.java │ │ │ ├── P3Package.java │ │ │ └── PackageManager.java │ │ │ ├── coordinator │ │ │ ├── CoordinatorMode.java │ │ │ ├── network │ │ │ │ ├── ProvisionResult.java │ │ │ │ ├── authenticator │ │ │ │ │ ├── IAuthenticator.java │ │ │ │ │ └── DeprovisionAuthenticator.java │ │ │ │ ├── Server.java │ │ │ │ ├── INetworkListener.java │ │ │ │ └── LocalCoordinator.java │ │ │ ├── client │ │ │ │ ├── ClientMode.java │ │ │ │ └── Client.java │ │ │ ├── VMShutdownThread.java │ │ │ ├── local │ │ │ │ ├── Server.java │ │ │ │ └── ConsoleMessageListener.java │ │ │ ├── PlayPen.java │ │ │ └── api │ │ │ │ └── APIClient.java │ │ │ ├── plugin │ │ │ ├── IEventListener.java │ │ │ ├── PluginSchema.java │ │ │ ├── IPlugin.java │ │ │ ├── AbstractPlugin.java │ │ │ ├── PluginClassLoader.java │ │ │ ├── EventManager.java │ │ │ └── PluginManager.java │ │ │ ├── utils │ │ │ ├── process │ │ │ │ ├── IProcessListener.java │ │ │ │ ├── ShutdownProcessListener.java │ │ │ │ ├── ProcessBuffer.java │ │ │ │ ├── FileProcessListener.java │ │ │ │ └── XProcess.java │ │ │ ├── JarUtils.java │ │ │ ├── STUtils.java │ │ │ ├── AbortableCountDownLatch.java │ │ │ └── AuthUtils.java │ │ │ ├── ConfigException.java │ │ │ ├── networking │ │ │ ├── ITransactionListener.java │ │ │ ├── TransactionInfo.java │ │ │ ├── AbstractTransactionListener.java │ │ │ ├── netty │ │ │ │ ├── AuthenticatedMessageInitializer.java │ │ │ │ └── AuthenticatedMessageHandler.java │ │ │ └── TransactionManager.java │ │ │ ├── Initialization.java │ │ │ ├── P3Tool.java │ │ │ └── Bootstrap.java │ └── proto │ │ ├── p3.proto │ │ ├── protocol.proto │ │ ├── coordinator.proto │ │ └── command.proto └── test │ └── java │ └── io │ └── playpen │ └── core │ └── ProcessBufferTest.java ├── .gitignore ├── LICENSE.md ├── README.md └── pom.xml /samples/asset-package/foo.txt: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /samples/test-with-parent/test2.txt: -------------------------------------------------------------------------------- 1 | Someone says ! -------------------------------------------------------------------------------- /samples/testpackage/test.txt: -------------------------------------------------------------------------------- 1 | Hello, ! 2 | 3 | I have an asset package at /asset-package/1.0! -------------------------------------------------------------------------------- /src/main/resources/playpen-p3.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Dlog4j.configurationFile=logging-p3.xml -jar "%~dp0${project.build.finalName}.jar" p3 %* -------------------------------------------------------------------------------- /src/main/resources/playpen-cli.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Dlog4j.configurationFile=logging-cli.xml -jar "%~dp0${project.build.finalName}.jar" cli %* -------------------------------------------------------------------------------- /src/main/resources/packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Used to keep track of the latest version of packages. Do not edit manually!", 3 | "promoted": {} 4 | } -------------------------------------------------------------------------------- /src/main/resources/playpen-local.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Dlog4j.configurationFile=logging-local.xml -Xmx512M -Xms512M -jar "%~dp0${project.build.finalName}.jar" local %* -------------------------------------------------------------------------------- /src/main/resources/playpen-network.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Dlog4j.configurationFile=logging-network.xml -Xmx512M -Xms512M -jar "%~dp0${project.build.finalName}.jar" network %* -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/ExecutionType.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | public enum ExecutionType { 4 | PROVISION, 5 | EXECUTE, 6 | SHUTDOWN 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/CoordinatorMode.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator; 2 | 3 | public enum CoordinatorMode { 4 | NETWORK, 5 | LOCAL, 6 | CLIENT 7 | } 8 | -------------------------------------------------------------------------------- /samples/asset-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "id": "asset-package", 4 | "version": "1.0" 5 | }, 6 | 7 | "provision": [ 8 | {"id": "expand-assets"} 9 | ] 10 | } -------------------------------------------------------------------------------- /src/main/resources/keystore.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Network coordinator keys file. Do not edit yourself unless removing the default client!", 3 | "coordinators": [ 4 | { 5 | "uuid": "uid-default-client", 6 | "key": "default-client" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/ProvisionResult.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ProvisionResult { 7 | private String coordinator; 8 | private String server; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/IEventListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | public interface IEventListener> { 4 | void onListenerRegistered(EventManager em); 5 | 6 | void onListenerRemoved(EventManager em); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/IPackageStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | import org.json.JSONObject; 4 | 5 | public interface IPackageStep { 6 | String getStepId(); 7 | 8 | boolean runStep(P3Package p3, PackageContext ctx, JSONObject config); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/network.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Network coordinator configuration", 3 | "ip": "0.0.0.0", 4 | "port": 25501, 5 | "strings": {}, 6 | 7 | "_comment2": "# of megabytes at which a package will be split into multiple messages", 8 | "package-size-split": 100 9 | } -------------------------------------------------------------------------------- /samples/test-with-parent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "id": "test-with-parent", 4 | "version": "1.0" 5 | }, 6 | "provision": [ 7 | {"id": "expand"}, 8 | { 9 | "id": "string-template", 10 | "files": [ 11 | "test2.txt" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/main/resources/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Local coordinator configuration", 3 | "name": null, 4 | "uuid": null, 5 | "key": null, 6 | "coord-ip": "127.0.0.1", 7 | "coord-port": 25501, 8 | "resources": {}, 9 | "attributes": [], 10 | "strings": {}, 11 | "use-name-for-logs": true 12 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/IPackageResolver.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | import java.util.Collection; 4 | 5 | public interface IPackageResolver { 6 | P3Package resolvePackage(PackageManager pm, String id, String version); 7 | Collection getPackageList(PackageManager pm); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/PluginSchema.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class PluginSchema { 9 | private String id; 10 | private String version; 11 | private String main; 12 | private List files; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/client/ClientMode.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.client; 2 | 3 | public enum ClientMode { 4 | NONE, 5 | LIST, 6 | PROVISION, 7 | DEPROVISION, 8 | SHUTDOWN, 9 | PROMOTE, 10 | GENERATE_KEYPAIR, 11 | SEND_INPUT, 12 | ATTACH, 13 | FREEZE, 14 | UPLOAD 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/process/IProcessListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils.process; 2 | 3 | public interface IProcessListener { 4 | void onProcessAttach(XProcess proc); 5 | void onProcessDetach(XProcess proc); 6 | void onProcessOutput(XProcess proc, String out); 7 | void onProcessInput(XProcess proc, String in); 8 | void onProcessEnd(XProcess proc); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/ConfigException.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core; 2 | 3 | public class ConfigException extends Exception { 4 | public ConfigException() { 5 | super(); 6 | } 7 | 8 | public ConfigException(String message) { 9 | super(message); 10 | } 11 | 12 | public ConfigException(String message, Throwable inner) { 13 | super(message, inner); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/VMShutdownThread.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | 5 | @Log4j2 6 | public class VMShutdownThread extends Thread { 7 | @Override 8 | public void run() { 9 | log.info("Shutting down playpen"); 10 | if(PlayPen.get() != null) { 11 | PlayPen.get().onVMShutdown(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/PackageException.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | public class PackageException extends Exception { 4 | public PackageException() { 5 | super(); 6 | } 7 | 8 | public PackageException(String message) { 9 | super(message); 10 | } 11 | 12 | public PackageException(String message, Throwable inner) { 13 | super(message, inner); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/authenticator/IAuthenticator.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network.authenticator; 2 | 3 | import io.playpen.core.coordinator.network.LocalCoordinator; 4 | import io.playpen.core.networking.TransactionInfo; 5 | import io.playpen.core.protocol.Commands; 6 | 7 | public interface IAuthenticator { 8 | String getName(); 9 | boolean hasAccess(Commands.BaseCommand command, TransactionInfo info, LocalCoordinator from); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/IPlugin.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.io.File; 6 | 7 | public interface IPlugin { 8 | void setSchema(PluginSchema schema); 9 | 10 | PluginSchema getSchema(); 11 | 12 | void setPluginDir(File pluginDir); 13 | 14 | File getPluginDir(); 15 | 16 | void setConfig(JSONObject config); 17 | 18 | JSONObject getConfig(); 19 | 20 | boolean onStart(); 21 | 22 | void onStop(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/PackageContext.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.File; 6 | import java.util.*; 7 | 8 | @Data 9 | public class PackageContext { 10 | private PackageManager packageManager; 11 | 12 | private File destination; 13 | 14 | private Map properties = new HashMap<>(); 15 | 16 | private Map resources = new HashMap<>(); 17 | 18 | private List dependencyChain = new ArrayList<>(); 19 | 20 | private Object user; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/playpen-p3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE="${BASH_SOURCE[0]}" 3 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 4 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 5 | SOURCE="$(readlink "$SOURCE")" 6 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 7 | done 8 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 9 | java -Dlog4j.configurationFile=logging-p3.xml -jar "${DIR}/${project.build.finalName}.jar" p3 $@ -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/ITransactionListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking; 2 | 3 | import io.playpen.core.protocol.Protocol; 4 | 5 | public interface ITransactionListener { 6 | void onTransactionReceive(TransactionManager tm, TransactionInfo info, Protocol.Transaction message); 7 | 8 | void onTransactionSend(TransactionManager tm, TransactionInfo info); 9 | 10 | void onTransactionComplete(TransactionManager tm, TransactionInfo info); 11 | 12 | void onTransactionCancel(TransactionManager tm, TransactionInfo info); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/playpen-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE="${BASH_SOURCE[0]}" 3 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 4 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 5 | SOURCE="$(readlink "$SOURCE")" 6 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 7 | done 8 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 9 | java -Dlog4j.configurationFile=logging-cli.xml -jar "${DIR}/${project.build.finalName}.jar" cli $@ -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/TransactionInfo.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking; 2 | 3 | import io.playpen.core.protocol.Protocol; 4 | import lombok.Data; 5 | 6 | import java.util.concurrent.ScheduledFuture; 7 | 8 | @Data 9 | public class TransactionInfo { 10 | private String id; 11 | 12 | private String target = null; 13 | 14 | private Protocol.Transaction transaction = null; 15 | 16 | private ITransactionListener handler = null; 17 | 18 | private ScheduledFuture cancelTask = null; 19 | 20 | private boolean done = false; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/playpen-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE="${BASH_SOURCE[0]}" 3 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 4 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 5 | SOURCE="$(readlink "$SOURCE")" 6 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 7 | done 8 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 9 | java -Dlog4j.configurationFile=logging-local.xml -Xmx512M -Xms512M -jar "${DIR}/${project.build.finalName}.jar" local $@ -------------------------------------------------------------------------------- /src/main/resources/playpen-network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE="${BASH_SOURCE[0]}" 3 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 4 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 5 | SOURCE="$(readlink "$SOURCE")" 6 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 7 | done 8 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 9 | java -Dlog4j.configurationFile=logging-network.xml -Xmx512M -Xms512M -jar "${DIR}/${project.build.finalName}.jar" network $@ -------------------------------------------------------------------------------- /src/main/resources/logging-cli.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c: %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/testpackage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "id": "test-package", 4 | "version": "1.0", 5 | 6 | "depends": [ 7 | { 8 | "id": "test-with-parent", 9 | "version": "1.0" 10 | }, 11 | { 12 | "id": "asset-package", 13 | "version": "1.0" 14 | } 15 | ], 16 | 17 | "resources": { 18 | "memory": 1024, 19 | "cpu": 1 20 | }, 21 | 22 | "requires": ["java-8"] 23 | }, 24 | 25 | "strings": { 26 | "foo": "bar" 27 | }, 28 | 29 | "provision": [ 30 | {"id": "expand"}, 31 | { 32 | "id": "string-template", 33 | "files": [ 34 | "test.txt" 35 | ] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse stuff 2 | /.classpath 3 | /.project 4 | /.settings 5 | 6 | # netbeans 7 | /nbproject 8 | /nbactions.xml 9 | /nb-configuration.xml 10 | 11 | # we use maven! 12 | /build.xml 13 | 14 | # maven 15 | /target 16 | /dependency-reduced-pom.xml 17 | */target 18 | */dependency-reduced-pom.xml 19 | 20 | # vim 21 | .*.sw[a-p] 22 | 23 | # various other potential build files 24 | /build 25 | /bin 26 | /dist 27 | /manifest.mf 28 | 29 | # Mac filesystem dust 30 | /.DS_Store 31 | 32 | # intellij 33 | *.iml 34 | *.ipr 35 | *.iws 36 | .idea/ 37 | 38 | /staging 39 | /live 40 | 41 | # Protobuf package 42 | /src/main/java/io/playpen/core/protocol -------------------------------------------------------------------------------- /src/main/proto/p3.proto: -------------------------------------------------------------------------------- 1 | package io.playpen.core.protocol; 2 | 3 | option java_outer_classname = "P3"; 4 | 5 | message P3Meta { 6 | required string id = 1; 7 | required string version = 2; 8 | optional bool promoted = 3 [default = false]; // only used in client communication at the moment 9 | } 10 | 11 | message PackageData { 12 | required P3Meta meta = 1; 13 | required string checksum = 3; 14 | required bytes data = 2; 15 | } 16 | 17 | message SplitPackageData { 18 | required P3Meta meta = 1; 19 | optional string checksum = 2; 20 | required bool endOfFile = 3; 21 | optional uint32 chunkCount = 4; 22 | optional uint32 chunkId = 5; 23 | optional bytes data = 6; 24 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/AbstractTransactionListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking; 2 | 3 | import io.playpen.core.protocol.Protocol; 4 | 5 | public abstract class AbstractTransactionListener implements ITransactionListener { 6 | @Override 7 | public void onTransactionReceive(TransactionManager tm, TransactionInfo info, Protocol.Transaction message) { 8 | } 9 | 10 | @Override 11 | public void onTransactionSend(TransactionManager tm, TransactionInfo info) { 12 | } 13 | 14 | @Override 15 | public void onTransactionComplete(TransactionManager tm, TransactionInfo info) { 16 | } 17 | 18 | @Override 19 | public void onTransactionCancel(TransactionManager tm, TransactionInfo info) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/proto/protocol.proto: -------------------------------------------------------------------------------- 1 | package io.playpen.core.protocol; 2 | 3 | option java_outer_classname = "Protocol"; 4 | 5 | import "command.proto"; 6 | 7 | message AuthenticatedMessage { 8 | required string uuid = 1; 9 | required uint32 version = 4; // VERSION IS SET IN io.playpen.core.Bootstrap 10 | required string hash = 2; 11 | required bytes payload = 3; 12 | } 13 | 14 | message Transaction { 15 | enum Mode { 16 | CREATE = 1; // creates a transaction 17 | CONTINUE = 2; // uses an existing transaction 18 | COMPLETE = 3; // ends an existing transaction 19 | SINGLE = 4; // creates and ends a transaction 20 | } 21 | 22 | required string id = 1; 23 | required Mode mode = 2; 24 | required BaseCommand payload = 3; 25 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/local/Server.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.local; 2 | 3 | import io.playpen.core.p3.P3Package; 4 | import io.playpen.core.utils.process.XProcess; 5 | import lombok.Data; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | @Data 11 | public class Server { 12 | private P3Package p3; 13 | 14 | private String uuid; 15 | 16 | private String name; 17 | 18 | private Map properties = new ConcurrentHashMap<>(); 19 | 20 | private String localPath; 21 | 22 | private XProcess process; 23 | 24 | private boolean freezeOnShutdown = false; 25 | 26 | public String getSafeName() { 27 | if (name != null) 28 | return name; 29 | return uuid; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/logging-p3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-5p %c{1}: %m%n 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c: %m%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/logging-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-5p %c{1}: %m%n 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c: %m%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/logging-network.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-5p %c{1}: %m%n 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c: %m%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/proto/coordinator.proto: -------------------------------------------------------------------------------- 1 | package io.playpen.core.protocol; 2 | 3 | option java_outer_classname = "Coordinator"; 4 | 5 | import "p3.proto"; 6 | 7 | message Resource { 8 | required string name = 1; 9 | required sint32 value = 2; 10 | } 11 | 12 | message Property { 13 | required string name = 1; 14 | required string value = 2; 15 | } 16 | 17 | message Server { 18 | required P3Meta p3 = 1; 19 | required string uuid = 2; 20 | optional string name = 3; 21 | optional bool active = 5 [default = true]; 22 | repeated Property properties = 4; 23 | } 24 | 25 | message LocalCoordinator { 26 | required string uuid = 1; 27 | optional string name = 2; 28 | required bool enabled = 3; 29 | 30 | repeated Resource resources = 4; 31 | repeated string attributes = 5; 32 | 33 | repeated Server servers = 6; 34 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/Server.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network; 2 | 3 | import io.playpen.core.p3.P3Package; 4 | import lombok.Data; 5 | 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | @Data 11 | public class Server { 12 | private P3Package p3; 13 | 14 | private String uuid; 15 | 16 | private String name; 17 | 18 | private Map properties = new ConcurrentHashMap<>(); 19 | 20 | private boolean active = false; 21 | 22 | private LocalCoordinator coordinator = null; 23 | 24 | public String getName() { 25 | if(name == null) { 26 | return uuid; 27 | } 28 | 29 | return name; 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return Objects.hash(uuid, coordinator == null ? "0" : coordinator.getUuid()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/authenticator/DeprovisionAuthenticator.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network.authenticator; 2 | 3 | import io.playpen.core.coordinator.network.LocalCoordinator; 4 | import io.playpen.core.networking.TransactionInfo; 5 | import io.playpen.core.protocol.Commands; 6 | 7 | public class DeprovisionAuthenticator implements IAuthenticator { 8 | 9 | @Override 10 | public String getName() { 11 | return "deprovision"; 12 | } 13 | 14 | @Override 15 | public boolean hasAccess(Commands.BaseCommand command, TransactionInfo info, LocalCoordinator from) { 16 | switch (command.getType()) 17 | { 18 | case SYNC: 19 | case C_GET_COORDINATOR_LIST: 20 | case C_DEPROVISION: 21 | case C_FREEZE_SERVER: 22 | case C_ACK: 23 | case C_REQUEST_PACKAGE_LIST: 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/ExpandStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.p3.IPackageStep; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageContext; 6 | import lombok.extern.log4j.Log4j2; 7 | import org.json.JSONObject; 8 | import org.zeroturnaround.zip.ZipException; 9 | import org.zeroturnaround.zip.ZipUtil; 10 | 11 | import java.io.File; 12 | 13 | @Log4j2 14 | public class ExpandStep implements IPackageStep { 15 | @Override 16 | public String getStepId() { 17 | return "expand"; 18 | } 19 | 20 | @Override 21 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 22 | log.info("Expanding package to " + ctx.getDestination().getPath()); 23 | try { 24 | ZipUtil.unpack(new File(p3.getLocalPath()), ctx.getDestination()); 25 | } 26 | catch(ZipException e) { 27 | log.error("Unable to expand package", e); 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/process/ShutdownProcessListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils.process; 2 | 3 | import io.playpen.core.coordinator.local.Local; 4 | import io.playpen.core.coordinator.local.Server; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | @Log4j2 9 | @AllArgsConstructor 10 | public class ShutdownProcessListener implements IProcessListener { 11 | private final Server server; 12 | 13 | @Override 14 | public void onProcessAttach(XProcess proc) { 15 | 16 | } 17 | 18 | @Override 19 | public void onProcessDetach(XProcess proc) { 20 | 21 | } 22 | 23 | @Override 24 | public void onProcessOutput(XProcess proc, String out) { 25 | 26 | } 27 | 28 | @Override 29 | public void onProcessInput(XProcess proc, String in) { 30 | 31 | } 32 | 33 | @Override 34 | public void onProcessEnd(XProcess proc) { 35 | log.info("Server process for " + server.getUuid() + " has ended, informing network controller of server shutdown"); 36 | Local.get().notifyServerShutdown(server.getUuid()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sam Bloomberg 2 | Copyright (c) 2016 PlayPen Contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/AbstractPlugin.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.io.File; 6 | 7 | public abstract class AbstractPlugin implements IPlugin { 8 | private PluginSchema schema = null; 9 | 10 | private JSONObject config = null; 11 | 12 | private File pluginDir = null; 13 | 14 | @Override 15 | public void setSchema(PluginSchema schema) { 16 | this.schema = schema; 17 | } 18 | 19 | @Override 20 | public PluginSchema getSchema() { 21 | return schema; 22 | } 23 | 24 | @Override 25 | public void setPluginDir(File pluginDir) { 26 | this.pluginDir = pluginDir; 27 | } 28 | 29 | @Override 30 | public File getPluginDir() { 31 | return pluginDir; 32 | } 33 | 34 | @Override 35 | public void setConfig(JSONObject config) { 36 | this.config = config; 37 | } 38 | 39 | @Override 40 | public JSONObject getConfig() { 41 | return config; 42 | } 43 | 44 | @Override 45 | public boolean onStart() { 46 | return true; 47 | } 48 | 49 | @Override 50 | public void onStop() { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/local/ConsoleMessageListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.local; 2 | 3 | import io.playpen.core.utils.process.IProcessListener; 4 | import io.playpen.core.utils.process.XProcess; 5 | 6 | public class ConsoleMessageListener implements IProcessListener { 7 | private String consoleId = null; 8 | 9 | private XProcess process = null; 10 | 11 | public ConsoleMessageListener(String id) { 12 | consoleId = id; 13 | } 14 | 15 | public void remove() { 16 | if(process != null) 17 | process.removeListener(this); 18 | } 19 | 20 | @Override 21 | public void onProcessAttach(XProcess proc) { 22 | process = proc; 23 | } 24 | 25 | @Override 26 | public void onProcessDetach(XProcess proc) { 27 | } 28 | 29 | @Override 30 | public void onProcessOutput(XProcess proc, String out) { 31 | Local.get().sendConsoleMessage(consoleId, out); 32 | } 33 | 34 | @Override 35 | public void onProcessInput(XProcess proc, String in) { 36 | } 37 | 38 | @Override 39 | public void onProcessEnd(XProcess proc) { 40 | Local.get().detachConsole(consoleId); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/netty/AuthenticatedMessageInitializer.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking.netty; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.socket.nio.NioSocketChannel; 5 | import io.netty.handler.codec.protobuf.ProtobufDecoder; 6 | import io.netty.handler.codec.protobuf.ProtobufEncoder; 7 | import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; 8 | import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; 9 | import io.playpen.core.protocol.Protocol; 10 | 11 | public class AuthenticatedMessageInitializer extends ChannelInitializer { 12 | @Override 13 | protected void initChannel(NioSocketChannel channel) throws Exception { 14 | channel.pipeline().addLast("lengthDecoder", new ProtobufVarint32FrameDecoder()); 15 | channel.pipeline().addLast("protobufDecoder", new ProtobufDecoder(Protocol.AuthenticatedMessage.getDefaultInstance())); 16 | 17 | channel.pipeline().addLast("lengthPrepender", new ProtobufVarint32LengthFieldPrepender()); 18 | channel.pipeline().addLast("protobufEncoder", new ProtobufEncoder()); 19 | 20 | channel.pipeline().addLast(new AuthenticatedMessageHandler()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/JarUtils.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils; 2 | 3 | import java.io.FileOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.net.URISyntaxException; 8 | 9 | public final class JarUtils { 10 | 11 | public static void exportResource(Class fromClass, String resourceName, String exportPath) throws IOException, URISyntaxException { 12 | InputStream in = null; 13 | OutputStream out = null; 14 | try { 15 | in = fromClass.getResourceAsStream(resourceName); 16 | if(in == null) { 17 | throw new IOException("Cannot get resource \"" + resourceName + "\" from jar."); 18 | } 19 | 20 | int readBytes; 21 | byte[] buffer = new byte[4096]; 22 | out = new FileOutputStream(exportPath); 23 | while((readBytes = in.read(buffer)) > 0) { 24 | out.write(buffer, 0, readBytes); 25 | } 26 | } 27 | finally { 28 | if(in != null) 29 | in.close(); 30 | 31 | if(out != null) 32 | out.close(); 33 | } 34 | } 35 | 36 | private JarUtils() {} 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/resolver/InMemoryCacheResolver.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.resolver; 2 | 3 | import io.playpen.core.p3.IPackageResolver; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageManager; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import java.io.File; 9 | import java.util.Collection; 10 | 11 | @Log4j2 12 | public class InMemoryCacheResolver implements IPackageResolver { 13 | @Override 14 | public P3Package resolvePackage(PackageManager pm, String id, String version) { 15 | P3Package.P3PackageInfo info = new P3Package.P3PackageInfo(); 16 | info.setId(id); 17 | info.setVersion(version); 18 | 19 | P3Package p3 = pm.getPackageCache().get(info); 20 | if(p3 == null) 21 | return null; 22 | 23 | File p3File = new File(p3.getLocalPath()); 24 | if(!p3File.exists() || !p3File.isFile()) { 25 | pm.getPackageCache().remove(info); 26 | log.warn("In-memory package " + id + " at " + version + " is invalid, removing from cache"); 27 | return null; 28 | } 29 | 30 | return p3; 31 | } 32 | 33 | @Override 34 | public Collection getPackageList(PackageManager pm) { 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/ExpandAssetsStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.Bootstrap; 4 | import io.playpen.core.p3.IPackageStep; 5 | import io.playpen.core.p3.P3Package; 6 | import io.playpen.core.p3.PackageContext; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.json.JSONObject; 9 | import org.zeroturnaround.zip.ZipException; 10 | import org.zeroturnaround.zip.ZipUtil; 11 | 12 | import java.io.File; 13 | import java.nio.file.Paths; 14 | 15 | @Log4j2 16 | public class ExpandAssetsStep implements IPackageStep { 17 | @Override 18 | public String getStepId() { 19 | return "expand-assets"; 20 | } 21 | 22 | @Override 23 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 24 | File path = Paths.get(Bootstrap.getHomeDir().getPath(), "assets", p3.getId(), p3.getVersion()).toFile(); 25 | if(path.exists() && path.isDirectory()) { 26 | log.info("Not expanding asset package (already exists)"); 27 | return true; 28 | } 29 | 30 | log.info("Expanding asset package to " + path.getPath()); 31 | try { 32 | ZipUtil.unpack(new File(p3.getLocalPath()), path); 33 | } 34 | catch(ZipException e) { 35 | log.error("Unable to expand package", e); 36 | return false; 37 | } 38 | 39 | return true; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/PluginClassLoader.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import java.net.URL; 4 | import java.net.URLClassLoader; 5 | import java.util.Set; 6 | import java.util.concurrent.CopyOnWriteArraySet; 7 | 8 | public class PluginClassLoader extends URLClassLoader { 9 | private static final Set allLoaders = new CopyOnWriteArraySet<>(); 10 | 11 | static { 12 | ClassLoader.registerAsParallelCapable(); 13 | } 14 | 15 | public PluginClassLoader(URL[] urls) { 16 | super(urls); 17 | allLoaders.add(this); 18 | } 19 | 20 | @Override 21 | protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 22 | return loadClass0(name, resolve, true); 23 | } 24 | 25 | protected Class loadClass0(String name, boolean resolve, boolean inOthers) throws ClassNotFoundException { 26 | try { 27 | return super.loadClass(name, resolve); 28 | } catch (ClassNotFoundException e) { 29 | } 30 | 31 | if (inOthers) { 32 | for (PluginClassLoader loader : allLoaders) { 33 | try { 34 | return loader.loadClass0(name, resolve, false); 35 | } catch (ClassNotFoundException e) { 36 | 37 | } 38 | } 39 | } 40 | 41 | throw new ClassNotFoundException(name); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/STUtils.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils; 2 | 3 | import io.playpen.core.Bootstrap; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageContext; 6 | import org.stringtemplate.v4.ST; 7 | import org.stringtemplate.v4.misc.Aggregate; 8 | 9 | import java.nio.file.Paths; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class STUtils { 14 | 15 | public static void buildSTProperties(P3Package p3, PackageContext ctx, ST template) { 16 | template.add("package-id", p3.getId()); 17 | template.add("package-version", p3.getVersion()); 18 | template.add("resources", ctx.getResources()); 19 | template.add("asset_path", Paths.get(Bootstrap.getHomeDir().getPath(), "assets").toFile().getAbsolutePath()); 20 | template.add("server_path", ctx.getDestination().getAbsolutePath()); 21 | 22 | for(Map.Entry entry : ctx.getProperties().entrySet()) { 23 | template.add(entry.getKey(), entry.getValue()); 24 | } 25 | 26 | Map versions = new HashMap<>(); 27 | for (P3Package p3Package : ctx.getDependencyChain()) { 28 | versions.put(p3Package.getId(), p3Package.getVersion()); 29 | } 30 | versions.put(p3.getId(), p3.getVersion()); 31 | 32 | template.add("package_versions", versions); 33 | } 34 | 35 | private STUtils() {} 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/resolver/PromotedResolver.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.resolver; 2 | 3 | import io.playpen.core.p3.IPackageResolver; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageManager; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import java.util.Collection; 9 | 10 | @Log4j2 11 | public class PromotedResolver implements IPackageResolver { 12 | @Override 13 | public P3Package resolvePackage(PackageManager pm, String id, String version) { 14 | if(!version.equals("promoted")) 15 | return null; 16 | 17 | log.info("Attempting promoted resolution of " + id); 18 | String realVersion = pm.getPromotedVersion(id); 19 | if(realVersion == null) { 20 | log.error("No promoted package for " + id + ", we're gunna have a bad time"); 21 | return null; 22 | } 23 | 24 | if(realVersion.equals("promoted")) { 25 | log.error("'promoted' cannot be the promoted version of a package!"); 26 | return null; 27 | } 28 | 29 | P3Package p3 = pm.resolve(id, realVersion); 30 | if(p3 == null) { 31 | log.error("Promoted package " + id + " at " + realVersion + " could not be resolved "); 32 | return null; 33 | } 34 | 35 | return p3; 36 | } 37 | 38 | @Override 39 | public Collection getPackageList(PackageManager pm) { 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/EventManager.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | 5 | import java.util.List; 6 | import java.util.concurrent.CopyOnWriteArrayList; 7 | import java.util.function.Consumer; 8 | 9 | @Log4j2 10 | public class EventManager> { 11 | private List listeners = new CopyOnWriteArrayList<>(); 12 | 13 | public boolean registerListener(T listener) { 14 | if(listeners.contains(listener)) 15 | return false; 16 | 17 | listeners.add(listener); 18 | listener.onListenerRegistered(this); 19 | return true; 20 | } 21 | 22 | public boolean removeListener(T listener) { 23 | if(listeners.remove(listener)) { 24 | listener.onListenerRemoved(this); 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * @param call 33 | * @return True if no errors occured, false if there were errors. 34 | */ 35 | public boolean callEvent(Consumer call) { 36 | boolean result = true; 37 | for(T listener : listeners) { 38 | try { 39 | call.accept(listener); 40 | } 41 | catch(Exception e) { 42 | log.error("Unable to pass event to listener " + (listener == null ? "null" : listener.getClass()), e); 43 | result = false; 44 | } 45 | } 46 | 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/netty/AuthenticatedMessageHandler.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.SimpleChannelInboundHandler; 5 | import io.playpen.core.Bootstrap; 6 | import io.playpen.core.coordinator.PlayPen; 7 | import io.playpen.core.protocol.Protocol; 8 | import lombok.extern.log4j.Log4j2; 9 | 10 | @Log4j2 11 | public class AuthenticatedMessageHandler extends SimpleChannelInboundHandler { 12 | @Override 13 | protected void channelRead0(ChannelHandlerContext ctx, Protocol.AuthenticatedMessage msg) throws Exception { 14 | if (msg.getVersion() != Bootstrap.getProtocolVersion()) { 15 | log.error("Protocol version mismatch! Expected " + Bootstrap.getProtocolVersion() + ", got " 16 | + msg.getVersion()); 17 | log.error("Disconnecting due to version mismatch."); 18 | ctx.channel().close(); 19 | return; 20 | } 21 | 22 | if(!PlayPen.get().receive(msg, ctx.channel())) { 23 | log.error("Message failed"); 24 | return; 25 | } 26 | } 27 | 28 | @Override 29 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 30 | log.error("Caught an exception while listening to a channel (closing connection)", cause); 31 | ctx.channel().close(); // closing the connection is fine, since a sane coordinator will just reconnect in a bit 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/PlayPen.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator; 2 | 3 | 4 | import io.netty.channel.Channel; 5 | import io.playpen.core.networking.TransactionInfo; 6 | import io.playpen.core.p3.PackageManager; 7 | import io.playpen.core.plugin.PluginManager; 8 | import io.playpen.core.protocol.Commands; 9 | import io.playpen.core.protocol.Protocol; 10 | 11 | import java.util.UUID; 12 | import java.util.concurrent.ScheduledExecutorService; 13 | 14 | public abstract class PlayPen { 15 | private static PlayPen instance = null; 16 | 17 | public static PlayPen get() { 18 | return instance; 19 | } 20 | 21 | // do not call except from Bootstrap 22 | public static void reset() { 23 | instance = null; 24 | } 25 | 26 | public PlayPen() { 27 | instance = this; 28 | } 29 | 30 | public abstract String getServerId(); 31 | 32 | public abstract CoordinatorMode getCoordinatorMode(); 33 | 34 | public abstract PackageManager getPackageManager(); 35 | 36 | public abstract PluginManager getPluginManager(); 37 | 38 | public abstract ScheduledExecutorService getScheduler(); 39 | 40 | public String generateId() { 41 | return getServerId() + "-" + UUID.randomUUID().toString(); 42 | } 43 | 44 | public abstract boolean send(Protocol.Transaction message, String target); 45 | 46 | public abstract boolean receive(Protocol.AuthenticatedMessage auth, Channel from); 47 | 48 | public abstract boolean process(Commands.BaseCommand command, TransactionInfo info, String from); 49 | 50 | public abstract void onVMShutdown(); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/AbortableCountDownLatch.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils; 2 | 3 | import java.util.concurrent.CountDownLatch; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | // credit to https://stackoverflow.com/a/10455821/646180 7 | public class AbortableCountDownLatch extends CountDownLatch { 8 | protected boolean aborted = false; 9 | 10 | public AbortableCountDownLatch(int count) { 11 | super(count); 12 | } 13 | 14 | 15 | /** 16 | * Unblocks all threads waiting on this latch and cause them to receive an 17 | * AbortedException. If the latch has already counted all the way down, 18 | * this method does nothing. 19 | */ 20 | public void abort() { 21 | if( getCount()==0 ) 22 | return; 23 | 24 | this.aborted = true; 25 | while(getCount()>0) 26 | countDown(); 27 | } 28 | 29 | 30 | @Override 31 | public boolean await(long timeout, TimeUnit unit) throws InterruptedException { 32 | final boolean rtrn = super.await(timeout,unit); 33 | if (aborted) 34 | throw new AbortedException(); 35 | return rtrn; 36 | } 37 | 38 | @Override 39 | public void await() throws InterruptedException { 40 | super.await(); 41 | if (aborted) 42 | throw new AbortedException(); 43 | } 44 | 45 | 46 | public static class AbortedException extends InterruptedException { 47 | public AbortedException() { 48 | } 49 | 50 | public AbortedException(String detailMessage) { 51 | super(detailMessage); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/Initialization.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core; 2 | 3 | import io.playpen.core.coordinator.network.Network; 4 | import io.playpen.core.coordinator.network.authenticator.DeprovisionAuthenticator; 5 | import io.playpen.core.p3.PackageManager; 6 | import io.playpen.core.p3.resolver.InMemoryCacheResolver; 7 | import io.playpen.core.p3.resolver.LocalRepositoryResolver; 8 | import io.playpen.core.p3.resolver.PromotedResolver; 9 | import io.playpen.core.p3.step.*; 10 | 11 | import java.nio.file.Paths; 12 | 13 | public class Initialization { 14 | 15 | public static void packageManager(PackageManager pm) { 16 | // Promoted, should always come first 17 | pm.addPackageResolver(new PromotedResolver()); 18 | 19 | // In-memory cache 20 | pm.addPackageResolver(new InMemoryCacheResolver()); 21 | 22 | // Main package repository 23 | pm.addPackageResolver(new LocalRepositoryResolver(Paths.get(Bootstrap.getHomeDir().getPath(), "packages").toFile())); 24 | 25 | // Package cache 26 | pm.addPackageResolver(new LocalRepositoryResolver(Paths.get(Bootstrap.getHomeDir().getPath(), "cache", "packages").toFile())); 27 | 28 | pm.addPackageStep(new ExpandStep()); 29 | pm.addPackageStep(new StringTemplateStep()); 30 | pm.addPackageStep(new ExecuteStep()); 31 | pm.addPackageStep(new PipeStep()); 32 | pm.addPackageStep(new ExpandAssetsStep()); 33 | pm.addPackageStep(new CopyStep()); 34 | } 35 | 36 | public static void networkCoordinator(Network net) { 37 | net.addAuthenticator(new DeprovisionAuthenticator()); 38 | } 39 | 40 | private Initialization() {} 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/CopyStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.p3.IPackageStep; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageContext; 6 | import io.playpen.core.utils.STUtils; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.apache.commons.io.FileUtils; 9 | import org.json.JSONObject; 10 | import org.stringtemplate.v4.ST; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | 15 | @Log4j2 16 | public class CopyStep implements IPackageStep { 17 | @Override 18 | public String getStepId() { 19 | return "copy-directory"; 20 | } 21 | 22 | @Override 23 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 24 | String from = config.optString("from"); 25 | if(from == null) { 26 | log.error("'from' is not defined as a string in config"); 27 | return false; 28 | } 29 | 30 | String to = config.optString("to"); 31 | if (to == null) { 32 | log.error("'to' is not defined as a string in config"); 33 | return false; 34 | } 35 | 36 | ST templateFrom = new ST(from); 37 | STUtils.buildSTProperties(p3, ctx, templateFrom); 38 | 39 | ST templateTo = new ST(to); 40 | STUtils.buildSTProperties(p3, ctx, templateTo); 41 | 42 | from = templateFrom.render(); 43 | to = templateTo.render(); 44 | 45 | log.info("Copying from " + from + " to " + to); 46 | try { 47 | FileUtils.copyDirectory(new File(from), new File(to)); 48 | } catch (IOException e) { 49 | log.error("Unable to copy directory", e); 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/PipeStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.coordinator.local.Server; 4 | import io.playpen.core.p3.IPackageStep; 5 | import io.playpen.core.p3.P3Package; 6 | import io.playpen.core.p3.PackageContext; 7 | import io.playpen.core.utils.STUtils; 8 | import lombok.extern.log4j.Log4j2; 9 | import org.json.JSONObject; 10 | import org.stringtemplate.v4.ST; 11 | 12 | @Log4j2 13 | public class PipeStep implements IPackageStep { 14 | @Override 15 | public String getStepId() { 16 | return "pipe"; 17 | } 18 | 19 | @Override 20 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 21 | if(!(ctx.getUser() instanceof Server)) { 22 | log.error("Must be executed on a local coordinator"); 23 | return false; 24 | } 25 | 26 | Server server = (Server)ctx.getUser(); 27 | if(server.getProcess() == null) { 28 | log.error("No process found to pipe to"); 29 | return false; 30 | } 31 | 32 | if(!server.getProcess().isRunning()) { 33 | log.warn("Server process is not running, continuing"); 34 | return true; 35 | } 36 | 37 | String str = config.optString("string"); 38 | if(str == null) { 39 | log.error("'string' is not defined as a string in config"); 40 | return false; 41 | } 42 | 43 | if(config.has("template")) { 44 | boolean useTemplate = config.getBoolean("template"); 45 | if(useTemplate) { 46 | log.info("Running ST on string"); 47 | ST template = new ST(str); 48 | 49 | STUtils.buildSTProperties(p3, ctx, template); 50 | 51 | str = template.render(); 52 | } 53 | } 54 | 55 | log.info("Piping string to process"); 56 | server.getProcess().sendInput(str + '\n'); 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/process/ProcessBuffer.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils.process; 2 | 3 | import java.nio.CharBuffer; 4 | import java.util.concurrent.locks.Lock; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | /** 8 | * ProcessBuffer implements a way to turn {@code CharBuffers} from NuProcess into by-line output. 9 | * 10 | * This class is thread-safe (the buffer is protected by a lock). 11 | */ 12 | public abstract class ProcessBuffer { 13 | private final StringBuilder rumpBuffer = new StringBuilder(128); 14 | private volatile boolean rumpBufferHasContents = false; 15 | private final Lock lock = new ReentrantLock(); 16 | 17 | public void append(CharBuffer buffer) { 18 | // Okay, I'll admit it: this thing is literally a Rube Goldberg machine. But it works well enough! 19 | StringBuilder found = new StringBuilder(); 20 | 21 | if (rumpBufferHasContents) { 22 | lock.lock(); 23 | try { 24 | found.append(rumpBuffer); 25 | rumpBuffer.delete(0, rumpBuffer.length()); 26 | rumpBufferHasContents = false; 27 | } finally { 28 | lock.unlock(); 29 | } 30 | } 31 | 32 | for (int i = 0; i < buffer.remaining(); i++) { 33 | char c = buffer.get(i); 34 | if (c == '\r') { 35 | // When it looks like a hammer, there must be a sickle too. 36 | continue; 37 | } 38 | if (c == '\n') { 39 | if (found.length() == 0) 40 | continue; 41 | onOutput(found.toString()); 42 | found.delete(0, found.length()); 43 | } else { 44 | found.append(c); 45 | } 46 | } 47 | 48 | // Consume the buffer. 49 | buffer.position(buffer.remaining()); 50 | 51 | if (found.length() != 0) { 52 | lock.lock(); 53 | try { 54 | this.rumpBuffer.append(found); 55 | rumpBufferHasContents = true; 56 | } finally { 57 | lock.unlock(); 58 | } 59 | } 60 | } 61 | 62 | protected abstract void onOutput(String output); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/INetworkListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network; 2 | 3 | import io.playpen.core.plugin.IEventListener; 4 | import io.playpen.core.plugin.IPlugin; 5 | 6 | public interface INetworkListener extends IEventListener { 7 | /** 8 | * Called right after netty startup. 9 | */ 10 | void onNetworkStartup(); 11 | 12 | /** 13 | * Called right before netty shutdown. 14 | */ 15 | void onNetworkShutdown(); 16 | 17 | /** 18 | * Called after a coordinator has been created with a new keypair. 19 | * @param coordinator 20 | */ 21 | void onCoordinatorCreated(LocalCoordinator coordinator); 22 | 23 | /** 24 | * Called after a coordinator has synced. 25 | * @param coordinator 26 | */ 27 | void onCoordinatorSync(LocalCoordinator coordinator); 28 | 29 | /** 30 | * Called when the network requests provisioning of a local coordinator. 31 | * @param coordinator 32 | * @param server 33 | */ 34 | void onRequestProvision(LocalCoordinator coordinator, Server server); 35 | 36 | /** 37 | * Called when a local coordinator responds to a provision request. 38 | * @param coordinator 39 | * @param server 40 | * @param ok 41 | */ 42 | void onProvisionResponse(LocalCoordinator coordinator, Server server, boolean ok); 43 | 44 | /** 45 | * Called when the network requests deprovisioning of a server. 46 | * @param coordinator 47 | * @param server 48 | */ 49 | void onRequestDeprovision(LocalCoordinator coordinator, Server server); 50 | 51 | /** 52 | * Called when a local coordinator notifies the network of a server shutdown. 53 | * @param coordinator 54 | * @param server 55 | */ 56 | void onServerShutdown(LocalCoordinator coordinator, Server server); 57 | 58 | /** 59 | * Called when the network requests shutdown of a local coordinator. 60 | * @param coordinator 61 | */ 62 | void onRequestShutdown(LocalCoordinator coordinator); 63 | 64 | /** 65 | * Called when a plugin broadcasts a message to other plugins 66 | * @param id 67 | * @param args 68 | */ 69 | void onPluginMessage(IPlugin plugin, String id, Object... args); 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/process/FileProcessListener.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils.process; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.StandardOpenOption; 8 | import java.text.DateFormat; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | 14 | public class FileProcessListener implements IProcessListener { 15 | private BufferedWriter writer = null; 16 | private final ExecutorService service; 17 | 18 | public FileProcessListener(File file) throws IOException { 19 | DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // quick, will probably change later 20 | Date date = new Date(); 21 | 22 | writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); 23 | service = Executors.newSingleThreadExecutor(); 24 | writer.write("-- FileProcessListener SESSION STARTED " + dateFormat.format(date) + "\r\n"); 25 | writer.flush(); 26 | } 27 | 28 | @Override 29 | public void onProcessAttach(XProcess proc) { 30 | } 31 | 32 | @Override 33 | public void onProcessDetach(XProcess proc) { 34 | } 35 | 36 | @Override 37 | public void onProcessOutput(XProcess proc, String out) { 38 | if (!service.isShutdown()) { 39 | service.execute(() -> { 40 | try { 41 | writer.write(out); 42 | writer.write(System.lineSeparator()); 43 | writer.flush(); 44 | } catch (IOException e) { 45 | } 46 | }); 47 | } 48 | } 49 | 50 | @Override 51 | public void onProcessInput(XProcess proc, String in) { 52 | if (!service.isShutdown()) { 53 | service.execute(() -> { 54 | try { 55 | writer.write(in); 56 | writer.write(System.lineSeparator()); 57 | writer.flush(); 58 | } catch (IOException e) { 59 | } 60 | }); 61 | } 62 | } 63 | 64 | @Override 65 | public void onProcessEnd(XProcess proc) { 66 | service.shutdownNow(); 67 | try { 68 | writer.write("-- FileProcessListener SESSION ENDED\r\n"); 69 | writer.flush(); 70 | } 71 | catch(IOException e) {} 72 | finally { 73 | try { 74 | writer.close(); 75 | } 76 | catch(IOException e) {} 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/StringTemplateStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.p3.IPackageStep; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageContext; 6 | import io.playpen.core.utils.STUtils; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | import org.stringtemplate.v4.AutoIndentWriter; 11 | import org.stringtemplate.v4.ST; 12 | 13 | import java.io.BufferedWriter; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.nio.file.StandardOpenOption; 19 | import java.util.Locale; 20 | 21 | @Log4j2 22 | public class StringTemplateStep implements IPackageStep { 23 | @Override 24 | public String getStepId() { 25 | return "string-template"; 26 | } 27 | 28 | @Override 29 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 30 | JSONArray jsonFiles = config.optJSONArray("files"); 31 | if(jsonFiles == null) { 32 | log.error("'files' not defined as an array"); 33 | return false; 34 | } 35 | 36 | File[] files = new File[jsonFiles.length()]; 37 | for(int i = 0; i < jsonFiles.length(); ++i) { 38 | String fileName = jsonFiles.optString(i); 39 | if(fileName == null) { 40 | log.error("Unable to read files entry #" + i); 41 | return false; 42 | } 43 | 44 | File file = Paths.get(ctx.getDestination().getPath(), fileName).toFile(); 45 | if(!file.exists()) { 46 | log.error("File does not exist: " + file.getPath()); 47 | return false; 48 | } 49 | 50 | files[i] = file; 51 | } 52 | 53 | for(File file : files) { 54 | String fileContents; 55 | try { 56 | fileContents = new String(Files.readAllBytes(file.toPath())); 57 | } 58 | catch(IOException e) { 59 | log.error("Unable to read file " + file.getPath(), e); 60 | return false; 61 | } 62 | 63 | log.info("Rendering " + file.getPath()); 64 | 65 | ST template = new ST(fileContents); 66 | 67 | STUtils.buildSTProperties(p3, ctx, template); 68 | 69 | try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.TRUNCATE_EXISTING)) { 70 | template.write(new AutoIndentWriter(writer), Locale.US); 71 | } 72 | catch(IOException e) { 73 | log.error("Unable to write file " + file.getPath(), e); 74 | return false; 75 | } 76 | } 77 | 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/io/playpen/core/ProcessBufferTest.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.Lists; 5 | import io.playpen.core.utils.process.ProcessBuffer; 6 | import io.playpen.core.utils.process.ProcessBuffer; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | 10 | import java.nio.CharBuffer; 11 | import java.util.List; 12 | 13 | public class ProcessBufferTest { 14 | private static final String LINE_SEPERATOR = System.lineSeparator(); 15 | 16 | private static String fullBuffer(List strings) { 17 | return String.join(LINE_SEPERATOR, strings) + LINE_SEPERATOR; 18 | } 19 | 20 | @Test 21 | public void verifyFunctionality() { 22 | TestBuffer buffer = new TestBuffer(Lists.newArrayList("Test")); 23 | buffer.append(CharBuffer.wrap(fullBuffer(ImmutableList.of("Test")))); 24 | 25 | sanityCheck(buffer); 26 | } 27 | 28 | @Test 29 | public void verifyMultipleLines() { 30 | List strings = Lists.newArrayList("Test", "Testing2", "Potato"); 31 | TestBuffer buffer = new TestBuffer(strings); 32 | buffer.append(CharBuffer.wrap(fullBuffer(strings))); 33 | 34 | sanityCheck(buffer); 35 | } 36 | 37 | @Test 38 | public void verifyIncompleteLine() { 39 | List strings = Lists.newArrayList("Test", "Testing2"); 40 | TestBuffer buffer = new TestBuffer(strings); 41 | buffer.append(CharBuffer.wrap(fullBuffer(strings) + "Potato")); 42 | 43 | sanityCheck(buffer); 44 | } 45 | 46 | @Test 47 | public void verifyLaterCompletedLine() { 48 | List strings = Lists.newArrayList("Test", "Testing2", "Potato"); 49 | TestBuffer buffer = new TestBuffer(strings); 50 | buffer.append(CharBuffer.wrap(String.join(LINE_SEPERATOR, strings))); 51 | buffer.append(CharBuffer.wrap(LINE_SEPERATOR)); 52 | sanityCheck(buffer); 53 | } 54 | 55 | private static void sanityCheck(TestBuffer buffer) { 56 | if (!buffer.expected.isEmpty()) { 57 | int hasRead = buffer.origSize - buffer.expected.size(); 58 | Assert.fail("Didn't read enough data (read " + hasRead + " lines, wanted " + buffer.origSize + " lines)"); 59 | } 60 | } 61 | 62 | private class TestBuffer extends ProcessBuffer { 63 | private List expected; 64 | private int origSize; 65 | 66 | private TestBuffer(List expected) { 67 | this.expected = expected; 68 | origSize = expected.size(); 69 | } 70 | 71 | @Override 72 | protected void onOutput(String output) { 73 | if (expected.isEmpty()) { 74 | // We are reading too much. 75 | Assert.fail("Read too much data (wanted " + origSize + " elements)"); 76 | return; 77 | } 78 | Assert.assertEquals(expected.remove(0), output); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/resolver/LocalRepositoryResolver.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.resolver; 2 | 3 | import io.playpen.core.p3.IPackageResolver; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageException; 6 | import io.playpen.core.p3.PackageManager; 7 | import lombok.extern.log4j.Log4j2; 8 | 9 | import java.io.File; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | 15 | @Log4j2 16 | public class LocalRepositoryResolver implements IPackageResolver { 17 | private File localRepoDir = null; 18 | 19 | public LocalRepositoryResolver(File dir) { 20 | localRepoDir = dir; 21 | } 22 | 23 | @Override 24 | public P3Package resolvePackage(PackageManager pm, String id, String version) { 25 | if(!localRepoDir.exists() || !localRepoDir.isDirectory()) { 26 | log.error("Package repository at " + localRepoDir.getPath() + " doesn't exist!"); 27 | return null; 28 | } 29 | 30 | File[] packageFiles = localRepoDir.listFiles((dir, name) -> { 31 | return name.endsWith(".p3"); 32 | }); 33 | 34 | for(File p3File : packageFiles) { 35 | if(!p3File.isFile()) 36 | continue; 37 | 38 | P3Package p3 = null; 39 | try { 40 | p3 = pm.readPackage(p3File); 41 | } 42 | catch(PackageException e) { 43 | log.warn("Unable to read file " + p3File.getPath()); 44 | continue; 45 | } 46 | 47 | if(id.equals(p3.getId()) && version.equals(p3.getVersion())) { 48 | log.info("Found matching package at " + p3File.getPath()); 49 | return p3; 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | @Override 57 | public Collection getPackageList(PackageManager pm) { 58 | if(!localRepoDir.exists() || !localRepoDir.isDirectory()) { 59 | log.error("Package repository at " + localRepoDir.getPath() + " doesn't exist!"); 60 | return null; 61 | } 62 | 63 | File[] packageFiles = localRepoDir.listFiles((dir, name) -> name.endsWith(".p3")); 64 | 65 | List packages = new ArrayList<>(); 66 | for(File p3File : packageFiles) { 67 | if(!p3File.isFile()) 68 | continue; 69 | 70 | P3Package p3 = null; 71 | try { 72 | p3 = pm.readPackage(p3File); 73 | } 74 | catch(PackageException e) { 75 | log.warn("Unable to read file " + p3File.getPath()); 76 | continue; 77 | } 78 | 79 | P3Package.P3PackageInfo info = new P3Package.P3PackageInfo(); 80 | info.setId(p3.getId()); 81 | info.setVersion(p3.getVersion()); 82 | packages.add(info); 83 | } 84 | 85 | return packages; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/P3Package.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | import io.playpen.core.utils.AuthUtils; 4 | import lombok.Data; 5 | import lombok.extern.log4j.Log4j2; 6 | import org.json.JSONObject; 7 | 8 | import java.io.IOException; 9 | import java.util.*; 10 | 11 | @Data 12 | @Log4j2 13 | public class P3Package { 14 | 15 | @Data 16 | public static class PackageStepConfig { 17 | IPackageStep step; 18 | JSONObject config; 19 | } 20 | 21 | @Data 22 | public static class P3PackageInfo { 23 | private String id; 24 | private String version; 25 | 26 | @Override 27 | public boolean equals(Object other) { 28 | if(other instanceof P3PackageInfo) { 29 | P3PackageInfo o = (P3PackageInfo)other; 30 | return id.equals(o.getId()) && version.equals(o.getVersion()); 31 | } 32 | 33 | return false; 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return id.hashCode() ^ version.hashCode(); 39 | } 40 | } 41 | 42 | private String localPath; 43 | 44 | private boolean resolved; 45 | 46 | // ALWAYS call calculateChecksum() before using this! 47 | private String checksum = null; 48 | 49 | private String id; 50 | 51 | private String version; 52 | 53 | private List dependencies = new ArrayList<>(); 54 | 55 | private Map resources = new HashMap<>(); 56 | 57 | private Set attributes = new HashSet<>(); 58 | 59 | private Map strings = new HashMap<>(); 60 | 61 | private List provisionSteps = new ArrayList<>(); 62 | 63 | private List executionSteps = new ArrayList<>(); 64 | 65 | private List shutdownSteps = new ArrayList<>(); 66 | 67 | /** 68 | * Checks to make sure required fields are filled. Does not check resolution status! 69 | */ 70 | public boolean validate() { 71 | if(id == null || id.isEmpty() || version == null || version.isEmpty()) 72 | return false; 73 | 74 | for(P3Package p3 : dependencies) 75 | { 76 | if(!p3.validate()) 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | public void calculateChecksum() throws PackageException { 84 | calculateChecksum(false); 85 | } 86 | 87 | public synchronized void calculateChecksum(boolean force) throws PackageException { 88 | if (!force && checksum != null) 89 | return; 90 | 91 | if (!resolved) 92 | throw new PackageException("Cannot calculate checksum on unresolved package"); 93 | 94 | if (localPath == null || localPath.isEmpty()) 95 | throw new PackageException("Cannot calculate checksum on package with invalid localPath"); 96 | 97 | log.debug("Recalculating checksum on " + id + " (" + version + ")"); 98 | 99 | try { 100 | checksum = AuthUtils.createPackageChecksum(localPath); 101 | } catch (IOException e) { 102 | throw new PackageException("Unable to calculate checksum from package file", e); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/AuthUtils.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.playpen.core.protocol.Protocol; 5 | import org.apache.commons.codec.binary.Hex; 6 | import org.jasypt.encryption.pbe.StandardPBEByteEncryptor; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.nio.ByteBuffer; 12 | import java.nio.charset.StandardCharsets; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.zip.Adler32; 19 | import java.util.zip.CRC32; 20 | import java.util.zip.CheckedInputStream; 21 | import java.util.zip.Checksum; 22 | 23 | public class AuthUtils { 24 | 25 | private static StandardPBEByteEncryptor getEncryptor(String key) 26 | { 27 | StandardPBEByteEncryptor encryptor = new StandardPBEByteEncryptor(); 28 | encryptor.setPassword(key); 29 | encryptor.setAlgorithm("PBEWithSHA1AndRC4_128"); 30 | encryptor.setKeyObtentionIterations(4000); 31 | return encryptor; 32 | } 33 | 34 | public static byte[] encrypt(byte[] bytes, String key) 35 | { 36 | StandardPBEByteEncryptor encryptor = getEncryptor(key); 37 | return encryptor.encrypt(bytes); 38 | } 39 | 40 | public static byte[] decrypt(byte[] bytes, String key) 41 | { 42 | StandardPBEByteEncryptor encryptor = getEncryptor(key); 43 | return encryptor.decrypt(bytes); 44 | } 45 | 46 | public static String createHash(String key, byte[] message) { 47 | MessageDigest digest; 48 | 49 | try { 50 | digest = MessageDigest.getInstance("SHA-1"); 51 | } catch (NoSuchAlgorithmException e) { 52 | throw new AssertionError(e); 53 | } 54 | 55 | digest.update(message); 56 | digest.update(key.getBytes(StandardCharsets.UTF_8)); 57 | 58 | return Hex.encodeHexString(digest.digest()); 59 | } 60 | 61 | public static String createPackageChecksum(String fp) throws IOException { 62 | Path path = Paths.get(fp); 63 | try (CheckedInputStream is = new CheckedInputStream(Files.newInputStream(path), new Adler32())) { 64 | byte[] buf = new byte[1024*1024]; 65 | int total = 0; 66 | int c = 0; 67 | while (total < 100*1024*1024 && (c = is.read(buf)) >= 0) { 68 | total += c; 69 | } 70 | 71 | ByteBuffer bb = ByteBuffer.allocate(Long.BYTES); 72 | bb.putLong(path.toFile().length()); 73 | buf = bb.array(); 74 | is.getChecksum().update(buf, 0, buf.length); 75 | return Long.toHexString(is.getChecksum().getValue()); 76 | } 77 | } 78 | 79 | public static boolean validateHash(String hash, String key, String message) { 80 | return validateHash(hash, key, message.getBytes(StandardCharsets.UTF_8)); 81 | } 82 | 83 | public static boolean validateHash(String hash, String key, byte[] message) { 84 | return hash.equals(createHash(key, message)); 85 | } 86 | 87 | public static boolean validateHash(Protocol.AuthenticatedMessage payload, String key) { 88 | return validateHash(payload.getHash(), key, payload.getPayload().toByteArray()); 89 | } 90 | 91 | private AuthUtils() {} 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/step/ExecuteStep.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3.step; 2 | 3 | import io.playpen.core.Bootstrap; 4 | import io.playpen.core.coordinator.local.Local; 5 | import io.playpen.core.coordinator.local.Server; 6 | import io.playpen.core.p3.IPackageStep; 7 | import io.playpen.core.p3.P3Package; 8 | import io.playpen.core.p3.PackageContext; 9 | import io.playpen.core.utils.STUtils; 10 | import io.playpen.core.utils.process.FileProcessListener; 11 | import io.playpen.core.utils.process.ShutdownProcessListener; 12 | import io.playpen.core.utils.process.XProcess; 13 | import lombok.extern.log4j.Log4j2; 14 | import org.json.JSONArray; 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | import org.stringtemplate.v4.ST; 18 | 19 | import java.io.IOException; 20 | import java.nio.file.Paths; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | @Log4j2 27 | public class ExecuteStep implements IPackageStep { 28 | @Override 29 | public String getStepId() { 30 | return "execute"; 31 | } 32 | 33 | @Override 34 | public boolean runStep(P3Package p3, PackageContext ctx, JSONObject config) { 35 | Server server = null; 36 | if(ctx.getUser() instanceof Server) { 37 | server = (Server)ctx.getUser(); 38 | } 39 | 40 | List command = new ArrayList<>(); 41 | try { 42 | command.add(config.getString("command")); 43 | 44 | JSONArray args = config.optJSONArray("arguments"); 45 | if(args != null) { 46 | for(int i = 0; i < args.length(); ++i) { 47 | command.add(args.getString(i)); 48 | } 49 | } 50 | } 51 | catch(JSONException e) { 52 | log.error("Configuration error", e); 53 | return false; 54 | } 55 | 56 | if(config.has("template")) { 57 | boolean useTemplate = config.getBoolean("template"); 58 | if(useTemplate) { 59 | log.info("Running ST on command"); 60 | for (int i = 0; i < command.size(); ++i) { 61 | ST template = new ST(command.get(i)); 62 | 63 | STUtils.buildSTProperties(p3, ctx, template); 64 | 65 | command.set(i, template.render()); 66 | } 67 | } 68 | } 69 | 70 | Map environment = new HashMap<>(); 71 | if(config.has("environment")) { 72 | JSONObject environmentObj = config.getJSONObject("environment"); 73 | for (String s : environmentObj.keySet()) { 74 | environment.put(s, environmentObj.getString(s)); 75 | } 76 | } 77 | 78 | log.info("Running command " + command.get(0)); 79 | 80 | XProcess proc = new XProcess(command, ctx.getDestination().toString(), environment, server == null); 81 | 82 | if(server != null) { 83 | log.info("Registering process with server " + server.getUuid()); 84 | server.setProcess(proc); 85 | 86 | try { 87 | proc.addListener(new FileProcessListener(Paths.get(Bootstrap.getHomeDir().getPath(), "server-logs", 88 | (Local.get().isUseNameForLogs() ? server.getSafeName() : server.getUuid()) + ".log").toFile())); 89 | } 90 | catch(IOException e) { 91 | log.warn("Unable to create log for server, no logging of console output will be done"); 92 | } 93 | 94 | proc.addListener(new ShutdownProcessListener(server)); 95 | } 96 | 97 | if(!proc.run()) { 98 | log.info("Command " + command.get(0) + " failed"); 99 | return false; 100 | } 101 | 102 | return true; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/utils/process/XProcess.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.utils.process; 2 | 3 | import com.zaxxer.nuprocess.NuProcess; 4 | import com.zaxxer.nuprocess.NuProcessBuilder; 5 | import com.zaxxer.nuprocess.codec.NuAbstractCharsetHandler; 6 | import lombok.Getter; 7 | import lombok.extern.log4j.Log4j2; 8 | 9 | import java.nio.ByteBuffer; 10 | import java.nio.CharBuffer; 11 | import java.nio.charset.CoderResult; 12 | import java.nio.charset.StandardCharsets; 13 | import java.nio.file.Paths; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentLinkedQueue; 17 | import java.util.concurrent.CopyOnWriteArrayList; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | @Log4j2 21 | public class XProcess extends NuAbstractCharsetHandler { 22 | public static final int MAX_LINE_STORAGE = 25; 23 | 24 | private final List command; 25 | private final String workingDir; 26 | private NuProcess process = null; 27 | private final List listeners = new CopyOnWriteArrayList<>(); 28 | private final OutputBuffer outputBuffer = new OutputBuffer(); 29 | private final Map environment; 30 | private boolean wait; 31 | 32 | @Getter 33 | private ConcurrentLinkedQueue lastLines = new ConcurrentLinkedQueue<>(); 34 | 35 | public XProcess(List command, String workingDir, Map environment, boolean wait) { 36 | super(StandardCharsets.UTF_8); 37 | this.command = command; 38 | this.workingDir = workingDir; 39 | this.environment = environment; 40 | this.wait = wait; 41 | } 42 | 43 | public void addListener(IProcessListener listener) { 44 | listeners.add(listener); 45 | listener.onProcessAttach(this); 46 | } 47 | 48 | public void removeListener(IProcessListener listener) { 49 | listeners.remove(listener); 50 | listener.onProcessDetach(this); 51 | } 52 | 53 | public void sendInput(String in) { 54 | byte[] inAsBytes = in.getBytes(StandardCharsets.UTF_8); 55 | process.writeStdin(ByteBuffer.wrap(inAsBytes)); 56 | 57 | for(IProcessListener listener : listeners) { 58 | listener.onProcessInput(this, in); 59 | } 60 | } 61 | 62 | public boolean run() { 63 | NuProcessBuilder builder = new NuProcessBuilder(command); 64 | builder.setCwd(Paths.get(workingDir)); 65 | builder.setProcessListener(this); 66 | builder.environment().putAll(environment); 67 | process = builder.start(); 68 | 69 | if (wait) { 70 | try { 71 | process.waitFor(0, TimeUnit.SECONDS); 72 | } catch (InterruptedException e) { 73 | log.info("Interrupted while waiting for process to complete", e); 74 | } 75 | } 76 | 77 | return true; 78 | } 79 | 80 | public boolean isRunning() { 81 | return process.isRunning(); 82 | } 83 | 84 | public void stop() { 85 | process.destroy(true); 86 | try { 87 | process.waitFor(5, TimeUnit.SECONDS); 88 | } 89 | catch (InterruptedException e) { 90 | log.info("Interrupted while waiting for process to shutdown", e); 91 | } 92 | } 93 | 94 | @Override 95 | protected void onStdoutChars(CharBuffer buffer, boolean closed, CoderResult coderResult) { 96 | outputBuffer.append(buffer); 97 | } 98 | 99 | @Override 100 | protected void onStderrChars(CharBuffer buffer, boolean closed, CoderResult coderResult) { 101 | onStdoutChars(buffer, closed, coderResult); 102 | } 103 | 104 | @Override 105 | public void onExit(int exitCode) { 106 | for (IProcessListener listener : listeners) { 107 | listener.onProcessEnd(this); 108 | } 109 | } 110 | 111 | protected void receiveOutput(String out) { 112 | for(IProcessListener listener : listeners) { 113 | listener.onProcessOutput(this, out); 114 | } 115 | 116 | lastLines.add(out); 117 | if (lastLines.size() > MAX_LINE_STORAGE) 118 | lastLines.remove(); 119 | } 120 | 121 | private class OutputBuffer extends ProcessBuffer { 122 | @Override 123 | protected void onOutput(String output) { 124 | receiveOutput(output); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/network/LocalCoordinator.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.network; 2 | 3 | import io.netty.channel.Channel; 4 | import io.playpen.core.coordinator.network.authenticator.IAuthenticator; 5 | import io.playpen.core.networking.TransactionInfo; 6 | import io.playpen.core.p3.P3Package; 7 | import io.playpen.core.protocol.Commands; 8 | import lombok.Data; 9 | import lombok.extern.log4j.Log4j2; 10 | 11 | import java.util.*; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.ConcurrentSkipListSet; 14 | 15 | @Data 16 | @Log4j2 17 | public class LocalCoordinator { 18 | private String uuid; 19 | 20 | private String key; 21 | 22 | private String name; 23 | 24 | private String keyName = ""; 25 | 26 | private Map resources = new ConcurrentHashMap<>(); 27 | 28 | private Set attributes = new ConcurrentSkipListSet<>(); 29 | 30 | private Map servers = new ConcurrentHashMap<>(); 31 | 32 | private Channel channel = null; 33 | 34 | private boolean enabled = false; 35 | 36 | /** 37 | * Setting this to true will prevent automatic selection of this coordinator for provisioning operations. 38 | * This must be manually set and unset, but will also reset if the network coordinator is restarted. 39 | */ 40 | private boolean restricted = false; 41 | 42 | private List authenticators = new ArrayList<>(); 43 | 44 | public String getName() { 45 | if(name == null) { 46 | return uuid; 47 | } 48 | 49 | return name; 50 | } 51 | 52 | public boolean isEnabled() { 53 | return enabled && channel != null && channel.isActive(); 54 | } 55 | 56 | public Server getServer(String idOrName) { 57 | if(servers.containsKey(idOrName)) 58 | return servers.get(idOrName); 59 | 60 | for(Server server : servers.values()) { 61 | if(server.getName() != null && server.getName().equals(idOrName)) 62 | return server; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | public Map getAvailableResources() { 69 | Map used = new HashMap<>(); 70 | for(Map.Entry entry : resources.entrySet()) { 71 | Integer value = entry.getValue(); 72 | for(Server server : servers.values()) { 73 | value -= server.getP3().getResources().getOrDefault(entry.getKey(), 0); 74 | } 75 | 76 | used.put(entry.getKey(), value); 77 | } 78 | 79 | return used; 80 | } 81 | 82 | public boolean canProvisionPackage(P3Package p3) { 83 | for(String attr : p3.getAttributes()) { 84 | if(!attributes.contains(attr)) { 85 | log.warn("Coordinator " + getUuid() + " doesn't have attribute " + attr + " for " + p3.getId() + " at " + p3.getVersion()); 86 | return false; 87 | } 88 | } 89 | 90 | Map resources = getAvailableResources(); 91 | for(Map.Entry entry : p3.getResources().entrySet()) { 92 | if(!resources.containsKey(entry.getKey())) { 93 | log.warn("Coordinator " + getUuid() + " doesn't have resource " + entry.getKey() + " for " + p3.getId() + " at " + p3.getVersion()); 94 | return false; 95 | } 96 | 97 | if(resources.get(entry.getKey()) - entry.getValue() < 0) { 98 | log.warn("Coordinator " + getUuid() + " doesn't have enough of resource " + entry.getKey() + " for " + p3.getId() + " at " + p3.getVersion()); 99 | return false; 100 | } 101 | } 102 | 103 | return true; 104 | } 105 | 106 | public Server createServer(P3Package p3, String name, Map properties) { 107 | if(!p3.isResolved()) { 108 | log.error("Cannot create server for unresolved package"); 109 | return null; 110 | } 111 | 112 | if(!canProvisionPackage(p3)) { 113 | log.error("Coordinator " + getUuid() + " failed provision check for package " + p3.getId() + " at " + p3.getVersion()); 114 | return null; 115 | } 116 | 117 | Server server = new Server(); 118 | server.setUuid(UUID.randomUUID().toString()); 119 | while(servers.containsKey(server.getUuid())) 120 | server.setUuid(UUID.randomUUID().toString()); 121 | 122 | server.setP3(p3); 123 | server.setName(name); 124 | server.getProperties().putAll(properties); 125 | server.setCoordinator(this); 126 | servers.put(server.getUuid(), server); 127 | 128 | return server; 129 | } 130 | 131 | /** 132 | * Normalized resource usage is the sum of (resource / max resource) divided by the 133 | * number of resources. This should be between 0 and 1. 134 | */ 135 | public double getNormalizedResourceUsage() { 136 | double result = 0.0; 137 | Map available = getAvailableResources(); 138 | for(Map.Entry max : resources.entrySet()) { 139 | if(max.getValue() <= 0) // wat 140 | continue; 141 | 142 | Integer used = available.getOrDefault(max.getKey(), 0); 143 | result += used.doubleValue() / max.getValue().doubleValue(); 144 | } 145 | 146 | return result; 147 | } 148 | 149 | public boolean authenticate(Commands.BaseCommand command, TransactionInfo info) 150 | { 151 | if (authenticators.isEmpty()) 152 | return true; 153 | 154 | for (IAuthenticator auth : authenticators) 155 | { 156 | if (auth.hasAccess(command, info, this)) 157 | return true; 158 | } 159 | 160 | return false; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/networking/TransactionManager.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.networking; 2 | 3 | import io.playpen.core.coordinator.PlayPen; 4 | import io.playpen.core.protocol.Commands; 5 | import io.playpen.core.protocol.Protocol; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @Log4j2 13 | public class TransactionManager { 14 | public static final long TRANSACTION_TIMEOUT = 340; // seconds 15 | 16 | private static TransactionManager instance = new TransactionManager(); 17 | 18 | public static TransactionManager get() { 19 | return instance; 20 | } 21 | 22 | private Map transactions = new ConcurrentHashMap<>(); 23 | 24 | private TransactionManager() {} 25 | 26 | boolean isActive(String id) { 27 | return transactions.containsKey(id); 28 | } 29 | 30 | public TransactionInfo getTransaction(String id) { 31 | return transactions.get(id); 32 | } 33 | 34 | public Protocol.Transaction build(String id, Protocol.Transaction.Mode mode, Commands.BaseCommand command) { 35 | TransactionInfo info = getTransaction(id); 36 | if(info == null) { 37 | log.error("Unable to build unknown transaction " + id); 38 | return null; 39 | } 40 | 41 | return Protocol.Transaction.newBuilder() 42 | .setId(info.getId()) 43 | .setMode(mode) 44 | .setPayload(command) 45 | .build(); 46 | } 47 | 48 | public TransactionInfo begin() { 49 | TransactionInfo info = new TransactionInfo(); 50 | 51 | info.setId(PlayPen.get().generateId()); 52 | while(transactions.containsKey(info.getId())) 53 | info.setId(PlayPen.get().generateId()); 54 | 55 | transactions.put(info.getId(), info); 56 | 57 | final String tid = info.getId(); 58 | if(PlayPen.get().getScheduler() != null) { 59 | info.setCancelTask(PlayPen.get().getScheduler().schedule(() -> { 60 | log.warn("Transaction " + tid + " has been cancelled due to timeout"); 61 | cancel(tid, true); 62 | }, TRANSACTION_TIMEOUT, TimeUnit.SECONDS)); 63 | } 64 | 65 | return info; 66 | } 67 | 68 | public boolean send(String id, Protocol.Transaction message, String target) { 69 | TransactionInfo info = getTransaction(id); 70 | if(info == null) { 71 | log.error("Cannot send unknown transaction " + id); 72 | return false; 73 | } 74 | 75 | if(!info.getId().equals(message.getId())) { 76 | log.error("Message id does not match transaction id " + id); 77 | return false; 78 | } 79 | 80 | info.setTransaction(message); 81 | info.setTarget(target); 82 | 83 | if(info.getHandler() != null) { 84 | info.getHandler().onTransactionSend(this, info); 85 | } 86 | 87 | if(message.getMode() == Protocol.Transaction.Mode.COMPLETE || message.getMode() == Protocol.Transaction.Mode.SINGLE) { 88 | complete(info.getId()); 89 | } 90 | 91 | return PlayPen.get().send(message, info.getTarget()); 92 | } 93 | 94 | public boolean cancel(String id) { 95 | return cancel(id, false); 96 | } 97 | 98 | public boolean cancel(String id, boolean silentFail) { 99 | TransactionInfo info = getTransaction(id); 100 | if(info == null) { 101 | if(!silentFail) log.error("Cannot cancel unknown transaction " + id); 102 | return false; 103 | } 104 | 105 | if(info.getHandler() != null) { 106 | info.getHandler().onTransactionCancel(this, info); 107 | } 108 | 109 | if(info.getCancelTask() != null && !info.getCancelTask().isDone()) { 110 | info.getCancelTask().cancel(false); 111 | } 112 | 113 | info.setDone(true); 114 | 115 | transactions.remove(id); 116 | return true; 117 | } 118 | 119 | public boolean complete(String id) { 120 | TransactionInfo info = getTransaction(id); 121 | if(info == null) { 122 | log.error("Cannot complete unknown transaction " + id); 123 | return false; 124 | } 125 | 126 | if(info.getHandler() != null) { 127 | info.getHandler().onTransactionComplete(this, info); 128 | } 129 | 130 | if(info.getCancelTask() != null && !info.getCancelTask().isDone()) { 131 | info.getCancelTask().cancel(false); 132 | } 133 | 134 | info.setDone(true); 135 | 136 | transactions.remove(id); 137 | return true; 138 | } 139 | 140 | public void receive(Protocol.Transaction message, String from) { 141 | TransactionInfo info = null; 142 | switch(message.getMode()) { 143 | case CREATE: 144 | if(transactions.containsKey(message.getId())) { 145 | log.error("Received CREATE on an id that already exists (" + message.getId() + ")"); 146 | return; 147 | } 148 | 149 | info = new TransactionInfo(); 150 | info.setId(message.getId()); 151 | info.setTarget(from); 152 | transactions.put(info.getId(), info); 153 | break; 154 | 155 | case SINGLE: 156 | info = new TransactionInfo(); 157 | info.setDone(true); 158 | break; 159 | 160 | case CONTINUE: 161 | info = getTransaction(message.getId()); 162 | if(info == null) { 163 | log.error("Received CONTINUE on an id that doesn't exist (" + message.getId() + ")"); 164 | return; 165 | } 166 | 167 | if(info.getHandler() != null) { 168 | info.getHandler().onTransactionReceive(this, info, message); 169 | } 170 | break; 171 | 172 | case COMPLETE: 173 | info = getTransaction(message.getId()); 174 | if(info == null) { 175 | log.error("Received COMPLETE on an id that doesn't exist (" + message.getId() + ")"); 176 | return; 177 | } 178 | 179 | if(info.getHandler() != null) { 180 | info.getHandler().onTransactionReceive(this, info, message); 181 | } 182 | 183 | info.setDone(true); 184 | if(!complete(info.getId())) { 185 | log.error("Unable to complete transaction " + info.getId()); 186 | return; 187 | } 188 | break; 189 | } 190 | 191 | PlayPen.get().process(message.getPayload(), info, from); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/P3Tool.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core; 2 | 3 | import io.playpen.core.p3.ExecutionType; 4 | import io.playpen.core.p3.P3Package; 5 | import io.playpen.core.p3.PackageException; 6 | import io.playpen.core.p3.PackageManager; 7 | import io.playpen.core.p3.resolver.LocalRepositoryResolver; 8 | import org.zeroturnaround.zip.ZipException; 9 | import org.zeroturnaround.zip.ZipUtil; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | public class P3Tool { 20 | public static void run(String[] args) { 21 | if(args.length < 2) { 22 | System.err.println("playpen p3 [arguments...]"); 23 | return; 24 | } 25 | 26 | switch(args[1].toLowerCase()) { 27 | case "inspect": 28 | inspect(args); 29 | break; 30 | 31 | case "pack": 32 | pack(args); 33 | break; 34 | } 35 | } 36 | 37 | private static void inspect(String[] args) { 38 | if(args.length != 3) { 39 | System.err.println("playpen p3 inspect "); 40 | return; 41 | } 42 | 43 | File p3File = new File(args[2]); 44 | if(!p3File.exists() || !p3File.isFile()) { 45 | System.err.println("Package doesn't exist or isn't a file!"); 46 | return; 47 | } 48 | 49 | PackageManager pm = new PackageManager(); 50 | Initialization.packageManager(pm); 51 | pm.addPackageResolver(new LocalRepositoryResolver(new File("."))); 52 | 53 | P3Package p3 = null; 54 | try { 55 | p3 = pm.readPackage(p3File); 56 | } 57 | catch(PackageException e) { 58 | System.err.println("Unable to read package"); 59 | e.printStackTrace(System.err); 60 | return; 61 | } 62 | 63 | System.out.println("=== Package ==="); 64 | System.out.println("Id: " + p3.getId()); 65 | System.out.println("Version: " + p3.getVersion()); 66 | 67 | if(p3.getDependencies().size() == 0) { 68 | System.out.println("Dependencies: none"); 69 | } 70 | else { 71 | for(P3Package dep : p3.getDependencies()) { 72 | System.out.println("Dependency: " + dep.getId() + " at " + dep.getVersion()); 73 | } 74 | } 75 | 76 | if(p3.getResources().size() == 0) { 77 | System.err.println("-- Package doesn't take any resources"); 78 | } 79 | else { 80 | for (Map.Entry resource : p3.getResources().entrySet()) { 81 | System.out.println("Resource: " + resource.getKey() + " = " + resource.getValue()); 82 | } 83 | } 84 | 85 | if(p3.getAttributes().size() == 0) { 86 | System.err.println("-- Package doesn't require any attributes"); 87 | } 88 | else { 89 | for(String attr : p3.getAttributes()) { 90 | System.out.println("Requires: " + attr); 91 | } 92 | } 93 | 94 | for(Map.Entry str : p3.getStrings().entrySet()) { 95 | System.out.println("String: " + str.getKey() + " = " + str.getValue()); 96 | } 97 | 98 | if(p3.getProvisionSteps().size() == 0) { 99 | System.err.println("-- Package doesn't define any provisioning steps"); 100 | } 101 | else { 102 | for(P3Package.PackageStepConfig config : p3.getProvisionSteps()) { 103 | System.out.println("Provision step: " + config.getStep().getStepId()); 104 | } 105 | } 106 | 107 | if(p3.getExecutionSteps().size() == 0) { 108 | System.err.println("-- Package doesn't define any execution steps"); 109 | } 110 | else { 111 | for(P3Package.PackageStepConfig config : p3.getExecutionSteps()) { 112 | System.out.println("Execution step: " + config.getStep().getStepId()); 113 | } 114 | } 115 | 116 | if(p3.getShutdownSteps().size() == 0) { 117 | System.err.println("-- Package doesn't define any shutdown steps"); 118 | } 119 | else { 120 | for(P3Package.PackageStepConfig config : p3.getShutdownSteps()) { 121 | System.out.println("Shutdown step: " + config.getStep().getStepId()); 122 | } 123 | } 124 | 125 | System.out.println("=== End Package ==="); 126 | } 127 | 128 | private static void pack(String[] args) { 129 | if(args.length != 3 && args.length != 4) { 130 | System.err.println("playpen p3 pack [destination]"); 131 | return; 132 | } 133 | 134 | File p3Dir = new File(args[2]); 135 | if(!p3Dir.exists() || !p3Dir.isDirectory()) { 136 | System.err.println("Source doesn't exist or isn't a directory!"); 137 | return; 138 | } 139 | 140 | Path schemaPath = Paths.get(p3Dir.getPath(), "package.json"); 141 | File schemaFile = schemaPath.toFile(); 142 | if(!schemaFile.exists() || !schemaFile.isFile()) { 143 | System.err.println("package.json is either missing or a directory"); 144 | return; 145 | } 146 | 147 | String schemaString = null; 148 | try { 149 | schemaString = new String(Files.readAllBytes(schemaPath)); 150 | } 151 | catch(IOException e) { 152 | System.err.println("Unable to read schema"); 153 | e.printStackTrace(System.err); 154 | return; 155 | } 156 | 157 | PackageManager pm = new PackageManager(); 158 | Initialization.packageManager(pm); 159 | pm.addPackageResolver(new LocalRepositoryResolver(new File("."))); 160 | 161 | System.out.println("Reading package schema..."); 162 | 163 | P3Package p3 = null; 164 | try { 165 | p3 = pm.readSchema(schemaString); 166 | } 167 | catch(PackageException e) { 168 | System.err.println("Unable to read schema"); 169 | e.printStackTrace(System.err); 170 | return; 171 | } 172 | 173 | String destDir = "."; 174 | if(args.length == 4) 175 | { 176 | destDir = args[3]; 177 | } 178 | 179 | String resultFileName = p3.getId() + "_" + p3.getVersion() + ".p3"; 180 | File resultFile = Paths.get(destDir, resultFileName).toFile(); 181 | if(resultFile.exists()) 182 | { 183 | if(!resultFile.delete()) { 184 | System.err.println("Unable to remove old package (" + resultFileName + ")"); 185 | return; 186 | } 187 | } 188 | System.out.println("Creating package " + resultFileName); 189 | 190 | try { 191 | ZipUtil.pack(p3Dir, resultFile); 192 | } 193 | catch(ZipException e) { 194 | System.err.println("Unable to create package"); 195 | e.printStackTrace(System.err); 196 | return; 197 | } 198 | 199 | System.out.println("Finished packing!"); 200 | } 201 | 202 | private P3Tool() {} 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayPen Core 2 | 3 | PlayPen is a generic cross-platform server management and load balancing framework. PlayPen is designed primarily for 4 | ephemeral services, ones that do not need to store any permanent data and can be shut down or spun up at will. 5 | 6 | By itself, playpen does not do anything and must receive commands from somewhere whether that be the command line 7 | client, [PVI](https://github.com/PlayPen/PVI), another client, or a plugin. 8 | 9 | PlayPen was originally developed to automatically balance minecraft game servers at [The Chunk](https://thechunk.net), 10 | but can be used to deploy and manage any kind of self-contained service. 11 | 12 | ## Protobuf Compilation 13 | 14 | Protobuf files should automatically compile when you compile PlayPen using Maven. However, you can still build the 15 | Protobuf files manually should the need arise. You must have the [protoc compiler](https://developers.google.com/protocol-buffers/docs/downloads) 16 | installed in order to do this . Run `build_protocol.bat` or `build_protocol.sh` from the project's root 17 | directory. 18 | 19 | __Make sure you use protobuf 2.x.x! 3.x.x is not currently supported!__ 20 | 21 | ## Usage 22 | 23 | Before using any playpen tools, PlayPen needs to set itself up. Simply place the 24 | playpen jar wherever you wish playpen's home directory to be, and run it without 25 | any arguments. It will create the default configuration files and exit. 26 | 27 | It is generally recommended to use the bundled scripts to run playpen instead of 28 | launching the jar manually. To set this up, simply run 29 | 30 | java -jar PlayPen-1.0-SNAPSHOT.jar 31 | 32 | This should copy some scripts and configuration files into the current folder. 33 | 34 | To start the network coordinator, run 35 | 36 | playpen-network 37 | 38 | To start a local coordinator, run 39 | 40 | playpen-local 41 | 42 | Packaging tools are found at the "p3" command. Run it to see a list of them: 43 | 44 | playpen-p3 45 | 46 | The playpen cli client can be run with: 47 | 48 | playpen-cli 49 | 50 | Note that the cli uses the "local.json" configuration file as it represents itself to the 51 | network as a local coordinator. 52 | 53 | **Warning: You cannot run multiple instances of playpen from the same installation!** If you need to run multiple 54 | instances of playpen (for example, a network coordinator and a local coordinator), use two separate directories and 55 | playpen jars. 56 | 57 | ## Plugins 58 | 59 | Plugins are currently only supported for the network coordinator. Local coordinator support will come soon, but until 60 | then it is impossible to add custom package execution steps. 61 | 62 | ## Servers/Services 63 | 64 | A service (or server) is a single instance of a running package (see below). When a service runs on a local coordinator, 65 | it is given a unique working directory based on a UUID assigned by the network. Good practice dictates that any work 66 | with the filesystem should ideally be done in this directory in order to not interrupt other services, but in cases 67 | where that isn't possible it's fine to access other files on a coordinator provided you know what you are doing. 68 | 69 | ## Packages 70 | 71 | PlayPen uses a package system known as _P3_ to send files across the network. The network coordinator acts as a package 72 | repository which all local coordinators can pull from. All services run by playpen must be contained in a package. 73 | Common components shared between services can be split off into their own package using P3's dependency system. Local 74 | coordinators cache packages locally so that they do not have to be continuously sent over the network. 75 | 76 | There are three main types of packages: script packages, standard packages, and asset packages. PlayPen doesn't 77 | technically makea distinction between the types of packages, but it's good to think of each package as being one of 78 | these. 79 | 80 | ### Script packages 81 | 82 | A script package contains no files except the package metadata (package.json). As such, the package should not have an 83 | "expand" provision step (see below). Script packages will generally just run some commands via the package metadata 84 | file. 85 | 86 | ### Standard Packages 87 | 88 | Standard packages contain a set of files that are extracted from the package into the service's working directory. These 89 | packages will use an "expand" provision step to expand/extract the package files into the appropriate directory. They 90 | will then generally run a set of commands during the execute step in order to start the service. 91 | 92 | ### Asset Packages 93 | 94 | Asset packages contain files that should be extracted into a location where multiple services can access them. They 95 | should be used for things like storing common map data for a game when you know the files will never be modified. That 96 | way you only have to extract them a single time for each local coordinator, and all services will have access to those 97 | files. 98 | 99 | Asset packages use the "expand-assets" provision step almost exclusively. They generally should not be provisioned 100 | directly, and should instead be listed as a dependency. 101 | 102 | ## Reliability 103 | 104 | Local coordinators should be able to run for months on end without being restarted (bar needing to update to a newer version). The network coordinator can be restarted without affecting the operation of the network (aside from losing the ability to control the network for the time that the network coordinator is down). 105 | 106 | PlayPen is designed with reliability in mind. Here are some neat features relating to reliability: 107 | 108 | * The network coordinator is only required to control local coordinators. Local coordinators can operate in a "holding pattern" without a connection to the network. 109 | * The network coordinator can be restarted safely for any reason at any time. This ties into the idea that local coordinators don't need to be connected all the time to do their work (though that is ideal). 110 | * It is easy to change where the network coordinator is located. Current implementation simply needs you to change each local coordinator's config file and shut down the old network coordinator. Local coordinators will automatically reload their config after being disconnected. 111 | * The state of the network is figured out on the fly; there is no database for services since they are generally ephemeral. The network coordinator figures out what goes where without any extra help. 112 | 113 | ## Warning 114 | 115 | PlayPen is not an out of the box solution for server management. Even at The Chunk we had a huge custom stack that actually allowed us to make use of PlayPen. We had a plugin in PlayPen that would send the IP and port of every server to redis, then a plugin on bungeecord that read in the list of servers from redis. Finally we had a component on our hub server (which was not managed by PlayPen as it had to always exist in the same location -- not ephemeral) which also read the list of servers from redis in order to display available servers to players. 116 | 117 | ## Contributing 118 | 119 | Feel free to send pull requests! If you want a change made in PlayPen, just remember the following: 120 | 121 | * PlayPen is designed to be generic. Anything built to run specific services should be placed in their own plugins, not in playpen-core. 122 | * Changes should have little to no impact on reliability. 123 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.playpen 8 | PlayPen 9 | jar 10 | 1.0 11 | 12 | 13 | 14 | bintray-playpen-playpen 15 | playpen-playpen 16 | https://api.bintray.com/maven/playpen/playpen/PlayPen/;publish=1 17 | 18 | 19 | 20 | 21 | 22 | 23 | kr.motd.maven 24 | os-maven-plugin 25 | 1.3.0.Final 26 | 27 | 28 | 29 | 30 | 31 | true 32 | src/main/resources 33 | 34 | 35 | 36 | 37 | 38 | org.xolstice.maven.plugins 39 | protobuf-maven-plugin 40 | 0.5.0 41 | true 42 | 43 | 44 | 45 | compile 46 | test-compile 47 | 48 | 49 | com.google.protobuf:protoc:2.6.1:exe:${os.detected.classifier} 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.5 58 | 59 | 1.8 60 | 1.8 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-shade-plugin 66 | 2.3 67 | 68 | 69 | package 70 | 71 | shade 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-jar-plugin 79 | 2.4 80 | 81 | 82 | **/log4j-provider.properties 83 | 84 | 85 | 86 | true 87 | io.playpen.core.Bootstrap 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.projectlombok 98 | lombok 99 | 1.16.0 100 | provided 101 | 102 | 103 | org.json 104 | json 105 | 20141113 106 | compile 107 | 108 | 109 | org.zeroturnaround 110 | zt-zip 111 | 1.8 112 | jar 113 | compile 114 | 115 | 116 | org.apache.logging.log4j 117 | log4j-api 118 | 2.17.0 119 | compile 120 | 121 | 122 | org.apache.logging.log4j 123 | log4j-core 124 | 2.17.0 125 | compile 126 | 127 | 128 | org.apache.logging.log4j 129 | log4j-slf4j-impl 130 | 2.17.0 131 | compile 132 | 133 | 134 | org.antlr 135 | stringtemplate 136 | 4.0.2 137 | compile 138 | 139 | 140 | commons-codec 141 | commons-codec 142 | 1.10 143 | compile 144 | 145 | 146 | com.google.protobuf 147 | protobuf-java 148 | 2.6.1 149 | compile 150 | 151 | 152 | io.netty 153 | netty-handler 154 | 4.0.23.Final 155 | compile 156 | 157 | 158 | commons-io 159 | commons-io 160 | 2.4 161 | compile 162 | 163 | 164 | com.google.guava 165 | guava 166 | 18.0 167 | compile 168 | 169 | 170 | org.jasypt 171 | jasypt 172 | 1.9.2 173 | compile 174 | 175 | 176 | com.zaxxer 177 | nuprocess 178 | 1.1.2 179 | compile 180 | 181 | 182 | junit 183 | junit 184 | 4.12 185 | test 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/main/proto/command.proto: -------------------------------------------------------------------------------- 1 | package io.playpen.core.protocol; 2 | 3 | option java_outer_classname = "Commands"; 4 | 5 | import "coordinator.proto"; 6 | import "p3.proto"; 7 | 8 | message Sync { 9 | optional string name = 1; 10 | 11 | repeated Resource resources = 2; 12 | 13 | repeated string attributes = 3; 14 | 15 | repeated Server servers = 4; 16 | 17 | required bool enabled = 5 [default=false]; 18 | } 19 | 20 | message Provision { 21 | required Server server = 1; 22 | } 23 | 24 | message ProvisionResponse { 25 | required bool ok = 1; 26 | } 27 | 28 | message PackageRequest { 29 | required P3Meta p3 = 1; 30 | } 31 | 32 | message PackageResponse { 33 | required bool ok = 1; 34 | optional PackageData data = 2; 35 | } 36 | 37 | message SplitPackageResponse { 38 | required bool ok = 1; 39 | optional SplitPackageData data = 2; 40 | } 41 | 42 | message PackageChecksumRequest { 43 | required P3Meta p3 = 1; 44 | } 45 | 46 | message PackageChecksumResponse { 47 | required bool ok = 1; 48 | optional string checksum = 2; 49 | } 50 | 51 | message Deprovision { 52 | required string uuid = 1; 53 | required bool force = 2 [default=false]; // if true, local should terminate the process without using shutdown steps 54 | } 55 | 56 | message ServerShutdown { 57 | required string uuid = 1; 58 | } 59 | 60 | message SendInput { 61 | required string id = 1; 62 | required string input = 2; 63 | } 64 | 65 | message AttachConsole { 66 | required string serverId = 1; 67 | required string consoleId = 2; 68 | } 69 | 70 | message ConsoleMessage { 71 | required string consoleId = 1; 72 | required string value = 2; 73 | } 74 | 75 | message DetachConsole { 76 | required string consoleId = 1; 77 | } 78 | 79 | message FreezeServer { 80 | required string uuid = 1; 81 | } 82 | 83 | message C_CoordinatorListResponse { 84 | repeated LocalCoordinator coordinators = 1; 85 | } 86 | 87 | message C_Provision { 88 | required P3Meta p3 = 1; 89 | optional string coordinator = 2; 90 | optional string serverName = 3; 91 | repeated Property properties = 4; 92 | } 93 | 94 | message C_ProvisionResponse { 95 | required bool ok = 1; 96 | optional string coordinatorId = 2; 97 | optional string serverId = 3; 98 | } 99 | 100 | message C_Deprovision { 101 | required string coordinatorId = 1; 102 | required string serverId = 2; 103 | required bool force = 3 [default=false]; 104 | } 105 | 106 | message C_Shutdown { 107 | required string uuid = 1; 108 | } 109 | 110 | message C_Promote { 111 | required P3Meta p3 = 1; 112 | } 113 | 114 | message C_CreateCoordinator { 115 | optional string keyName = 1; 116 | } 117 | 118 | message C_CoordinatorCreated { 119 | required string uuid = 1; 120 | required string key = 2; 121 | } 122 | 123 | message C_SendInput { 124 | required string coordinatorId = 1; 125 | required string serverId = 2; 126 | required string input = 3; 127 | } 128 | 129 | message C_AttachConsole { 130 | required string coordinatorId = 1; 131 | required string serverId = 2; 132 | } 133 | 134 | message C_ConsoleAttached { 135 | optional string consoleId = 1; 136 | required bool ok = 2; 137 | } 138 | 139 | message C_ConsoleMessage { 140 | required string value = 1; 141 | required string consoleId = 2; 142 | } 143 | 144 | message C_ConsoleDetached { 145 | required string consoleId = 1; 146 | required bool useServerId = 2 [default = false]; 147 | } 148 | 149 | message C_DetachConsole { 150 | optional string consoleId = 1; 151 | } 152 | 153 | message C_FreezeServer { 154 | required string coordinatorId = 1; 155 | required string serverId = 2; 156 | } 157 | 158 | message C_UploadPackage { 159 | required PackageData data = 1; 160 | } 161 | 162 | message C_UploadSplitPackage { 163 | optional SplitPackageData data = 2; 164 | } 165 | 166 | message C_Ack { 167 | optional string result = 1; 168 | } 169 | 170 | message C_PackageList { 171 | repeated P3Meta packages = 1; 172 | } 173 | 174 | message C_AccessDenied { 175 | required string result = 1; 176 | required string tid = 2; 177 | } 178 | 179 | message BaseCommand { 180 | enum CommandType { 181 | // Coordination commands 182 | NOOP = 0; 183 | SYNC = 1; 184 | PROVISION = 2; 185 | PROVISION_RESPONSE = 3; 186 | PACKAGE_REQUEST = 4; 187 | PACKAGE_RESPONSE = 5; 188 | DEPROVISION = 6; 189 | SERVER_SHUTDOWN = 7; 190 | SHUTDOWN = 8; // no message body 191 | SEND_INPUT = 9; 192 | ATTACH_CONSOLE = 10; 193 | CONSOLE_MESSAGE = 11; 194 | DETACH_CONSOLE = 12; 195 | FREEZE_SERVER = 26; 196 | PACKAGE_CHECKSUM_REQUEST = 35; 197 | PACKAGE_CHECKSUM_RESPONSE = 36; 198 | SPLIT_PACKAGE_RESPONSE = 37; 199 | 200 | // Client commands 201 | C_GET_COORDINATOR_LIST = 13; // no message body 202 | C_COORDINATOR_LIST_RESPONSE = 14; 203 | C_PROVISION = 15; 204 | C_PROVISION_RESPONSE = 16; 205 | C_DEPROVISION = 17; 206 | C_SHUTDOWN = 18; 207 | C_PROMOTE = 19; 208 | C_CREATE_COORDINATOR = 20; // no message body 209 | C_COORDINATOR_CREATED = 21; 210 | C_SEND_INPUT = 22; 211 | C_ATTACH_CONSOLE = 23; 212 | C_CONSOLE_ATTACHED = 31; 213 | C_CONSOLE_MESSAGE = 24; 214 | C_CONSOLE_DETACHED = 32; 215 | C_DETACH_CONSOLE = 25; 216 | C_FREEZE_SERVER = 27; 217 | C_UPLOAD_PACKAGE = 28; 218 | C_UPLOAD_SPLIT_PACKAGE = 38; 219 | C_ACK = 30; 220 | C_REQUEST_PACKAGE_LIST = 33; // no message body 221 | C_PACKAGE_LIST = 34; 222 | C_ACCESS_DENIED = 39; 223 | } 224 | 225 | required CommandType type = 1; 226 | 227 | optional Sync sync = 2; 228 | optional Provision provision = 3; 229 | optional ProvisionResponse provisionResponse = 4; 230 | optional PackageRequest packageRequest = 5; 231 | optional PackageResponse packageResponse = 6; 232 | optional Deprovision deprovision = 7; 233 | optional ServerShutdown serverShutdown = 8; 234 | optional SendInput sendInput = 9; 235 | optional AttachConsole attachConsole = 10; 236 | optional ConsoleMessage consoleMessage = 11; 237 | optional DetachConsole detachConsole = 12; 238 | optional FreezeServer freezeServer = 23; 239 | optional PackageChecksumRequest checksumRequest = 33; 240 | optional PackageChecksumResponse checksumResponse = 34; 241 | optional SplitPackageResponse splitPackageResponse = 35; 242 | 243 | optional C_CoordinatorListResponse c_coordinatorListResponse = 13; 244 | optional C_Provision c_provision = 14; 245 | optional C_ProvisionResponse c_provisionResponse = 15; 246 | optional C_Deprovision c_deprovision = 16; 247 | optional C_Shutdown c_shutdown = 17; 248 | optional C_Promote c_promote = 18; 249 | optional C_CreateCoordinator c_createCoordinator = 28; 250 | optional C_CoordinatorCreated c_coordinatorCreated = 19; 251 | optional C_SendInput c_sendInput = 20; 252 | optional C_AttachConsole c_attachConsole = 21; 253 | optional C_ConsoleAttached c_consoleAttached = 29; 254 | optional C_ConsoleMessage c_consoleMessage = 22; 255 | optional C_ConsoleDetached c_consoleDetached = 30; 256 | optional C_DetachConsole c_detachConsole = 31; 257 | optional C_FreezeServer c_freezeServer = 24; 258 | optional C_UploadPackage c_uploadPackage = 25; 259 | optional C_Ack c_ack = 27; 260 | optional C_PackageList c_packageList = 32; 261 | optional C_UploadSplitPackage c_uploadSplitPackage = 37; 262 | optional C_AccessDenied c_accessDenied = 38; 263 | } -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core; 2 | 3 | import io.playpen.core.coordinator.PlayPen; 4 | import io.playpen.core.coordinator.VMShutdownThread; 5 | import io.playpen.core.coordinator.client.Client; 6 | import io.playpen.core.coordinator.local.Local; 7 | import io.playpen.core.coordinator.network.Network; 8 | import io.playpen.core.utils.JarUtils; 9 | import lombok.Getter; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.net.URISyntaxException; 15 | import java.nio.file.Paths; 16 | 17 | @Log4j2 18 | public class Bootstrap { 19 | @Getter 20 | private static final int protocolVersion = 5; // update ONLY on breaking protocol changes 21 | 22 | @Getter 23 | private static File homeDir; 24 | 25 | private static boolean copyFileFromJar(String file) throws IOException, URISyntaxException { 26 | File f = Paths.get(homeDir.getPath(), file).toFile(); 27 | if(!f.exists()) { 28 | JarUtils.exportResource(Bootstrap.class, "/" + file, f.getPath()); 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | private static boolean copyFilesFromJar(String[] files) throws IOException, URISyntaxException { 36 | boolean didCopy = false; 37 | for(String file : files) { 38 | if(copyFileFromJar(file)) 39 | didCopy = true; 40 | } 41 | 42 | return didCopy; 43 | } 44 | 45 | public static void main(String[] args) { 46 | boolean didCopyResources = false; 47 | 48 | String homeDirEnv = System.getenv("PLAYPEN_HOME"); 49 | if (homeDirEnv != null) { 50 | homeDir = new File(homeDirEnv); 51 | } else { 52 | try { 53 | homeDir = new File(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile(); 54 | } catch (URISyntaxException e) { 55 | } 56 | } 57 | 58 | if(!"true".equalsIgnoreCase(System.getenv("PLAYPEN_NO_SETUP"))) { 59 | try { 60 | didCopyResources = copyFilesFromJar(new String[]{ 61 | "logging-network.xml", 62 | "logging-local.xml", 63 | "logging-cli.xml", 64 | "logging-p3.xml", 65 | "keystore.json", 66 | "packages.json", 67 | "local.json", 68 | "network.json", 69 | "playpen-network.bat", 70 | "playpen-local.bat", 71 | "playpen-cli.bat", 72 | "playpen-p3.bat", 73 | "playpen-network.sh", 74 | "playpen-local.sh", 75 | "playpen-cli.sh", 76 | "playpen-p3.sh" 77 | }); 78 | 79 | if (Paths.get(homeDir.getPath(), "cache", "packages").toFile().mkdirs()) 80 | didCopyResources = true; 81 | 82 | if (Paths.get(homeDir.getPath(), "packages").toFile().mkdirs()) 83 | didCopyResources = true; 84 | 85 | if (Paths.get(homeDir.getPath(), "assets").toFile().mkdirs()) 86 | didCopyResources = true; 87 | 88 | if (Paths.get(homeDir.getPath(), "plugins").toFile().mkdirs()) 89 | didCopyResources = true; 90 | 91 | if (Paths.get(homeDir.getPath(), "servers").toFile().mkdirs()) 92 | didCopyResources = true; 93 | 94 | if (Paths.get(homeDir.getPath(), "frozen").toFile().mkdirs()) 95 | didCopyResources = true; 96 | 97 | if (Paths.get(homeDir.getPath(), "temp").toFile().mkdirs()) 98 | didCopyResources = true; 99 | 100 | if (Paths.get(homeDir.getPath(), "server-logs").toFile().mkdirs()) 101 | didCopyResources = true; 102 | } catch (Exception e) { 103 | System.err.println("Unable to copy default resources"); 104 | e.printStackTrace(System.err); 105 | return; 106 | } 107 | } 108 | 109 | if(didCopyResources) { 110 | System.err.println("It looks like you were missing some resource files, so I've copied some defaults for you! " + 111 | "I'll give you a chance to edit them. Bye!"); 112 | return; 113 | } 114 | 115 | if(args.length < 1) { 116 | System.err.println("playpen [arguments...]"); 117 | return; 118 | } 119 | 120 | switch(args[0].toLowerCase()) { 121 | case "local": 122 | runLocalCoordinator(); 123 | break; 124 | 125 | case "network": 126 | runNetworkCoordinator(); 127 | break; 128 | 129 | case "p3": 130 | P3Tool.run(args); 131 | break; 132 | 133 | case "cli": 134 | runClient(args); 135 | break; 136 | 137 | default: 138 | System.err.println("playpen [arguments...]"); 139 | return; 140 | } 141 | } 142 | 143 | private static void runLocalCoordinator() { 144 | log.info("Bootstrap starting local coordinator (autorestart enabled)"); 145 | 146 | Runtime.getRuntime().addShutdownHook(new VMShutdownThread()); 147 | 148 | try { 149 | while(true) { 150 | //PlayPen.reset(); // DO NOT RESET PLAYPEN! If we get disconnected from the network, we don't want to 151 | // shutdown any servers that are running. We just want to reconnect to the network. 152 | if(!Local.get().run()) 153 | break; 154 | 155 | log.info("Waiting 10 seconds before restarting..."); 156 | Thread.sleep(1000L * 10L); 157 | } 158 | } 159 | catch(Exception e) { 160 | log.fatal("Caught exception at bootstrap level while running local coordinator", e); 161 | return; 162 | } 163 | 164 | log.info("Ending local coordinator session"); 165 | } 166 | 167 | private static void runNetworkCoordinator() { 168 | log.info("Bootstrap starting network coordinator (autorestart enabled)"); 169 | 170 | Runtime.getRuntime().addShutdownHook(new VMShutdownThread()); 171 | 172 | try { 173 | while (true) { 174 | PlayPen.reset(); 175 | if(!Network.get().run()) 176 | break; 177 | 178 | log.info("Waiting 10 seconds before restarting..."); 179 | Thread.sleep(1000L * 10L); 180 | } 181 | } 182 | catch(Exception e) { 183 | log.fatal("Caught exception at bootstrap level while running network coordinator", e); 184 | return; 185 | } 186 | 187 | log.info("Ending network coordinator session"); 188 | } 189 | 190 | private static void runClient(String[] arguments) { 191 | log.info("Bootstrap starting client"); 192 | 193 | Runtime.getRuntime().addShutdownHook(new VMShutdownThread()); 194 | 195 | try { 196 | Client.get().run(arguments); 197 | } 198 | catch(Exception e) { 199 | log.fatal("Caught exception at bootstrap level while running client", e); 200 | return; 201 | } 202 | 203 | log.info("Ending client session"); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/plugin/PluginManager.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.plugin; 2 | 3 | import io.playpen.core.Bootstrap; 4 | import lombok.Getter; 5 | import lombok.extern.log4j.Log4j2; 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | import org.zeroturnaround.zip.ZipUtil; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.net.MalformedURLException; 14 | import java.net.URL; 15 | import java.nio.file.Files; 16 | import java.nio.file.Paths; 17 | import java.util.ArrayList; 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | @Log4j2 22 | public class PluginManager { 23 | @Getter 24 | private Map plugins = new ConcurrentHashMap<>(); 25 | 26 | public boolean loadPlugins() { 27 | if(!plugins.isEmpty()) { 28 | log.error("Cannot call PluginManager.loadPlugins() when there are already plugins loaded!"); 29 | return false; 30 | } 31 | 32 | File pluginsDir = Paths.get(Bootstrap.getHomeDir().getPath(), "plugins").toFile(); 33 | if(!pluginsDir.exists() || !pluginsDir.isDirectory()) { 34 | log.error("Plugins directory either does not exist or is not a file"); 35 | return false; 36 | } 37 | 38 | File[] jarFiles = pluginsDir.listFiles((dir, name) -> { 39 | return name.endsWith(".jar"); 40 | }); 41 | 42 | for(File jarFile : jarFiles) { 43 | if(!jarFile.isFile()) 44 | continue; 45 | 46 | log.debug("Attempting plugin load of " + jarFile.getPath()); 47 | if(!ZipUtil.containsEntry(jarFile, "plugin.json")) { 48 | log.warn("Jar " + jarFile.getPath() + " does not contain a plugin.json"); 49 | continue; 50 | } 51 | 52 | byte[] schemaBytes = ZipUtil.unpackEntry(jarFile, "plugin.json"); 53 | String schemaString = new String(schemaBytes); 54 | 55 | PluginSchema schema = new PluginSchema(); 56 | try { 57 | JSONObject config = new JSONObject(schemaString); 58 | schema.setId(config.getString("id")); 59 | schema.setVersion(config.getString("version")); 60 | schema.setMain(config.getString("main")); 61 | schema.setFiles(new ArrayList<>()); 62 | 63 | if (config.has("files")) { 64 | JSONArray arr = config.getJSONArray("files"); 65 | for (int i = 0; i < arr.length(); ++i) { 66 | schema.getFiles().add(arr.getString(i)); 67 | } 68 | } 69 | } 70 | catch(JSONException e) { 71 | log.warn("Unable to read plugin.json from " + jarFile.getPath(), e); 72 | continue; 73 | } 74 | 75 | if(plugins.containsKey(schema.getId())) { 76 | log.error("Multiple instances of plugin " + schema.getId() + " exist"); 77 | return false; 78 | } 79 | 80 | File pluginDir = Paths.get(pluginsDir.getPath(), schema.getId()).toFile(); 81 | if(!pluginDir.exists()) { 82 | pluginDir.mkdir(); 83 | } 84 | 85 | JSONObject config = null; 86 | 87 | if (ZipUtil.containsEntry(jarFile, "config.json")) { 88 | schema.getFiles().add("config.json"); 89 | } 90 | 91 | for (String filename : schema.getFiles()) { 92 | if (!ZipUtil.containsEntry(jarFile, filename)) { 93 | log.error("Plugin file " + filename + " for " + schema.getId() + " doesn't exist in jar"); 94 | return false; 95 | } 96 | 97 | File pluginFile = Paths.get(pluginDir.getPath(), filename).toFile(); 98 | byte[] fileBytes = null; 99 | if (!pluginFile.exists()) { 100 | fileBytes = ZipUtil.unpackEntry(jarFile, filename); 101 | try { 102 | Files.write(pluginFile.toPath(), fileBytes); 103 | } 104 | catch (IOException e) { 105 | log.error("Unable to copy " + schema.getId() + " file " + filename, e); 106 | return false; 107 | } 108 | } 109 | } 110 | 111 | if(ZipUtil.containsEntry(jarFile, "config.json")) { 112 | File configFile = Paths.get(pluginDir.getPath(), "config.json").toFile(); 113 | byte[] configBytes = null; 114 | try { 115 | configBytes = Files.readAllBytes(configFile.toPath()); 116 | } 117 | catch(IOException e) { 118 | log.error("Unable to read " + schema.getId() + " config file", e); 119 | return false; 120 | } 121 | try { 122 | config = new JSONObject(new String(configBytes)); 123 | } 124 | catch(JSONException e) { 125 | log.error("Unable to parse " + schema.getId() + " config file", e); 126 | return false; 127 | } 128 | } 129 | 130 | IPlugin instance = null; 131 | 132 | try { 133 | PluginClassLoader loader = new PluginClassLoader( 134 | new URL[]{jarFile.toURI().toURL()}); 135 | 136 | Class mainClass = Class.forName(schema.getMain(), true, loader); 137 | if(!IPlugin.class.isAssignableFrom(mainClass)) { 138 | log.warn("Main class " + schema.getMain() + " for plugin " + schema.getId() + " does not implement IPlugin"); 139 | continue; 140 | } 141 | 142 | instance = (IPlugin)mainClass.newInstance(); 143 | } 144 | catch(MalformedURLException e) { 145 | log.warn("WTF? Shouldn't happen", e); 146 | continue; 147 | } 148 | catch(ClassNotFoundException e) { 149 | log.warn("Main class " + schema.getMain() + " for plugin " + schema.getId() + " not found", e); 150 | continue; 151 | } 152 | catch(InstantiationException e) { 153 | log.warn("Unable to instantiate main class " + schema.getMain() + " for plugin " + schema.getId(), e); 154 | continue; 155 | } 156 | catch(IllegalAccessException e) { 157 | log.warn("Illegal access while instantiating main class " + schema.getMain() + " for plugin " + schema.getId(), e); 158 | continue; 159 | } 160 | 161 | if(instance == null) { // just in case 162 | log.warn("Instance of main class " + schema.getMain() + " for plugin " + schema.getId() + " is null"); 163 | continue; 164 | } 165 | 166 | instance.setSchema(schema); 167 | instance.setPluginDir(pluginDir); 168 | instance.setConfig(config); 169 | 170 | plugins.put(schema.getId(), instance); 171 | log.info("Loaded plugin " + schema.getId()); 172 | } 173 | 174 | for(IPlugin plugin : plugins.values()) { 175 | log.info("Starting plugin " + plugin.getSchema().getId()); 176 | try { 177 | if (!plugin.onStart()) { 178 | log.error("Plugin " + plugin.getSchema().getId() + " failed to start"); 179 | return false; 180 | } 181 | } 182 | catch(Exception e) { 183 | log.fatal("Exception thrown by plugin " + plugin.getSchema().getId(), e); 184 | return false; 185 | } 186 | } 187 | 188 | log.info(plugins.size() + " plugins loaded"); 189 | 190 | return true; 191 | } 192 | 193 | public void stopPlugins() { 194 | for(IPlugin plugin : plugins.values()) { 195 | try { 196 | plugin.onStop(); 197 | } 198 | catch(Exception e) { 199 | log.warn("Encountered exception while stopping plugin " + plugin.getSchema().getId(), e); 200 | } 201 | } 202 | 203 | plugins.clear(); 204 | } 205 | 206 | public IPlugin getPlugin(String id) { 207 | return plugins.get(id); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/api/APIClient.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.api; 2 | 3 | import com.google.protobuf.ByteString; 4 | import com.google.protobuf.InvalidProtocolBufferException; 5 | import io.netty.bootstrap.Bootstrap; 6 | import io.netty.channel.Channel; 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.ChannelOption; 9 | import io.netty.channel.EventLoopGroup; 10 | import io.netty.channel.nio.NioEventLoopGroup; 11 | import io.netty.channel.socket.nio.NioSocketChannel; 12 | import io.playpen.core.coordinator.CoordinatorMode; 13 | import io.playpen.core.coordinator.PlayPen; 14 | import io.playpen.core.networking.TransactionInfo; 15 | import io.playpen.core.networking.TransactionManager; 16 | import io.playpen.core.networking.netty.AuthenticatedMessageInitializer; 17 | import io.playpen.core.p3.PackageManager; 18 | import io.playpen.core.plugin.PluginManager; 19 | import io.playpen.core.protocol.Commands; 20 | import io.playpen.core.protocol.Protocol; 21 | import io.playpen.core.utils.AuthUtils; 22 | import lombok.Getter; 23 | import lombok.extern.log4j.Log4j2; 24 | 25 | import java.net.InetAddress; 26 | import java.util.concurrent.Executors; 27 | import java.util.concurrent.ScheduledExecutorService; 28 | 29 | @Log4j2 30 | public abstract class APIClient extends PlayPen { 31 | private ScheduledExecutorService scheduler = null; 32 | 33 | @Getter 34 | private Channel channel = null; 35 | 36 | protected APIClient() { 37 | super(); 38 | } 39 | 40 | public boolean start() { 41 | log.info("Starting client " + getUUID()); 42 | EventLoopGroup group = new NioEventLoopGroup(); 43 | try { 44 | scheduler = Executors.newScheduledThreadPool(1); 45 | 46 | Bootstrap b = new Bootstrap(); 47 | b.group(group) 48 | .channel(NioSocketChannel.class) 49 | .option(ChannelOption.SO_KEEPALIVE, true) 50 | .handler(new AuthenticatedMessageInitializer()); 51 | 52 | ChannelFuture f = b.connect(getNetworkIP(), getNetworkPort()).await(); 53 | 54 | if (!f.isSuccess()) { 55 | log.error("Unable to connect to network coordinator at " + getNetworkIP() + " port " + getNetworkPort()); 56 | return false; 57 | } 58 | 59 | channel = f.channel(); 60 | 61 | log.info("Connected to network coordinator at " + getNetworkIP() + " port " + getNetworkPort()); 62 | } catch (InterruptedException e) { 63 | log.warn("Operation interrupted!", e); 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public void stop() { 71 | if (scheduler != null && !scheduler.isShutdown()) { 72 | scheduler.shutdownNow(); 73 | } 74 | 75 | if (channel != null) { 76 | channel.close().syncUninterruptibly(); 77 | } 78 | } 79 | 80 | @Override 81 | public void onVMShutdown() { 82 | log.info("VM shutting down, stopping all tasks"); 83 | 84 | if (scheduler != null && !scheduler.isShutdown()) { 85 | scheduler.shutdownNow(); 86 | } 87 | 88 | if (channel != null) { 89 | channel.close().syncUninterruptibly(); 90 | } 91 | } 92 | 93 | @Override 94 | public String getServerId() { 95 | return getName(); 96 | } 97 | 98 | @Override 99 | public CoordinatorMode getCoordinatorMode() { 100 | return CoordinatorMode.CLIENT; 101 | } 102 | 103 | @Override 104 | public PackageManager getPackageManager() { 105 | return null; 106 | } 107 | 108 | @Override 109 | public PluginManager getPluginManager() { 110 | log.error("PlayPen API client does not currently support the plugin system!"); 111 | return null; 112 | } 113 | 114 | @Override 115 | public ScheduledExecutorService getScheduler() { 116 | return scheduler; 117 | } 118 | 119 | public boolean isConnected() { 120 | return channel != null && channel.isActive(); 121 | } 122 | 123 | public abstract String getName(); 124 | 125 | public abstract String getUUID(); 126 | 127 | public abstract String getKey(); 128 | 129 | public abstract InetAddress getNetworkIP(); 130 | 131 | public abstract int getNetworkPort(); 132 | 133 | @Override 134 | public boolean send(Protocol.Transaction message, String target) { 135 | if (channel == null || !channel.isActive()) { 136 | log.error("Unable to send transaction " + message.getId() + " as the channel is invalid."); 137 | return false; 138 | } 139 | 140 | if (!message.isInitialized()) { 141 | log.error("Transaction is not initialized (protobuf)"); 142 | return false; 143 | } 144 | 145 | ByteString messageBytes = message.toByteString(); 146 | byte[] encBytes = AuthUtils.encrypt(messageBytes.toByteArray(), getKey()); 147 | String hash = AuthUtils.createHash(getKey(), encBytes); 148 | messageBytes = ByteString.copyFrom(encBytes); 149 | 150 | Protocol.AuthenticatedMessage auth = Protocol.AuthenticatedMessage.newBuilder() 151 | .setUuid(getUUID()) 152 | .setVersion(io.playpen.core.Bootstrap.getProtocolVersion()) 153 | .setHash(hash) 154 | .setPayload(messageBytes) 155 | .build(); 156 | 157 | if (!auth.isInitialized()) { 158 | log.error("Message is not initialized (protobuf)"); 159 | return false; 160 | } 161 | 162 | channel.writeAndFlush(auth); 163 | return true; 164 | } 165 | 166 | @Override 167 | public boolean receive(Protocol.AuthenticatedMessage auth, Channel from) { 168 | if (!auth.getUuid().equalsIgnoreCase(getUUID()) || !AuthUtils.validateHash(auth, getKey())) { 169 | log.error("Invalid hash on message"); 170 | return false; 171 | } 172 | 173 | ByteString payload = auth.getPayload(); 174 | byte[] payloadBytes = AuthUtils.decrypt(payload.toByteArray(), getKey()); 175 | payload = ByteString.copyFrom(payloadBytes); 176 | 177 | Protocol.Transaction transaction = null; 178 | try { 179 | transaction = Protocol.Transaction.parseFrom(payload); 180 | } catch (InvalidProtocolBufferException e) { 181 | log.error("Unable to read transaction from message", e); 182 | return false; 183 | } 184 | 185 | TransactionManager.get().receive(transaction, null); 186 | return true; 187 | } 188 | 189 | @Override 190 | public boolean process(Commands.BaseCommand command, TransactionInfo info, String from) { 191 | switch(command.getType()) { 192 | default: 193 | log.error("Client cannot process command " + command.getType()); 194 | return false; 195 | 196 | case C_COORDINATOR_LIST_RESPONSE: 197 | return processListResponse(command.getCCoordinatorListResponse(), info); 198 | 199 | case C_PROVISION_RESPONSE: 200 | return processProvisionResponse(command.getCProvisionResponse(), info); 201 | 202 | case C_COORDINATOR_CREATED: 203 | return processCoordinatorCreated(command.getCCoordinatorCreated(), info); 204 | 205 | case C_CONSOLE_MESSAGE: 206 | return processConsoleMessage(command.getCConsoleMessage(), info); 207 | 208 | case C_CONSOLE_ATTACHED: 209 | return processConsoleAttached(command.getCConsoleAttached(), info); 210 | 211 | case C_DETACH_CONSOLE: 212 | return processDetachConsole(command.getCConsoleDetached(), info); 213 | 214 | case C_ACK: 215 | return processAck(command.getCAck(), info); 216 | 217 | case C_PACKAGE_LIST: 218 | return processPackageList(command.getCPackageList(), info); 219 | 220 | case C_ACCESS_DENIED: 221 | return processAccessDenied(command.getCAccessDenied(), info); 222 | 223 | case PACKAGE_RESPONSE: 224 | return processPackageResponse(command.getPackageResponse(), info); 225 | } 226 | } 227 | 228 | public abstract boolean processProvisionResponse(Commands.C_ProvisionResponse response, TransactionInfo info); 229 | public abstract boolean processCoordinatorCreated(Commands.C_CoordinatorCreated response, TransactionInfo info); 230 | public abstract boolean processConsoleMessage(Commands.C_ConsoleMessage message, TransactionInfo info); 231 | public abstract boolean processDetachConsole(Commands.C_ConsoleDetached message, TransactionInfo info); 232 | public abstract boolean processConsoleAttached(Commands.C_ConsoleAttached message, TransactionInfo info); 233 | public abstract boolean processListResponse(Commands.C_CoordinatorListResponse message, TransactionInfo info); 234 | public abstract boolean processAck(Commands.C_Ack message, TransactionInfo info); 235 | public abstract boolean processPackageList(Commands.C_PackageList message, TransactionInfo info); 236 | public abstract boolean processAccessDenied(Commands.C_AccessDenied message, TransactionInfo info); 237 | public abstract boolean processPackageResponse(Commands.PackageResponse response, TransactionInfo info); 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/p3/PackageManager.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.p3; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.playpen.core.Bootstrap; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.apache.commons.io.IOUtils; 9 | import org.json.JSONArray; 10 | import org.json.JSONException; 11 | import org.json.JSONObject; 12 | import org.zeroturnaround.zip.ZipUtil; 13 | 14 | import java.io.File; 15 | import java.io.FileOutputStream; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.util.*; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | @Log4j2 22 | public class PackageManager { 23 | private List resolvers = new ArrayList<>(); 24 | 25 | private Map packageSteps = new ConcurrentHashMap<>(); 26 | 27 | @Getter 28 | private Map packageCache = new ConcurrentHashMap<>(); 29 | 30 | private Map promoted = new ConcurrentHashMap<>(); 31 | 32 | @Getter 33 | @Setter 34 | private IPackageResolver fallbackResolver = null; 35 | 36 | public PackageManager() { 37 | try { 38 | File packagesFile = Paths.get(Bootstrap.getHomeDir().getPath(), "packages.json").toFile(); 39 | String packagesStr = new String(Files.readAllBytes(packagesFile.toPath())); 40 | JSONObject config = new JSONObject(packagesStr); 41 | JSONObject packages = config.getJSONObject("promoted"); 42 | for(String key : packages.keySet()) { 43 | promoted.put(key, packages.getString(key)); 44 | } 45 | } 46 | catch(Exception e) { 47 | log.error("Unable to read packages.json (promoted packages may not be loaded)", e); 48 | } 49 | } 50 | 51 | public String getPromotedVersion(String id) { 52 | return promoted.get(id); 53 | } 54 | 55 | public boolean isPromotedVersion(String id, String version) { 56 | return Objects.equals(version, getPromotedVersion(id)); 57 | } 58 | 59 | public synchronized boolean promote(P3Package p3) { 60 | if(!p3.isResolved()) { 61 | log.error("Cannot promote unresolved package " + p3.getId() + " at " + p3.getVersion()); 62 | return false; 63 | } 64 | 65 | if(p3.getVersion().equalsIgnoreCase("promoted")) { 66 | log.error("Cannot promote package of version 'promoted'"); 67 | return false; 68 | } 69 | 70 | log.info("Promoted " + p3.getId() + " at " + p3.getVersion()); 71 | promoted.put(p3.getId(), p3.getVersion()); 72 | 73 | try { 74 | File packagesFile = Paths.get(Bootstrap.getHomeDir().getPath(), "packages.json").toFile(); 75 | String packagesStr = new String(Files.readAllBytes(packagesFile.toPath())); 76 | JSONObject config = new JSONObject(packagesStr); 77 | JSONObject packages = config.getJSONObject("promoted"); 78 | packages.put(p3.getId(), p3.getVersion()); 79 | config.put("promoted", packages); 80 | String jsonStr = config.toString(2); 81 | try (FileOutputStream out = new FileOutputStream(packagesFile)) { 82 | IOUtils.write(jsonStr, out); 83 | } 84 | 85 | log.info("Saved packages.json"); 86 | } 87 | catch(Exception e) { 88 | log.error("Unable to save promoted packages", e); 89 | } 90 | 91 | return true; 92 | } 93 | 94 | public void addPackageResolver(IPackageResolver resolver) { 95 | resolvers.add(resolver); 96 | } 97 | 98 | public void addPackageStep(IPackageStep step) { 99 | packageSteps.put(step.getStepId(), step); 100 | } 101 | 102 | public IPackageStep getPackageStep(String id) { 103 | return packageSteps.get(id); 104 | } 105 | 106 | public P3Package resolve(String id, String version) { 107 | return resolve(id, version, true); 108 | } 109 | 110 | public P3Package resolve(String id, String version, boolean allowFallback) { 111 | log.info("Attempting package resolution for " + id + " at " + version); 112 | P3Package p3 = null; 113 | for(IPackageResolver resolver : resolvers) { 114 | p3 = resolver.resolvePackage(this, id, version); 115 | if(p3 != null) { 116 | log.info("Package resolved by " + resolver.getClass().getName()); 117 | if(!p3.validate()) { 118 | log.warn("Package failed to validate. Continuing resolution!"); 119 | continue; 120 | } 121 | 122 | P3Package.P3PackageInfo info = new P3Package.P3PackageInfo(); 123 | info.setId(p3.getId()); 124 | info.setVersion(p3.getVersion()); 125 | packageCache.put(info, p3); 126 | 127 | return p3; 128 | } 129 | } 130 | 131 | if(allowFallback && fallbackResolver != null) { 132 | p3 = fallbackResolver.resolvePackage(this, id, version); 133 | if(p3 != null) { 134 | log.info("Package resolved by fallback " + fallbackResolver.getClass().getName()); 135 | if(!p3.validate()) { 136 | log.warn("Package failed to validate!"); 137 | return null; 138 | } 139 | 140 | // fallbacks do not add to the package cache 141 | 142 | return p3; 143 | } 144 | } 145 | 146 | log.error("Package could not be resolved!"); 147 | return null; 148 | } 149 | 150 | public Set getPackageList() { 151 | Set packages = new HashSet<>(); 152 | for(IPackageResolver resolver : resolvers) { 153 | Collection resolverPackages = resolver.getPackageList(this); 154 | if(resolverPackages != null) { 155 | packages.addAll(resolverPackages); 156 | } 157 | } 158 | 159 | return packages; 160 | } 161 | 162 | public P3Package readSchema(String schema) throws PackageException { 163 | try { 164 | return readSchema(new JSONObject(schema)); 165 | } 166 | catch(JSONException e) { 167 | throw new PackageException("Invalid package schema", e); 168 | } 169 | } 170 | 171 | public P3Package readSchema(JSONObject schema) throws PackageException { 172 | try { 173 | JSONObject meta = schema.optJSONObject("package"); 174 | if (meta == null) 175 | throw new PackageException("Schema is invalid (no package metadata)"); 176 | 177 | P3Package p3 = new P3Package(); 178 | p3.setResolved(true); 179 | p3.setId(meta.optString("id")); 180 | p3.setVersion(meta.optString("version")); 181 | 182 | JSONArray deps = meta.optJSONArray("depends"); 183 | if (deps != null) { 184 | for(int i = 0; i < deps.length(); ++i) { 185 | JSONObject dep = deps.getJSONObject(i); 186 | P3Package depP3 = new P3Package(); 187 | depP3.setResolved(false); 188 | depP3.setId(dep.optString("id")); 189 | depP3.setVersion(dep.optString("version")); 190 | p3.getDependencies().add(depP3); 191 | } 192 | } 193 | 194 | JSONObject resources = meta.optJSONObject("resources"); 195 | if (resources != null) { 196 | for (String key : resources.keySet()) { 197 | p3.getResources().put(key, resources.optInt(key)); 198 | } 199 | } 200 | 201 | JSONArray attributes = meta.optJSONArray("requires"); 202 | if (attributes != null) { 203 | for (int i = 0; i < attributes.length(); ++i) { 204 | p3.getAttributes().add(attributes.optString(i)); 205 | } 206 | } 207 | 208 | JSONObject strings = schema.optJSONObject("strings"); 209 | if (strings != null) { 210 | for (String key : strings.keySet()) { 211 | p3.getStrings().put(key, strings.optString(key)); 212 | } 213 | } 214 | 215 | JSONArray provision = schema.optJSONArray("provision"); 216 | if (provision != null) { 217 | for (int i = 0; i < provision.length(); ++i) { 218 | JSONObject obj = provision.optJSONObject(i); 219 | if (obj != null) { 220 | String id = obj.optString("id"); 221 | IPackageStep step = getPackageStep(id); 222 | if (step == null) 223 | throw new PackageException("Unknown package step \"" + id + "\""); 224 | 225 | P3Package.PackageStepConfig config = new P3Package.PackageStepConfig(); 226 | config.setStep(step); 227 | config.setConfig(obj); 228 | p3.getProvisionSteps().add(config); 229 | } 230 | else { 231 | throw new PackageException("Provision step #" + i + " is not an object!"); 232 | } 233 | } 234 | } 235 | 236 | JSONArray execute = schema.optJSONArray("execute"); 237 | if (execute != null) { 238 | for(int i = 0; i < execute.length(); ++i) { 239 | JSONObject obj = execute.optJSONObject(i); 240 | if(obj != null) { 241 | String id = obj.optString("id"); 242 | IPackageStep step = getPackageStep(id); 243 | if (step == null) 244 | throw new PackageException("Unknown package step \"" + id + "\""); 245 | 246 | P3Package.PackageStepConfig config = new P3Package.PackageStepConfig(); 247 | config.setStep(step); 248 | config.setConfig(obj); 249 | p3.getExecutionSteps().add(config); 250 | } 251 | } 252 | } 253 | 254 | JSONArray shutdown = schema.optJSONArray("shutdown"); 255 | if (shutdown != null) { 256 | for(int i = 0; i < shutdown.length(); ++i) { 257 | JSONObject obj = shutdown.optJSONObject(i); 258 | if(obj != null) { 259 | String id = obj.optString("id"); 260 | IPackageStep step = getPackageStep(id); 261 | if (step == null) 262 | throw new PackageException("Unknown package step \"" + id + "\""); 263 | 264 | P3Package.PackageStepConfig config = new P3Package.PackageStepConfig(); 265 | config.setStep(step); 266 | config.setConfig(obj); 267 | p3.getShutdownSteps().add(config); 268 | } 269 | } 270 | } 271 | 272 | if (!p3.validate()) { 273 | throw new PackageException("Package validation failed (check id, version, and dependency metadata)!"); 274 | } 275 | 276 | return p3; 277 | } 278 | catch(JSONException e) { 279 | throw new PackageException("Invalid package schema", e); 280 | } 281 | } 282 | 283 | public P3Package readPackage(File file) throws PackageException { 284 | try { 285 | if (!ZipUtil.containsEntry(file, "package.json")) { 286 | throw new PackageException("No package schema found"); 287 | } 288 | 289 | byte[] schemaBytes = ZipUtil.unpackEntry(file, "package.json"); 290 | String schemaString = new String(schemaBytes); 291 | 292 | JSONObject schema = new JSONObject(schemaString); 293 | P3Package p3 = readSchema(schema); 294 | p3.setLocalPath(file.getPath()); 295 | 296 | return p3; 297 | } 298 | catch(JSONException e) { 299 | throw new PackageException("Invalid package schema", e); 300 | } 301 | } 302 | 303 | public Map buildProperties(P3Package initialP3) { 304 | Map props = new HashMap<>(); 305 | List chain = resolveDependencyChain(initialP3); 306 | if (chain == null) 307 | return props; 308 | 309 | for (P3Package p3 : chain) { 310 | props.putAll(p3.getStrings()); 311 | } 312 | 313 | return props; 314 | } 315 | 316 | public boolean execute(ExecutionType type, P3Package initialP3, File destination, Map properties, Object user) { 317 | log.info("Executing " + type + " " + initialP3.getId() + " (" + initialP3.getVersion() + ")"); 318 | PackageContext ctx = new PackageContext(); 319 | ctx.setUser(user); 320 | ctx.setDestination(destination); 321 | ctx.setPackageManager(this); 322 | ctx.setDependencyChain(resolveDependencyChain(initialP3)); 323 | if(ctx.getDependencyChain() == null) { 324 | log.error("Unable to resolve dependency chain for " + initialP3.getId() + " (" + initialP3.getVersion() + ")"); 325 | return false; 326 | } 327 | 328 | for(P3Package p3 : ctx.getDependencyChain()) { 329 | ctx.getResources().putAll(p3.getResources()); 330 | } 331 | 332 | ctx.getProperties().putAll(properties); 333 | 334 | for(P3Package p3 : ctx.getDependencyChain()) { 335 | List steps = null; 336 | switch(type) 337 | { 338 | case PROVISION: 339 | steps = p3.getProvisionSteps(); 340 | break; 341 | 342 | case EXECUTE: 343 | steps = p3.getExecutionSteps(); 344 | break; 345 | 346 | case SHUTDOWN: 347 | steps = p3.getShutdownSteps(); 348 | break; 349 | } 350 | 351 | for(P3Package.PackageStepConfig config : steps) { 352 | log.info("package step - " + config.getStep().getStepId()); 353 | if(!config.getStep().runStep(p3, ctx, config.getConfig())) { 354 | log.error("Step failed!"); 355 | return false; 356 | } 357 | } 358 | } 359 | 360 | return true; 361 | } 362 | 363 | private List resolveDependencyChain(P3Package initialP3) 364 | { 365 | log.info("Building dependency chain for " + initialP3.getId() + " (" + initialP3.getVersion() + ")"); 366 | List chain = new LinkedList<>(); 367 | Queue toResolve = new LinkedList<>(); 368 | Set resolved = new HashSet<>(); 369 | 370 | toResolve.add(initialP3); 371 | while(!toResolve.isEmpty()) { 372 | P3Package p3 = toResolve.remove(); 373 | P3Package.P3PackageInfo info = new P3Package.P3PackageInfo(); 374 | info.setId(p3.getId()); 375 | info.setVersion(p3.getVersion()); 376 | if(resolved.contains(info)) { 377 | log.error("Multiple dependency found with " + info.getId() + " (" + info.getVersion() + ")"); 378 | return null; // TODO: Figure out if there's actually a circular dependency 379 | } 380 | 381 | if(!p3.isResolved()) 382 | p3 = resolve(p3.getId(), p3.getVersion()); 383 | 384 | if(p3 == null) { 385 | log.error("Unable to resolve " + info.getId() + " (" + info.getVersion() + ")"); 386 | return null; 387 | } 388 | 389 | chain.add(p3); 390 | resolved.add(info); 391 | toResolve.addAll(p3.getDependencies()); 392 | } 393 | 394 | log.info("Dependency chain resolution complete: " + chain.size() + " packages"); 395 | 396 | return Lists.reverse(chain); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/main/java/io/playpen/core/coordinator/client/Client.java: -------------------------------------------------------------------------------- 1 | package io.playpen.core.coordinator.client; 2 | 3 | import com.google.protobuf.ByteString; 4 | import com.google.protobuf.InvalidProtocolBufferException; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFuture; 7 | import io.netty.channel.ChannelOption; 8 | import io.netty.channel.EventLoopGroup; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.nio.NioSocketChannel; 11 | import io.playpen.core.Bootstrap; 12 | import io.playpen.core.Initialization; 13 | import io.playpen.core.coordinator.CoordinatorMode; 14 | import io.playpen.core.coordinator.PlayPen; 15 | import io.playpen.core.networking.TransactionInfo; 16 | import io.playpen.core.networking.TransactionManager; 17 | import io.playpen.core.networking.netty.AuthenticatedMessageInitializer; 18 | import io.playpen.core.p3.P3Package; 19 | import io.playpen.core.p3.PackageException; 20 | import io.playpen.core.p3.PackageManager; 21 | import io.playpen.core.plugin.PluginManager; 22 | import io.playpen.core.protocol.Commands; 23 | import io.playpen.core.protocol.Coordinator; 24 | import io.playpen.core.protocol.P3; 25 | import io.playpen.core.protocol.Protocol; 26 | import io.playpen.core.utils.AbortableCountDownLatch; 27 | import io.playpen.core.utils.AuthUtils; 28 | import lombok.Getter; 29 | import lombok.Setter; 30 | import lombok.extern.log4j.Log4j2; 31 | import org.apache.logging.log4j.Level; 32 | import org.json.JSONObject; 33 | 34 | import java.io.File; 35 | import java.io.FileInputStream; 36 | import java.io.IOException; 37 | import java.io.InputStream; 38 | import java.net.InetAddress; 39 | import java.nio.file.Files; 40 | import java.nio.file.Paths; 41 | import java.util.*; 42 | import java.util.concurrent.CountDownLatch; 43 | import java.util.concurrent.Executors; 44 | import java.util.concurrent.ScheduledExecutorService; 45 | import java.util.regex.Pattern; 46 | 47 | /** 48 | * Client is basically a "light" version of a local coordinator. It implements a local coordinator that 49 | * can never be enabled, and doesn't accept anything but client commands. 50 | */ 51 | @Log4j2 52 | public class Client extends PlayPen { 53 | public static Client get() { 54 | if(PlayPen.get() == null) { 55 | new Client(); 56 | } 57 | 58 | return (Client)PlayPen.get(); 59 | } 60 | 61 | private ScheduledExecutorService scheduler = null; 62 | 63 | @Getter 64 | private String coordName; 65 | 66 | @Getter 67 | private String uuid; 68 | 69 | @Getter 70 | private String key; 71 | 72 | @Getter 73 | private Channel channel = null; 74 | 75 | @Getter 76 | private ClientMode clientMode = ClientMode.NONE; 77 | 78 | private Commands.C_CoordinatorListResponse coordList = null; 79 | 80 | private AttachInputListenThread ailThread = null; 81 | 82 | private AbortableCountDownLatch latch = null; 83 | 84 | private int acks = 0; 85 | 86 | private Client() { 87 | super(); 88 | } 89 | 90 | protected void printHelpText() { 91 | System.err.println("playpen cli [arguments...]"); 92 | System.err.println("Commands: list, provision, deprovision, shutdown, promote, generate-keypair, send, attach, " + 93 | "freeze, upload"); 94 | } 95 | 96 | public void run(String[] arguments) { 97 | if(arguments.length < 2) { 98 | printHelpText(); 99 | return; 100 | } 101 | 102 | log.info("Starting PlayPen Client"); 103 | 104 | log.info("Reading local configuration"); 105 | String configStr; 106 | try { 107 | configStr = new String(Files.readAllBytes(Paths.get(Bootstrap.getHomeDir().getPath(), "local.json"))); 108 | } 109 | catch(IOException e) { 110 | log.fatal("Unable to read configuration file", e); 111 | return; 112 | } 113 | 114 | InetAddress coordIp = null; 115 | int coordPort = 0; 116 | 117 | try { 118 | JSONObject config = new JSONObject(configStr); 119 | coordName = config.getString("name"); 120 | uuid = config.getString("uuid"); 121 | key = config.getString("key"); 122 | coordIp = InetAddress.getByName(config.getString("coord-ip")); 123 | coordPort = config.getInt("coord-port"); 124 | } 125 | catch(Exception e) { 126 | log.fatal("Unable to read configuration file", e); 127 | return; 128 | } 129 | 130 | if(uuid == null || uuid.isEmpty() || key == null || key.isEmpty()) { 131 | log.fatal("No UUID or secret key specified in local.json"); 132 | return; 133 | } 134 | 135 | if(coordName == null) { 136 | log.warn("No coordinator name specified in local.json"); // not fatal 137 | } 138 | 139 | log.info("Starting client " + uuid); 140 | EventLoopGroup group = new NioEventLoopGroup(); 141 | try { 142 | scheduler = Executors.newScheduledThreadPool(1); 143 | 144 | io.netty.bootstrap.Bootstrap b = new io.netty.bootstrap.Bootstrap(); 145 | b.group(group) 146 | .channel(NioSocketChannel.class) 147 | .option(ChannelOption.SO_KEEPALIVE, true) 148 | .handler(new AuthenticatedMessageInitializer()); 149 | 150 | ChannelFuture f = b.connect(coordIp, coordPort).await(); 151 | 152 | if(!f.isSuccess()) { 153 | log.error("Unable to connect to network coordinator at " + coordIp + " port " + coordPort); 154 | System.err.println("Unable to connect to network coordinator at " + coordIp + " port " + coordPort); 155 | return; 156 | } 157 | 158 | channel = f.channel(); 159 | 160 | log.info("Connected to network coordinator at " + coordIp + " port " + coordPort); 161 | 162 | runCommand(arguments); 163 | 164 | f.channel().closeFuture().sync(); 165 | } 166 | catch(InterruptedException e) { 167 | log.warn("Operation interrupted!", e); 168 | return; 169 | } 170 | finally { 171 | scheduler.shutdownNow(); 172 | scheduler = null; 173 | 174 | if(ailThread != null && ailThread.isAlive()) { 175 | ailThread.setActive(false); 176 | ailThread.interrupt(); 177 | } 178 | 179 | group.shutdownGracefully(); 180 | } 181 | 182 | System.exit(0); 183 | } 184 | 185 | @Override 186 | public void onVMShutdown() { 187 | log.info("VM shutting down, stopping all tasks"); 188 | 189 | if(clientMode == ClientMode.ATTACH) { 190 | sendDetachConsole(); 191 | } 192 | 193 | if(scheduler != null && !scheduler.isShutdown()) { 194 | scheduler.shutdownNow(); 195 | } 196 | 197 | if(ailThread != null && ailThread.isAlive()) { 198 | ailThread.setActive(false); 199 | ailThread.interrupt(); 200 | } 201 | 202 | if(channel != null) { 203 | channel.close().syncUninterruptibly(); 204 | } 205 | } 206 | 207 | @Override 208 | public String getServerId() { 209 | return coordName; 210 | } 211 | 212 | @Override 213 | public CoordinatorMode getCoordinatorMode() { 214 | return CoordinatorMode.CLIENT; 215 | } 216 | 217 | @Override 218 | public PackageManager getPackageManager() { 219 | return null; 220 | } 221 | 222 | @Override 223 | public PluginManager getPluginManager() { 224 | log.error("PlayPen client does not currently support the plugin system!"); 225 | return null; 226 | } 227 | 228 | @Override 229 | public ScheduledExecutorService getScheduler() { 230 | return scheduler; 231 | } 232 | 233 | @Override 234 | public boolean send(Protocol.Transaction message, String target) { 235 | if(channel == null || !channel.isActive()) { 236 | log.error("Unable to send transaction " + message.getId() + " as the channel is invalid."); 237 | return false; 238 | } 239 | 240 | if(!message.isInitialized()) { 241 | log.error("Transaction is not initialized (protobuf)"); 242 | return false; 243 | } 244 | 245 | ByteString messageBytes = message.toByteString(); 246 | byte[] encBytes = AuthUtils.encrypt(messageBytes.toByteArray(), getKey()); 247 | String hash = AuthUtils.createHash(getKey(), encBytes); 248 | messageBytes = ByteString.copyFrom(encBytes); 249 | 250 | Protocol.AuthenticatedMessage auth = Protocol.AuthenticatedMessage.newBuilder() 251 | .setUuid(getUuid()) 252 | .setVersion(Bootstrap.getProtocolVersion()) 253 | .setHash(hash) 254 | .setPayload(messageBytes) 255 | .build(); 256 | 257 | if(!auth.isInitialized()) { 258 | log.error("Message is not initialized (protobuf)"); 259 | return false; 260 | } 261 | 262 | channel.writeAndFlush(auth); 263 | return true; 264 | } 265 | 266 | @Override 267 | public boolean receive(Protocol.AuthenticatedMessage auth, Channel from) { 268 | if(!auth.getUuid().equals(uuid) || !AuthUtils.validateHash(auth, key)) { 269 | log.error("Invalid hash on message"); 270 | System.err.println("Received an invalid hash on a message from the network coordinator."); 271 | System.err.println("This is likely due to us having an invalid UUID or secret key. Please check your local.json!"); 272 | from.close(); 273 | 274 | if (latch != null) 275 | latch.abort(); 276 | 277 | return false; 278 | } 279 | 280 | ByteString payload = auth.getPayload(); 281 | byte[] payloadBytes = AuthUtils.decrypt(payload.toByteArray(), key); 282 | payload = ByteString.copyFrom(payloadBytes); 283 | 284 | Protocol.Transaction transaction = null; 285 | try { 286 | transaction = Protocol.Transaction.parseFrom(payload); 287 | } 288 | catch(InvalidProtocolBufferException e) { 289 | log.error("Unable to read transaction from message", e); 290 | System.err.println("Received an unreadable message from the network coordinator."); 291 | System.err.println("This is likely due to us having an invalid UUID or secret key. Please check your local.json!"); 292 | from.close(); 293 | 294 | if (latch != null) 295 | latch.abort(); 296 | 297 | return false; 298 | } 299 | 300 | TransactionManager.get().receive(transaction, null); 301 | return true; 302 | } 303 | 304 | @Override 305 | public boolean process(Commands.BaseCommand command, TransactionInfo info, String from) { 306 | switch(command.getType()) { 307 | default: 308 | log.error("Client cannot process command " + command.getType()); 309 | return false; 310 | 311 | case C_COORDINATOR_LIST_RESPONSE: 312 | return processListResponse(command.getCCoordinatorListResponse(), info); 313 | 314 | case C_PROVISION_RESPONSE: 315 | return processProvisionResponse(command.getCProvisionResponse(), info); 316 | 317 | case C_COORDINATOR_CREATED: 318 | return processCoordinatorCreated(command.getCCoordinatorCreated(), info); 319 | 320 | case C_CONSOLE_MESSAGE: 321 | return processConsoleMessage(command.getCConsoleMessage(), info); 322 | 323 | case C_DETACH_CONSOLE: 324 | return processDetachConsole(info); 325 | 326 | case C_ACK: 327 | return processAck(command.getCAck(), info); 328 | } 329 | } 330 | 331 | protected void runCommand(String[] arguments) { 332 | if(arguments.length < 2) { 333 | printHelpText(); 334 | channel.close(); 335 | return; 336 | } 337 | 338 | if(!sendSync()) { 339 | log.error("Unable to SYNC"); 340 | channel.close(); 341 | return; 342 | } 343 | 344 | switch(arguments[1].toLowerCase()) { 345 | case "list": 346 | runListCommand(arguments); 347 | break; 348 | 349 | case "provision": 350 | runProvisionCommand(arguments); 351 | break; 352 | 353 | case "deprovision": 354 | runDeprovisionCommand(arguments); 355 | break; 356 | 357 | case "shutdown": 358 | runShutdownCommand(arguments); 359 | break; 360 | 361 | case "promote": 362 | runPromoteCommand(arguments); 363 | break; 364 | 365 | case "generate-keypair": 366 | runGenerateKeypairCommand(arguments); 367 | break; 368 | 369 | case "send": 370 | runSendCommand(arguments); 371 | break; 372 | 373 | case "attach": 374 | runAttachCommand(arguments); 375 | break; 376 | 377 | case "freeze": 378 | runFreezeCommand(arguments); 379 | break; 380 | 381 | case "upload": 382 | runUploadCommand(arguments); 383 | break; 384 | 385 | default: 386 | printHelpText(); 387 | channel.close(); 388 | break; 389 | } 390 | } 391 | 392 | protected void runListCommand(String[] arguments) { 393 | if(arguments.length != 2) { 394 | System.err.println("list"); 395 | System.err.println("Retrieves and displays the list of all active coordinators, their configurations, and active servers."); 396 | channel.close(); 397 | return; 398 | } 399 | 400 | clientMode = ClientMode.LIST; 401 | if(!sendListRequest()) { 402 | log.error("Unable to send list request to coordinator"); 403 | System.err.println("Unable to send list request to coordinator"); 404 | channel.close(); 405 | return; 406 | } 407 | 408 | System.out.println("Retrieving coordinator list..."); 409 | } 410 | 411 | protected void runProvisionCommand(String[] arguments) { 412 | if(arguments.length < 3) { 413 | System.err.println("provision [properties...]"); 414 | System.err.println("Provisions a server on the network."); 415 | System.err.println("The property 'version' will specify the version of the package (default: promoted)"); 416 | System.err.println("The property 'coordinator' will specify which coordinator to provision on."); 417 | System.err.println("The property 'name' will specify the name of the server."); 418 | channel.close(); 419 | return; 420 | } 421 | 422 | clientMode = ClientMode.PROVISION; 423 | 424 | String id = arguments[2]; 425 | String version = "promoted"; 426 | String coordinator = null; 427 | String serverName = null; 428 | Map properties = new HashMap<>(); 429 | 430 | for(int i = 3; i < arguments.length; i += 2) { 431 | if(i + 1 >= arguments.length) { 432 | System.err.println("Properties must be in the form "); 433 | channel.close(); 434 | return; 435 | } 436 | 437 | String key = arguments[i]; 438 | String value = arguments[i+1]; 439 | 440 | String lowerKey = key.trim().toLowerCase(); 441 | switch(lowerKey) { 442 | case "version": 443 | version = value; 444 | break; 445 | 446 | case "coordinator": 447 | coordinator = value; 448 | break; 449 | 450 | case "name": 451 | serverName = value; 452 | break; 453 | 454 | default: 455 | properties.put(key, value); 456 | break; 457 | } 458 | } 459 | 460 | if(!sendProvision(id, version, coordinator, serverName, properties)) { 461 | log.error("Unable to send provision to network"); 462 | System.err.println("Unable to send provision to network"); 463 | channel.close(); 464 | return; 465 | } 466 | 467 | System.out.println("Waiting for provision response..."); 468 | } 469 | 470 | protected void runDeprovisionCommand(String[] arguments) { 471 | if(arguments.length != 4 && arguments.length != 5) { 472 | System.err.println("deprovision [force=false]"); 473 | System.err.println("Deprovisions a server from the network. Coordinator and server accept regex."); 474 | System.err.println("For safety, all regex will have ^ prepended and $ appended."); 475 | channel.close(); 476 | return; 477 | } 478 | 479 | clientMode = ClientMode.DEPROVISION; 480 | 481 | Pattern coordPattern = Pattern.compile('^' + arguments[2] + '$'); 482 | Pattern serverPattern = Pattern.compile('^' + arguments[3] + '$'); 483 | boolean force = arguments.length == 5 && (arguments[4].trim().toLowerCase().equals("true")); 484 | 485 | System.out.println("Retrieving coordinator list..."); 486 | if(!blockUntilCoordList()) { 487 | System.err.println("Operation cancelled!"); 488 | channel.close(); 489 | return; 490 | } 491 | 492 | if(force) { 493 | System.out.println("NOTE: forcing deprovision operation"); 494 | } 495 | 496 | Map> servers = getServersFromList(coordPattern, serverPattern); 497 | if(servers.isEmpty()) { 498 | System.err.println("No coordinators/servers match patterns given."); 499 | channel.close(); 500 | return; 501 | } 502 | 503 | System.out.println("Sending deprovision operations..."); 504 | int count = 0; 505 | for(Map.Entry> entry : servers.entrySet()) { 506 | String coordId = entry.getKey(); 507 | System.out.println("Coordinator " + coordId + ":"); 508 | for(String serverId : entry.getValue()) { 509 | if(sendDeprovision(coordId, serverId, force)) { 510 | System.out.println("\tSent deprovision of " + serverId); 511 | count++; 512 | } 513 | else { 514 | System.err.println("\tUnable to send deprovision of " + serverId); 515 | } 516 | } 517 | } 518 | 519 | System.out.println("Operation completed, waiting for ack..."); 520 | latch = new AbortableCountDownLatch(count - acks); 521 | try { 522 | latch.await(); 523 | } 524 | catch(InterruptedException e) {} 525 | channel.close(); 526 | } 527 | 528 | protected void runShutdownCommand(String[] arguments) { 529 | if(arguments.length != 3) { 530 | System.err.println("shutdown "); 531 | System.err.println("Shuts down a coordinator and any related servers."); 532 | channel.close(); 533 | return; 534 | } 535 | 536 | clientMode = ClientMode.SHUTDOWN; 537 | 538 | String coordId = arguments[2]; 539 | 540 | if(sendShutdown(coordId)) { 541 | System.out.println("Sent shutdown to network, waiting for ack..."); 542 | } 543 | else { 544 | System.err.println("Unable to send shutdown to network"); 545 | channel.close(); 546 | return; 547 | } 548 | 549 | latch = new AbortableCountDownLatch(1 - acks); 550 | try { 551 | latch.await(); 552 | } 553 | catch(InterruptedException e) {} 554 | channel.close(); 555 | } 556 | 557 | protected void runPromoteCommand(String[] arguments) { 558 | if(arguments.length != 4) { 559 | System.err.println("promote "); 560 | System.err.println("Promotes a package."); 561 | channel.close(); 562 | return; 563 | } 564 | 565 | clientMode = ClientMode.PROMOTE; 566 | 567 | String id = arguments[2]; 568 | String version = arguments[3]; 569 | if(version.equalsIgnoreCase("promoted")) { 570 | System.err.println("Cannot promote a package of version 'promoted'"); 571 | channel.close(); 572 | return; 573 | } 574 | 575 | if(sendPromote(id, version)) { 576 | System.out.println("Sent promote to network, waiting for ack..."); 577 | } 578 | else { 579 | System.err.println("Unable to send promote to network"); 580 | channel.close(); 581 | return; 582 | } 583 | 584 | latch = new AbortableCountDownLatch(1 - acks); 585 | try { 586 | latch.await(); 587 | } 588 | catch(InterruptedException e) {} 589 | channel.close(); 590 | } 591 | 592 | protected void runGenerateKeypairCommand(String[] arguments) { 593 | if(arguments.length != 2 && arguments.length != 3) { 594 | System.err.println("generate-keypair [keyname]"); 595 | System.err.println("Generates a new coordinator keypair"); 596 | channel.close(); 597 | return; 598 | } 599 | 600 | clientMode = ClientMode.GENERATE_KEYPAIR; 601 | 602 | if(sendCreateCoordinator(arguments.length == 3 ? arguments[2] : null)) { 603 | System.out.println("Requesting generation of new keypair..."); 604 | // don't close channel, just wait 605 | } 606 | else { 607 | System.err.println("Unable to send generation request to network"); 608 | channel.close(); 609 | } 610 | } 611 | 612 | protected void runSendCommand(String[] arguments) { 613 | if(arguments.length != 5) { 614 | System.err.println("send "); 615 | System.err.println("Sends a command to the console of a server."); 616 | System.err.println("Coordinator and server accept regex."); 617 | System.err.println("For safety, all regex will have ^ prepended and $ appended."); 618 | channel.close(); 619 | return; 620 | } 621 | 622 | clientMode = ClientMode.SEND_INPUT; 623 | 624 | Pattern coordPattern = Pattern.compile('^' + arguments[2] + '$'); 625 | Pattern serverPattern = Pattern.compile('^' + arguments[3] + "$"); 626 | String input = arguments[4] + '\n'; 627 | 628 | System.out.println("Retrieving coordinator list..."); 629 | if(!blockUntilCoordList()) { 630 | System.err.println("Operation cancelled!"); 631 | channel.close(); 632 | return; 633 | } 634 | 635 | Map> servers = getServersFromList(coordPattern, serverPattern); 636 | if(servers.isEmpty()) { 637 | System.err.println("No coordinators/servers match patterns given."); 638 | channel.close(); 639 | return; 640 | } 641 | 642 | System.out.println("Sending input operations..."); 643 | int count = 0; 644 | for(Map.Entry> entry : servers.entrySet()) { 645 | String coordId = entry.getKey(); 646 | System.out.println("Coordinator " + coordId + ":"); 647 | for(String serverId : entry.getValue()) { 648 | if(sendInput(coordId, serverId, input)) { 649 | System.out.println("\tSent input to " + serverId); 650 | count++; 651 | } 652 | else { 653 | System.err.println("\tUnable to send input to " + serverId); 654 | } 655 | } 656 | } 657 | 658 | System.out.println("Operation completed, waiting for ack..."); 659 | latch = new AbortableCountDownLatch(count - acks); 660 | try { 661 | latch.await(); 662 | } 663 | catch(InterruptedException e) {} 664 | channel.close(); 665 | } 666 | 667 | protected void runAttachCommand(String[] arguments) { 668 | if(arguments.length != 4) { 669 | System.err.println("attach "); 670 | System.err.println("Attaches to the console of the specified server"); 671 | System.err.println("NOTE: Regex is not supported by this command."); 672 | channel.close(); 673 | return; 674 | } 675 | 676 | clientMode = ClientMode.ATTACH; 677 | 678 | String coordId = arguments[2]; 679 | String serverId = arguments[3]; 680 | 681 | // hacky fix to prevent double-printing messages 682 | sendDetachConsole(); 683 | 684 | if(!sendAttachConsole(coordId, serverId)) { 685 | System.err.println("Unable to send attach command. Exiting."); 686 | channel.close(); 687 | return; 688 | } 689 | else { 690 | System.out.println("Attaching console..."); 691 | ailThread = new AttachInputListenThread(coordId, serverId); 692 | ailThread.start(); 693 | } 694 | } 695 | 696 | protected void runFreezeCommand(String[] arguments) { 697 | if(arguments.length != 4) { 698 | System.err.println("freeze "); 699 | System.err.println("Marks a server to be saved after it shuts down."); 700 | System.err.println("Coordinator and server accept regex."); 701 | System.err.println("For safety, all regex will have ^ prepended and $ appended."); 702 | channel.close(); 703 | return; 704 | } 705 | 706 | clientMode = ClientMode.FREEZE; 707 | 708 | Pattern coordPattern = Pattern.compile('^' + arguments[2] + '$'); 709 | Pattern serverPattern = Pattern.compile('^' + arguments[3] + "$"); 710 | 711 | System.out.println("Retrieving coordinator list..."); 712 | if(!blockUntilCoordList()) { 713 | System.err.println("Operation cancelled!"); 714 | channel.close(); 715 | return; 716 | } 717 | 718 | Map> servers = getServersFromList(coordPattern, serverPattern); 719 | if(servers.isEmpty()) { 720 | System.err.println("No coordinators/servers match patterns given."); 721 | channel.close(); 722 | return; 723 | } 724 | 725 | System.out.println("Sending freeze operations..."); 726 | int count = 0; 727 | for(Map.Entry> entry : servers.entrySet()) { 728 | String coordId = entry.getKey(); 729 | System.out.println("Coordinator " + coordId + ":"); 730 | for(String serverId : entry.getValue()) { 731 | if(sendFreezeServer(coordId, serverId)) { 732 | System.out.println("\tSent freeze to " + serverId); 733 | count++; 734 | } 735 | else { 736 | System.err.println("\tUnable to send freeze to " + serverId); 737 | } 738 | } 739 | } 740 | 741 | System.out.println("Operation completed, waiting for ack..."); 742 | latch = new AbortableCountDownLatch(count - acks); 743 | try { 744 | latch.await(); 745 | } 746 | catch(InterruptedException e) {} 747 | channel.close(); 748 | } 749 | 750 | protected void runUploadCommand(String[] arguments) { 751 | if(arguments.length < 3) { 752 | System.err.println("upload "); 753 | System.err.println("Upload packages to the network, expiring the cache if needed."); 754 | channel.close(); 755 | return; 756 | } 757 | 758 | clientMode = ClientMode.UPLOAD; 759 | 760 | int count = 0; 761 | for(int argN = 2; argN < arguments.length; ++argN) { 762 | String pathStr = arguments[argN]; 763 | System.out.println("Attempting upload of " + pathStr); 764 | File p3File = new File(pathStr); 765 | if (!p3File.exists()) { 766 | System.err.println("Unknown file \"" + pathStr + "\""); 767 | continue; 768 | } 769 | 770 | if (!p3File.isFile()) { 771 | System.err.println("\"" + pathStr + "\" is not a file"); 772 | continue; 773 | } 774 | 775 | PackageManager packageManager = new PackageManager(); 776 | Initialization.packageManager(packageManager); 777 | 778 | P3Package p3; 779 | try { 780 | p3 = packageManager.readPackage(p3File); 781 | } catch (PackageException e) { 782 | System.err.println("Unable to read package:"); 783 | e.printStackTrace(System.err); 784 | continue; 785 | } 786 | 787 | if (p3 == null) { 788 | System.err.println("Unable to read package"); 789 | continue; 790 | } 791 | 792 | System.out.println("Sending package " + p3.getId() + " (" + p3.getVersion() + ") to network..."); 793 | if (!sendPackage(p3)) { 794 | System.err.println("Unable to send package!"); 795 | continue; 796 | } 797 | 798 | count++; 799 | } 800 | 801 | if(count == 0) { 802 | System.err.println("No successful uploads!"); 803 | channel.close(); 804 | return; 805 | } 806 | 807 | System.out.println("Operation completed, waiting for ack..."); 808 | latch = new AbortableCountDownLatch(count - acks); 809 | try { 810 | latch.await(); 811 | } catch (InterruptedException e) { 812 | } 813 | channel.close(); 814 | } 815 | 816 | protected boolean sendSync() { 817 | Commands.Sync.Builder syncBuilder = Commands.Sync.newBuilder() 818 | .setEnabled(false); 819 | 820 | if(coordName != null) 821 | syncBuilder.setName(coordName); 822 | 823 | Commands.Sync sync = syncBuilder.build(); 824 | 825 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 826 | .setType(Commands.BaseCommand.CommandType.SYNC) 827 | .setSync(sync) 828 | .build(); 829 | 830 | TransactionInfo info = TransactionManager.get().begin(); 831 | 832 | Protocol.Transaction message = TransactionManager.get() 833 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 834 | if(message == null) { 835 | log.error("Unable to build message for sync"); 836 | TransactionManager.get().cancel(info.getId()); 837 | return false; 838 | } 839 | 840 | log.info("Sending SYNC to network coordinator"); 841 | return TransactionManager.get().send(info.getId(), message, null); 842 | } 843 | 844 | protected boolean sendListRequest() { 845 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 846 | .setType(Commands.BaseCommand.CommandType.C_GET_COORDINATOR_LIST) 847 | .build(); 848 | 849 | TransactionInfo info = TransactionManager.get().begin(); 850 | 851 | Protocol.Transaction message = TransactionManager.get() 852 | .build(info.getId(), Protocol.Transaction.Mode.CREATE, command); 853 | if(message == null) { 854 | log.error("Unable to build message for coordinator list"); 855 | TransactionManager.get().cancel(info.getId()); 856 | return false; 857 | } 858 | 859 | log.info("Sending C_GET_COORDINATOR_LIST to network coordinator"); 860 | return TransactionManager.get().send(info.getId(), message, null); 861 | } 862 | 863 | protected boolean processListResponse(Commands.C_CoordinatorListResponse response, TransactionInfo info) { 864 | log.info("Received C_COORDINATOR_LIST_RESPONSE"); 865 | 866 | switch(clientMode) { 867 | case LIST: 868 | System.out.println(response.toString()); 869 | channel.close(); 870 | return true; 871 | 872 | case DEPROVISION: 873 | case SEND_INPUT: 874 | case FREEZE: 875 | coordList = response; 876 | return true; 877 | } 878 | 879 | return false; 880 | } 881 | 882 | protected boolean sendProvision(String id, String version, String coordinator, String serverName, Map properties) { 883 | P3.P3Meta meta = P3.P3Meta.newBuilder() 884 | .setId(id) 885 | .setVersion(version) 886 | .build(); 887 | 888 | Commands.C_Provision.Builder provisionBuilder = Commands.C_Provision.newBuilder() 889 | .setP3(meta); 890 | 891 | if(coordinator != null) { 892 | provisionBuilder.setCoordinator(coordinator); 893 | } 894 | 895 | if(serverName != null) { 896 | provisionBuilder.setServerName(serverName); 897 | } 898 | 899 | for(Map.Entry prop : properties.entrySet()) { 900 | provisionBuilder.addProperties(Coordinator.Property.newBuilder().setName(prop.getKey()).setValue(prop.getValue()).build()); 901 | } 902 | 903 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 904 | .setType(Commands.BaseCommand.CommandType.C_PROVISION) 905 | .setCProvision(provisionBuilder.build()) 906 | .build(); 907 | 908 | TransactionInfo info = TransactionManager.get().begin(); 909 | 910 | Protocol.Transaction message = TransactionManager.get() 911 | .build(info.getId(), Protocol.Transaction.Mode.CREATE, command); 912 | if(message == null) { 913 | log.error("Unable to build message for provision"); 914 | TransactionManager.get().cancel(info.getId()); 915 | return false; 916 | } 917 | 918 | log.info("Sending C_PROVISION to network coordinator"); 919 | return TransactionManager.get().send(info.getId(), message, null); 920 | } 921 | 922 | protected boolean processProvisionResponse(Commands.C_ProvisionResponse response, TransactionInfo info) { 923 | switch(clientMode) { 924 | case PROVISION: 925 | log.info("Provision response: " + (response.getOk() ? "ok" : "not ok")); 926 | if(response.getOk()) { 927 | System.out.println("Provision operation succeeded"); 928 | System.out.println("Coordinator: " + response.getCoordinatorId()); 929 | System.out.println("Server: " + response.getServerId()); 930 | } 931 | else { 932 | System.err.println("Provision operation unsuccessful"); 933 | } 934 | channel.close(); 935 | return true; 936 | } 937 | 938 | return false; 939 | } 940 | 941 | protected boolean sendDeprovision(String coordId, String serverId, boolean force) { 942 | Commands.C_Deprovision deprovision = Commands.C_Deprovision.newBuilder() 943 | .setCoordinatorId(coordId) 944 | .setServerId(serverId) 945 | .setForce(force) 946 | .build(); 947 | 948 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 949 | .setType(Commands.BaseCommand.CommandType.C_DEPROVISION) 950 | .setCDeprovision(deprovision) 951 | .build(); 952 | 953 | TransactionInfo info = TransactionManager.get().begin(); 954 | 955 | Protocol.Transaction message = TransactionManager.get() 956 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 957 | if(message == null) { 958 | log.error("Unable to build message for deprovision"); 959 | TransactionManager.get().cancel(info.getId()); 960 | return false; 961 | } 962 | 963 | log.info("Sending C_DEPROVISION to network coordinator"); 964 | return TransactionManager.get().send(info.getId(), message, null); 965 | } 966 | 967 | protected boolean sendShutdown(String coordId) { 968 | Commands.C_Shutdown shutdown = Commands.C_Shutdown.newBuilder() 969 | .setUuid(coordId) 970 | .build(); 971 | 972 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 973 | .setType(Commands.BaseCommand.CommandType.C_SHUTDOWN) 974 | .setCShutdown(shutdown) 975 | .build(); 976 | 977 | TransactionInfo info = TransactionManager.get().begin(); 978 | 979 | Protocol.Transaction message = TransactionManager.get() 980 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 981 | if(message == null) { 982 | log.error("Unable to build message for shutdown"); 983 | TransactionManager.get().cancel(info.getId()); 984 | return false; 985 | } 986 | 987 | log.info("Sending C_SHUTDOWN to network coordinator"); 988 | return TransactionManager.get().send(info.getId(), message, null); 989 | } 990 | 991 | protected boolean sendPromote(String id, String version) { 992 | Commands.C_Promote promote = Commands.C_Promote.newBuilder() 993 | .setP3(P3.P3Meta.newBuilder().setId(id).setVersion(version).build()) 994 | .build(); 995 | 996 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 997 | .setType(Commands.BaseCommand.CommandType.C_PROMOTE) 998 | .setCPromote(promote) 999 | .build(); 1000 | 1001 | TransactionInfo info = TransactionManager.get().begin(); 1002 | 1003 | Protocol.Transaction message = TransactionManager.get() 1004 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 1005 | if(message == null) { 1006 | log.error("Unable to build message for promote"); 1007 | TransactionManager.get().cancel(info.getId()); 1008 | return false; 1009 | } 1010 | 1011 | log.info("Sending C_PROMOTE to network coordinator"); 1012 | return TransactionManager.get().send(info.getId(), message, null); 1013 | } 1014 | 1015 | protected boolean sendCreateCoordinator(String keyName) { 1016 | Commands.C_CreateCoordinator.Builder create = Commands.C_CreateCoordinator.newBuilder(); 1017 | if (keyName != null) 1018 | create.setKeyName(keyName); 1019 | 1020 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1021 | .setType(Commands.BaseCommand.CommandType.C_CREATE_COORDINATOR) 1022 | .setCCreateCoordinator(create.build()) 1023 | .build(); 1024 | 1025 | TransactionInfo info = TransactionManager.get().begin(); 1026 | 1027 | Protocol.Transaction message = TransactionManager.get() 1028 | .build(info.getId(), Protocol.Transaction.Mode.CREATE, command); 1029 | if(message == null) { 1030 | log.error("Unable to build message for create coordinator"); 1031 | TransactionManager.get().cancel(info.getId()); 1032 | return false; 1033 | } 1034 | 1035 | log.info("Sending C_CREATE_COORDINATOR to network coordinator"); 1036 | return TransactionManager.get().send(info.getId(), message, null); 1037 | } 1038 | 1039 | protected boolean processCoordinatorCreated(Commands.C_CoordinatorCreated response, TransactionInfo info) { 1040 | switch(clientMode) { 1041 | case GENERATE_KEYPAIR: 1042 | log.info("Received C_COORDINATOR_CREATED uuid = " + response.getUuid() + ", key = " + response.getKey()); 1043 | System.out.println("Keypair generation successful:"); 1044 | System.out.println("UUID: " + response.getUuid()); 1045 | System.out.println("Key: " + response.getKey()); 1046 | channel.close(); 1047 | return true; 1048 | } 1049 | 1050 | return false; 1051 | } 1052 | 1053 | protected boolean sendInput(String coordId, String serverId, String input) { 1054 | Commands.C_SendInput protoInput = Commands.C_SendInput.newBuilder() 1055 | .setCoordinatorId(coordId) 1056 | .setServerId(serverId) 1057 | .setInput(input) 1058 | .build(); 1059 | 1060 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1061 | .setType(Commands.BaseCommand.CommandType.C_SEND_INPUT) 1062 | .setCSendInput(protoInput) 1063 | .build(); 1064 | 1065 | TransactionInfo info = TransactionManager.get().begin(); 1066 | 1067 | Protocol.Transaction message = TransactionManager.get() 1068 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 1069 | if(message == null) { 1070 | log.error("Unable to build message for send input"); 1071 | TransactionManager.get().cancel(info.getId()); 1072 | return false; 1073 | } 1074 | 1075 | log.info("Sending C_SEND_INPUT to network coordinator"); 1076 | return TransactionManager.get().send(info.getId(), message, null); 1077 | } 1078 | 1079 | protected boolean sendAttachConsole(String coordId, String serverId) { 1080 | Commands.C_AttachConsole attach = Commands.C_AttachConsole.newBuilder() 1081 | .setCoordinatorId(coordId) 1082 | .setServerId(serverId) 1083 | .build(); 1084 | 1085 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1086 | .setType(Commands.BaseCommand.CommandType.C_ATTACH_CONSOLE) 1087 | .setCAttachConsole(attach) 1088 | .build(); 1089 | 1090 | TransactionInfo info = TransactionManager.get().begin(); 1091 | 1092 | Protocol.Transaction message = TransactionManager.get() 1093 | .build(info.getId(), Protocol.Transaction.Mode.CREATE, command); 1094 | if(message == null) { 1095 | log.error("Unable to build message for C_ATTACH_CONSOLE"); 1096 | TransactionManager.get().cancel(info.getId()); 1097 | return false; 1098 | } 1099 | 1100 | log.info("Sending C_ATTACH_CONSOLE to network coordinator"); 1101 | return TransactionManager.get().send(info.getId(), message, null); 1102 | } 1103 | 1104 | protected boolean processConsoleMessage(Commands.C_ConsoleMessage message, TransactionInfo info) { 1105 | switch(clientMode) { 1106 | case ATTACH: 1107 | System.out.println(message.getValue()); 1108 | return true; 1109 | } 1110 | 1111 | return false; 1112 | } 1113 | 1114 | protected boolean processDetachConsole(TransactionInfo info) { 1115 | switch(clientMode) { 1116 | case ATTACH: 1117 | System.out.println("Console detached!"); 1118 | channel.close(); 1119 | return true; 1120 | } 1121 | 1122 | return false; 1123 | } 1124 | 1125 | protected boolean sendDetachConsole() { 1126 | Commands.C_DetachConsole detach = Commands.C_DetachConsole.newBuilder() 1127 | .build(); 1128 | 1129 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1130 | .setType(Commands.BaseCommand.CommandType.C_DETACH_CONSOLE) 1131 | .setCDetachConsole(detach) 1132 | .build(); 1133 | 1134 | TransactionInfo info = TransactionManager.get().begin(); 1135 | 1136 | Protocol.Transaction message = TransactionManager.get() 1137 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 1138 | if(message == null) { 1139 | log.error("Unable to build message for C_DETACH_CONSOLE"); 1140 | TransactionManager.get().cancel(info.getId()); 1141 | return false; 1142 | } 1143 | 1144 | log.info("Sending C_DETACH_CONSOLE to network coordinator"); 1145 | return TransactionManager.get().send(info.getId(), message, null); 1146 | } 1147 | 1148 | protected boolean sendFreezeServer(String coordId, String serverId) { 1149 | Commands.C_FreezeServer freeze = Commands.C_FreezeServer.newBuilder() 1150 | .setCoordinatorId(coordId) 1151 | .setServerId(serverId) 1152 | .build(); 1153 | 1154 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1155 | .setType(Commands.BaseCommand.CommandType.C_FREEZE_SERVER) 1156 | .setCFreezeServer(freeze) 1157 | .build(); 1158 | 1159 | TransactionInfo info = TransactionManager.get().begin(); 1160 | 1161 | Protocol.Transaction message = TransactionManager.get() 1162 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 1163 | if(message == null) { 1164 | log.error("Unable to build message for C_FREEZE_SERVER"); 1165 | TransactionManager.get().cancel(info.getId()); 1166 | return false; 1167 | } 1168 | 1169 | log.info("Sending C_FREEZE_SERVER to network coordinator"); 1170 | return TransactionManager.get().send(info.getId(), message, null); 1171 | } 1172 | 1173 | protected boolean sendPackage(P3Package p3) 1174 | { 1175 | if(!p3.isResolved()) { 1176 | log.error("Cannot pass an unresolved package to sendPackage"); 1177 | return false; 1178 | } 1179 | 1180 | P3.P3Meta meta = P3.P3Meta.newBuilder() 1181 | .setId(p3.getId()) 1182 | .setVersion(p3.getVersion()) 1183 | .build(); 1184 | 1185 | try { 1186 | p3.calculateChecksum(); 1187 | } 1188 | catch (PackageException e) { 1189 | log.log(Level.ERROR, "Unable to calculate package checksum", e); 1190 | return false; 1191 | } 1192 | 1193 | File packageFile = new File(p3.getLocalPath()); 1194 | long fileLength = packageFile.length(); 1195 | if (fileLength / 1024 / 1024 > 100) { 1196 | System.out.println("Sending chunked package " + p3.getId() + " at " + p3.getVersion()); 1197 | System.out.println("Checksum: " + p3.getChecksum()); 1198 | 1199 | TransactionInfo info = TransactionManager.get().begin(); 1200 | 1201 | Commands.BaseCommand noop = Commands.BaseCommand.newBuilder() 1202 | .setType(Commands.BaseCommand.CommandType.NOOP) 1203 | .build(); 1204 | 1205 | Protocol.Transaction noopMessage = TransactionManager.get() 1206 | .build(info.getId(), Protocol.Transaction.Mode.CREATE, noop); 1207 | if (noopMessage == null) { 1208 | System.out.println("Unable to build transaction for split package response"); 1209 | return false; 1210 | } 1211 | 1212 | if (!TransactionManager.get().send(info.getId(), noopMessage, null)) { 1213 | System.out.println("Unable to send transaction for split package response"); 1214 | return false; 1215 | } 1216 | 1217 | try (FileInputStream in = new FileInputStream(packageFile)) { 1218 | byte[] packageBytes = new byte[1048576]; 1219 | int chunkLen = 0; 1220 | int chunkId = 0; 1221 | while ((chunkLen = in.read(packageBytes)) != -1) { 1222 | P3.SplitPackageData data = P3.SplitPackageData.newBuilder() 1223 | .setMeta(meta) 1224 | .setEndOfFile(false) 1225 | .setChunkId(chunkId) 1226 | .setData(ByteString.copyFrom(packageBytes, 0, chunkLen)) 1227 | .build(); 1228 | 1229 | Commands.C_UploadSplitPackage response = Commands.C_UploadSplitPackage.newBuilder() 1230 | .setData(data) 1231 | .build(); 1232 | 1233 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1234 | .setType(Commands.BaseCommand.CommandType.C_UPLOAD_SPLIT_PACKAGE) 1235 | .setCUploadSplitPackage(response) 1236 | .build(); 1237 | 1238 | Protocol.Transaction message = TransactionManager.get() 1239 | .build(info.getId(), Protocol.Transaction.Mode.CONTINUE, command); 1240 | if (message == null) { 1241 | System.out.println("Unable to build transaction for split package response"); 1242 | return false; 1243 | } 1244 | 1245 | if (!TransactionManager.get().send(info.getId(), message, null)) { 1246 | System.out.println("Unable to send transaction for split package response"); 1247 | return false; 1248 | } 1249 | 1250 | ++chunkId; 1251 | } 1252 | 1253 | P3.SplitPackageData data = P3.SplitPackageData.newBuilder() 1254 | .setMeta(meta) 1255 | .setEndOfFile(true) 1256 | .setChecksum(p3.getChecksum()) 1257 | .setChunkCount(chunkId) 1258 | .build(); 1259 | 1260 | Commands.C_UploadSplitPackage response = Commands.C_UploadSplitPackage.newBuilder() 1261 | .setData(data) 1262 | .build(); 1263 | 1264 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1265 | .setType(Commands.BaseCommand.CommandType.C_UPLOAD_SPLIT_PACKAGE) 1266 | .setCUploadSplitPackage(response) 1267 | .build(); 1268 | 1269 | Protocol.Transaction message = TransactionManager.get() 1270 | .build(info.getId(), Protocol.Transaction.Mode.COMPLETE, command); 1271 | if (message == null) { 1272 | System.out.println("Unable to build transaction for split package response"); 1273 | return false; 1274 | } 1275 | 1276 | System.out.println("Finishing split package response (" + chunkId + " chunks)"); 1277 | System.out.println("Checksum: " + p3.getChecksum()); 1278 | 1279 | return TransactionManager.get().send(info.getId(), message, null); 1280 | } catch (IOException e) { 1281 | log.error("Unable to read package data", e); 1282 | return false; 1283 | } 1284 | } 1285 | else { 1286 | ByteString packageData; 1287 | try (InputStream stream = Files.newInputStream(Paths.get(p3.getLocalPath()))) { 1288 | packageData = ByteString.readFrom(stream); 1289 | } 1290 | catch(IOException e) { 1291 | log.fatal("Unable to read package file", e); 1292 | return false; 1293 | } 1294 | 1295 | try { 1296 | p3.calculateChecksum(); 1297 | } catch (PackageException e) { 1298 | log.error("Unable to calculate checksum on package", e); 1299 | return false; 1300 | } 1301 | 1302 | Commands.C_UploadPackage upload = Commands.C_UploadPackage.newBuilder() 1303 | .setData(P3.PackageData.newBuilder() 1304 | .setMeta(P3.P3Meta.newBuilder().setId(p3.getId()).setVersion(p3.getVersion())) 1305 | .setChecksum(p3.getChecksum()) 1306 | .setData(packageData)) 1307 | .build(); 1308 | 1309 | Commands.BaseCommand command = Commands.BaseCommand.newBuilder() 1310 | .setType(Commands.BaseCommand.CommandType.C_UPLOAD_PACKAGE) 1311 | .setCUploadPackage(upload) 1312 | .build(); 1313 | 1314 | TransactionInfo info = TransactionManager.get().begin(); 1315 | 1316 | Protocol.Transaction message = TransactionManager.get() 1317 | .build(info.getId(), Protocol.Transaction.Mode.SINGLE, command); 1318 | if(message == null) { 1319 | log.error("Unable to build message for C_UPLOAD_PACKAGE"); 1320 | TransactionManager.get().cancel(info.getId()); 1321 | return false; 1322 | } 1323 | 1324 | return TransactionManager.get().send(info.getId(), message, null); 1325 | } 1326 | } 1327 | 1328 | protected boolean processAck(Commands.C_Ack ack, TransactionInfo info) { 1329 | if(clientMode == ClientMode.ATTACH) 1330 | return true; 1331 | 1332 | System.out.println("ACK: " + ack.getResult()); 1333 | 1334 | log.info("ACK: " + ack.getResult()); 1335 | 1336 | acks++; 1337 | if(latch == null) 1338 | return false; 1339 | 1340 | latch.countDown(); 1341 | return true; 1342 | } 1343 | 1344 | protected boolean blockUntilCoordList() { 1345 | if(!sendListRequest()) 1346 | return false; 1347 | 1348 | try { 1349 | do { 1350 | Thread.sleep(1000); 1351 | } 1352 | while(coordList == null && channel.isActive()); 1353 | } 1354 | catch(InterruptedException e) { 1355 | return false; 1356 | } 1357 | 1358 | return channel.isActive(); 1359 | } 1360 | 1361 | protected Map> getServersFromList(Pattern coordPattern, Pattern serverPattern) { 1362 | Map> result = new HashMap<>(); 1363 | for(Coordinator.LocalCoordinator coord : coordList.getCoordinatorsList()) { 1364 | if(coordPattern.matcher(coord.getUuid()).matches() || 1365 | (coord.hasName() && coordPattern.matcher(coord.getName()).matches())) { 1366 | List servers = new ArrayList<>(); 1367 | for(Coordinator.Server server : coord.getServersList()) { 1368 | if(serverPattern.matcher(server.getUuid()).matches() || 1369 | (server.hasName() && serverPattern.matcher(server.getName()).matches())) { 1370 | servers.add(server.getUuid()); 1371 | } 1372 | } 1373 | 1374 | if(!servers.isEmpty()) 1375 | result.put(coord.getUuid(), servers); 1376 | } 1377 | } 1378 | 1379 | return result; 1380 | } 1381 | 1382 | private static class AttachInputListenThread extends Thread { 1383 | private String coordId; 1384 | private String serverId; 1385 | @Setter 1386 | @Getter 1387 | private boolean active = true; 1388 | 1389 | public AttachInputListenThread(String c, String s) { 1390 | coordId = c; 1391 | serverId = s; 1392 | } 1393 | 1394 | @Override 1395 | public void run() { 1396 | while(active) { 1397 | String input = System.console().readLine() + '\n'; 1398 | Client.get().sendInput(coordId, serverId, input); 1399 | } 1400 | } 1401 | } 1402 | } 1403 | --------------------------------------------------------------------------------