1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
package com.vci.ubcs.deploy.service.impl;
 
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.shaded.com.google.protobuf.ServiceException;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.vci.ubcs.deploy.entity.DeployApps;
import com.vci.ubcs.deploy.enumpack.CmdConfigEnum;
import com.vci.ubcs.deploy.mapper.DeployAppsMapper;
import com.vci.ubcs.deploy.service.IDeployAppsService;
import com.vci.ubcs.deploy.vo.DeployAppsVO;
import com.vci.ubcs.starter.util.HttpUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
 
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
 
/**
 * @author ludc
 * @date 2024/1/7 19:54
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class DeployAppsServiceImpl extends ServiceImpl<DeployAppsMapper, DeployApps> implements IDeployAppsService, EnvironmentAware {
 
    private final DeployAppsMapper deployAppsMapper;
 
    @Value("${password-free.pwd-free-addr:localhost}")
    private String pwdFreeAddr;
 
    /**
     * 通过服务注册中心获取网关的端口号
     */
    @Autowired
    private DiscoveryClient discoveryClient;
 
    /**
     * 各个服务存放的的父路径
     */
    @Value("${local-log.parent-path:/data1/ubcs/ubcs-server}")
    private String PARENTPATH;
 
    /**
     * 日志文件的具体位置
     */
    @Value("${local-log.log-path:/target/log}")
    private String LOGPATH;
 
    /**
     * 根据操作系统生成分隔符
     */
    private String SEPARATOR = "/";
 
    /**
     * 根据当前运行的环境,对配置的日志路径格式进行调整
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        String os = environment.getProperty("os.name").toLowerCase();
        if (!os.contains("win")) {
            this.SEPARATOR = "/";
        }else{
            this.SEPARATOR = "\\";
        }
    }
 
    @Override
    public List<DeployAppsVO> getApplications(ServletRequest servletRequest) throws ServiceException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String baldeAuth = request.getHeaders("Blade-Auth").nextElement();
        String cookie = request.getHeaders("Cookie").nextElement();
        // 免密登录接口地址
        String loginUrl = "http://"+pwdFreeAddr+":"+this.getGatewayPort("ubcs-gateway")+"/ubcs-admin/applications";
        log.debug("获取服务信息调用地址:"+loginUrl);
        // 请求ubcs-admin获取服务信息
        HttpHeaders headers = new HttpHeaders();
        ArrayList<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.APPLICATION_JSON);
        headers.set("Authorization","Basic c2FiZXI6c2FiZXJfc2VjcmV0");
        headers.set("Blade-Auth",baldeAuth);
        headers.set("Cookie",cookie);
        headers.setAccept(mediaTypes);
        List<DeployAppsVO> deployAppsVOList = new ArrayList<>();
        try {
            // 发送GET请求
            String responseBody = HttpUtils.get(loginUrl, headers);
            if(Func.isNotEmpty(responseBody)){
                // 将 JSON 字符串转换为 JSONArray
                JSONArray jsonArray = JSON.parseArray(responseBody);
                for (Object obj : jsonArray) {
                    DeployAppsVO deployAppsVO = new DeployAppsVO();
                    JSONObject jsonObject = (JSONObject) obj;
                    List instances = ((List) jsonObject.get("instances"));
                    deployAppsVO.setName(jsonObject.get("name").toString());
                    String serviceUrl = ((JSONObject) ((JSONObject) instances.get(0)).get("registration")).get("serviceUrl").toString();
                    deployAppsVO.setPort(String.valueOf(new URL(serviceUrl).getPort()));
                    deployAppsVO.setServiceNum(instances.size());
                    deployAppsVO.setStatus(jsonObject.get("status").toString());
                    deployAppsVO.setStatusTimestamp(jsonObject.get("statusTimestamp").toString());
                    deployAppsVOList.add(deployAppsVO);
                }
            }
        }catch (Exception e){
            throw new ServiceException("调用ubcs-admin获取服务信息失败,原因:"+e.getMessage());
        }
        // 再查询库中已经存在的服务配置信息,进行是否已经启动的判断
        List<DeployApps> deployApps = deployAppsMapper.selectList(null);
        // 库中未配置,直接返回正在运行的服务信息
        if(deployApps.isEmpty()){
            return deployAppsVOList;
        }
        // 筛选出不在运行的并生成默认的服务信息
        List<DeployAppsVO> deployAppsVOS1 = deployApps.stream()
            .filter(deployApp -> deployAppsVOList.stream()
                .noneMatch(deployAppVO -> deployApp.getServerName().equals(deployAppVO.getName()) &&
                    deployApp.getServerName().equals(deployAppVO.getName())))
            .map(deployApp -> {
                DeployAppsVO deployAppsVO2 = new DeployAppsVO(deployApp.getServerName(), "DOWN", "", "", 0);
                if(deployApp.getServerName().equals("web")){
                    deployAppsVO2.setStatus("UP");
                    deployAppsVO2.setPort("8080");
                    deployAppsVO2.setServiceNum(1);
                }
                return deployAppsVO2;
            })
            .collect(Collectors.toList());
        deployAppsVOList.addAll(deployAppsVOS1);
        return deployAppsVOList;
    }
 
    @Override
    public DeployApps saveOrGetServiceConfInfo(DeployAppsVO deployAppsVO) throws ServiceException {
        if(deployAppsVO.getName().isEmpty()){
            throw new ServiceException("缺少必传参数name");
        }
        List<DeployApps> deployApps = deployAppsMapper.selectList(Wrappers.<DeployApps>query().lambda().eq(DeployApps::getServerName, deployAppsVO.getName()));
        if(Func.isNotEmpty(deployApps)){
            return deployApps.get(0);
        }
        // 未从库中查询到,需要生成服务信息保存默认信息到库中
        DeployApps defaultDeployApps = new DeployApps();
        defaultDeployApps.setLogPath(PARENTPATH + SEPARATOR + deployAppsVO.getName().replace("-","_") + LOGPATH);
        defaultDeployApps.setServerName(deployAppsVO.getName());
        defaultDeployApps.setStartCmd(CmdConfigEnum.START_CMD.getValue() + deployAppsVO.getName());
        defaultDeployApps.setStopCmd(CmdConfigEnum.STOP_CMD.getValue() + deployAppsVO.getName());
        defaultDeployApps.setRestartCmd(CmdConfigEnum.RESTART_CMD.getValue() + deployAppsVO.getName());
        defaultDeployApps.setServerPath(PARENTPATH + SEPARATOR + deployAppsVO.getName().replace("-","_"));
        int eft = deployAppsMapper.insert(defaultDeployApps);
        if (!SqlHelper.retBool(eft)) {
            throw new ServiceException("生成默认服务信息到库中时失败!");
        }
        return defaultDeployApps;
    }
 
    /**
     * 修改或保存
     * @param deployAppsVO
     * @return
     * @throws ServiceException
     */
    @Override
    public R saveOrUpdateServiceInfo(DeployAppsVO deployAppsVO) throws ServiceException {
        boolean checkBoolean = checkCmdVer(deployAppsVO.getStartCmd()) && checkCmdVer(deployAppsVO.getStopCmd()) && checkCmdVer(deployAppsVO.getRestartCmd());
        if(checkBoolean){
            return R.fail("配置的命令中包含危险命令,如:rm、mv、rm -rf、chmod等命令和关键字! ");
        }
        return R.status(this.saveOrUpdate(deployAppsVO));
    }
 
    /**
     * 新增服务信息
     * @param deployApps
     * @return
     * @throws ServiceException
     */
    @Override
    public boolean addSave(DeployApps deployApps) throws ServiceException {
        if (Func.isBlank(deployApps.getServerName()) || Func.isBlank(deployApps.getServerPath())) {
            throw new ServiceException("必传参数[服务名称,服务存放路径]不能为空");
        }
        List<DeployApps> deployAppsList = deployAppsMapper.selectList(Wrappers.<DeployApps>query().lambda().eq(DeployApps::getServerName, deployApps.getServerName()));
        if (!deployAppsList.isEmpty()) {
            throw new ServiceException("新增服务的服务名重复!");
        }
        return SqlHelper.retBool(deployAppsMapper.insert(deployApps));
    }
 
    /**
     * 更新文件上传
     * @param files
     * @param serverName
     * @return
     */
    @Override
    public R importJarUpdate(MultipartFile[] files, String serverName) throws ServiceException {
        // 根据服务名查看到服务相关信息
        List<DeployApps> deployAppsDB = deployAppsMapper.selectList(Wrappers.<DeployApps>query().lambda().eq(DeployApps::getServerName, serverName));
        if(deployAppsDB.isEmpty()){
            return R.fail("No configuration information related to "+ serverName +" service found");
        }
        // 遍历MultipartFile数组,逐个处理文件
        try {
            for (MultipartFile file : files) {
                // 配置了备份文件路径,先备份再替换
                if(Func.isNotEmpty(deployAppsDB.get(0).getFileBack())){
                    File backFile = new File(deployAppsDB.get(0).getFileBack());
                    // 路径不存在就创建
                    if (!backFile.exists()) {
                        backFile.mkdirs();
                    }
                    String backName = "";
                    String fileType = "file";
                    // 是压缩文件,因为只会存在两种情况,文件名是压缩文件,或者文件(.jar类型的文件)
                    // 当前时间
                    String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    if (file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) {
                        backName = file.getOriginalFilename().replace(".zip","_" + currentDateTime);
                    }else{
                        backName = file.getOriginalFilename().replace(".","_" + currentDateTime + ".");
                    }
                    File source = new File(deployAppsDB.get(0).getServerPath() + this.SEPARATOR + file.getOriginalFilename().replace(".zip", ""));
                    File destination = new File(deployAppsDB.get(0).getFileBack() + this.SEPARATOR + backName);
                    copyFolder(source, destination);
                }
                Path filePath = Paths.get(deployAppsDB.get(0).getServerPath(), file.getOriginalFilename());
                Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
                // 检查文件类型,如果是压缩文件则解压缩
                if (file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) {
                    //sourcePath压缩包文件路径
                    try (ZipFile zipFile = new ZipFile(new File(deployAppsDB.get(0).getServerPath()+ this.SEPARATOR +file.getOriginalFilename()))) {
                        Enumeration enumeration = zipFile.entries();
                        while (enumeration.hasMoreElements()) {
                            //依次获取压缩包内的文件实体对象
                            ZipEntry entry = (ZipEntry) enumeration.nextElement();
                            String name = entry.getName();
                            if (entry.isDirectory()) {
                                continue;
                            }
                            try (BufferedInputStream inputStream = new BufferedInputStream(zipFile.getInputStream(entry))) {
                                // 需要判断文件所在的目录是否存在,处理压缩包里面有文件夹的情况
                                String outName = deployAppsDB.get(0).getServerPath() + this.SEPARATOR + name;
                                File outFile = new File(outName);
                                File tempFile = new File(outName.substring(0, outName.lastIndexOf("/")));
                                if (!tempFile.exists()) {
                                    tempFile.mkdirs();
                                }
                                try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outFile))) {
                                    int len;
                                    byte[] buffer = new byte[1024];
                                    while ((len = inputStream.read(buffer)) > 0) {
                                        outputStream.write(buffer, 0, len);
                                    }
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    File file1 = new File(deployAppsDB.get(0).getServerPath() + SEPARATOR + file.getOriginalFilename());
                    // 压缩文件上传成功之后,删除解压文件
                    file1.delete();
                }
            }
            String output = execute(deployAppsDB.get(0),"UP");
            return R.success(output.toString());
        } catch (IOException e) {
            e.printStackTrace();
            log.error(e.getMessage());
            return R.fail("Failed to upload files");
        }
 
    }
 
    /**
     * 执行命令
     * @param deployAppsVO
     * @return
     */
    @Override
    public R cmdExecute(DeployAppsVO deployAppsVO) throws ServiceException {
        String excuteRes = "";
        try {
            List<DeployApps> deployAppsDB = deployAppsMapper.selectList(Wrappers.<DeployApps>query().lambda().eq(DeployApps::getServerName, deployAppsVO.getName()));
            if(deployAppsDB.isEmpty()){
                return R.fail("命令执行出错,库中未找到"+ deployAppsVO.getName() +"服务相关配置:" );
            }
            excuteRes = execute(deployAppsDB.get(0),deployAppsVO.getStatus());
            return R.success("命令执行结束:"+excuteRes);
        }catch (Exception e){
            throw new ServiceException(e.getMessage());
        }
    }
 
    /**
     * 执行命令
     * @param deployApps
     * @return
     * @throws ServiceException
     */
    public String execute(DeployApps deployApps,String type) throws ServiceException {
        // 处理上传文件的逻辑
        StringBuilder output = new StringBuilder();
        try {
            String cmd = "";
            if(type.equalsIgnoreCase("UP")){
                cmd = deployApps.getRestartCmd();
            }else {
                cmd = deployApps.getStartCmd();
            }
            if(Func.isEmpty(cmd)){
                return "The executed command is empty";
            }
            if(checkCmdVer(cmd)){
                return "配置的命令中包含危险命令,如:rm、mv、rm -rf、chmod等命令和关键词! ";
            }
            // 执行Linux命令
            log.info("开始执行命令:"+cmd);
            // Process process = Runtime.getRuntime().exec(cmd);
            ProcessBuilder processBuilder = new ProcessBuilder(cmd.split("\\s"));
            // processBuilder.command(cmd);
            Process process = processBuilder.start();
            // 等待命令执行完成
            int exitCode = process.waitFor();
 
            InputStream inputStream = process.getInputStream();
            OutputStream outputStream = process.getOutputStream();
            InputStream errorStream = process.getErrorStream();
            // 读取命令执行结果
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            reader.close();
 
            // 读取命令执行的错误输出流
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream));
            StringBuilder errorOutput = new StringBuilder();
            String errorLine;
            while ((errorLine = errorReader.readLine()) != null) {
                errorOutput.append(errorLine).append("\n");
            }
            String errorOutputString = errorOutput.toString();
            errorReader.close();
 
            log.info("错误输出:" + errorOutputString);
            int exitValue = process.exitValue();
            log.info("命令执行结果:" + output.toString()+":"+exitCode+","+exitValue);
            return output.toString();
        }catch (IOException | InterruptedException e){
            e.printStackTrace();
            log.error("命令执行出错,原因:" + e.getMessage());
            throw new ServiceException("Command execution failed"+e.getMessage());
        }
    }
 
    /**
     * 文件备份操作
     * @param source 源文件
     * @param destination 文件备份路径
     * @throws IOException
     */
    private void copyFolder(File source, File destination) throws IOException {
        // 文件存在才需要备份
        if(source.exists()){
            if (source.isDirectory()) {
                if (!destination.exists()) {
                    destination.mkdir();
                }
                String[] files = source.list();
                for (String file : files) {
                    File srcFile = new File(source, file);
                    File destFile = new File(destination, file);
                    copyFolder(srcFile, destFile);
                }
            } else {
                Files.copy(source.toPath(), destination.toPath());
            }
        }
    }
 
    /**
     * 根据服务名获取端口号
     * @param serviceId
     * @return
     */
    private String getGatewayPort(String serviceId) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        if (!instances.isEmpty()) {
            ServiceInstance gatewayInstance = instances.get(0);
            return String.valueOf(gatewayInstance.getPort());
        }
        return "8080";
    }
 
    /**
     * 启动、停止、重启命令校验
     * @param cmd
     * @return true: 包含高风险命令, false:不包含
     */
    private boolean checkCmdVer(String cmd){
        if(Func.isEmpty(cmd)){
            return false;
        }
        String[] highRiskCommands = {"rm", "rmdir", "mv", "unlink", "rm -rf", "mv -rf", "dd", "chmod", "chown", "mkfs", "shutdown", "reboot", "kill"};
        for(String highRiskCmd : highRiskCommands){
            if(cmd.contains(highRiskCmd)){
                return true;
            }
        }
        return false;
    }
 
}