Эх сурвалжийг харах

初步实现弃用`SSE`,改用`WebSocket`实现

mxd 4 жил өмнө
parent
commit
7a57129032
18 өөрчлөгдсөн 824 нэмэгдсэн , 375 устгасан
  1. 18 1
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java
  2. 4 0
      magic-api/pom.xml
  3. 14 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/Message.java
  4. 20 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/MessageType.java
  5. 72 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/WebSocketSessionManager.java
  6. 47 0
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDebugHandler.java
  7. 80 0
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWebSocketDispatcher.java
  8. 0 15
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java
  9. 43 200
      magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java
  10. 7 46
      magic-api/src/main/java/org/ssssssss/magicapi/logging/MagicLoggerContext.java
  11. 10 0
      magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java
  12. 6 12
      magic-api/src/main/java/org/ssssssss/magicapi/model/RequestEntity.java
  13. 0 4
      magic-editor/src/console/src/api/request.js
  14. 72 97
      magic-editor/src/console/src/components/editor/magic-script-editor.vue
  15. 4 0
      magic-editor/src/console/src/components/magic-editor.vue
  16. 1 0
      magic-editor/src/console/src/scripts/contants.js
  17. 382 0
      magic-editor/src/console/src/scripts/reconnecting-websocket.js
  18. 44 0
      magic-editor/src/console/src/scripts/websocket.js

+ 18 - 1
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java

@@ -30,6 +30,10 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 import org.ssssssss.magicapi.adapter.ColumnMapperAdapter;
 import org.ssssssss.magicapi.adapter.DialectAdapter;
 import org.ssssssss.magicapi.adapter.Resource;
@@ -73,7 +77,8 @@ import java.util.function.BiFunction;
 @ConditionalOnClass({RequestMappingHandlerMapping.class})
 @EnableConfigurationProperties(MagicAPIProperties.class)
 @Import({MagicRedisAutoConfiguration.class, MagicMongoAutoConfiguration.class, MagicSwaggerConfiguration.class, MagicJsonAutoConfiguration.class, ApplicationUriPrinter.class})
-public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
+@EnableWebSocket
+public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketConfigurer {
 
 	private static final Logger logger = LoggerFactory.getLogger(MagicAPIAutoConfiguration.class);
 
@@ -575,4 +580,16 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		return restTemplate;
 	}
 
+	@Override
+	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
+		String web = properties.getWeb();
+		if (web != null) {
+			WebSocketHandlerRegistration registration = webSocketHandlerRegistry.addHandler(new MagicWebSocketDispatcher(Arrays.asList(
+					new MagicDebugHandler()
+			)), web + "/console");
+			if (properties.isSupportCrossDomain()) {
+				registration.setAllowedOrigins("*");
+			}
+		}
+	}
 }

+ 4 - 0
magic-api/pom.xml

@@ -22,6 +22,10 @@
             <artifactId>spring-boot-starter</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>

+ 14 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/Message.java

@@ -0,0 +1,14 @@
+package org.ssssssss.magicapi.config;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Message {
+
+	/**
+	 * 消息类型
+	 */
+	MessageType value();
+}

+ 20 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/MessageType.java

@@ -0,0 +1,20 @@
+package org.ssssssss.magicapi.config;
+
+/**
+ * 消息类型
+ * */
+public enum MessageType {
+	/* S -> C message */
+	/* 日志消息 */
+	LOG,
+	/* 进入断点 */
+	BREAKPOINT,
+
+	/* C -> S message */
+	/* 设置断点 */
+	SET_BREAKPOINT,
+	/* 恢复断点 */
+	RESUME_BREAKPOINT,
+	/* 设置 Session ID */
+	SET_SESSION_ID,
+}

+ 72 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/WebSocketSessionManager.java

@@ -0,0 +1,72 @@
+package org.ssssssss.magicapi.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.ssssssss.magicapi.model.Constants;
+import org.ssssssss.magicapi.utils.JsonUtils;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class WebSocketSessionManager {
+
+	private static Logger logger = LoggerFactory.getLogger(WebSocketSessionManager.class);
+
+	private static final Map<String, WebSocketSession> SESSION = new ConcurrentHashMap<>();
+
+	public static void add(WebSocketSession session) {
+		SESSION.put(session.getId(), session);
+	}
+
+	public static void remove(WebSocketSession session) {
+		SESSION.remove(session.getId());
+	}
+
+	public static void sendBySessionId(String sessionId, MessageType messageType, Object... values) {
+		WebSocketSession session = findSession(sessionId);
+		if (session != null) {
+			StringBuilder builder = new StringBuilder(messageType.name().toLowerCase());
+			if (values != null) {
+				for (int i = 0, len = values.length; i < len; i++) {
+					builder.append(",");
+					Object value = values[i];
+					if (i + 1 < len || value instanceof CharSequence || value instanceof Number) {
+						builder.append(value);
+					} else {
+						builder.append(JsonUtils.toJsonString(value));
+					}
+				}
+			}
+			try {
+				session.sendMessage(new TextMessage(builder.toString()));
+			} catch (IOException e) {
+				logger.error("发送WebSocket消息失败", e);
+			}
+		}
+	}
+
+	/**
+	 * 获取Session中的属性
+	 */
+	public static <T> T getSessionAttribute(String sessionId, String key) {
+		WebSocketSession session = findSession(sessionId);
+		if (session != null) {
+			return (T) session.getAttributes().get(key);
+		}
+		return null;
+	}
+
+	public static void setSessionAttribute(String sessionId, String key, Object value) {
+		WebSocketSession session = findSession(sessionId);
+		if (session != null) {
+			session.getAttributes().put(key, value);
+		}
+	}
+
+	private static WebSocketSession findSession(String sessionId) {
+		return SESSION.values().stream().filter(it -> sessionId.equals(it.getAttributes().get(Constants.WS_DEBUG_SESSION_KEY))).findFirst().orElse(null);
+	}
+}

+ 47 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDebugHandler.java

