You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2020/01/06 06:21:56 UTC
[zeppelin] branch master updated: [ZEPPELIN-4525]. Support Shiny in
R Interpreter
This is an automated email from the ASF dual-hosted git repository.
zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new e1ba413 [ZEPPELIN-4525]. Support Shiny in R Interpreter
e1ba413 is described below
commit e1ba4131ceb297d7399fdc5a6980cbeb619307e6
Author: Jeff Zhang <zj...@apache.org>
AuthorDate: Thu Jan 2 09:55:52 2020 +0800
[ZEPPELIN-4525]. Support Shiny in R Interpreter
### What is this PR for?
This PR is to support shiny app in R interpreter. It is based on jupyter's irkernel, so that means you need to install irkernel first, and install shiny package as well.
Writing shiny app in Zeppelin requires at 3 paragraphs.
* UI paragraph e.g. %r.shiny(type=ui)
* Server paragraph e.g. %r.shiny(type=server)
* Run paragraph e.g. %r.shiny(type=run)
* Normal R code paragraph(optional) e.g. %r.shiny
See the screenshot below for more details.
### What type of PR is it?
[Feature]
### Todos
* [ ] - Task
### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-4525
### How should this be tested?
* CI pass
### Screenshots (if appropriate)
![shiny_app](https://user-images.githubusercontent.com/164491/71650190-706bca00-2d4f-11ea-8706-9ca674799bae.gif)
### Questions:
* Does the licenses files need update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Author: Jeff Zhang <zj...@apache.org>
Closes #3582 from zjffdu/ZEPPELIN-4525 and squashes the following commits:
93edffc5f [Jeff Zhang] [ZEPPELIN-4525]. Support Shiny in R Interpreter
---
rlang/pom.xml | 28 +++
.../java/org/apache/zeppelin/r/IRInterpreter.java | 80 +++++++
.../org/apache/zeppelin/r/ShinyInterpreter.java | 145 ++++++++++++
rlang/src/main/resources/interpreter-setting.json | 36 +++
.../apache/zeppelin/r/ShinyInterpreterTest.java | 246 +++++++++++++++++++++
rlang/src/test/resources/invalid_ui.R | 1 +
rlang/src/test/resources/log4j.properties | 1 +
rlang/src/test/resources/server.R | 23 ++
rlang/src/test/resources/ui.R | 35 +++
testing/install_external_dependencies.sh | 1 +
.../zeppelin/interpreter/util/ProcessLauncher.java | 23 ++
.../zeppelin/jupyter/JupyterKernelClient.java | 58 ++++-
.../zeppelin/jupyter/JupyterKernelInterpreter.java | 9 +-
.../main/resources/grpc/jupyter/kernel_server.py | 7 +-
.../apache/zeppelin/jupyter/IPythonKernelTest.java | 8 +
.../org/apache/zeppelin/jupyter/IRKernelTest.java | 4 +-
.../org/apache/zeppelin/notebook/Paragraph.java | 4 +-
17 files changed, 695 insertions(+), 14 deletions(-)
diff --git a/rlang/pom.xml b/rlang/pom.xml
index 5a7de91..a8021b4 100644
--- a/rlang/pom.xml
+++ b/rlang/pom.xml
@@ -59,6 +59,27 @@
</dependency>
<dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty</artifactId>
+ <version>${grpc.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-protobuf</artifactId>
+ <version>${grpc.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ <version>${grpc.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.zeppelin</groupId>
<artifactId>zeppelin-jupyter-interpreter</artifactId>
<scope>test</scope>
@@ -95,6 +116,13 @@
<version>${jsoup.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.mashape.unirest</groupId>
+ <artifactId>unirest-java</artifactId>
+ <version>1.4.9</version>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
<build>
diff --git a/rlang/src/main/java/org/apache/zeppelin/r/IRInterpreter.java b/rlang/src/main/java/org/apache/zeppelin/r/IRInterpreter.java
index f04b091..d9076b4 100644
--- a/rlang/src/main/java/org/apache/zeppelin/r/IRInterpreter.java
+++ b/rlang/src/main/java/org/apache/zeppelin/r/IRInterpreter.java
@@ -20,16 +20,23 @@ package org.apache.zeppelin.r;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.zeppelin.interpreter.BaseZeppelinContext;
+import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterException;
+import org.apache.zeppelin.interpreter.InterpreterResult;
import org.apache.zeppelin.interpreter.jupyter.proto.ExecuteRequest;
import org.apache.zeppelin.interpreter.jupyter.proto.ExecuteResponse;
import org.apache.zeppelin.interpreter.jupyter.proto.ExecuteStatus;
+import org.apache.zeppelin.interpreter.remote.RemoteInterpreterUtils;
import org.apache.zeppelin.jupyter.JupyterKernelInterpreter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.io.StringReader;
+import java.nio.file.Files;
import java.util.Properties;
/**
@@ -41,6 +48,9 @@ public class IRInterpreter extends JupyterKernelInterpreter {
private static final Logger LOGGER = LoggerFactory.getLogger(IRInterpreter.class);
+ // It is used to store shiny related code (ui.R & server.R)
+ // only one shiny app can be hosted in one R session.
+ private File shinyAppFolder;
private SparkRBackend sparkRBackend;
public IRInterpreter(Properties properties) {
@@ -98,6 +108,13 @@ public class IRInterpreter extends JupyterKernelInterpreter {
throw new InterpreterException("Fail to init IR Kernel:\n" +
ExceptionUtils.getStackTrace(e), e);
}
+
+ try {
+ this.shinyAppFolder = Files.createTempDirectory("zeppelin-shiny").toFile();
+ this.shinyAppFolder.deleteOnExit();
+ } catch (IOException e) {
+ throw new InterpreterException(e);
+ }
}
/**
@@ -134,4 +151,67 @@ public class IRInterpreter extends JupyterKernelInterpreter {
return new RZeppelinContext(getInterpreterGroup().getInterpreterHookRegistry(),
Integer.parseInt(getProperty("zeppelin.r.maxResult", "1000")));
}
+
+ public InterpreterResult shinyUI(String st,
+ InterpreterContext context) throws InterpreterException {
+ File uiFile = new File(shinyAppFolder, "ui.R");
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(uiFile);
+ IOUtils.copy(new StringReader(st), writer);
+ return new InterpreterResult(InterpreterResult.Code.SUCCESS, "Write ui.R to "
+ + shinyAppFolder.getAbsolutePath() + " successfully.");
+ } catch (IOException e) {
+ throw new InterpreterException("Fail to write shiny file ui.R", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ throw new InterpreterException(e);
+ }
+ }
+ }
+ }
+
+ public InterpreterResult shinyServer(String st,
+ InterpreterContext context) throws InterpreterException {
+ File serverFile = new File(shinyAppFolder, "server.R");
+ FileWriter writer = null;
+ try {
+ writer = new FileWriter(serverFile);
+ IOUtils.copy(new StringReader(st), writer);
+ return new InterpreterResult(InterpreterResult.Code.SUCCESS, "Write server.R to "
+ + shinyAppFolder.getAbsolutePath() + " successfully.");
+ } catch (IOException e) {
+ throw new InterpreterException("Fail to write shiny file server.R", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ throw new InterpreterException(e);
+ }
+ }
+ }
+ }
+
+ public InterpreterResult runShinyApp(InterpreterContext context)
+ throws IOException, InterpreterException {
+ // redirect R kernel process to InterpreterOutput of current paragraph
+ // because the error message after shiny app launched is printed in R kernel process
+ getKernelProcessLauncher().setRedirectedContext(context);
+ try {
+ StringBuilder builder = new StringBuilder("library(shiny)\n");
+ String host = RemoteInterpreterUtils.findAvailableHostAddress();
+ int port = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces();
+ builder.append("runApp(appDir='" + shinyAppFolder.getAbsolutePath() + "', " +
+ "port=" + port + ", host='" + host + "', launch.browser=FALSE)");
+ // shiny app will launch and block there until user cancel the paragraph.
+ LOGGER.info("Run shiny app code: " + builder.toString());
+ return internalInterpret(builder.toString(), context);
+ } finally {
+ getKernelProcessLauncher().setRedirectedContext(null);
+ }
+ }
}
diff --git a/rlang/src/main/java/org/apache/zeppelin/r/ShinyInterpreter.java b/rlang/src/main/java/org/apache/zeppelin/r/ShinyInterpreter.java
new file mode 100644
index 0000000..b2dc5f3
--- /dev/null
+++ b/rlang/src/main/java/org/apache/zeppelin/r/ShinyInterpreter.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.r;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.zeppelin.interpreter.AbstractInterpreter;
+import org.apache.zeppelin.interpreter.BaseZeppelinContext;
+import org.apache.zeppelin.interpreter.InterpreterContext;
+import org.apache.zeppelin.interpreter.InterpreterException;
+import org.apache.zeppelin.interpreter.InterpreterResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * One shiny Interpreter can host more than one Shiny app.
+ * They are organized by app name which you specify by paragraph local properties.
+ * e.g. %shiny(app=app_1)
+ *
+ * If you don't specify 'app', then default app name 'default' will be used.
+ *
+ * One shiny app is composed of at least 3 paragraph (last one is optional)
+ * <p>
+ * <ul>
+ * <li>UI paragraph e.g. %r.shiny(type=ui) </li>
+ * <li>Server paragraph e.g. %r.shiny(type=server) </li>
+ * <li>Run paragraph e.g. %r.shiny(type=run) </li>
+ * <li>Normal R code paragraph(optional) e.g. %r.shiny </li>
+ * </ul>
+ * <p>
+ */
+public class ShinyInterpreter extends AbstractInterpreter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ShinyInterpreter.class);
+
+ private static final String DEFAULT_APP_NAME = "default";
+ private Map<String, IRInterpreter> shinyIRInterpreters = new HashMap<>();
+ private RZeppelinContext z;
+
+ public ShinyInterpreter(Properties properties) {
+ super(properties);
+ }
+
+ @Override
+ public void open() throws InterpreterException {
+ this.z = new RZeppelinContext(getInterpreterGroup().getInterpreterHookRegistry(), 1000);
+ }
+
+ @Override
+ public void close() throws InterpreterException {
+ for (Map.Entry<String,IRInterpreter> entry : shinyIRInterpreters.entrySet()) {
+ LOGGER.info("Closing IRInterpreter: " + entry.getKey());
+ // Stop shiny app first otherwise the R process can not be terminated.
+ entry.getValue().cancel(InterpreterContext.get());
+ entry.getValue().close();
+ }
+ }
+
+ @Override
+ public void cancel(InterpreterContext context) throws InterpreterException {
+ String shinyApp = context.getStringLocalProperty("app", DEFAULT_APP_NAME);
+ IRInterpreter irInterpreter = getIRInterpreter(shinyApp);
+ irInterpreter.cancel(context);
+ }
+
+ @Override
+ public FormType getFormType() throws InterpreterException {
+ return FormType.NATIVE;
+ }
+
+ @Override
+ public int getProgress(InterpreterContext context) throws InterpreterException {
+ return 0;
+ }
+
+ @Override
+ public BaseZeppelinContext getZeppelinContext() {
+ return this.z;
+ }
+
+ @Override
+ public InterpreterResult internalInterpret(String st, InterpreterContext context)
+ throws InterpreterException {
+ String shinyApp = context.getStringLocalProperty("app", DEFAULT_APP_NAME);
+ String shinyType = context.getStringLocalProperty("type", "");
+ IRInterpreter irInterpreter = getIRInterpreter(shinyApp);
+ if (StringUtils.isBlank(shinyType)) {
+ return irInterpreter.internalInterpret(st, context);
+ } else if (shinyType.equals("run")) {
+ try {
+ return irInterpreter.runShinyApp(context);
+ } catch (IOException e) {
+ throw new InterpreterException(e);
+ }
+ } else if (shinyType.equals("ui")) {
+ return irInterpreter.shinyUI(st, context);
+ } else if (shinyType.equals("server")) {
+ return irInterpreter.shinyServer(st, context);
+ } else {
+ throw new InterpreterException("Unknown shiny type: " + shinyType);
+ }
+ }
+
+ /**
+ * Get the specific IRInterpreter for this shinyApp.
+ * One ShinyApp is owned by one IRInterpreter(R session).
+ *
+ * @param shinyApp
+ * @return
+ * @throws InterpreterException
+ */
+ private IRInterpreter getIRInterpreter(String shinyApp) throws InterpreterException {
+ IRInterpreter irInterpreter = null;
+ synchronized (shinyIRInterpreters) {
+ irInterpreter = shinyIRInterpreters.get(shinyApp);
+ if (irInterpreter == null) {
+ irInterpreter = new IRInterpreter(properties);
+ irInterpreter.setInterpreterGroup(getInterpreterGroup());
+ irInterpreter.open();
+ shinyIRInterpreters.put(shinyApp, irInterpreter);
+ }
+ }
+ return irInterpreter;
+ }
+
+}
diff --git a/rlang/src/main/resources/interpreter-setting.json b/rlang/src/main/resources/interpreter-setting.json
index 47697bd..1026067 100644
--- a/rlang/src/main/resources/interpreter-setting.json
+++ b/rlang/src/main/resources/interpreter-setting.json
@@ -32,6 +32,11 @@
"description": "",
"type": "textarea"
}
+ },
+ "editor": {
+ "language": "r",
+ "editOnDblClick": false,
+ "completionSupport": true
}
},
{
@@ -39,6 +44,37 @@
"name": "ir",
"className": "org.apache.zeppelin.r.IRInterpreter",
"properties": {
+ },
+ "editor": {
+ "language": "r",
+ "editOnDblClick": false,
+ "completionSupport": true
+ }
+ },
+ {
+ "group": "r",
+ "name": "shiny",
+ "className": "org.apache.zeppelin.r.ShinyInterpreter",
+ "properties": {
+ "zeppelin.R.shiny.iframe_width": {
+ "envName": "",
+ "propertyName": "zeppelin.R.shiny.iframe_width",
+ "defaultValue": "100%",
+ "description": "",
+ "type": "text"
+ },
+ "zeppelin.R.shiny.iframe_height": {
+ "envName": "",
+ "propertyName": "zeppelin.R.shiny.iframe_height",
+ "defaultValue": "500px",
+ "description": "",
+ "type": "text"
+ }
+ },
+ "editor": {
+ "language": "r",
+ "editOnDblClick": false,
+ "completionSupport": true
}
}
]
diff --git a/rlang/src/test/java/org/apache/zeppelin/r/ShinyInterpreterTest.java b/rlang/src/test/java/org/apache/zeppelin/r/ShinyInterpreterTest.java
new file mode 100644
index 0000000..5939f99
--- /dev/null
+++ b/rlang/src/test/java/org/apache/zeppelin/r/ShinyInterpreterTest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.r;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+import org.apache.commons.io.IOUtils;
+import org.apache.zeppelin.interpreter.InterpreterContext;
+import org.apache.zeppelin.interpreter.InterpreterException;
+import org.apache.zeppelin.interpreter.InterpreterGroup;
+import org.apache.zeppelin.interpreter.InterpreterOutput;
+import org.apache.zeppelin.interpreter.InterpreterResult;
+import org.apache.zeppelin.interpreter.InterpreterResultMessage;
+import org.apache.zeppelin.interpreter.LazyOpenInterpreter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ShinyInterpreterTest {
+
+ private ShinyInterpreter interpreter;
+
+ @Before
+ public void setUp() throws InterpreterException {
+ Properties properties = new Properties();
+
+ InterpreterContext context = getInterpreterContext();
+ InterpreterContext.set(context);
+ interpreter = new ShinyInterpreter(properties);
+
+ InterpreterGroup interpreterGroup = new InterpreterGroup();
+ interpreterGroup.addInterpreterToSession(new LazyOpenInterpreter(interpreter), "session_1");
+ interpreter.setInterpreterGroup(interpreterGroup);
+
+ interpreter.open();
+ }
+
+ @After
+ public void tearDown() throws InterpreterException {
+ if (interpreter != null) {
+ interpreter.close();
+ }
+ }
+
+ @Test
+ public void testShinyApp() throws
+ IOException, InterpreterException, InterruptedException, UnirestException {
+ /****************** Launch Shiny app with default app name *****************************/
+ InterpreterContext context = getInterpreterContext();
+ context.getLocalProperties().put("type", "ui");
+ InterpreterResult result =
+ interpreter.interpret(IOUtils.toString(getClass().getResource("/ui.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ context = getInterpreterContext();
+ context.getLocalProperties().put("type", "server");
+ result = interpreter.interpret(IOUtils.toString(getClass().getResource("/server.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ final InterpreterContext context2 = getInterpreterContext();
+ context2.getLocalProperties().put("type", "run");
+ Thread thread = new Thread(() -> {
+ try {
+ interpreter.interpret("", context2);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ thread.start();
+ // wait for the shiny app start
+ Thread.sleep(5 * 1000);
+ // extract shiny url
+ List<InterpreterResultMessage> resultMessages = context2.out.toInterpreterResultMessage();
+ assertEquals(1, resultMessages.size());
+ assertEquals(InterpreterResult.Type.HTML, resultMessages.get(0).getType());
+ String resultMessageData = resultMessages.get(0).getData();
+ assertTrue(resultMessageData, resultMessageData.contains("<iframe"));
+ Pattern urlPattern = Pattern.compile(".*src=\"(http\\S*)\".*", Pattern.DOTALL);
+ Matcher matcher = urlPattern.matcher(resultMessageData);
+ if (!matcher.matches()) {
+ fail("Unable to extract url: " + resultMessageData);
+ }
+ String shinyURL = matcher.group(1);
+
+ // verify shiny app via calling its rest api
+ HttpResponse<String> response = Unirest.get(shinyURL).asString();
+ assertEquals(200, response.getStatus());
+ assertTrue(response.getBody(), response.getBody().contains("Shiny Text"));
+
+ /************************ Launch another shiny app (app2) *****************************/
+ context = getInterpreterContext();
+ context.getLocalProperties().put("type", "ui");
+ context.getLocalProperties().put("app", "app2");
+ result =
+ interpreter.interpret(IOUtils.toString(getClass().getResource("/ui.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ context = getInterpreterContext();
+ context.getLocalProperties().put("type", "server");
+ context.getLocalProperties().put("app", "app2");
+ result = interpreter.interpret(IOUtils.toString(getClass().getResource("/server.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ final InterpreterContext context3 = getInterpreterContext();
+ context3.getLocalProperties().put("type", "run");
+ context3.getLocalProperties().put("app", "app2");
+ thread = new Thread(() -> {
+ try {
+ interpreter.interpret("", context3);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ thread.start();
+ // wait for the shiny app start
+ Thread.sleep(5 * 1000);
+ // extract shiny url
+ resultMessages = context3.out.toInterpreterResultMessage();
+ assertEquals(1, resultMessages.size());
+ assertEquals(InterpreterResult.Type.HTML, resultMessages.get(0).getType());
+ resultMessageData = resultMessages.get(0).getData();
+ assertTrue(resultMessageData, resultMessageData.contains("<iframe"));
+ matcher = urlPattern.matcher(resultMessageData);
+ if (!matcher.matches()) {
+ fail("Unable to extract url: " + resultMessageData);
+ }
+ String shinyURL2 = matcher.group(1);
+
+ // verify shiny app via calling its rest api
+ response = Unirest.get(shinyURL2).asString();
+ assertEquals(200, response.getStatus());
+ assertTrue(response.getBody(), response.getBody().contains("Shiny Text"));
+
+ // cancel paragraph to stop the first shiny app
+ interpreter.cancel(getInterpreterContext());
+ // wait for shiny app to be stopped
+ Thread.sleep(1000);
+ try {
+ Unirest.get(shinyURL).asString();
+ fail("Should fail to connect to shiny app");
+ } catch (Exception e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("Connection refused"));
+ }
+
+ // the second shiny app still works
+ response = Unirest.get(shinyURL2).asString();
+ assertEquals(200, response.getStatus());
+ assertTrue(response.getBody(), response.getBody().contains("Shiny Text"));
+ }
+
+ @Test
+ public void testInvalidShinyApp()
+ throws IOException, InterpreterException, InterruptedException, UnirestException {
+ InterpreterContext context = getInterpreterContext();
+ context.getLocalProperties().put("type", "ui");
+ InterpreterResult result =
+ interpreter.interpret(IOUtils.toString(getClass().getResource("/invalid_ui.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ context = getInterpreterContext();
+ context.getLocalProperties().put("type", "server");
+ result = interpreter.interpret(IOUtils.toString(getClass().getResource("/server.R")), context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+
+ final InterpreterContext context2 = getInterpreterContext();
+ context2.getLocalProperties().put("type", "run");
+ Thread thread = new Thread(() -> {
+ try {
+ interpreter.interpret("", context2);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ thread.start();
+ // wait for the shiny app start
+ Thread.sleep(5 * 1000);
+ List<InterpreterResultMessage> resultMessages = context2.out.toInterpreterResultMessage();
+ assertEquals(1, resultMessages.size());
+ assertEquals(InterpreterResult.Type.HTML, resultMessages.get(0).getType());
+ String resultMessageData = resultMessages.get(0).getData();
+ assertTrue(resultMessageData, resultMessageData.contains("<iframe"));
+ Pattern urlPattern = Pattern.compile(".*src=\"(http\\S*)\".*", Pattern.DOTALL);
+ Matcher matcher = urlPattern.matcher(resultMessageData);
+ if (!matcher.matches()) {
+ fail("Unable to extract url: " + resultMessageData);
+ }
+ String shinyURL = matcher.group(1);
+
+ // call shiny app via rest api
+ HttpResponse<String> response = Unirest.get(shinyURL).asString();
+ assertEquals(500, response.getStatus());
+
+ resultMessages = context2.out.toInterpreterResultMessage();
+ assertTrue(resultMessages.get(1).getData(),
+ resultMessages.get(1).getData().contains("object 'Invalid_code' not found"));
+
+ // cancel paragraph to stop shiny app
+ interpreter.cancel(getInterpreterContext());
+ // wait for shiny app to be stopped
+ Thread.sleep(1000);
+ try {
+ Unirest.get(shinyURL).asString();
+ fail("Should fail to connect to shiny app");
+ } catch (Exception e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("Connection refused"));
+ }
+ }
+
+ protected InterpreterContext getInterpreterContext() {
+ InterpreterContext context = InterpreterContext.builder()
+ .setNoteId("note_1")
+ .setParagraphId("paragraph_1")
+ .setInterpreterOut(new InterpreterOutput(null))
+ .setLocalProperties(new HashMap<>())
+ .setInterpreterClassName(ShinyInterpreter.class.getName())
+ .build();
+ return context;
+ }
+}
diff --git a/rlang/src/test/resources/invalid_ui.R b/rlang/src/test/resources/invalid_ui.R
new file mode 100644
index 0000000..9f08258
--- /dev/null
+++ b/rlang/src/test/resources/invalid_ui.R
@@ -0,0 +1 @@
+Invalid_code
\ No newline at end of file
diff --git a/rlang/src/test/resources/log4j.properties b/rlang/src/test/resources/log4j.properties
index 0d6d5f1..4836e66 100644
--- a/rlang/src/test/resources/log4j.properties
+++ b/rlang/src/test/resources/log4j.properties
@@ -24,3 +24,4 @@ log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%d] ({%t} %F[%M]:%L) - %m%n
log4j.logger.org.apache.zeppelin.interpreter.util=DEBUG
+log4j.logger.org.apache.zeppelin.jupyter=DEBUG
diff --git a/rlang/src/test/resources/server.R b/rlang/src/test/resources/server.R
new file mode 100644
index 0000000..eb67ffb
--- /dev/null
+++ b/rlang/src/test/resources/server.R
@@ -0,0 +1,23 @@
+# Define server logic to summarize and view selected dataset ----
+server <- function(input, output) {
+
+ # Return the requested dataset ----
+ datasetInput <- reactive({
+ switch(input$dataset,
+ "rock" = rock,
+ "pressure" = pressure,
+ "cars" = cars)
+ })
+
+ # Generate a summary of the dataset ----
+ output$summary <- renderPrint({
+ dataset <- datasetInput()
+ summary(dataset)
+ })
+
+ # Show the first "n" observations ----
+ output$view <- renderTable({
+ head(datasetInput(), n = input$obs)
+ })
+
+}
\ No newline at end of file
diff --git a/rlang/src/test/resources/ui.R b/rlang/src/test/resources/ui.R
new file mode 100644
index 0000000..282a9d5
--- /dev/null
+++ b/rlang/src/test/resources/ui.R
@@ -0,0 +1,35 @@
+# Define UI for dataset viewer app ----
+ui <- fluidPage(
+
+# App title ----
+titlePanel("Shiny Text"),
+
+# Sidebar layout with a input and output definitions ----
+sidebarLayout(
+
+# Sidebar panel for inputs ----
+sidebarPanel(
+
+# Input: Selector for choosing dataset ----
+selectInput(inputId = "dataset",
+label = "Choose a dataset:",
+choices = c("rock", "pressure", "cars")),
+
+# Input: Numeric entry for number of obs to view ----
+numericInput(inputId = "obs",
+label = "Number of observations to view:",
+value = 10)
+),
+
+# Main panel for displaying outputs ----
+mainPanel(
+
+# Output: Verbatim text for data summary ----
+verbatimTextOutput("summary"),
+
+# Output: HTML table with requested number of observations ----
+tableOutput("view")
+
+)
+)
+)
\ No newline at end of file
diff --git a/testing/install_external_dependencies.sh b/testing/install_external_dependencies.sh
index 8102edc..492f37e 100755
--- a/testing/install_external_dependencies.sh
+++ b/testing/install_external_dependencies.sh
@@ -67,5 +67,6 @@ if [[ "$R" == "true" ]] ; then
R -e "install.packages('knitr', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1
R -e "install.packages('ggplot2', repos = 'http://cran.us.r-project.org', lib='~/R')" > /dev/null 2>&1
R -e "install.packages('IRkernel', repos = 'http://cran.us.r-project.org', lib='~/R');IRkernel::installspec()" > /dev/null 2>&1
+ R -e "install.packages('shiny', repos = 'http://cran.us.r-project.org', lib='~/R');IRkernel::installspec()" > /dev/null 2>&1
fi
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/ProcessLauncher.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/ProcessLauncher.java
index d544211..3d37c65 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/ProcessLauncher.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/util/ProcessLauncher.java
@@ -26,6 +26,7 @@ import org.apache.commons.exec.LogOutputStream;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.zeppelin.interpreter.InterpreterContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -70,6 +71,20 @@ public abstract class ProcessLauncher implements ExecuteResultHandler {
this.processOutput = processLogOutput;
}
+ /**
+ * In some cases we need to redirect process output to paragraph's InterpreterOutput.
+ * e.g. In %r.shiny for shiny app
+ * @param redirectedContext
+ */
+ public void setRedirectedContext(InterpreterContext redirectedContext) {
+ if (redirectedContext != null) {
+ LOGGER.info("Start to redirect process output to interpreter output");
+ } else {
+ LOGGER.info("Stop to redirect process output to interpreter output");
+ }
+ this.processOutput.redirectedContext = redirectedContext;
+ }
+
public void launch() {
DefaultExecutor executor = new DefaultExecutor();
executor.setStreamHandler(new PumpStreamHandler(processOutput));
@@ -152,6 +167,7 @@ public abstract class ProcessLauncher implements ExecuteResultHandler {
private boolean catchLaunchOutput = true;
private StringBuilder launchOutput = new StringBuilder();
+ private InterpreterContext redirectedContext;
public void stopCatchLaunchOutput() {
this.catchLaunchOutput = false;
@@ -172,6 +188,13 @@ public abstract class ProcessLauncher implements ExecuteResultHandler {
if (catchLaunchOutput) {
launchOutput.append(s + "\n");
}
+ if (redirectedContext != null) {
+ try {
+ redirectedContext.out.write(s + "\n");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
}
}
diff --git a/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelClient.java b/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelClient.java
index 8b64a9b..54112d4 100644
--- a/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelClient.java
+++ b/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelClient.java
@@ -21,6 +21,7 @@ import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.jupyter.proto.JupyterKernelGrpc;
import org.apache.zeppelin.interpreter.util.InterpreterOutputStream;
import org.apache.zeppelin.interpreter.jupyter.proto.CancelRequest;
@@ -40,8 +41,11 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Iterator;
+import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* Grpc client for Jupyter kernel
@@ -49,34 +53,78 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class JupyterKernelClient {
private static final Logger LOGGER = LoggerFactory.getLogger(JupyterKernelClient.class.getName());
+ // used for matching shiny url
+ private static Pattern ShinyListeningPattern =
+ Pattern.compile(".*Listening on (http:\\S*).*", Pattern.DOTALL);
private final ManagedChannel channel;
private final JupyterKernelGrpc.JupyterKernelBlockingStub blockingStub;
private final JupyterKernelGrpc.JupyterKernelStub asyncStub;
private volatile boolean maybeKernelFailed = false;
+ private Properties properties;
+ private InterpreterContext context;
private SecureRandom random = new SecureRandom();
/**
* Construct client for accessing RouteGuide server at {@code host:port}.
*/
- public JupyterKernelClient(String host, int port) {
- this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true));
+ public JupyterKernelClient(String host,
+ int port) {
+ this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true), new Properties());
}
/**
* Construct client for accessing RouteGuide server using the existing channel.
*/
- public JupyterKernelClient(ManagedChannelBuilder<?> channelBuilder) {
+ public JupyterKernelClient(ManagedChannelBuilder<?> channelBuilder, Properties properties) {
channel = channelBuilder.build();
blockingStub = JupyterKernelGrpc.newBlockingStub(channel);
asyncStub = JupyterKernelGrpc.newStub(channel);
+ this.properties = properties;
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
+ /**
+ * set current InterpreterContext.
+ * @param context
+ */
+ public void setInterpreterContext(InterpreterContext context) {
+ this.context = context;
+ }
+
+ /**
+ * This is for shiny interpreter. It's better not to put this in the general
+ * JupyterKernelClient, we may need to create a specififc JupyterKernelClient for R Kernel.
+ * @param response
+ * @return true if shiny url is matched
+ * @throws IOException
+ */
+ private boolean checkForShinyApp(String response) throws IOException {
+ if (context.getInterpreterClassName() != null &&
+ context.getInterpreterClassName().equals("org.apache.zeppelin.r.ShinyInterpreter")) {
+ Matcher matcher = ShinyListeningPattern.matcher(response);
+ if (matcher.matches()) {
+ String url = matcher.group(1);
+ LOGGER.info("Matching shiny app url: " + url);
+ context.out.clear();
+ String defaultHeight = properties.getProperty("zeppelin.R.shiny.iframe_height", "500px");
+ String height = context.getLocalProperties().getOrDefault("height", defaultHeight);
+ String defaultWidth = properties.getProperty("zeppelin.R.shiny.iframe_width", "100%");
+ String width = context.getLocalProperties().getOrDefault("width", defaultWidth);
+ context.out.write("\n%html " + "<iframe src=\"" + url + "\" height =\"" +
+ height + "\" width=\"" + width + "\" frameBorder=\"0\"></iframe>");
+ context.out.flush();
+ context.out.write("\n%text ");
+ return true;
+ }
+ }
+ return false;
+ }
+
// execute the code and make the output as streaming by writing it to InterpreterOutputStream
// one by one.
public ExecuteResponse stream_execute(ExecuteRequest request,
@@ -96,6 +144,9 @@ public class JupyterKernelClient {
switch (executeResponse.getType()) {
case TEXT:
try {
+ if (checkForShinyApp(executeResponse.getOutput())) {
+ break;
+ }
if (executeResponse.getOutput().startsWith("%")) {
// the output from jupyter kernel maybe specify format already.
interpreterOutput.write((executeResponse.getOutput()).getBytes());
@@ -239,6 +290,5 @@ public class JupyterKernelClient {
ExecuteResponse response = client.block_execute(ExecuteRequest.newBuilder().
setCode("abcd=2").build());
System.out.println(response.getOutput());
-
}
}
diff --git a/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelInterpreter.java b/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelInterpreter.java
index 3a15337..d1dc351 100644
--- a/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelInterpreter.java
+++ b/zeppelin-jupyter-interpreter/src/main/java/org/apache/zeppelin/jupyter/JupyterKernelInterpreter.java
@@ -123,10 +123,10 @@ public class JupyterKernelInterpreter extends AbstractInterpreter {
int kernelPort = RemoteInterpreterUtils.findRandomAvailablePortOnAllLocalInterfaces();
int message_size = Integer.parseInt(getProperty("zeppelin.jupyter.kernel.grpc.message_size",
32 * 1024 * 1024 + ""));
- jupyterKernelClient = new JupyterKernelClient(ManagedChannelBuilder.forAddress("127.0.0.1",
- kernelPort)
- .usePlaintext(true).maxInboundMessageSize(message_size));
+ jupyterKernelClient = new JupyterKernelClient(ManagedChannelBuilder.forAddress("127.0.0.1",
+ kernelPort).usePlaintext(true).maxInboundMessageSize(message_size),
+ getProperties());
launchJupyterKernel(kernelPort);
} catch (Exception e) {
throw new InterpreterException("Fail to open JupyterKernelInterpreter:\n" +
@@ -221,7 +221,7 @@ public class JupyterKernelInterpreter extends AbstractInterpreter {
@Override
public void close() throws InterpreterException {
if (jupyterKernelProcessLauncher != null) {
- LOGGER.info("Killing Jupyter Kernel Process");
+ LOGGER.info("Shutdown Jupyter Kernel Process");
if (jupyterKernelProcessLauncher.isRunning()) {
jupyterKernelClient.stop(StopRequest.newBuilder().build());
try {
@@ -243,6 +243,7 @@ public class JupyterKernelInterpreter extends AbstractInterpreter {
z.setNoteGui(context.getNoteGui());
z.setInterpreterContext(context);
interpreterOutput.setInterpreterOutput(context.out);
+ jupyterKernelClient.setInterpreterContext(context);
try {
ExecuteResponse response =
jupyterKernelClient.stream_execute(ExecuteRequest.newBuilder().setCode(st).build(),
diff --git a/zeppelin-jupyter-interpreter/src/main/resources/grpc/jupyter/kernel_server.py b/zeppelin-jupyter-interpreter/src/main/resources/grpc/jupyter/kernel_server.py
index 5708c71..ac11ea3 100644
--- a/zeppelin-jupyter-interpreter/src/main/resources/grpc/jupyter/kernel_server.py
+++ b/zeppelin-jupyter-interpreter/src/main/resources/grpc/jupyter/kernel_server.py
@@ -51,22 +51,21 @@ class KernelServer(kernel_pb2_grpc.JupyterKernelServicer):
self._status = kernel_pb2.RUNNING
def execute(self, request, context):
- print("execute code:\n")
- print(request.code.encode('utf-8'))
+ # print("execute code:\n")
+ # print(request.code.encode('utf-8'))
sys.stdout.flush()
stream_reply_queue = queue.Queue(maxsize = 30)
payload_reply = []
def _output_hook(msg):
msg_type = msg['header']['msg_type']
content = msg['content']
- print("******************* CONTENT ******************")
outStatus, outType, output = kernel_pb2.SUCCESS, None, None
# prepare the reply
if msg_type == 'stream':
outType = kernel_pb2.TEXT
output = content['text']
elif msg_type in ('display_data', 'execute_result'):
- print(content['data'])
+ # print(content['data'])
# The if-else order matters, can not be changed. Because ipython may provide multiple output.
# TEXT is the last resort type.
if 'text/html' in content['data']:
diff --git a/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IPythonKernelTest.java b/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IPythonKernelTest.java
index 59b957f..ed53169 100644
--- a/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IPythonKernelTest.java
+++ b/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IPythonKernelTest.java
@@ -29,6 +29,7 @@ import org.apache.zeppelin.interpreter.remote.RemoteInterpreterEventClient;
import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
import org.apache.zeppelin.resource.LocalResourcePool;
import org.apache.zeppelin.resource.ResourcePool;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -68,6 +69,13 @@ public class IPythonKernelTest {
interpreter.open();
}
+ @After
+ public void tearDown() throws InterpreterException {
+ if (interpreter != null) {
+ interpreter.close();
+ }
+ }
+
@Test
public void testPythonBasics() throws InterpreterException, InterruptedException, IOException {
InterpreterContext context = getInterpreterContext();
diff --git a/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IRKernelTest.java b/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IRKernelTest.java
index 246400f..002e97b 100644
--- a/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IRKernelTest.java
+++ b/zeppelin-jupyter-interpreter/src/test/java/org/apache/zeppelin/jupyter/IRKernelTest.java
@@ -64,7 +64,9 @@ public class IRKernelTest {
@After
public void tearDown() throws InterpreterException {
- interpreter.close();
+ if (interpreter != null) {
+ interpreter.close();
+ }
}
@Test
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java
index 09838f4..5405d9e 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Paragraph.java
@@ -396,7 +396,9 @@ public class Paragraph extends JobWithProgressPoller<InterpreterResult> implemen
}
}
- return Strings.isNullOrEmpty(scriptText);
+ // don't skip paragraph when local properties is not empty.
+ // local properties can customize the behavior of interpreter. e.g. %r.shiny(type=run)
+ return Strings.isNullOrEmpty(scriptText) && localProperties.isEmpty();
}
public boolean execute(boolean blocking) {