diff --git a/README.md b/README.md index bf6dfdc..122e8ec 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ mvn test * build all ``` -mvn clean package +mvn -DskipTests=true clean package ``` * build backend ``` @@ -29,7 +29,6 @@ cd ui npm run build ``` - ## Installation Currently supports MacOS, Linux (CentOS), and plans to support Windows in the future @@ -55,12 +54,16 @@ Execute start command ./path/to/softwarelab/bin/startup.sh ``` +Open your browser ,enter the address `http://ip:8080` and login, you can see a page like this: + +![image](./docs/images/dashboard.png) + ## Use Use a browser to open the address `http://ip:8080/`, the default username and password: ʻadmin/123456` -//todo add video +![image](./docs/images/how_to_use.gif) ### Restart diff --git a/application/pom.xml b/application/pom.xml index c55d1f6..8941721 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -81,7 +81,6 @@ - ${pkg.name}-${project.version} org.apache.maven.plugins @@ -110,7 +109,7 @@ single - ${project.artifactId} + ${pkg.name} src/main/assembly/assembly.xml diff --git a/application/src/bin/restart.sh b/application/src/bin/restart.sh index 7c08cf0..d394862 100644 --- a/application/src/bin/restart.sh +++ b/application/src/bin/restart.sh @@ -1,11 +1,15 @@ #!/bin/sh # project name -APPLICATION="@project.artifactId@" +APPLICATION="@pkg.name@" # java file name APPLICATION_JAR="@build.finalName@.jar" +# bin directory absolute path +cd `dirname $0` +cd .. + # stop app echo stop ${APPLICATION} Application... sh shutdown.sh diff --git a/application/src/bin/shutdown.sh b/application/src/bin/shutdown.sh index b0097c6..99be0e9 100644 --- a/application/src/bin/shutdown.sh +++ b/application/src/bin/shutdown.sh @@ -1,7 +1,7 @@ #!/bin/sh # project name -APPLICATION="@project.artifactId@" +APPLICATION="@pkg.name@" # jar file name APPLICATION_JAR="@build.finalName@.jar" diff --git a/application/src/bin/startup.sh b/application/src/bin/startup.sh index 64a3460..15ff3e0 100644 --- a/application/src/bin/startup.sh +++ b/application/src/bin/startup.sh @@ -1,7 +1,7 @@ #!/bin/sh # project name -APPLICATION="@project.artifactId@" +APPLICATION="@pkg.name@" # java file name APPLICATION_JAR="@build.finalName@.jar" diff --git a/application/src/main/java/com/softwarelab/application/SoftwareLabApplication.java b/application/src/main/java/com/softwarelab/application/SoftwareLabApplication.java index bcddb32..e23349b 100755 --- a/application/src/main/java/com/softwarelab/application/SoftwareLabApplication.java +++ b/application/src/main/java/com/softwarelab/application/SoftwareLabApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.socket.config.annotation.EnableWebSocket; @SpringBootApplication -@Transactional +@EnableWebSocket +@EnableTransactionManagement public class SoftwareLabApplication { diff --git a/application/src/main/java/com/softwarelab/application/common/DbConst.java b/application/src/main/java/com/softwarelab/application/common/DbConst.java index 5e16284..a7aa2db 100755 --- a/application/src/main/java/com/softwarelab/application/common/DbConst.java +++ b/application/src/main/java/com/softwarelab/application/common/DbConst.java @@ -2,16 +2,16 @@ /** * @author black-star - * + *