@@ -0,0 +1,47 @@
+package org.ssssssss.magicapi.controller;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.socket.WebSocketSession;
+import org.ssssssss.magicapi.config.Message;
+import org.ssssssss.magicapi.config.MessageType;
+import org.ssssssss.magicapi.model.Constants;
+import org.ssssssss.script.MagicScriptDebugContext;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class MagicDebugHandler {
+
+	/**
+	 * 设置会话ID
+	 */
+	@Message(MessageType.SET_SESSION_ID)
+	public void setSessionId(WebSocketSession session, String sessionId) {
+		if(StringUtils.isNotBlank(sessionId)){
+			session.getAttributes().put(Constants.WS_DEBUG_SESSION_KEY, sessionId);
+		}
+	}
+	/**
+	 * 设置断点
+	 */
+	@Message(MessageType.SET_BREAKPOINT)
+	public void setBreakPoint(WebSocketSession session, String breakpoints) {
+		if(StringUtils.isNotBlank(breakpoints)){
+			MagicScriptDebugContext context = (MagicScriptDebugContext) session.getAttributes().get(Constants.WS_DEBUG_MAGIC_SCRIPT_CONTEXT);
+			context.setBreakpoints(Stream.of(breakpoints.split(",")).map(Integer::valueOf).collect(Collectors.toList()));
+		}
+	}
+
+	/**
+	 * 恢复断点
+	 */
+	@Message(MessageType.RESUME_BREAKPOINT)
+	public void resumeBreakpoint(WebSocketSession session, String stepInto) {
+		MagicScriptDebugContext context = (MagicScriptDebugContext) session.getAttributes().get(Constants.WS_DEBUG_MAGIC_SCRIPT_CONTEXT);
+		context.setStepInto("1".equals(stepInto));
+		try {
+			context.singal();
+		} catch (InterruptedException ignored) {
+		}
+	}
+}

+ 80 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWebSocketDispatcher.java

@@ -0,0 +1,80 @@
+package org.ssssssss.magicapi.controller;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+import org.ssssssss.magicapi.config.Message;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
+import org.ssssssss.magicapi.utils.JsonUtils;
+import org.ssssssss.script.reflection.MethodInvoker;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class MagicWebSocketDispatcher extends TextWebSocketHandler {
+
+	private static final Logger logger = LoggerFactory.getLogger(MagicWebSocketDispatcher.class);
+
+	private final Map<String, MethodInvoker> handlers = new HashMap<>();
+
+	public MagicWebSocketDispatcher(List<Object> websocketMessageHandlers) {
+		websocketMessageHandlers.forEach(websocketMessageHandler ->
+				Stream.of(websocketMessageHandler.getClass().getDeclaredMethods())
+						.forEach(method -> handlers.put(method.getAnnotation(Message.class).value().name().toLowerCase(), new MethodInvoker(method, websocketMessageHandler)))
+		);
+	}
+
+	@Override
+	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+		WebSocketSessionManager.add(session);
+	}
+
+	@Override
+	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+		WebSocketSessionManager.remove(session);
+	}
+
+	@Override
+	protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+		// messageType[, data][,data]
+		String payload = message.getPayload();
+		int index = payload.indexOf(",");
+		String msgType = index == -1 ? payload : payload.substring(0, index);
+		MethodInvoker invoker = handlers.get(msgType);
+		if (invoker != null) {
+			try {
+				Class<?>[] pTypes = invoker.getParameterTypes();
+				int pCount = pTypes.length;
+				if (pCount == 0) {
+					invoker.invoke0(null, null);
+				} else {
+					Object[] pValues = new Object[pCount];
+					for (int i = 0; i < pCount; i++) {
+						Class<?> pType = pTypes[i];
+						if (pType == WebSocketSession.class) {
+							pValues[i] = session;
+						} else if (pType == String.class) {
+							int subIndex = payload.indexOf(",", index + 1);
+							if (subIndex > -1) {
+								pValues[i] = payload.substring(index + 1, index = subIndex);
+							} else if(index > -1){
+								pValues[i] = payload.substring(index + 1);
+							}
+						} else {
+							pValues[i] = JsonUtils.readValue(payload, pType);
+						}
+					}
+					invoker.invoke0(null, null, pValues);
+				}
+			} catch (Throwable e) {
+				logger.error("WebSocket消息处理出错", e);
+			}
+		}
+	}
+
+}

+ 0 - 15
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -14,14 +14,11 @@ import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import org.ssssssss.magicapi.adapter.Resource;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.exception.MagicLoginException;
 import org.ssssssss.magicapi.interceptor.Authorization;
 import org.ssssssss.magicapi.interceptor.MagicUser;
-import org.ssssssss.magicapi.logging.MagicLoggerContext;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
 import org.ssssssss.magicapi.modules.SQLModule;
@@ -148,18 +145,6 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	}
 
 
-	/**
-	 * 创建控制台输出
-	 */
-	@RequestMapping("/console")
-	@Valid(requireLogin = false)
-	public SseEmitter console() throws IOException {
-		String sessionId = UUID.randomUUID().toString().replace("-", "");
-		SseEmitter emitter = MagicLoggerContext.createEmitter(sessionId);
-		emitter.send(SseEmitter.event().data(sessionId).name("create"));
-		return emitter;
-	}
-
 	@RequestMapping("/options")
 	@ResponseBody
 	@Valid(requireLogin = false)

+ 43 - 200
magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java

@@ -1,13 +1,8 @@
 package org.ssssssss.magicapi.controller;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.slf4j.event.Level;
-import org.springframework.core.io.InputStreamSource;
-import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageConverter;
@@ -16,16 +11,12 @@ import org.springframework.http.server.ServletServerHttpRequest;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.ssssssss.magicapi.config.MagicConfiguration;
-import org.ssssssss.magicapi.config.MappingHandlerMapping;
-import org.ssssssss.magicapi.config.Valid;
+import org.ssssssss.magicapi.config.*;
 import org.ssssssss.magicapi.context.CookieContext;
 import org.ssssssss.magicapi.context.RequestContext;
 import org.ssssssss.magicapi.context.SessionContext;
 import org.ssssssss.magicapi.exception.ValidateException;
 import org.ssssssss.magicapi.interceptor.RequestInterceptor;
-import org.ssssssss.magicapi.logging.LogInfo;
 import org.ssssssss.magicapi.logging.MagicLoggerContext;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
@@ -44,13 +35,13 @@ import org.ssssssss.script.reflection.JavaInvoker;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.io.InputStream;
 import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS;
+import static org.ssssssss.magicapi.config.MessageType.BREAKPOINT;
 import static org.ssssssss.magicapi.model.Constants.*;
 
 public class RequestHandler extends MagicController {
@@ -74,14 +65,9 @@ public class RequestHandler extends MagicController {
 	public Object invoke(HttpServletRequest request, HttpServletResponse response,
 						 @PathVariable(required = false) Map<String, Object> pathVariables,
 						 @RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
-		RequestEntity requestEntity = new RequestEntity(request, response, isRequestedFromTest(request), parameters, pathVariables);
-		if (requestEntity.isRequestedFromTest()) {
-			response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_TRUE);
-			response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, HEADER_RESPONSE_WITH_MAGIC_API);
-			if(requestEntity.isRequestedFromContinue()){
-				return invokeContinueRequest(requestEntity);
-			}
-		}
+		String sessionId = null;
+		boolean requestedFromTest = configuration.isEnableWeb() && (sessionId = request.getHeader(HEADER_REQUEST_SESSION)) != null;
+		RequestEntity requestEntity = new RequestEntity(request, response, requestedFromTest, parameters, pathVariables);
 		if (requestEntity.getApiInfo() == null) {
 			logger.error("{}找不到对应接口", request.getRequestURI());
 			return buildResult(requestEntity, API_NOT_FOUND, "接口不存在");
@@ -115,8 +101,7 @@ public class RequestHandler extends MagicController {
 				}}, BODY_INVALID);
 			}
 		} catch (ValidateException e) {
-			Object value = resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, e.getMessage());
-			return requestEntity.isRequestedFromTest() ? new JsonBean<>(e.getJsonCode(), value) : value;
+			return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, e.getMessage());
 		} catch (Throwable root) {
 			return processException(requestEntity, root);
 		}
