blob: bc61636b7ebefcb82ddade333124af237c4dc688 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.rj.server.srv.engine;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import static org.eclipse.statet.rj.server.util.ServerUtils.MISSING_ANSWER_STATUS;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.login.LoginException;
import org.eclipse.statet.jcommons.io.FileUtils;
import org.eclipse.statet.jcommons.lang.NonNull;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.rj.RjException;
import org.eclipse.statet.rj.data.RDataUtils;
import org.eclipse.statet.rj.data.RObject;
import org.eclipse.statet.rj.data.UnexpectedRDataException;
import org.eclipse.statet.rj.server.DataCmdItem;
import org.eclipse.statet.rj.server.MainCmdC2SList;
import org.eclipse.statet.rj.server.MainCmdItem;
import org.eclipse.statet.rj.server.MainCmdS2CList;
import org.eclipse.statet.rj.server.RjsComConfig;
import org.eclipse.statet.rj.server.RjsComObject;
import org.eclipse.statet.rj.server.RjsException;
import org.eclipse.statet.rj.server.RjsStatus;
import org.eclipse.statet.rj.server.Server;
import org.eclipse.statet.rj.server.ServerInfo;
import org.eclipse.statet.rj.server.ServerLogin;
import org.eclipse.statet.rj.server.srv.RMIServerControl;
import org.eclipse.statet.rj.server.srvext.Client;
import org.eclipse.statet.rj.server.srvext.ServerAuthMethod;
import org.eclipse.statet.rj.server.srvext.ServerRuntimePlugin;
@NonNullByDefault
public class SrvEngineServer implements Server, RjsComConfig.PathResolver {
private static final List<Remote> clients= new CopyOnWriteArrayList<>();
public static void addClient(final Remote remote) {
clients.add(nonNullAssert(remote));
}
public static void removeClient(final @Nullable Remote remote) {
if (remote != null) {
clients.remove(remote);
}
}
public static boolean isValid(final @Nullable Remote remote) {
return (remote != null && clients.contains(remote));
}
protected static final Logger LOGGER= Logger.getLogger("org.eclipse.statet.rj.server");
protected final RMIServerControl control;
protected SrvEngine srvEngine;
private final String[] userTypes;
private @Nullable String[] userNames;
protected Path workingDirectory;
protected long timestamp;
protected ServerAuthMethod authMethod;
protected final Client serverClient= new Client("rservi", "dummy", (byte)1);
private final MainCmdC2SList serverC2SList= new MainCmdC2SList();
public SrvEngineServer(final RMIServerControl control, final ServerAuthMethod authMethod) {
this.control= nonNullAssert(control);
this.userTypes= createUserTypes();
this.userNames= new @Nullable String[this.userTypes.length];
setUserName(ServerInfo.USER_OWNER, System.getProperty("user.name"));
this.workingDirectory= FileUtils.getUserWorkingDirectory();
this.authMethod= authMethod;
}
protected String[] createUserTypes() {
return new String[] { ServerInfo.USER_OWNER, ServerInfo.USER_CONSOLE };
}
protected void setUserName(final String type, final @Nullable String name) {
for (int i= 0; i < this.userTypes.length; i++) {
if (this.userTypes[i].equals(type)) {
if (!Objects.equals(this.userNames[i], name)) {
final @Nullable String[] newNames= new @Nullable String[this.userTypes.length];
System.arraycopy(this.userNames, 0, newNames, 0, this.userTypes.length);
newNames[i]= name;
this.userNames= newNames;
}
return;
}
}
}
public boolean getConfigUnbindOnStartup() {
return true;
}
public void setEngine(final SrvEngine engine) {
if (this.srvEngine != null) {
throw new IllegalStateException();
}
this.srvEngine= engine;
}
public void start(final ServerRuntimePlugin runtimePlugin) throws Exception {
if (this.srvEngine instanceof SrvEnginePluginExtension) {
((SrvEnginePluginExtension) this.srvEngine).addPlugin(runtimePlugin);
}
}
@Override
public int getState() throws RemoteException {
return this.srvEngine.getState();
}
@Override
public int[] getVersion() throws RemoteException {
final int[] internalVersion= this.srvEngine.getVersion();
final int[] version= new int[internalVersion.length];
System.arraycopy(internalVersion, 0, version, 0, internalVersion.length);
return version;
}
@Override
public ServerInfo getInfo() throws RemoteException {
return new ServerInfo(this.control.getName(),
this.workingDirectory.toString(), this.timestamp,
this.userTypes, this.userNames,
this.srvEngine.getState());
}
protected ServerAuthMethod getAuthMethod(final String command) {
if (command.startsWith("console.")) {
return this.authMethod;
}
throw new UnsupportedOperationException();
}
@Override
public final ServerLogin createLogin(final String command) throws RemoteException {
final ServerAuthMethod authMethod= getAuthMethod(command);
try {
return authMethod.createLogin();
}
catch (final RjException e) {
final String message= "Initializing login failed.";
LOGGER.log(Level.SEVERE, message, e);
throw new RemoteException(message, e);
}
}
protected Client connectClient(final String command, final ServerLogin login) throws RemoteException, LoginException {
final ServerAuthMethod authMethod= getAuthMethod(command);
try {
return authMethod.performLogin(login);
}
catch (final RjException e) {
final String message= "Performing login failed.";
LOGGER.log(Level.SEVERE, message, e);
throw new RemoteException(message, e);
}
}
@Override
public @Nullable Object execute(final String command, final Map<String, ? extends @NonNull Object> args,
final ServerLogin login) throws RemoteException, LoginException {
try {
if (command.equals(C_CONSOLE_START)) {
final Client client= connectClient(command, login);
final Object r= this.srvEngine.start(client, args);
final Object startupTime= args.get("rj.session.startup.time");
if (startupTime instanceof Long) {
this.timestamp= ((Long) startupTime).longValue();
}
return r;
}
if (command.equals(C_CONSOLE_CONNECT)) {
final Client client= connectClient(command, login);
final Object r= this.srvEngine.connect(client, args);
return r;
}
return null;
}
finally {
final Client client= this.srvEngine.getCurrentClient();
setUserName(ServerInfo.USER_CONSOLE, (client != null) ? client.getUsername() : null);
}
}
protected RObject runServerLoopCommand(final DataCmdItem sendItem) throws RjException {
RjsComObject sendCom= null;
DataCmdItem answer= null;
try {
this.serverC2SList.setObjects(sendItem);
sendCom= this.serverC2SList;
WAIT_FOR_ANSWER: while (true) {
final RjsComObject receivedCom= runMainLoop(sendCom, null);
sendCom= null;
COM_TYPE: switch (receivedCom.getComType()) {
case RjsComObject.T_PING:
sendCom= RjsStatus.OK_STATUS;
break COM_TYPE;
case RjsComObject.T_STATUS:
final RjsStatus status= (RjsStatus) receivedCom;
switch (status.getSeverity()) {
case RjsStatus.OK:
break COM_TYPE;
case RjsStatus.INFO:
break COM_TYPE;
case RjsStatus.ERROR:
if (status.getCode() == RjsStatus.ERROR) {
break WAIT_FOR_ANSWER;
}
throw new RjsException(status.getCode(), status.getMessage());
default:
break COM_TYPE;
}
case RjsComObject.T_MAIN_LIST:
final MainCmdS2CList list= (MainCmdS2CList) receivedCom;
MainCmdItem item= list.getItems();
while (item != null) {
if (item == sendItem) {
answer= sendItem;
break COM_TYPE;
}
item= item.next;
}
break COM_TYPE;
}
}
this.serverC2SList.clear();
if (answer == null || !answer.isOK()) {
final RjsStatus status= (answer != null) ? answer.getStatus() : MISSING_ANSWER_STATUS;
throw new RjException("R commands failed: "+status.getMessage()+".");
}
return answer.getData();
}
catch (final Exception e) {
if (e instanceof RjException) {
throw (RjException) e;
}
throw new RjException("An error when executing R command.", e);
}
}
protected RjsComObject runMainLoop(final RjsComObject com, final Object caller) throws RemoteException {
return this.srvEngine.runMainLoop(this.serverClient, com);
}
@Override
public Path resolve(final Remote ref, final String pathString) throws RjException {
if (!SrvEngineServer.isValid(ref)) {
throw new RjException("Invalid access.");
}
final var path= Path.of(pathString);
if (path.isAbsolute()) {
return path;
}
else {
try {
final var rwd= Path.of(RDataUtils.checkSingleCharValue(
runServerLoopCommand(new DataCmdItem(DataCmdItem.EVAL_EXPR_DATA, 0,
(byte)-1, "getwd()", null, null, null, null )) ));
if (rwd.isAbsolute()) {
return rwd.resolve(path);
}
else {
throw new UnexpectedRDataException("wd path is relative.");
}
}
catch (final RjException | UnexpectedRDataException | InvalidPathException e) {
throw new RjException("Resolving relative path failed.");
}
}
}
}