* status constant */ public class DbConst { - public static final String COLUMN_ID="id"; + public static final String COLUMN_ID = "id"; - public static final String COLUMN_STATUS="status"; + public static final String COLUMN_STATUS = "status"; - public static final String COLUMN_TYPE="type"; + public static final String COLUMN_TYPE = "type"; public static final String COLUMN_USER_ID = "user_id"; @@ -21,7 +21,6 @@ public class DbConst { public static final String COLUMN_DOWNLOAD_STATUS = "download_status"; - public static final int RUNNING_STATUS_START = 1; public static final int RUNNING_STATUS_STOP = 0; @@ -30,6 +29,10 @@ public class DbConst { public static final int STATUS_NORMAL = 0; + public static final int APP_SOURCE_RELOAD = 1; + + public static final int APP_SOURCE_UPGRADE = 2; + public static final int DOWNLOAD_STATUS_FINISH = 2; public static final int DOWNLOAD_STATUS_BEGIN = 1; diff --git a/application/src/main/java/com/softwarelab/application/config/WebSocketConfig.java b/application/src/main/java/com/softwarelab/application/config/WebSocketConfig.java index bd254fd..bb803e3 100644 --- a/application/src/main/java/com/softwarelab/application/config/WebSocketConfig.java +++ b/application/src/main/java/com/softwarelab/application/config/WebSocketConfig.java @@ -2,14 +2,11 @@ import com.softwarelab.application.bean.SecurityUser; import com.softwarelab.application.checker.ImageChecker; +import com.softwarelab.application.common.DbConst; import com.softwarelab.application.entity.Instance; -import com.softwarelab.application.service.ContainerService; -import com.softwarelab.application.service.IAppService; -import com.softwarelab.application.service.IAppVersionService; -import com.softwarelab.application.service.IInstanceService; +import com.softwarelab.application.service.*; import com.softwarelab.application.websocket.MessageWebSocketHandler; import com.softwarelab.application.websocket.TerminalWebSocketHandler; -import com.softwarelab.application.common.DbConst; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -17,7 +14,6 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator; @@ -28,7 +24,6 @@ import java.util.Map; @Configuration -@EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { public static final String WS_PREFIX = "/api/ws/"; @@ -53,6 +48,8 @@ public class WebSocketConfig implements WebSocketConfigurer { @Autowired private IAppVersionService appVersionService; + @Autowired + private IAppSourceService appSourceService; @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { @@ -78,7 +75,7 @@ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse res return false; } else { if (((ExceptionWebSocketHandlerDecorator) wsHandler).getLastHandler() instanceof TerminalWebSocketHandler) { - return handshakeTerminal(request,instanceService); + return handshakeTerminal(request, instanceService); } return true; } @@ -106,14 +103,13 @@ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse respons }); } - @Bean + public TerminalWebSocketHandler newTerminalHandler() { return new TerminalWebSocketHandler(instanceService, containerService); } - @Bean public MessageWebSocketHandler newMessageHandler() { - return new MessageWebSocketHandler(imageChecker, containerService, appService,appVersionService); + return new MessageWebSocketHandler(imageChecker, containerService, appService, appVersionService, appSourceService); } protected SecurityUser getCurrentUser() { diff --git a/application/src/main/java/com/softwarelab/application/controller/StatisticsController.java b/application/src/main/java/com/softwarelab/application/controller/StatisticsController.java index cfc7e0a..e72b78a 100755 --- a/application/src/main/java/com/softwarelab/application/controller/StatisticsController.java +++ b/application/src/main/java/com/softwarelab/application/controller/StatisticsController.java @@ -6,6 +6,7 @@ import com.softwarelab.application.checker.HardwareChecker; import com.softwarelab.application.entity.Instance; import com.softwarelab.application.service.IAppService; +import com.softwarelab.application.service.IAppSourceService; import com.softwarelab.application.service.IInstanceService; import com.softwarelab.application.common.BaseController; import com.softwarelab.application.common.DbConst; @@ -44,6 +45,9 @@ public class StatisticsController extends BaseController { @Autowired private HardwareChecker hardwareChecker; + @Autowired + private IAppSourceService appSourceService; + public StatisticsController() { } @@ -67,9 +71,10 @@ public Map getOsInfo() { } @RequestMapping(method = RequestMethod.GET, value = "/software") - public Map getSoftwareInfo() { + public Map getSoftwareInfo() { - HashMap map = new HashMap<>(); + HashMap map = new HashMap<>(); + map.put("appStoreVersion",appSourceService.list().get(0).getVersion()); map.put("appTotal",appService.count()); map.put("instanceTotal",instanceService.count()); map.put("runningInstanceTotal",instanceService.count(new QueryWrapper().eq(DbConst.COLUMN_RUNNING_STATUS, DbConst.RUNNING_STATUS_START))); diff --git a/application/src/main/java/com/softwarelab/application/entity/AppVersion.java b/application/src/main/java/com/softwarelab/application/entity/AppVersion.java index f8e53d3..9b2380b 100644 --- a/application/src/main/java/com/softwarelab/application/entity/AppVersion.java +++ b/application/src/main/java/com/softwarelab/application/entity/AppVersion.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.*; import lombok.experimental.Accessors; @@ -25,8 +26,10 @@ public class AppVersion implements Serializable { private static final long serialVersionUID = 1L; + @TableField private String appName; + @TableField private String version; private String additionalInfo; @@ -40,4 +43,5 @@ public class AppVersion implements Serializable { private Integer downloadStatus; + } diff --git a/application/src/main/java/com/softwarelab/application/service/impl/AppSourceServiceImpl.java b/application/src/main/java/com/softwarelab/application/service/impl/AppSourceServiceImpl.java index 7419d83..80f6c88 100644 --- a/application/src/main/java/com/softwarelab/application/service/impl/AppSourceServiceImpl.java +++ b/application/src/main/java/com/softwarelab/application/service/impl/AppSourceServiceImpl.java @@ -1,5 +1,6 @@ package com.softwarelab.application.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.softwarelab.application.bean.AppSourceInfo; import com.softwarelab.application.bean.SourceRelease; @@ -26,6 +27,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -50,8 +52,12 @@ public class AppSourceServiceImpl extends ServiceImpl list = objectMapper.readValue(forObject, collectionType); SourceRelease latestSourceRelease = list.get(0); @@ -87,9 +96,8 @@ public boolean upgrade() { return loadToDb(); } } - } catch (IOException e) { - log.error("upgrade apps error", e); - return false; + } catch (Exception e){ + throw new RuntimeException(e); } return true; } @@ -109,16 +117,21 @@ public boolean load(MultipartFile file) { } @Transactional - public boolean loadToDb() { - LocalDateTime now = LocalDateTime.now(); - File appsFile = new File(appsDir); - String[] fileNames = appsFile.list(); - for (String fileName : fileNames) { - try { + public boolean loadToDb(){ + try { + AppSource appSource = getById(APP_SOURCE_ID); + if(appSource != null) { + appSource.setStatus(DbConst.APP_SOURCE_RELOAD); + appSourceService.updateById(appSource); + } + LocalDateTime now = LocalDateTime.now(); + File appsFile = new File(appsDir); + String[] fileNames = appsFile.list(); + for (String fileName : fileNames) { File appFile = new File(appsDir + File.separator + fileName); AppSourceInfo appSourceInfo = objectMapper.readValue(appFile, AppSourceInfo.class); String appName = fileName.split("\\.")[0]; - appService.save(App.builder() + appService.saveOrUpdate(App.builder() .name(appName) .type(appSourceInfo.getType()) .description(appSourceInfo.getDescription()) @@ -131,31 +144,45 @@ public boolean loadToDb() { List versions = appSourceInfo.getVersions(); if (versions != null && versions.size() > 0) { for (AppSourceInfo.AppSourceVersion appSourceVersion : versions) { - appVersionService.save(AppVersion.builder() + QueryWrapper appVersionWrapper = new QueryWrapper() + .eq(DbConst.COLUMN_APP_NAME, appName) + .eq(DbConst.COLUMN_VERSION, appSourceVersion.getVersion()); + AppVersion existAppVersion = appVersionService.getOne(appVersionWrapper); + AppVersion newAppVersion = AppVersion.builder() .appName(appName) .version(appSourceVersion.getVersion()) - .additionalInfo(appSourceVersion.getAdditionalInfo() != null ? appSourceVersion.getAdditionalInfo().toString(): null) + .additionalInfo(appSourceVersion.getAdditionalInfo() != null ? appSourceVersion.getAdditionalInfo().toString() : null) .createTime(now) .updateTime(now) .status(DbConst.STATUS_NORMAL) .downloadStatus(DbConst.DOWNLOAD_STATUS_INIT) - .build()); + .build(); + if(existAppVersion == null) { + appVersionService.save(newAppVersion); + } else { + appVersionService.update(newAppVersion,appVersionWrapper); + } } } - - } catch (IOException e) { - log.error("load to db error",e); + } + byte[] versionContext = FileUtil.getContent(targetDir + File.separator + versionFileName); + if (versionContext == null) { + log.error("version file can't find"); return false; + } + appSourceService.saveOrUpdate(AppSource.builder() + .id(APP_SOURCE_ID) + //set true version + .version(new String(versionContext, StandardCharsets.UTF_8)) + .repository(null) + .createTime(now) + .updateTime(now) + .status(DbConst.STATUS_NORMAL) + .build()); + } catch (Exception e){ + throw new RuntimeException(e); } - appSourceService.save(AppSource.builder() - .id("00000000-0000-0000-0000-000000000000") - .version("0.0.0") - .repository(null) - .createTime(now) - .updateTime(now) - .status(DbConst.STATUS_NORMAL) - .build()); return true; } } diff --git a/application/src/main/java/com/softwarelab/application/websocket/MessageWebSocketHandler.java b/application/src/main/java/com/softwarelab/application/websocket/MessageWebSocketHandler.java index b402256..9e5b78d 100644 --- a/application/src/main/java/com/softwarelab/application/websocket/MessageWebSocketHandler.java +++ b/application/src/main/java/com/softwarelab/application/websocket/MessageWebSocketHandler.java @@ -2,15 +2,17 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; import com.softwarelab.application.bean.ContainerSetting; import com.softwarelab.application.checker.ImageChecker; +import com.softwarelab.application.common.DbConst; import com.softwarelab.application.entity.App; +import com.softwarelab.application.entity.AppSource; import com.softwarelab.application.entity.AppVersion; import com.softwarelab.application.service.ContainerService; import com.softwarelab.application.service.IAppService; +import com.softwarelab.application.service.IAppSourceService; import com.softwarelab.application.service.IAppVersionService; -import com.softwarelab.application.common.DbConst; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; @@ -29,13 +31,16 @@ public class MessageWebSocketHandler extends TextWebSocketHandler { private final IAppVersionService appVersionService; + private final IAppSourceService appSourceService; + private ObjectMapper objectMapper = new ObjectMapper(); - public MessageWebSocketHandler(ImageChecker imageChecker, ContainerService containerService,IAppService appService,IAppVersionService appVersionService) { + public MessageWebSocketHandler(ImageChecker imageChecker, ContainerService containerService, IAppService appService, IAppVersionService appVersionService, IAppSourceService appSourceService) { this.imageChecker = imageChecker; this.containerService = containerService; this.appService = appService; this.appVersionService = appVersionService; + this.appSourceService = appSourceService; } @Override @@ -52,6 +57,8 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) case "image": processImageMessage(session, webSocketRequestMessage); break; + case "app": + processAppMessage(session, webSocketRequestMessage); default: break; } @@ -60,7 +67,6 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) } private void processImageMessage(WebSocketSession session, WebSocketRequestMessage message) { - switch (message.getOperate()) { case "download": processImageDownload(session, message.getContent()); @@ -68,11 +74,22 @@ private void processImageMessage(WebSocketSession session, WebSocketRequestMessa default: break; } + } + private void processAppMessage(WebSocketSession session, WebSocketRequestMessage message) throws Exception{ + switch (message.getOperate()) { + case "upgrade": + processAppUpgrade(session, message.getContent()); + break; + case "reload": + processAppReload(session, message.getContent()); + break; + default: + break; + } } private void processImageDownload(WebSocketSession session, String content) { - String[] split = content.split(":"); if (split.length != 2) { // @@ -91,24 +108,24 @@ private void processImageDownload(WebSocketSession session, String content) { try { ContainerSetting containerSetting = objectMapper.readValue(app.getAdditionalInfo(), ContainerSetting.class); imageName = containerSetting.getImageName(); - if(imageName == null) { + if (imageName == null) { responseMessage = new WebSocketResponseMessage("failed", content + " can't find image"); session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } } catch (IOException e) { //do nothing } - if (!containerService.hasImage(appVersion.getAppName()+":"+appVersion.getVersion())) { + if (!containerService.hasImage(appVersion.getAppName() + ":" + appVersion.getVersion())) { imageChecker.add(appVersion, WebSocketSessionAndCallback.builder() .webSocketSession(session) - .callback(containerService.pullImage(imageName+":"+appVersion.getVersion())) + .callback(containerService.pullImage(imageName + ":" + appVersion.getVersion())) .build()); responseMessage = new WebSocketResponseMessage("success", content + " begin download"); } else { appVersionService.update(new UpdateWrapper() - .eq(DbConst.COLUMN_APP_NAME,appVersion.getAppName()) - .eq(DbConst.COLUMN_VERSION,appVersion.getVersion()) - .set(DbConst.COLUMN_DOWNLOAD_STATUS,DbConst.DOWNLOAD_STATUS_FINISH)); + .eq(DbConst.COLUMN_APP_NAME, appVersion.getAppName()) + .eq(DbConst.COLUMN_VERSION, appVersion.getVersion()) + .set(DbConst.COLUMN_DOWNLOAD_STATUS, DbConst.DOWNLOAD_STATUS_FINISH)); responseMessage = new WebSocketResponseMessage("success", content + " is exist"); } try { @@ -119,6 +136,29 @@ private void processImageDownload(WebSocketSession session, String content) { } + public void processAppUpgrade(WebSocketSession session, String content) throws Exception{ + String detail; + AppSource appSource = appSourceService.list().get(0); + if (appSource.getStatus() > 0) { + detail = "App store is Upgrading or reloading"; + } else { + detail = "Upgrade " + (appSourceService.upgrade() ? "success" : "failed"); + } + WebSocketResponseMessage responseMessage = new WebSocketResponseMessage("success", detail); + session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); + } + + public void processAppReload(WebSocketSession session, String content) throws Exception{ + String detail; + AppSource appSource = appSourceService.list().get(0); + if (appSource.getStatus() > 0) { + detail = "App store is Upgrading or reloading"; + } else { + detail = "Reload " + (appSourceService.loadToDb() ? "success" : "failed"); + } + WebSocketResponseMessage responseMessage = new WebSocketResponseMessage("success", detail); + session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); + } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { diff --git a/application/src/test/java/com/softwarelab/application/AbstractBaseTest.java b/application/src/test/java/com/softwarelab/application/AbstractBaseTest.java index 1f601fc..a74ca3a 100644 --- a/application/src/test/java/com/softwarelab/application/AbstractBaseTest.java +++ b/application/src/test/java/com/softwarelab/application/AbstractBaseTest.java @@ -156,7 +156,7 @@ public AbstractBaseTest() { public void setUp() throws Exception { if(!isInit){ checkTestImage(); - //todo use initdata.sql to init user\app\appversion\instance + userService.save(getTestUser()); appService.save(getHelloWorldApp()); diff --git a/application/src/test/java/com/softwarelab/application/service/AbstractServiceBaseTest.java b/application/src/test/java/com/softwarelab/application/service/AbstractServiceBaseTest.java new file mode 100644 index 0000000..fd84a0c --- /dev/null +++ b/application/src/test/java/com/softwarelab/application/service/AbstractServiceBaseTest.java @@ -0,0 +1,20 @@ +package com.softwarelab.application.service; + + +import com.softwarelab.application.AbstractBaseTest; +import org.junit.After; +import org.junit.Before; + +public abstract class AbstractServiceBaseTest extends AbstractBaseTest { + + @Before + public void setUp() throws Exception { + super.setUp(); + + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } +} diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png new file mode 100644 index 0000000..f4db8e7 Binary files /dev/null and b/docs/images/dashboard.png differ diff --git a/docs/images/how_to_use.gif b/docs/images/how_to_use.gif new file mode 100644 index 0000000..ace49a3 Binary files /dev/null and b/docs/images/how_to_use.gif differ diff --git a/ui/package.json b/ui/package.json index 965a3e6..0959e7e 100755 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,7 @@ "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" }, "dependencies": { - "axios": "0.18.1", + "axios": "0.21.1", "codemirror": "5.45.0", "element-ui": "2.13.2", "file-saver": "2.0.1", diff --git a/ui/pom.xml b/ui/pom.xml index 7995cf7..3f0bb49 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -14,7 +14,7 @@ - ${project.basedir}/dist + ${project.build.directory}/generate-resources @@ -24,7 +24,7 @@ exec-npm-run-build - generate-sources + package exec diff --git a/ui/src/main/assembly/assembly.xml b/ui/src/main/assembly/assembly.xml index 0ff25a5..56ab562 100644 --- a/ui/src/main/assembly/assembly.xml +++ b/ui/src/main/assembly/assembly.xml @@ -12,8 +12,8 @@ - dist - public + ${project.build.directory}/generate-resources + . 0644 diff --git a/ui/src/settings.js b/ui/src/settings.js index bb88b10..120176b 100755 --- a/ui/src/settings.js +++ b/ui/src/settings.js @@ -1,6 +1,6 @@ module.exports = { - title: '软件实验室', + title: 'Software Lab', /** * @type {boolean} true | false diff --git a/ui/src/views/app/index.vue b/ui/src/views/app/index.vue index 1af4c64..e66597f 100755 --- a/ui/src/views/app/index.vue +++ b/ui/src/views/app/index.vue @@ -1,24 +1,32 @@