@@ -126,13 +111,18 @@ public class RequestHandler extends MagicController {
 		Object value;
 		// 执行前置拦截器
 		if ((value = doPreHandle(requestEntity)) != null) {
-			if (requestEntity.isRequestedFromTest()) {
-				// 修正前端显示,当拦截器返回时,原样输出显示
-				response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-			}
 			return value;
 		}
-		return requestEntity.isRequestedFromTest() ?  invokeTestRequest(requestEntity) : invokeRequest(requestEntity);
+		if(requestedFromTest){
+			try {
+				MagicLoggerContext.SESSION.set(sessionId);
+				return invokeRequest(requestEntity);
+			} finally {
+				MagicLoggerContext.SESSION.remove();
+			}
+		}else{
+			return invokeRequest(requestEntity);
+		}
 	}
 
 	private Object buildResult(RequestEntity requestEntity, JsonCode code, Object data) {
@@ -249,61 +239,6 @@ public class RequestHandler extends MagicController {
 		}
 	}
 
-	private Object invokeContinueRequest(RequestEntity requestEntity) throws Exception {
-		HttpServletRequest request = requestEntity.getRequest();
-		MagicScriptDebugContext context = MagicScriptDebugContext.getDebugContext(requestEntity.getRequestedSessionId());
-		if (context == null) {
-			return new JsonBean<>(DEBUG_SESSION_NOT_FOUND, buildResult(requestEntity, DEBUG_SESSION_NOT_FOUND, null));
-		}
-		// 重置断点
-		context.setBreakpoints(requestEntity.getRequestedBreakpoints());
-		// 步进
-		context.setStepInto(CONST_STRING_TRUE.equalsIgnoreCase(request.getHeader(HEADER_REQUEST_STEP_INTO)));
-		try {
-			context.singal();    //等待语句执行到断点或执行完毕
-		} catch (InterruptedException e) {
-			e.printStackTrace();
-		}
-		if (context.isRunning()) {    //判断是否执行完毕
-			return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId()), context.getDebugInfo());
-		} else if (context.isException()) {
-			return resolveThrowableForTest(requestEntity, (Throwable) context.getReturnValue());
-		}
-		Object value = context.getReturnValue();
-		// 执行后置拦截器
-		if ((value = doPostHandle(requestEntity, value)) != null) {
-			// 修正前端显示,当拦截器返回时,原样输出显示
-			requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-			// 后置拦截器不包裹
-			return value;
-		}
-		return convertResult(requestEntity, context.getReturnValue());
-	}
-
-	private Object invokeTestRequest(RequestEntity requestEntity) {
-		try {
-			// 初始化debug操作
-			MagicScriptDebugContext context = initializeDebug(requestEntity);
-			Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
-			if (context.isRunning()) {
-				return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId(), result), result);
-			} else if (context.isException()) {    //判断是否出现异常
-				return resolveThrowableForTest(requestEntity, (Throwable) context.getReturnValue());
-			}
-			Object value = result;
-			// 执行后置拦截器
-			if ((value = doPostHandle(requestEntity, value)) != null) {
-				// 修正前端显示,当拦截器返回时,原样输出显示
-				requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
-				// 后置拦截器不包裹
-				return value;
-			}
-			return convertResult(requestEntity, result);
-		} catch (Exception e) {
-			return resolveThrowableForTest(requestEntity, e);
-		}
-	}
-
 	private Object invokeRequest(RequestEntity requestEntity) throws Throwable {
 		try {
 			Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
@@ -322,125 +257,28 @@ public class RequestHandler extends MagicController {
 	}
 
 	private Object processException(RequestEntity requestEntity, Throwable root) throws Throwable {
+		MagicScriptException se = null;
 		Throwable parent = root;
 		do {
 			if (parent instanceof MagicScriptAssertException) {
 				MagicScriptAssertException sae = (MagicScriptAssertException) parent;
 				return resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage());
 			}
+			if (parent instanceof MagicScriptException) {
+				se = (MagicScriptException) parent;
+			}
 		} while ((parent = parent.getCause()) != null);
 		if (configuration.isThrowException()) {
 			throw root;
 		}
 		logger.error("接口{}请求出错", requestEntity.getRequest().getRequestURI(), root);
-		return resultProvider.buildException(requestEntity, root);
-	}
-
-	/**
-	 * 转换请求结果
-	 */
-	private Object convertResult(RequestEntity requestEntity, Object result) throws IOException {
-		if (result instanceof ResponseEntity) {
-			ResponseEntity<?> entity = (ResponseEntity<?>) result;
-			List<String> headers = new ArrayList<>();
-			for (Map.Entry<String, List<String>> entry : entity.getHeaders().entrySet()) {
-				String key = entry.getKey();
-				for (String value : entry.getValue()) {
-					headers.add(HEADER_PREFIX_FOR_TEST + key);
-					requestEntity.getResponse().addHeader(HEADER_PREFIX_FOR_TEST + key, value);
-				}
-			}
-			headers.add(HEADER_RESPONSE_WITH_MAGIC_API);
-			// 允许前端读取自定义的header(跨域情况)。
-			requestEntity.getResponse().setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", headers));
-			if (entity.getHeaders().isEmpty()) {
-				return ResponseEntity.ok(new JsonBean<>(entity.getBody()));
-			}
-			return ResponseEntity.ok(new JsonBean<>(convertToBase64(entity.getBody())));
-		} else if (result instanceof ResponseModule.NullValue) {
-			// 对于return response.end() 的特殊处理
-			return new JsonBean<>(RESPONSE_CODE_SUCCESS, "empty.");
-		}
-		return new JsonBean<>(resultProvider.buildResult(requestEntity, result));
-	}
-
-	/**
-	 * 将结果转为base64
-	 */
-	private String convertToBase64(Object value) throws IOException {
-		if (value instanceof String || value instanceof Number) {
-			return convertToBase64(value.toString().getBytes());
-		} else if (value instanceof byte[]) {
-			return Base64.getEncoder().encodeToString((byte[]) value);
-		} else if (value instanceof InputStream) {
-			return convertToBase64(IOUtils.toByteArray((InputStream) value));
-		} else if (value instanceof InputStreamSource) {
-			InputStreamSource iss = (InputStreamSource) value;
-			return convertToBase64(iss.getInputStream());
-		} else {
-			return convertToBase64(new ObjectMapper().writeValueAsString(value));
-		}
-	}
-
-	/**
-	 * 解决异常
-	 */
-	private JsonBean<Object> resolveThrowableForTest(RequestEntity requestEntity, Throwable root) {
-		MagicScriptException se = null;
-		Throwable parent = root;
-		do {
-			if (parent instanceof MagicScriptAssertException) {
-				MagicScriptAssertException sae = (MagicScriptAssertException) parent;
-				return new JsonBean<>(resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage()));
-			}
-			if (parent instanceof MagicScriptException) {
-				se = (MagicScriptException) parent;
-			}
-		} while ((parent = parent.getCause()) != null);
-		logger.error("测试脚本出错", root);
-		if (se != null) {
+		if(se != null && requestEntity.isRequestedFromTest()){
 			Span.Line line = se.getLine();
+			requestEntity.getResponse().setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, HEADER_RESPONSE_WITH_MAGIC_API);
+			requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_TRUE);
 			return new JsonBodyBean<>(-1000, se.getSimpleMessage(), resultProvider.buildException(requestEntity, se), line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol()));
 		}
