diff --git a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java
index cd34834c8..405b23ac5 100644
--- a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java
+++ b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java
@@ -9,7 +9,6 @@ import com.taobao.arthas.core.shell.command.CommandResolver;
import com.taobao.arthas.core.shell.handlers.BindHandler;
import com.taobao.arthas.core.shell.impl.ShellServerImpl;
import com.taobao.arthas.core.shell.term.impl.HttpTermServer;
-import com.taobao.arthas.core.shell.term.impl.TelnetTermServer;
import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer;
import com.taobao.arthas.core.util.ArthasBanner;
import com.taobao.arthas.core.util.Constants;
@@ -19,6 +18,7 @@ import com.taobao.middleware.logger.Logger;
import io.netty.channel.ChannelFuture;
+import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
@@ -50,10 +50,16 @@ public class ArthasBootstrap {
private ExecutorService executorService;
private TunnelClient tunnelClient;
+ private File arthasOutputDir;
+
private ArthasBootstrap(int pid, Instrumentation instrumentation) {
this.pid = pid;
this.instrumentation = instrumentation;
+ String outputPath = System.getProperty("arthas.output.dir", "arthas-output");
+ arthasOutputDir = new File(outputPath);
+ arthasOutputDir.mkdirs();
+
executorService = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java
new file mode 100644
index 000000000..eae6e12b5
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java
@@ -0,0 +1,153 @@
+package com.taobao.arthas.core.shell.term.impl.httptelnet;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+
+import com.taobao.arthas.common.IOUtils;
+
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+
+/**
+ *
+ * @author hengyunabc 2019-11-06
+ *
+ */
+public class DirectoryBrowser {
+
+ //@formatter:off
+ private static String pageHeader = "\n" +
+ "\n" +
+ "\n" +
+ "
\n" +
+ " Arthas Resouces: %s\n" +
+ " \n" +
+ " \n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ "
\n" +
+ " \n" +
+ " \n";
+
+ private static String pageFooter = "
\n" +
+ " \n" +
+ "
\n" +
+ "\n" +
+ "\n" +
+ "";
+ //@formatter:on
+
+ private static String linePart1Str = "";
+ private static String linePart2Str = "%-60s";
+
+ private static String renderDir(File dir) {
+ File[] listFiles = dir.listFiles();
+
+ StringBuilder sb = new StringBuilder(8192);
+ String dirName = dir.getName() + "/";
+ sb.append(String.format(pageHeader, dirName, dirName));
+
+ sb.append("../\n");
+
+ if (listFiles != null) {
+ Arrays.sort(listFiles);
+ for (File f : listFiles) {
+ if (f.isDirectory()) {
+ String name = f.getName() + "/";
+ String part1Format = String.format(linePart1Str, name, name, name);
+ sb.append(part1Format);
+
+ String linePart2 = name + "";
+ String part2Format = String.format(linePart2Str, linePart2);
+ sb.append(part2Format);
+
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String modifyStr = simpleDateFormat.format(new Date(f.lastModified()));
+
+ sb.append(modifyStr);
+ sb.append(" - ").append("\r\n");
+ }
+ }
+
+ for (File f : listFiles) {
+ if (f.isFile()) {
+ String name = f.getName();
+ String part1Format = String.format(linePart1Str, name, name, name);
+ sb.append(part1Format);
+
+ String linePart2 = name + "";
+ String part2Format = String.format(linePart2Str, linePart2);
+ sb.append(part2Format);
+
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String modifyStr = simpleDateFormat.format(new Date(f.lastModified()));
+ sb.append(modifyStr);
+
+ String sizeStr = String.format("%10d ", f.length());
+ sb.append(sizeStr).append("\r\n");
+ }
+ }
+ }
+
+ sb.append(pageFooter);
+ return sb.toString();
+ }
+
+ public static DefaultFullHttpResponse view(File dir, String path, HttpVersion version) throws IOException {
+ if (path.startsWith("/")) {
+ path = path.substring(1, path.length());
+ }
+ File file = new File(path);
+
+ if (isSubFile(dir, file)) {
+ DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(version, HttpResponseStatus.OK);
+
+ if (file.isDirectory()) {
+ if (!path.endsWith("/")) {
+ fullResp.setStatus(HttpResponseStatus.FOUND).headers().set(HttpHeaderNames.LOCATION, "/" + path + "/");
+ }
+
+ String renderResult = renderDir(file);
+ fullResp.content().writeBytes(renderResult.getBytes("utf-8"));
+ fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, "content-type: text/html; charset=utf-8");
+ } else {
+ FileInputStream fileInputStream = new FileInputStream(file);
+ try {
+ byte[] content = IOUtils.getBytes(fileInputStream);
+ fullResp.content().writeBytes(content);
+ HttpUtil.setContentLength(fullResp, fullResp.content().readableBytes());
+ } finally {
+ IOUtils.close(fileInputStream);
+ }
+ }
+ return fullResp;
+ }
+
+ return null;
+ }
+
+ public static boolean isSubFile(File parent, File child) throws IOException {
+ String parentPath = parent.getCanonicalPath();
+ String childPath = child.getCanonicalPath();
+ if (parentPath.equals(childPath) || childPath.startsWith(parent.getCanonicalPath() + File.separator)) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java
new file mode 100644
index 000000000..12b598051
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java
@@ -0,0 +1,122 @@
+package com.taobao.arthas.core.shell.term.impl.httptelnet;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+
+import com.taobao.arthas.common.IOUtils;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.DefaultHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.LastHttpContent;
+import io.termd.core.http.HttpTtyConnection;
+import io.termd.core.util.Logging;
+
+/**
+ * @author Julien Viet
+ * @author hengyunabc 2019-11-06
+ */
+public class HttpRequestHandler extends SimpleChannelInboundHandler {
+
+ private final String wsUri;
+
+ private File dir;
+
+ public HttpRequestHandler(String wsUri, File dir) {
+ this.wsUri = wsUri;
+ this.dir = dir;
+ dir.mkdirs();
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
+ if (wsUri.equalsIgnoreCase(request.uri())) {
+ ctx.fireChannelRead(request.retain());
+ } else {
+ if (HttpUtil.is100ContinueExpected(request)) {
+ send100Continue(ctx);
+ }
+
+ HttpResponse response = new DefaultHttpResponse(request.protocolVersion(),
+ HttpResponseStatus.INTERNAL_SERVER_ERROR);
+
+ String path = new URI(request.uri()).getPath();
+
+ if ("/".equals(path)) {
+ path = "/index.html";
+ }
+
+ InputStream in = null;
+ try {
+
+ DefaultFullHttpResponse fileViewResult = DirectoryBrowser.view(dir, path, request.protocolVersion());
+
+ if (fileViewResult != null) {
+ response = fileViewResult;
+ } else {
+ URL res = HttpTtyConnection.class.getResource("/io/termd/core/http" + path);
+ if (res != null) {
+ DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(request.protocolVersion(),
+ HttpResponseStatus.OK);
+ in = res.openStream();
+ byte[] tmp = new byte[256];
+ for (int l = 0; l != -1; l = in.read(tmp)) {
+ fullResp.content().writeBytes(tmp, 0, l);
+ }
+ int li = path.lastIndexOf('.');
+ if (li != -1 && li != path.length() - 1) {
+ String ext = path.substring(li + 1, path.length());
+ String contentType;
+ if ("html".equals(ext)) {
+ contentType = "text/html";
+ } else if ("js".equals(ext)) {
+ contentType = "application/javascript";
+ } else if ("css".equals(ext)) {
+ contentType = "text/css";
+ } else {
+ contentType = null;
+ }
+
+ if (contentType != null) {
+ fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
+ }
+ }
+ response = fullResp;
+ } else {
+ response.setStatus(HttpResponseStatus.NOT_FOUND);
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ ctx.write(response);
+ ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+ future.addListener(ChannelFutureListener.CLOSE);
+ IOUtils.close(in);
+ }
+ }
+ }
+
+ private static void send100Continue(ChannelHandlerContext ctx) {
+ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
+ ctx.writeAndFlush(response);
+ }
+
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ Logging.logReportedIoError(cause);
+ ctx.close();
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java
index 6562cfaac..74d05a68c 100644
--- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java
+++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java
@@ -1,5 +1,6 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
+import java.io.File;
import java.util.concurrent.TimeUnit;
import io.netty.buffer.ByteBuf;
@@ -14,7 +15,6 @@ import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.concurrent.ScheduledFuture;
import io.termd.core.function.Consumer;
import io.termd.core.function.Supplier;
-import io.termd.core.http.netty.HttpRequestHandler;
import io.termd.core.http.netty.TtyWebSocketFrameHandler;
import io.termd.core.telnet.TelnetHandler;
import io.termd.core.telnet.netty.TelnetChannelHandler;
@@ -31,7 +31,7 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
private Consumer ttyConnectionFactory;
public ProtocolDetectHandler(ChannelGroup channelGroup, final Supplier handlerFactory,
- Consumer ttyConnectionFactory) {
+ Consumer ttyConnectionFactory) {
this.channelGroup = channelGroup;
this.handlerFactory = handlerFactory;
this.ttyConnectionFactory = ttyConnectionFactory;
@@ -41,29 +41,32 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
- detectTelnetFuture = ctx.executor().schedule(new Runnable() {
+ detectTelnetFuture = ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
channelGroup.add(ctx.channel());
TelnetChannelHandler handler = new TelnetChannelHandler(handlerFactory);
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast(handler);
- ctx.fireChannelActive();
pipeline.remove(ProtocolDetectHandler.this);
+ ctx.fireChannelActive(); // trigger TelnetChannelHandler init
}
+
}, 500, TimeUnit.MILLISECONDS);
}
@Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
if (in.readableBytes() < 3) {
return;
}
- detectTelnetFuture.cancel(false);
+ if (detectTelnetFuture != null && detectTelnetFuture.isCancellable()) {
+ detectTelnetFuture.cancel(false);
+ }
- byte[] bytes = new byte[4];
+ byte[] bytes = new byte[3];
in.getBytes(0, bytes);
String httpHeader = new String(bytes);
@@ -77,11 +80,12 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
- pipeline.addLast(new HttpRequestHandler("/ws"));
+ pipeline.addLast(new HttpRequestHandler("/ws", new File("arthas-output")));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TtyWebSocketFrameHandler(channelGroup, ttyConnectionFactory));
}
- ctx.fireChannelRead(msg);
+ ctx.fireChannelRead(in);
pipeline.remove(this);
}
+
}