-		return new JsonBean<>(-1, root.getMessage(), resultProvider.buildException(requestEntity, root));
-	}
-
-	/**
-	 * 初始化DEBUG
-	 */
-	private MagicScriptDebugContext initializeDebug(RequestEntity requestEntity) {
-		MagicScriptDebugContext context = (MagicScriptDebugContext) requestEntity.getMagicScriptContext();
-		HttpServletRequest request = requestEntity.getRequest();
-		// 由于debug是开启一个新线程,为了防止在子线程中无法获取request对象,所以将request放在InheritableThreadLocal中。
-		RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
-
-		String sessionId = requestEntity.getRequestedSessionId();
-		// 设置断点
-		context.setBreakpoints(requestEntity.getRequestedBreakpoints());
-		context.setTimeout(configuration.getDebugTimeout());
-		context.setId(sessionId);
-		// 设置相关回调,打印日志,回收资源
-		context.onComplete(() -> {
-			if (context.isException()) {
-				MagicLoggerContext.println(new LogInfo(Level.ERROR.name().toLowerCase(), "执行脚本出错", (Throwable) context.getReturnValue()));
-			}
-			logger.info("Close Console Session : {}", sessionId);
-			RequestContext.remove();
-			MagicLoggerContext.remove(sessionId);
-		});
-		context.onStart(() -> {
-			RequestContext.setRequestEntity(requestEntity);
-			MagicLoggerContext.SESSION.set(sessionId);
-			logger.info("Create Console Session : {}", sessionId);
-		});
-		return context;
-	}
-
-	/**
-	 * 判断是否是测试请求
-	 */
-	private boolean isRequestedFromTest(HttpServletRequest request) {
-		return configuration.isEnableWeb() && request.getHeader(HEADER_REQUEST_SESSION) != null;
+		return resultProvider.buildException(requestEntity, root);
 	}
 
 	/**
@@ -466,9 +304,22 @@ public class RequestHandler extends MagicController {
 	/**
 	 * 构建 MagicScriptContext
 	 */
-	private MagicScriptContext createMagicScriptContext(RequestEntity requestEntity, Object requestBody) throws IOException {
+	private MagicScriptContext createMagicScriptContext(RequestEntity requestEntity, Object requestBody) {
+		List<Integer> breakpoints = requestEntity.getRequestedBreakpoints();
 		// 构建脚本上下文
-		MagicScriptContext context = requestEntity.isRequestedFromTest() ? new MagicScriptDebugContext() : new MagicScriptContext();
+		MagicScriptContext context;
+		// TODO 安全校验
+		if(requestEntity.isRequestedFromDebug()){
+			MagicScriptDebugContext debugContext = new MagicScriptDebugContext(breakpoints);
+			String sessionId = requestEntity.getRequestedSessionId();
+			debugContext.setTimeout(configuration.getDebugTimeout());
+			debugContext.setId(sessionId);
+			WebSocketSessionManager.setSessionAttribute(sessionId, WS_DEBUG_MAGIC_SCRIPT_CONTEXT, debugContext);
+			debugContext.setCallback(variables -> WebSocketSessionManager.sendBySessionId(sessionId, BREAKPOINT, variables));
+			context = debugContext;
+		}else{
+			context = new MagicScriptContext();
+		}
 		Object wrap = requestEntity.getApiInfo().getOptionValue(Options.WRAP_REQUEST_PARAMETERS.getValue());
 		if (wrap != null && StringUtils.isNotBlank(wrap.toString())) {
 			context.set(wrap.toString(), requestEntity.getParameters());
@@ -514,19 +365,11 @@ public class RequestHandler extends MagicController {
 	 * 执行前置拦截器
 	 */
 	private Object doPreHandle(RequestEntity requestEntity) throws Exception {
-		try {
-			for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
-				Object value = requestInterceptor.preHandle(requestEntity);
-				if (value != null) {
-					return value;
-				}
-			}
-		} catch (Exception e) {
-			if (requestEntity.isRequestedFromTest()) {
-				// 修正前端显示,原样输出显示
-				requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
+		for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
+			Object value = requestInterceptor.preHandle(requestEntity);
+			if (value != null) {
+				return value;
 			}
-			throw e;
 		}
 		return null;
 	}

+ 7 - 46
magic-api/src/main/java/org/ssssssss/magicapi/logging/MagicLoggerContext.java

@@ -1,49 +1,23 @@
 package org.ssssssss.magicapi.logging;
 
+import org.slf4j.MDC;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import org.ssssssss.magicapi.config.MessageType;
+import org.ssssssss.magicapi.config.WebSocketSessionManager;
 import org.ssssssss.script.MagicScriptContext;
 import org.ssssssss.script.MagicScriptDebugContext;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 public interface MagicLoggerContext {
 
-	String LOGGER_NAME = "MagicAPI";
-
-	Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
+	String LOGGER_NAME = "magic";
 
 	ThreadLocal<String> SESSION = new InheritableThreadLocal<>();
 
-	/**
-	 * 创建sseEmitter推送
-	 *
-	 * @param sessionId 会话id
-	 */
-	static SseEmitter createEmitter(String sessionId) {
-		SseEmitter sseEmitter = new SseEmitter(0L);
-		emitterMap.put(sessionId, sseEmitter);
-		return sseEmitter;
-	}
-
-	/**
-	 * 删除会话
-	 *
-	 * @param sessionId 会话id
-	 */
-	static void remove(String sessionId) {
-		SseEmitter sseEmitter = emitterMap.remove(sessionId);
-		SESSION.remove();
-		if (sseEmitter != null) {
-			try {
-				sseEmitter.send(SseEmitter.event().data(sessionId).name("close"));
-			} catch (IOException ignored) {
-			}
-		}
-	}
-
-
 	/**
 	 * 打印日志
 	 *
@@ -51,22 +25,9 @@ public interface MagicLoggerContext {
 	 */
 	static void println(LogInfo logInfo) {
 		// 获取SessionId
-		MagicScriptContext context = MagicScriptContext.get();
-		String sessionId;
-		if (context instanceof MagicScriptDebugContext) {
-			sessionId = ((MagicScriptDebugContext) context).getId();
-		} else {
-			sessionId = SESSION.get();
-		}
+		String sessionId = SESSION.get();
 		if (sessionId != null) {
-			SseEmitter sseEmitter = emitterMap.get(sessionId);
-			if (sseEmitter != null) {
-				try {
-					// 推送日志事件
-					sseEmitter.send(SseEmitter.event().data(logInfo).name("log"));
-				} catch (IOException ignored) {
-				}
-			}
+			WebSocketSessionManager.sendBySessionId(sessionId, MessageType.LOG, logInfo);
 		}
 	}
 

+ 10 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java

@@ -78,6 +78,16 @@ public class Constants {
 	 */
 	public static final String VAR_NAME_PATH_VARIABLE = "path";
 
+	/**
+	 * WebSocket存储的sessionId
+	 */
+	public static final String WS_DEBUG_SESSION_KEY = "sessionId";
+
+	/**
+	 * WebSocket存储的MagicScriptDebugContext
+	 */
+	public static final String WS_DEBUG_MAGIC_SCRIPT_CONTEXT = "magicScriptContext";
+
 	/**
 	 * 脚本中header的变量名
 	 */

+ 6 - 12
magic-api/src/main/java/org/ssssssss/magicapi/model/RequestEntity.java

@@ -6,10 +6,7 @@ import org.ssssssss.script.functions.ObjectConvertExtension;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.ssssssss.magicapi.model.Constants.*;
@@ -103,6 +100,10 @@ public class RequestEntity {
 		return requestedFromTest;
 	}
 
+	public boolean isRequestedFromDebug(){
+		return requestedFromTest && !getRequestedBreakpoints().isEmpty();
+	}
+
 	public void setRequestedFromTest(boolean requestedFromTest) {
 		this.requestedFromTest = requestedFromTest;
 	}
@@ -147,13 +148,6 @@ public class RequestEntity {
 		return requestId;
 	}
 
-	/**
-	 * 判断是否是恢复断点
-	 */
-	public boolean isRequestedFromContinue() {
-		return request.getHeader(HEADER_REQUEST_CONTINUE) != null;
-	}
-
 	/**
 	 * 获取测试sessionId
 	 */
@@ -171,6 +165,6 @@ public class RequestEntity {
 					.map(val -> ObjectConvertExtension.asInt(val, -1))
 					.collect(Collectors.toList());
 		}
-		return null;
+		return Collections.emptyList();
 	}
 }

+ 0 - 4
magic-editor/src/console/src/api/request.js

@@ -165,10 +165,6 @@ class HttpRequest {
             })
         return httpResponse
     }
-
-    createConsole() {
-        return new EventSource(replaceURL(config.baseURL + '/console'));
-    }
 }
 
 export default new HttpRequest()

+ 72 - 97
magic-editor/src/console/src/components/editor/magic-script-editor.vue

@@ -193,6 +193,8 @@ export default {
         bus.$emit('delete-api', this.info)
       }
     })
+    bus.$on('ws_log', rows => this.onLogReceived(rows[0]))
+    bus.$on('ws_breakpoint', rows => this.onBreakpoint(rows[0]))
     let javaTypes = {
       'String': 'java.lang.String',
       'Integer': 'java.lang.Integer',
@@ -218,6 +220,44 @@ export default {
     })
   },
   methods: {
+    onLogReceived(row){
+      if(this.info){
+        row.timestamp = utils.formatDate(new Date())
+        let throwable = row.throwable
+        delete row.throwable
+        this.info.ext.logs.push(row)
+        if (throwable) {
+          let messages = throwable.replace(/ /g, '&nbsp;').split('\n');
+          for (let i = 0; i < messages.length; i++) {
+            this.info.ext.logs.push({
+              level: row.level,
+              message: messages[i],
+              throwable: true
+            })
+          }
+        }
+      }
+    },
+    onBreakpoint(data){
+      bus.$emit('report', 'debug_in')
+      bus.$emit('status', '进入断点...')
+      // 进入断点
+      this.info.ext.debuging = true
+
+      this.info.ext.variables = data.variables
+      let range = data.range
+      let decoration = {
+        range: new monaco.Range(range[0], 1, range[0], 1),
+        options: {
+          isWholeLine: true,
+          inlineClassName: 'debug-line',
+          className: 'debug-line'
+        }
+      }
+      this.info.ext.debugDecoration = decoration
+      this.info.ext.debugDecorations = [this.editor.deltaDecorations([], [decoration])]
+      bus.$emit('switch-tab', 'debug')
+    },
     doValidate() {
       try {
         let parser = new Parser(new TokenStream(tokenizer(this.editor.getValue())))
@@ -500,32 +540,10 @@ export default {
         }
       }
       const info = this.info
-      info.ext.eventSource = request.createConsole()
-      info.ext.eventSource.addEventListener('create', e => {
-        bus.$emit('report', 'run')
-        this.$nextTick(() => this.sendTestRequest(info, requestConfig, e.data))
-      })
-      info.ext.eventSource.addEventListener('log', e => {
-        let row = JSON.parse(e.data)
-        row.timestamp = utils.formatDate(new Date())
-        let throwable = row.throwable;
-        delete row.throwable;
-        info.ext.logs.push(row)
-        if (throwable) {
-          let messages = throwable.replace(/ /g, '&nbsp;').split('\n');
-          for (let i = 0; i < messages.length; i++) {
-            info.ext.logs.push({
-              level: row.level,
-              message: messages[i],
-              throwable: true
-            })
-          }
-        }
-
-      })
-      info.ext.eventSource.addEventListener('close', e => {
-        info.ext.eventSource.close()
-      })
+      info.ext.sessionId = new Date().getTime()
+      bus.$emit('message', 'set_session_id', info.ext.sessionId)
+      this.sendTestRequest(info, requestConfig, info.ext.sessionId)
+      bus.$emit('report', 'run')
     },
     viewHistory() {
       if (!this.selected) {
@@ -560,16 +578,10 @@ export default {
       }
       let target = this.info
       if (target.ext.debuging) {
+        target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
         target.ext.debuging = false
         target.ext.variables = []
-        let requestConfig = {
-          ...target.ext.requestConfig
-        }
-        delete requestConfig.data
-        delete requestConfig.params
-        requestConfig.headers[contants.HEADER_REQUEST_CONTINUE] = true
-        requestConfig.headers[contants.HEADER_REQUEST_STEP_INTO] = step === true
-        this.sendTestRequest(target, requestConfig, target.ext.sessionId)
+        bus.$emit('message', 'resume_breakpoint', step === true ? '1' : '0')
       }
     },
     doStepInto() {
@@ -606,19 +618,30 @@ export default {
           .filter(it => it.options.linesDecorationsClassName === 'breakpoints')
           .map(it => it.range.startLineNumber)
           .join(',')
+      requestConfig.responseType = 'blob'
+      requestConfig.transformResponse = [function(data){
+        return new Promise(function(resolve, reject){
+          let reader = new FileReader()
+          reader.readAsText(data)
+          reader.onload = function() {
+            try{
+              resolve(JSON.parse(this.result))
+            }catch(e){
+              resolve(data)
+            }
+          }
+        })
+      }]
       request
           .execute(requestConfig)
           .then(res => {
-            target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
-            target.ext.debugDecorations = target.ext.debugDecoration = null
-            if (res.headers[contants.HEADER_RESPONSE_WITH_MAGIC_API] === 'true') {
-              let data = res.data
-              if (data.code === contants.RESPONSE_CODE_SCRIPT_ERROR) {
-                bus.$emit('report', 'script_error')
-                bus.$emit('status', '脚本执行出错..')
-                // 脚本执行出错
+            res.data.then(data =>{
+              const contentType = res.headers['content-type']
+              target.ext.debugDecorations && this.editor.deltaDecorations(target.ext.debugDecorations, [])
+              target.ext.debugDecorations = target.ext.debugDecoration = null
+              if (contentType.indexOf('application/json') > -1) {
                 target.ext.debuging = target.running = false
-                if (data.body) {
+                if (data.body && res.headers[contants.HEADER_RESPONSE_WITH_MAGIC_API] === 'true') {
                   let line = data.body
                   if (this.info.id === target.id) {
                     let range = new monaco.Range(line[0], line[2], line[1], line[3] + 1)
@@ -642,42 +665,21 @@ export default {
                       setTimeout(() => this.editor.deltaDecorations(decorations, []), contants.DECORATION_TIMEOUT)
                     }
                   }
+                  data = data.data;
                 }
-                target.responseBody = utils.formatJson(data.data)
+                target.responseBody = utils.formatJson(data)
                 bus.$emit('switch-tab', 'result')
                 bus.$emit('update-response-body-definition', target.responseBodyDefinition);
                 bus.$emit('update-response-body', target.responseBody)
-                target.ext.eventSource.close();
-              } else if (data.code === contants.RESPONSE_CODE_DEBUG) {
-                bus.$emit('report', 'debug_in')
-                bus.$emit('status', '进入断点...')
-                // 进入断点
-                target.ext.debuging = true
-
-                target.ext.variables = data.body.variables
-                let range = data.body.range
-                let decoration = {
-                  range: new monaco.Range(range[0], 1, range[0], 1),
-                  options: {
-                    isWholeLine: true,
-                    inlineClassName: 'debug-line',
-                    className: 'debug-line'
-                  }
-                }
-                target.ext.debugDecoration = decoration
-                target.ext.debugDecorations = [this.editor.deltaDecorations([], [decoration])]
-                bus.$emit('switch-tab', 'debug')
               } else {
                 bus.$emit('status', '脚本执行完毕')
                 // 执行完毕
                 target.running = false
                 bus.$emit('switch-tab', 'result')
-
-                let contentType = res.headers[contants.HEADER_RESPONSE_MAGIC_CONTENT_TYPE]
                 if (contentType === contants.HEADER_APPLICATION_STREAM) {
                   // 下载
-                  var disposition = res.headers[contants.HEADER_CONTENT_DISPOSITION]
-                  var filename = 'output'
+                  let disposition = res.headers[contants.HEADER_CONTENT_DISPOSITION]
+                  let filename = 'output'
                   if (disposition) {
                     filename = decodeURIComponent(disposition.substring(disposition.indexOf('filename=') + 9))
                   }
@@ -697,45 +699,18 @@ export default {
                   bus.$emit('report', 'output_blob')
                 } else if (contentType && contentType.indexOf('image') === 0) {
                   // 图片
-                  this.imageUrl = `data:${contentType};base64,${data.data}`
+                  this.imageUrl = window.URL.createObjectURL(data)
                   this.showImageDialog = true
                   bus.$emit('update-response-body-definition', target.responseBodyDefinition);
                   target.responseBody = utils.formatJson(data.data)
                   bus.$emit('update-response-body', target.responseBody)
                   bus.$emit('report', 'output_image')
-                } else if (data.code === contants.RESPONSE_NO_PERMISSION) {
-                  this.$magicAlert({
-                    title: '无权限',
-                    content: '您没有权限执行测试'
-                  })
-                } else {
-                  bus.$emit('update-response-body-definition', target.responseBodyDefinition);
-                  target.responseBody = utils.formatJson(data.data)
-                  bus.$emit('update-response-body', target.responseBody)
                 }
-                target.ext.eventSource.close();
               }
-            } else {
-              target.ext.debuging = target.running = false
-              bus.$emit('switch-tab', 'result')
-              bus.$emit('status', '脚本执行完毕');
-              // TODO 对于拦截器返回的会有警告,暂时先屏蔽掉。
-              // this.$magicAlert({
-              //   title: '警告',
-              //   content: '检测到结果异常,请检查!'
-              // })
-              try {
-                target.ext.eventSource.close();
-                bus.$emit('update-response-body-definition', target.responseBodyDefinition);
-                target.responseBody = utils.formatJson(res.data)
-                bus.$emit('update-response-body', target.responseBody)
-              } catch (ignored) {
-              }
-            }
+            })
           })
           .catch(error => {
             target.ext.debuging = target.running = false
-            target.ext.eventSource.close();
             request.processError(error)
           })
     },

+ 4 - 0
magic-editor/src/console/src/components/magic-editor.vue

@@ -39,6 +39,7 @@ import MagicDatasourceList from './resources/magic-datasource-list.vue'
 import MagicScriptEditor from './editor/magic-script-editor.vue'
 import request from '@/api/request.js'
 import contants from '@/scripts/contants.js'
+import MagicWebSocket from '@/scripts/websocket.js'
 import store from '@/scripts/store.js'
 import Key from '@/scripts/hotkey.js'
 import { replaceURL } from '@/scripts/utils.js'
@@ -84,6 +85,7 @@ export default {
       toolboxWidth: 'auto', //工具条宽度
       themeStyle: {},
       showLogin: false,
+      websocket: null,
       onLogin: () => {
         this.showLogin = false
         this.$refs.apiList.initData()
@@ -96,6 +98,7 @@ export default {
   beforeMount() {
     contants.BASE_URL = this.config.baseURL || ''
     contants.SERVER_URL = this.config.serverURL || ''
+    this.websocket = new MagicWebSocket(contants.BASE_URL.replace(/^http/, 'ws') + '/console')
     contants.DEFAULT_EXPAND = this.config.defaultExpand !== false
     this.config.version = contants.MAGIC_API_VERSION_TEXT
     this.config.title = this.config.title || 'magic-api'
@@ -186,6 +189,7 @@ export default {
   destroyed() {
     bus.$off();
     Key.unbind();
+    this.websocket.close();
   },
   methods: {
     // 隐藏loading

+ 1 - 0
magic-editor/src/console/src/scripts/contants.js

@@ -3,6 +3,7 @@ let MAGIC_API_VERSION = 'V' + MAGIC_API_VERSION_TEXT.replace(/\./g, '_')
 
 const contants = {
   BASE_URL: '', //UI 对应的接口路径
+  WEBSOCKET_SERVER: '', //WebSocket服务地址
   SERVER_URL: '', //接口对应的路径
   AUTO_SAVE: true, // 是否自动保存
   DECORATION_TIMEOUT: 10000,

+ 382 - 0
magic-editor/src/console/src/scripts/reconnecting-websocket.js

@@ -0,0 +1,382 @@
+// MIT License:
+//
+// Copyright (c) 2010-2012, Joe Walnes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * This behaves like a WebSocket in every way, except if it fails to connect,
+ * or it gets disconnected, it will repeatedly poll until it successfully connects
+ * again.
+ *
+ * It is API compatible, so when you have:
+ *   ws = new WebSocket('ws://....');
+ * you can replace with:
+ *   ws = new ReconnectingWebSocket('ws://....');
+ *
+ * The event stream will typically look like:
+ *  onconnecting
+ *  onopen
+ *  onmessage
+ *  onmessage
+ *  onclose // lost connection
+ *  onconnecting
+ *  onopen  // sometime later...
+ *  onmessage
+ *  onmessage
+ *  etc...
+ *
+ * It is API compatible with the standard WebSocket API, apart from the following members:
+ *
+ * - `bufferedAmount`
+ * - `extensions`
+ * - `binaryType`
+ *
+ * Latest version: https://github.com/joewalnes/reconnecting-websocket/
+ * - Joe Walnes
+ *
+ * Syntax
+ * ======
+ * var socket = new ReconnectingWebSocket(url, protocols, options);
+ *
+ * Parameters
+ * ==========
+ * url - The url you are connecting to.
+ * protocols - Optional string or array of protocols.
+ * options - See below
+ *
+ * Options
+ * =======
+ * Options can either be passed upon instantiation or set after instantiation:
+ *
+ * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
+ *
+ * or
+ *
+ * var socket = new ReconnectingWebSocket(url);
+ * socket.debug = true;
+ * socket.reconnectInterval = 4000;
+ *
+ * debug
+ * - Whether this instance should log debug messages. Accepts true or false. Default: false.
+ *
+ * automaticOpen
+ * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
+ *
+ * reconnectInterval
+ * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
+ *
+ * maxReconnectInterval
+ * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
+ *
+ * reconnectDecay
+ * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
+ *
+ * timeoutInterval
+ * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
+ *
+ */
+(function (global, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define([], factory);
+    } else if (typeof module !== 'undefined' && module.exports) {
+        module.exports = factory();
+    } else {
+        global.ReconnectingWebSocket = factory();
+    }
+})(this, function () {
+
+    if (!('WebSocket' in window)) {
+        return;
+    }
+
+    function ReconnectingWebSocket(url, protocols, options) {
+
+        // Default settings
+        var settings = {
+
+            /** Whether this instance should log debug messages. */
+            debug: false,
+
+            /** Whether or not the websocket should attempt to connect immediately upon instantiation. */
+            automaticOpen: true,
+
+            /** The number of milliseconds to delay before attempting to reconnect. */
+            reconnectInterval: 1000,
+            /** The maximum number of milliseconds to delay a reconnection attempt. */
+            maxReconnectInterval: 30000,
+            /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
+            reconnectDecay: 1.5,
+
+            /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
+            timeoutInterval: 2000,
+
+            /** The maximum number of reconnection attempts to make. Unlimited if null. */
+            maxReconnectAttempts: null,
+
+            /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
+            binaryType: 'blob'
+        }
+        if (!options) {
+            options = {};
+        }
+
+        // Overwrite and define settings with options if they exist.
+        for (var key in settings) {
+            if (typeof options[key] !== 'undefined') {
+                this[key] = options[key];
+            } else {
+                this[key] = settings[key];
+            }
+        }
+
+        // These should be treated as read-only properties
+
+        /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
+        this.url = url;
+
+        /** The number of attempted reconnects since starting, or the last successful connection. Read only. */
+        this.reconnectAttempts = 0;
+
+        /**
+         * The current state of the connection.
+         * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
+         * Read only.
+         */
+        this.readyState = WebSocket.CONNECTING;
+
+        /**
+         * A string indicating the name of the sub-protocol the server selected; this will be one of
+         * the strings specified in the protocols parameter when creating the WebSocket object.
+         * Read only.
+         */
+        this.protocol = null;
+
+        // Private state variables
+
+        var self = this;
+        var ws;
+        var forcedClose = false;
+        var timedOut = false;
+        var eventTarget = document.createElement('div');
+
+        // Wire up "on*" properties as event handlers
+
+        eventTarget.addEventListener('open', function (event) {
+            self.onopen(event);
+        });
+        eventTarget.addEventListener('close', function (event) {
+            self.onclose(event);
+        });
+        eventTarget.addEventListener('connecting', function (event) {
+            self.onconnecting(event);
+        });
+        eventTarget.addEventListener('message', function (event) {
+            self.onmessage(event);
+        });
+        eventTarget.addEventListener('error', function (event) {
+            self.onerror(event);
+        });
+
+        // Expose the API required by EventTarget
+
+        this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
+        this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
+        this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
+
+        /**
+         * This function generates an event that is compatible with standard
+         * compliant browsers and IE9 - IE11
+         *
+         * This will prevent the error:
+         * Object doesn't support this action
+         *
+         * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
+         * @param s String The name that the event should use
+         * @param args Object an optional object that the event will use
+         */
+        function generateEvent(s, args) {
+            var evt = document.createEvent("CustomEvent");
+            evt.initCustomEvent(s, false, false, args);
+            return evt;
+        };
+
+        this.open = function (reconnectAttempt) {
+            ws = new WebSocket(self.url, protocols || []);
+            ws.binaryType = this.binaryType;
+
+            if (reconnectAttempt) {
+                if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
+                    return;
+                }
+            } else {
+                eventTarget.dispatchEvent(generateEvent('connecting'));
+                this.reconnectAttempts = 0;
+            }
+
+            if (self.debug || ReconnectingWebSocket.debugAll) {
+                console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
+            }
+
+            var localWs = ws;
+            var timeout = setTimeout(function () {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
+                }
+                timedOut = true;
+                localWs.close();
+                timedOut = false;
+            }, self.timeoutInterval);
+
+            ws.onopen = function (event) {
+                clearTimeout(timeout);
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onopen', self.url);
+                }
+                self.protocol = ws.protocol;
+                self.readyState = WebSocket.OPEN;
+                self.reconnectAttempts = 0;
+                var e = generateEvent('open');
+                e.isReconnect = reconnectAttempt;
+                reconnectAttempt = false;
+                eventTarget.dispatchEvent(e);
+            };
+
+            ws.onclose = function (event) {
+                clearTimeout(timeout);
+                ws = null;
+                if (forcedClose) {
+                    self.readyState = WebSocket.CLOSED;
+                    eventTarget.dispatchEvent(generateEvent('close'));
+                } else {
+                    self.readyState = WebSocket.CONNECTING;
+                    var e = generateEvent('connecting');
+                    e.code = event.code;
+                    e.reason = event.reason;
+                    e.wasClean = event.wasClean;
+                    eventTarget.dispatchEvent(e);
+                    if (!reconnectAttempt && !timedOut) {
+                        if (self.debug || ReconnectingWebSocket.debugAll) {
+                            console.debug('ReconnectingWebSocket', 'onclose', self.url);
+                        }
+                        eventTarget.dispatchEvent(generateEvent('close'));
+                    }
+
+                    var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
+                    setTimeout(function () {
+                        self.reconnectAttempts++;
+                        self.open(true);
+                    }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
+                }
+            };
+            ws.onmessage = function (event) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
+                }
+                var e = generateEvent('message');
+                e.data = event.data;
+                eventTarget.dispatchEvent(e);
+            };
+            ws.onerror = function (event) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
+                }
+                eventTarget.dispatchEvent(generateEvent('error'));
+            };
+        }
+
+        // Whether or not to create a websocket upon instantiation
+        if (this.automaticOpen == true) {
+            this.open(false);
+        }
+
+        /**
+         * Transmits data to the server over the WebSocket connection.
+         *
+         * @param data a text string, ArrayBuffer or Blob to send to the server.
+         */
+        this.send = function (data) {
+            if (ws) {
+                if (self.debug || ReconnectingWebSocket.debugAll) {
+                    console.debug('ReconnectingWebSocket', 'send', self.url, data);
+                }
+                return ws.send(data);
+            } else {
+                throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
+            }
+        };
+
+        /**
+         * Closes the WebSocket connection or connection attempt, if any.
+         * If the connection is already CLOSED, this method does nothing.
+         */
+        this.close = function (code, reason) {
+            // Default CLOSE_NORMAL code
+            if (typeof code == 'undefined') {
+                code = 1000;
+            }
+            forcedClose = true;
+            if (ws) {
+                ws.close(code, reason);
+            }
+        };
+
+        /**
+         * Additional public API method to refresh the connection if still open (close, re-open).
+         * For example, if the app suspects bad data / missed heart beats, it can try to refresh.
+         */
+        this.refresh = function () {
+            if (ws) {
+                ws.close();
+            }
+        };
+    }
+
+    /**
+     * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
+     * this indicates that the connection is ready to send and receive data.
+     */
+    ReconnectingWebSocket.prototype.onopen = function (event) {
+    };
+    /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
+    ReconnectingWebSocket.prototype.onclose = function (event) {
+    };
+    /** An event listener to be called when a connection begins being attempted. */
+    ReconnectingWebSocket.prototype.onconnecting = function (event) {
+    };
+    /** An event listener to be called when a message is received from the server. */
+    ReconnectingWebSocket.prototype.onmessage = function (event) {
+    };
+    /** An event listener to be called when an error occurs. */
+    ReconnectingWebSocket.prototype.onerror = function (event) {
+    };
+
+    /**
+     * Whether all instances of ReconnectingWebSocket should log debug messages.
+     * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
+     */
+    ReconnectingWebSocket.debugAll = false;
+
+    ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
+    ReconnectingWebSocket.OPEN = WebSocket.OPEN;
+    ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
+    ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
+
+    return ReconnectingWebSocket;
+});

+ 44 - 0
magic-editor/src/console/src/scripts/websocket.js

@@ -0,0 +1,44 @@
+import bus from "@/scripts/bus";
+import ReconnectingWebSocket from './reconnecting-websocket'
+function MagicWebSocket(url) {
+    this.listeners = {};
+    console.log(url)
+    this.socket = new ReconnectingWebSocket(url);
+    this.socket.onmessage = this.messageReceived;
+    bus.$on('message', (msgType, content) => {
+        if (content) {
+            this.socket.send(`${msgType},${content}`)
+        } else {
+            this.socket.send(msgType)
+        }
+    })
+}
+
+MagicWebSocket.prototype.on = function (msgType, callback) {
+    this.listeners[msgType] = this.listeners[msgType] || []
+    this.listeners[msgType].push(callback)
+}
+
+MagicWebSocket.prototype.messageReceived = function (e) {
+    let payload = e.data
+    let index = payload.indexOf(",")
+    let msgType = index === -1 ? payload : payload.substring(0, index)
+    let args = []
+    while (index > -1) {
+        payload = payload.substring(index + 1)
+        if (payload.startsWith('[') || payload.startsWith('{')) {
+            args.push(JSON.parse(payload))
+            break;
+        }
+        let newIndex = payload.indexOf(",", index + 1)
+        args.push(newIndex === -1 ? payload : payload.substring(index + 1, newIndex))
+        index = newIndex
+    }
+    console.log(msgType, args)
+    bus.$emit('ws_' + msgType, args)
+}
+MagicWebSocket.prototype.close = function () {
+    this.socket.close()
+}
+
+export default MagicWebSocket