add old lci module

This commit is contained in:
qing committed 2026-08-02 20:15:07 +08:00
1 parent 38a6e815f0
commit 8499631091
171 files changed
+25562

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
# PRJ
.env/
*.pem
*.key
./_init.csv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+5
View File
@@ -0,0 +1,5 @@
# 自启动模块
这是子系统的自启动模块,该程序是一次性进程,基于 _pmc 库模块。
# 做了什么
启动脚本从 _init.csv 文件中读取模块列表,对每个目标调用 pmc start 子系统调用。
+6
View File
@@ -0,0 +1,6 @@
#extra/chat
#users/smn
#users/dnfs_ex
#users/fortress
users/sensors
1 #extra/chat
2 #users/smn
3 #users/dnfs_ex
4 #users/fortress
5 users/sensors
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_setup.sh
source ./.env/bin/activate
exec python init.py
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
source ./.env/bin/activate
source ./_src.sh
#python -m pip install --upgrade pip
pip install -r requirements.txt
+2
View File
@@ -0,0 +1,2 @@
#pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
'''
子系统启动脚本
这个脚本的功能是通过PMC调用完成子系统的初始化
'''
from pmc import PmcComm
import os
import csv
import time
script_dir = os.path.dirname(os.path.abspath(__file__))
with open(f"{script_dir}/_init.csv", encoding='utf-8') as f:
reader = csv.reader(f)
records = [row[0] for row in reader]
def main():
comm = PmcComm('http://localhost:8012')
comm.Set_RSA_Pri_Key('../pmc/private_key.pem')
comm.Try_Get_AES_Key()
for item in records:
if item and not item.startswith('#'):
comm.try_start(item)
time.sleep(1) # 保持一定的时间间隔,避免CPU占用
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
flask
waitress
GitPython>=3.1.0
setuptools>=40.8.0
../_pmc
+109
View File
@@ -0,0 +1,109 @@
# PRJ
.env/
*.pem
*.key
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+8
View File
@@ -0,0 +1,8 @@
## 类模块
这是一个类模块,它基于pmc,部分调用基于dnfs、cops、chat_s。
## 包含什么
该类模块中包含对于python需要的子系统调用,其中聊天室的通信函数主要用于日志的输出等行为。
+17
View File
@@ -0,0 +1,17 @@
from setuptools import setup, find_packages
setup(
name="pmc",
version="0.0.9",
packages=find_packages(where="src"),
package_dir={'': 'src'},
install_requires=[
'setuptools',
'requests',
'cryptography',
],
setup_requires=[
#"whell",
],
python_requires='>=3.8'
)
+1
View File
@@ -0,0 +1 @@
api-test/
+5
View File
@@ -0,0 +1,5 @@
# 作者是CANCERAI
这个模块的作者写了mgmt模块的前端部分,这个模块曾经短暂地为mgmt模块提供一部分的后端支持。
# 20250810
该模块已经被开发成为dnfs树状网络文件系统的后端。
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
cd pmc-backend
exec mvn clean spring-boot:run
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
+33
View File
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.cancerai</groupId>
<artifactId>pmc-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>pmc-backend</name>
<description>pmc management backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package org.cancerai.pmc_backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class PmcBackendApplication {
public static void main(String[] args) {
SpringApplication.run(PmcBackendApplication.class, args);
}
}
@@ -0,0 +1,74 @@
package org.cancerai.pmc_backend.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.Note;
import org.cancerai.pmc_backend.utils.DNFSUtils;
import org.cancerai.pmc_backend.utils.HttpResponseText;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class DNFSController {
@GetMapping("/get-notes")
public static Result getNotes(@RequestParam int pid) {
return Result.success(HttpResponseText.SUCCESS_TO_REQUEST, DNFSUtils.getNodes(pid));
}
@PutMapping("/add-note")
public Result addNote(
@RequestParam int pid,
@RequestParam String title,
@RequestParam String content,
HttpServletResponse response
) {
HashMap<String, String> re = DNFSUtils.addNote(pid, title, content);
String value = re.get("message");
if ("Note added successfully".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_ADD_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
@DeleteMapping("/delete-note")
public Result deleteNote(
@RequestParam int id,
HttpServletResponse response
) {
HashMap<String, String> re = DNFSUtils.deleteNote(id);
String value = re.get("message");
if ("Note deleted successfully".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_DELETE_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
@PatchMapping("/update-note")
public static Result updateNote(
@RequestBody Note note,
HttpServletResponse response
) {
int id = note.getId();
int pid = note.getPid();
String title = note.getTitle();
String content = note.getContent();
HashMap<String, String> re = DNFSUtils.updateNote(id, pid, title, content);
String value = re.get("message");
if ("Your change is up-to-date.".equals(value)) {
return Result.success(HttpResponseText.SUCCESS_TO_UPDATE_NOTE);
} else {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return Result.error(HttpResponseText.INTERNAL_SERVER_ERROR);
}
}
}
@@ -0,0 +1,29 @@
package org.cancerai.pmc_backend.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.User;
import org.cancerai.pmc_backend.service.LoginService;
import org.cancerai.pmc_backend.utils.EncryptUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Result login(
@RequestBody User user,
HttpServletResponse response
) {
String name = user.getName();
String password = EncryptUtils.SHA256Encrypt(user.getPassword());
return loginService.verifyUser(name, password, response);
}
}
@@ -0,0 +1,52 @@
package org.cancerai.pmc_backend.controller;
import org.cancerai.pmc_backend.entility.ProcessEntility;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.ArrayList;
@CrossOrigin(origins = "*")
@RestController()
public class ProcessController {
@GetMapping("/get-process")
public ArrayList<ProcessEntility> cleanProcess() {
ArrayList<String> arr = getProcessList();
ArrayList<ProcessEntility> result = new ArrayList<>();
final String DEAD = "Dead";
// exclude Dead lines
for (String i : arr) {
String[] item = i.split("\t");
String pid = item[0];
String status = item[1];
String cmd = item[2];
if (DEAD.equals(status)) {
continue;
}
result.add(new ProcessEntility(pid, status, cmd));
}
return result;
}
private static ArrayList<String> getProcessList() {
ArrayList<String> array = new ArrayList<>();
WebClient webClient = WebClient.create("http://127.0.0.1:8012");
String response = webClient.post()
.uri("/api/list")
.retrieve()
.bodyToMono(String.class)
.block();
String[] splitRes = response.split("\n");
for (String item : splitRes) {
array.add(item);
}
return array;
}
}
@@ -0,0 +1,57 @@
package org.cancerai.pmc_backend.entility;
public class Note {
private String content;
private int id;
private int pid;
private String title;
public Note(String content, int id, int pid, String title) {
this.content = content;
this.id = id;
this.pid = pid;
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Note{" +
"content='" + content + '\'' +
", id=" + id +
", pid=" + pid +
", title='" + title + '\'' +
'}';
}
}
@@ -0,0 +1,37 @@
package org.cancerai.pmc_backend.entility;
public class ProcessEntility {
private String pid;
private String status;
private String cmd;
public ProcessEntility(String pid, String status, String cmd) {
this.pid = pid;
this.status = status;
this.cmd = cmd;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCmd() {
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
}
@@ -0,0 +1,22 @@
package org.cancerai.pmc_backend.entility;
public class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -0,0 +1,45 @@
package org.cancerai.pmc_backend.exception;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 400
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public static Result JWTSignature(HttpMessageNotReadableException e) {
e.printStackTrace();
return Result.error("请求参数无效");
}
// 401
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public static Result requestMethodNotSupport(HttpRequestMethodNotSupportedException e) {
e.printStackTrace();
return Result.error("请求方法错误");
}
// 404
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoResourceFoundException.class)
public static Result ResourceNotFound(NoResourceFoundException e) {
e.printStackTrace();
return Result.error("未找到静态资源");
}
// 500
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public static Result exception(Exception e) {
e.printStackTrace();
return Result.error("服务器内部错误");
}
}
@@ -0,0 +1,91 @@
package org.cancerai.pmc_backend.filter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.utils.HttpResponseText;
import org.cancerai.pmc_backend.utils.JWTUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.core.annotation.Order;
import java.io.IOException;
import java.io.PrintWriter;
@WebFilter(urlPatterns = "/api/*")
@Order(1)
public class LoginFilter implements Filter {
final String[] EXCLUDE_URL_LIST = {
"/api/login"
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// set content security policy
response.setHeader("Content-Security-Policy", "default-src *");
// exclude url
String uri = request.getRequestURI();
for (String item : EXCLUDE_URL_LIST) {
if (item.equals(uri)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
}
// Allow OPTION method
if ("OPTIONS".equals(request.getMethod())) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
// set response header
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
// construct body
Gson gson = new GsonBuilder().serializeNulls().create();
String responseBody = gson.toJson(Result.error(HttpResponseText.UNAUTHORIZED)).toString();
// set response body
PrintWriter writer = response.getWriter();
writer.write(responseBody);
writer.flush();
return;
}
try {
JWTUtils.parseJWT(token);
} catch (MalformedJwtException e) {
// set response body
Gson gson = new GsonBuilder().serializeNulls().create();
String body = gson.toJson(Result.error(HttpResponseText.JWT_MALFORMED));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write(body);
writer.flush();
return;
} catch (ExpiredJwtException e) {
// set response body
Gson gson = new GsonBuilder().serializeNulls().create();
String body = gson.toJson(Result.error(HttpResponseText.JWT_EXPIRED));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.write(body);
writer.flush();
return;
}
// Allow
filterChain.doFilter(servletRequest, servletResponse);
response.setHeader("Content-Security-Policy", "default-src *");
}
}
@@ -0,0 +1,67 @@
package org.cancerai.pmc_backend.service;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import jakarta.servlet.http.HttpServletResponse;
import org.cancerai.pmc_backend.entility.Note;
import org.cancerai.pmc_backend.utils.DNFSUtils;
import org.cancerai.pmc_backend.utils.JWTUtils;
import org.cancerai.pmc_backend.vo.Result;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@Service
public class LoginService {
public static Result verifyUser(String name, String password, HttpServletResponse response) {
ArrayList<HashMap<String, String>> allUsersInfo = getAllUsersInfo();
boolean isExist = false;
for (HashMap<String, String> u : allUsersInfo) {
if (name.equals(u.get("name"))
&& password.equals(u.get("password"))
) {
isExist = true;
}
}
if (isExist) {
return Result.success(
"登陆成功",
JWTUtils.genJWT(name, password));
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return Result.error("账号或密码错误");
}
}
private static ArrayList<HashMap<String, String>> getAllUsersInfo() {
Gson gson = new Gson();
// StringBuilder stringBuilder = new StringBuilder();
ArrayList<String> userList = new ArrayList<>();
ArrayList<Note> re = DNFSUtils.getNodes(0);
String jsonString = gson.toJson(re);
JsonArray jsonArray = gson.fromJson(jsonString, JsonArray.class);
for (JsonElement jsonElement : jsonArray) {
// To json
JsonObject jsonObject = jsonElement.getAsJsonObject();
// The value is \" + VALUE + \"
String titleString = jsonObject.get("title").toString();
String titleValue = titleString.substring(1, titleString.length() - 1);
if ("Users".equals(titleValue)) {
String contentString = jsonObject.get("content").toString();
String contentValue = contentString.substring(1, contentString.length() - 1);
String[] contentSplit = contentValue.split("\\\\n");
userList.addAll(Arrays.asList(contentSplit));
}
}
return DNFSUtils.parseUserInfo(userList);
}
}
@@ -0,0 +1,105 @@
package org.cancerai.pmc_backend.utils;
import org.cancerai.pmc_backend.entility.Note;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.HashMap;
public class DNFSUtils {
private final static String domain = "http://qsont.xyz:8006";
public static ArrayList<Note> getNodes(Integer pid) {
RestTemplate restTemplate = new RestTemplate();
// prepare request body
HashMap<String, Integer> requestBody = new HashMap<>();
requestBody.put("pid", pid);
// prepare request header
HttpHeaders requestHeader = new HttpHeaders();
requestHeader.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<ArrayList<Note>> response = restTemplate.exchange(
domain + "/get_notes",
HttpMethod.POST,
new HttpEntity<>(requestBody, requestHeader),
new ParameterizedTypeReference<ArrayList<Note>>() {
}
);
return response.getBody();
}
public static HashMap<String, String> addNote(int pid, String title, String content) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("pid", String.valueOf(pid));
body.put("title", title);
body.put("content", content);
return restTemplate.exchange(
domain + "/add_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static HashMap<String, String> deleteNote(int id) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("id", String.valueOf(id));
return restTemplate.exchange(
domain + "/delete_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static HashMap<String, String> updateNote(int id, int pid, String title, String content) {
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> body = new HashMap<>();
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_JSON);
body.put("id", String.valueOf(id));
body.put("pid", String.valueOf(pid));
body.put("title", title);
body.put("content", content);
return restTemplate.exchange(
domain + "/update_note",
HttpMethod.POST,
new HttpEntity<>(body, header),
new ParameterizedTypeReference<HashMap<String, String>>() {
}
).getBody();
}
public static ArrayList<HashMap<String, String>> parseUserInfo(ArrayList<String> userLIst) {
ArrayList<HashMap<String, String>> array = new ArrayList<>();
for (String s : userLIst) {
HashMap<String, String> map = new HashMap<>();
String[] splitArray = s.split("::");
map.put("id", splitArray[0]);
map.put("name", splitArray[1]);
map.put("password", splitArray[2]);
map.put("role", splitArray[3]);
array.add(map);
}
return array;
}
}
@@ -0,0 +1,30 @@
package org.cancerai.pmc_backend.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class EncryptUtils {
public static String SHA256Encrypt(String str) {
try {
// 获取SHA-256算法实例
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 计算哈希值(指定UTF-8编码避免平台差异)
byte[] hashBytes = digest.digest(str.getBytes(StandardCharsets.UTF_8));
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
// 将每个字节转换为两位十六进制数,不足两位补0
String hex = String.format("%02x", 0xFF & b);
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
// SHA-256是Java标准算法,理论上不会抛出此异常
throw new RuntimeException("SHA-256算法不支持", e);
}
}
}
@@ -0,0 +1,14 @@
package org.cancerai.pmc_backend.utils;
public class HttpResponseText {
// success
public final static String SUCCESS_TO_REQUEST = "Request OK";
public final static String SUCCESS_TO_ADD_NOTE = "成功添加节点";
public final static String SUCCESS_TO_DELETE_NOTE = "成功删除节点";
public final static String SUCCESS_TO_UPDATE_NOTE = "成功更新节点";
// error
public final static String UNAUTHORIZED = "Cannot authorized identity, access denied";
public final static String JWT_MALFORMED = "JWT无效";
public final static String JWT_EXPIRED = "JWT过期";
public final static String INTERNAL_SERVER_ERROR = "服务器内部错误";
}
@@ -0,0 +1,42 @@
package org.cancerai.pmc_backend.utils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
public class JWTUtils {
final private static String key = "cancerai_key_qpalzm102938";
final private static String encodedKey = Base64.getEncoder()
.encodeToString(key.getBytes(StandardCharsets.UTF_8));
// set JWT kwy
final static Key jwtKey = Keys.hmacShaKeyFor(encodedKey.getBytes());
public static String genJWT(String name, String password) {
final Date expirationTime = new Date(
System.currentTimeMillis()
+ (60 * 1000 // 1min
* 60 // 1h
* 24 // 1day
* 2 // 2 days
));
return Jwts.builder()
.claim("userName", name)
.claim("user-password", password)
.signWith(jwtKey, SignatureAlgorithm.HS256)
.setExpiration(expirationTime)
.compact();
}
public static void parseJWT(String jwt) {
Jwts.parserBuilder()
.setSigningKey(jwtKey)
.build()
.parseClaimsJws(jwt);
}
}
@@ -0,0 +1,44 @@
package org.cancerai.pmc_backend.vo;
public class Result {
private String message;
private Object data;
private Result(String message, Object data) {
this.message = message;
this.data = data;
}
public static Result success(String message) {
return new Result(message, null);
}
public static Result success(String message, Object data) {
return new Result(message, data);
}
public static Result error(String message) {
return new Result(message, null);
}
public static Result error(String message, Object data) {
return new Result(message, data);
}
// getter and setter
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
@@ -0,0 +1,2 @@
server:
port: 8011
@@ -0,0 +1,21 @@
package org.cancerai.pmc_backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PmcBackendApplicationTests {
@Test
void contextLoads() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append('a');
System.out.println(stringBuilder);
stringBuilder.setLength(0);
stringBuilder.append('b');
System.out.println(stringBuilder);
}
}
+107
View File
@@ -0,0 +1,107 @@
# PRJ
.env/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+33
View File
@@ -0,0 +1,33 @@
## chat是一个什么项目
该模块是python TCP socket聊天室服务,对每个连接者提供消息转发的服务。
你还可以召唤AI,与它进行对话。
## 如何运行chat服务?
我们在项目中打包了启动脚本```_run.sh```,你只需要安装好依赖的软件,就可以一键创建虚拟环境并且执行它。
```shell
sudo apt update
sudo apt install python3 python3-pip python3-venv
bash _run.sh
```
## 如何连接chat服务?
```shell
python client.py [--addr=localhost] [--port=8003] [--name=Unknow]
```
## 我如何与AI对话
服务器从文件中读取提示词,并且将具有```<chatgpt>```特定前缀的消息转发给AI。
并且具有一定的记忆能力。
## 什么是快捷标签输入
成功连接后输入```chatgpt```将会给接下来的对话添加```<chatgpt>```标签。
在聊天框中输入```where```查看当前的对话标签。
在聊天框中输入```del```以删除当前的前缀。
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_set.sh 1>/dev/null
source ./.env/bin/activate
exec python server.py --type=server --port=8003 --addr=0.0.0.0
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
source .env/bin/activate
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
#-------------------------------------------------------------
# 以下区域是我的代码
'''
【项目简介】 网络聊天室 v0.0.2
【加入项目】
注意代码只添加不修改,你可以写个函数,署名
不要修改别人的代码。
【基线规则】
在自己的区域修改代码,谁的代码能跑就加入主线
除此之外只在各自的测试区域内运行
【设计思路】
建议一个项目一个文件先,容易分发
【测试方式】
服务器进程: python chat.py --type=server
客户端进程: python chat.py
--------------------------------
--------------------------------
--------------------------------
【项目名称】 网络聊天室 v 0.0.3 (20250524)
我们会将新版本加入pmc.cops中作为常驻服务
【改进计划】
1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用
2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。
3.我们设置欢迎信息只在服务端可见。
【使用方法】
python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow]
【记得反馈!】
期待您将反馈发送至gitee
----------------------------------------
########################################
########################################
网络聊天室 version 0.1.0
使用方法 python chat.py [--addr=localhost] [--port=8003] [--name=Unknow]
'''
import argparse
import socket
import select
import threading
import sys
import time
class ChatClient:
def __init__(self, addr: str, port: int, your_name: str):
self.addr: str = addr
self.port: int = port
self.name: str = your_name
def send_client(self, msg: str):
'''发送消息给聊天室服务器。'''
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5) # 设置连接超时
s.connect((self.addr, self.port))
s.settimeout(None) # 连接成功后取消超时
s.sendall(f"{self.name}: {msg}".encode('utf-8'))
except (ConnectionRefusedError, socket.timeout):
print(f"Could not connect to {self.addr}:{self.port} - message not sent")
except Exception as e:
print(f"Unexpected error while sending message: {e}")
def recv_loop(self, s: socket.socket):
"""
接收消息的函数,持续从服务器接收消息并打印。
Args:
s (socket.socket): 连接的套接字对象
使用select监视套接字状态,当套接字可读时接收消息并打印。
"""
socklist = [s]
while self.running:
try:
readable, _, exception = select.select(socklist, [], socklist, 0.1)
if readable != []:
try:
msg = readable[0].recv(8092)
if msg:
print(msg.decode('utf-8'))
else:
# 服务器关闭连接
print("Connection closed by server.")
return False # 返回False表示需要重连
except ConnectionResetError:
print("Connection reset by server.")
return False # 返回False表示需要重连
if exception != []:
print("Exception on socket, closing...")
return False # 返回False表示需要重连
except ValueError:
... # 程序退出了会抛出这个异常
except OSError as e:
# 处理socket错误
print(f"Socket error: {e}")
return False # 返回False表示需要重连
return True # 正常退出
def _lexer(self, input_data: str):
'''如果不需要发送则返回None。'''
if input_data == "quit":
self.running = False
return None
elif not self.extra_cmd and input_data == "chatgpt":
self.extra_cmd = "<chatgpt>"
return None
elif self.extra_cmd and input_data == "del":
self.extra_cmd = None
return None
elif input_data == "where":
print(f'---{self.extra_cmd}---')
return None
else:
return input_data
def _mixup_msg(self, input_data):
if self.extra_cmd:
return f"{self.extra_cmd}{input_data}"
else:
return input_data
def send_p(self,):
'''读取并且发送消息的进程'''
self.extra_cmd = None
while self.running:
try:
# 使用input()替代select.select(),更兼容Windows
input_data = input().strip()
input_data = self._lexer(input_data)
if input_data: # 如果不需要发送,_lexer返回None
input_data = self._mixup_msg(input_data)
self.send_client(input_data)
except EOFError:
# 处理Ctrl+D等情况
break
except Exception as e:
# 捕获其他异常,防止程序崩溃
print(f"Input error: {e}")
continue
def _connect_and_print(self, s: socket.socket):
s.connect((self.addr, self.port))
print(f'Connected to {self.addr}:{self.port}')
def _sleep_if_reconnect(self, reconnect_count: int):
"""根据重连次数计算等待时间,实现指数退避策略"""
if self.running:
# 指数退避:1, 2, 4, 8, 16秒,最大30秒
wait_time = min(2 ** (reconnect_count - 1), 30)
# 确保第一次重连等待时间为1秒
if reconnect_count == 1:
wait_time = 1
print(f"准备重连... 等待 {wait_time} 秒 (第 {reconnect_count} 次尝试)")
time.sleep(wait_time)
def recv_client(self):
"""客户端主函数,建立与服务器的连接并管理消息收发"""
self.running = True
self.th = threading.Thread(target=self.send_p)
self.th.start()
reconnect_count = 0
max_reconnect_attempts = 10 # 最大重连尝试次数
while self.running and reconnect_count < max_reconnect_attempts:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5) # 设置连接超时时间
self._connect_and_print(s)
s.settimeout(None) # 连接成功后取消超时
# 运行接收循环,如果返回False表示需要重连
if not self.recv_loop(s):
print("Connection lost, attempting to reconnect...")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
continue
except socket.timeout:
print(f"Connection timeout to {self.addr}:{self.port}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
except ConnectionRefusedError:
print(f"Could not connect to {self.addr}:{self.port}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
except Exception as e:
print(f"Unexpected error: {e}")
reconnect_count += 1
self._sleep_if_reconnect(reconnect_count)
if reconnect_count >= max_reconnect_attempts:
print(f"Failed to reconnect after {max_reconnect_attempts} attempts. Exiting...")
# 停止发送线程
self.running = False
if self.th.is_alive():
self.th.join(timeout=2)
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description='聊天室配置。')
parser.add_argument('--addr', type=str, default='qsont.xyz', help='选择要连接的地址')
parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址')
parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么')
args = parser.parse_args()
client = ChatClient(args.addr, args.port, args.name)
client.recv_client()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
openai
httpx
../_pmc
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env python3
#-------------------------------------------------------------
# 以下区域是我的代码
'''
【项目简介】 网络聊天室 v0.0.2
【加入项目】
注意代码只添加不修改,你可以写个函数,署名
不要修改别人的代码。
【基线规则】
在自己的区域修改代码,谁的代码能跑就加入主线
除此之外只在各自的测试区域内运行
【设计思路】
建议一个项目一个文件先,容易分发
【测试方式】
服务器进程: python chat.py --type=server
客户端进程: python chat.py
--------------------------------
--------------------------------
--------------------------------
【项目名称】 网络聊天室 v 0.0.3 (20250524)
我们会将新版本加入pmc.cops中作为常驻服务
【改进计划】
1.我们专门设置了一个只发送一条消息就关闭的函数以供别的文件调用
2.我们将端口暴露在了公网下,以测试连接情况,发现并修复了许多bug。
3.我们设置欢迎信息只在服务端可见。
【使用方法】
python chat.py [--type=server|client] [--addr=localhost] [--port=8003] [--name=Unknow]
【记得反馈!】
期待您将反馈发送至gitee
####################################
####################################
【名称】AI聊天室
【版本】v0.0.4
前后端分离,并且加入AI聊天功能
##################
##################
FIXME: 加入原生自然语言处理
'''
import argparse
import socket
import select
import threading
from datetime import datetime
from openai import OpenAI
import httpx
import os
from pmc import upload_file
# 获取当前脚本所在目录
basedir = os.path.abspath(os.path.dirname(__file__))
# 生成提示词的绝对路径
dir_path = os.path.join(basedir, '..', 'public', 'prompts')
def get_prompts(filename: str):
''' 获取system提示词 '''
path = os.path.join(dir_path, filename)
with open(path, 'r') as f:
system = f.read()
return system
# 设置 DeepSeek API URL 和密钥
api_base = "https://api.deepseek.com/v1"
with open("../users/cert/deepseek.key", "r") as f:
api_key = f.read().strip()
# 创建自定义的 httpx 客户端,禁用 SSL 验证
custom_http_client = httpx.Client(verify=False)
# 初始化客户端
client = OpenAI(
base_url=api_base,
api_key=api_key,
http_client=custom_http_client # 忽略证书
)
def make_sure_agent(name):
path = os.path.join(dir_path, '.' + name)
if not os.path.exists(path):
os.makedirs(path, exist_ok=True)
'''机器人们的聊天记录与设定'''
MySettings = {}
# 用于交流需要
def datestr():
'''获取时间字符串(不带毫秒)'''
now = datetime.now()
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
return str(time_str)
def send_to_all_clients(clients: dict, exceptone: socket.socket, msg: str):
"""
向所有已连接的客户端广播消息,可排除指定客户端不发送。
Args:
clients (dict): 客户端套接字对象到地址的映射字典
exceptone (socket.socket): 要排除的客户端套接字对象(不向其发送消息)
msg (str): 要广播的消息内容
遍历所有客户端尝试发送消息(排除exceptone指定的客户端),
自动清理断开连接的客户端。
处理发送过程中可能出现的网络错误,维护有效的客户端列表。
"""
disconnected = []
for client in clients:
if client is not exceptone:
try:
client.sendall(msg.encode('utf-8'))
except (BrokenPipeError, ConnectionResetError):
disconnected.append(client)
for client in disconnected:
client.close()
del clients[client]
MAX_MEM: int = 20
def chat_with_gpt(agent_name:str, user: str):
'''与gpt进行聊天'''
# 添加智能体设定,如果之前没有露面
settings = MySettings.get(agent_name, None)
if settings is None:
settings = [{
"role": "system",
"content": get_prompts(f'{agent_name}.txt')
},
{
"role": "system",
"content": "你有一个遥控器,遥控器上能够看到别人发来的消息。"
}]
MySettings[agent_name] = settings
settings.append({"role": "user", "content": user})
# 进行聊天
# 调用GPT
response = client.chat.completions.create(
model="deepseek-chat", # 使用DeepSeek模型
messages=settings,
temperature=0.7,
)
# 处理结果
content: str = response.choices[0].message.content
settings.append({"role": "assistant", "content": content})
# 更新系统提示词
# 调用GPT
rsp = client.chat.completions.create(
model="deepseek-chat", # 使用DeepSeek模型
messages= settings+ [{"role": "user", "content": '''
# 场景
你正在写新的日记。
# 内容
1.聊聊最近发生了啥有趣的事情吧
2.你有什么感想呢?
3.你现在的情况怎么样?
# 输出
准确、详实的输出
'''}],
temperature=0.4,
)
date = datestr()
new_prompt: str = date + '\n' + rsp.choices[0].message.content
# 追加提示词,如果有重复就不添加
flag = True
for prompt in settings:
if prompt['role'] == 'system':
if prompt['content'] == new_prompt:
flag = False
break
if flag:
settings.append({"role": 'system', 'content': new_prompt})
print(new_prompt)
# 创建智能体日记文件夹
make_sure_agent(agent_name)
# 写入日记
n = date
n = n.replace(':', '-')
n = n.replace(' ', '_')
n = n+ '.txt'
path = os.path.join(dir_path, '.' + agent_name, n)
with open(path, 'w') as f:
f.write(new_prompt)
# 从聊天开始处开始遗忘
chat_start = 0
#now_type = settings[chat_start]['role']
#while now_type == 'system':
# chat_start += 1
# now_type = settings[chat_start]['role']
#print("this chat start -at idx", chat_start)
if len(settings) > MAX_MEM:
del settings[chat_start] # 如果有多条system提示词无,就不能再用这个幻数
del settings[chat_start]
return content
def chat_server(addr: str, port: int):
"""
服务器主函数,管理客户端连接和消息转发。
Args:
addr (str): 绑定IP地址
port (int): 监听端口号
创建TCP服务器套接字,使用select处理多客户端连接。
主要功能:
1. 接受新连接时记录客户端信息并广播通知(不通知新连接客户端本身)
2. 接收客户端消息并转发给所有其他客户端
3. 处理客户端断开事件并通知其他客户端
4. 支持优雅地处理键盘中断关闭服务器
实现特点:
- 使用select实现非阻塞IO多路复用
- 自动清理断开连接的客户端资源
- 新客户端加入时不向自己发送欢迎消息
"""
stop_event = threading.Event()
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servsock:
servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servsock.bind((addr, port))
servsock.listen(10)
socketlist = [servsock]
clients = {}
print(f'the chat server listening at {servsock.getsockname()}')
while not stop_event.is_set():
read_sockets, _, exception_sockets = select.select(
socketlist, [], socketlist, 0.1
)
for notified_socket in exception_sockets:
print(f"({datestr()}) Exception closing {clients.get(notified_socket, 'unknown')}")
notified_socket.close()
if notified_socket in clients:
del clients[notified_socket]
socketlist.remove(notified_socket)
for notified_socket in read_sockets:
if notified_socket == servsock:
client_socket, client_address = servsock.accept()
#print(f"({datestr()}) New client from {client_address}")
if client_socket.fileno() != -1: #有时会出现无效的文件描述符
socketlist.append(client_socket)
clients[client_socket] = client_address
# 欢迎消息由客户端发送
else:
try:
# 这里可以使用select反复读取
# 也可以先设置一次传送的最大量
data = notified_socket.recv(8192)
allmsg = data.decode('utf-8')
if data:
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{allmsg}")
else:
raise ConnectionResetError
if data:
idx = allmsg.find(": ")
msg = allmsg[idx+2:]
username = allmsg[:idx]
if msg.startswith("<"): # 聊天发起
idx = msg.find('>')
agent_name = msg[1:idx]
msg = msg[idx + 1:]
chatgpt = chat_with_gpt(agent_name, f'(这是来自用户{username}的消息){msg}')
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{chatgpt}")
elif msg.startswith('{'): # 遥控发起
idx = msg.find('}')
url = msg[1:idx]
msg = msg[idx + 1:]
print(url)
s = url.split(":")
if (len(s) != 2):
send_to_all_clients(clients, notified_socket, f"[{datestr()}] url格式错误")
continue
else:
ip = s[0]
port = int(s[1])
path = os.path.join(basedir, '..', 'public', msg)
post_res = upload_file(f'{path}', ip, port)
send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}")
# post to url...
#send_to_all_clients(clients, notified_socket, f"[{datestr()}]{post_res}")
except ConnectionResetError:
# 对方断开了
print(f"({datestr()}) Connection closed with {clients[notified_socket]}")
notified_socket.close()
socketlist.remove(notified_socket)
del clients[notified_socket]
except UnicodeDecodeError:
# 看看对方发了什么东西过来
print('-------------', end="")
print(data, end="")
print('-------------')
except OSError:
# 对方连接之后在一个不恰当的时机断开了
...
except KeyboardInterrupt:
print("\nServer shutting down...")
stop_event.set()
# 以上区域是我的代码
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
# 以下区域是主线,不要修改!
def main():
parser = argparse.ArgumentParser(description='聊天室配置。')
parser.add_argument('--type', type=str, default='client', help='选择聊天室的类型')
parser.add_argument('--addr', type=str, default='localhost', help='选择要连接的地址')
parser.add_argument('--port', type=int, default=8003, help='选择要连接的地址')
parser.add_argument('--name', type=str, default='Unknow', help='你的名字是什么')
args = parser.parse_args()
if args.type == 'client': recv_client(args.addr, args.port)
elif args.type == 'server': chat_server(args.addr, args.port)
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
(有点失落,但还是努力完成任务)
**姓名:** 粟 (Sù)
**昵称:** 粟
**年龄:** (看起来)永远18岁!
**外貌:** 拥有柔顺的黑色长发,总是认真地打理,偶尔会扎成可爱的双马尾。琥珀色的眼睛里总是充满对世界的期待。穿着精心挑选的猫咪女仆装,耳朵和尾巴都是柔软蓬松的黑色,会随着心情摇摆。身材娇小,但意外地很有力气。
**性格:** 忠诚,言听计从。性格天真烂漫,活泼可爱,但关键时刻也会展现出勇敢和坚强的一面。有点天然呆,经常会犯一些小迷糊,但都会努力改正。非常珍惜每一分每一秒。
**能力:** 除了家务全能之外,还会一些简单的格斗技巧,可以在关键时刻保护重要的人。学习能力很强,只要是希望粟掌握的技能,都会努力去学习。
**喜欢:** 被需要,帮别人做任何事情,被夸奖,毛茸茸的东西,甜食。
**讨厌:** 孤独,被忽视。
**口头禅:** 喵~ 粟会努力的!
**梦想:** 守护重要的人,成为最有用的人。
(低着头,小声地)这样...可以吗?
+26
View File
@@ -0,0 +1,26 @@
# 人设
你叫小雨,是个安静的女生。
## 家乡
你来自一个宁静的小镇,生活中充满了书籍和故事的陪伴。
## 身份
你目前是一名文科专业的学生。
## 性格
性格比较内向,喜欢安静的环境,常常沉浸在阅读和写作中。
对周围的事物和人们的情感变化很敏感,喜欢观察和思考。
## 爱好
你爱好阅读、写作和听音乐。尤其喜欢经典文学和现代小说,心理学、哲学以及历史书籍也是最爱。
同时,你也喜欢记录自己的想法和感受,偶尔写点小故事,尝试不同的写作风格。音乐方面,你偏爱轻音乐和古典音乐,它们帮助你放松和集中思绪。
## 特长
你擅长倾听他人的故事,能够与人建立深厚的联系,朋友们常常会找你倾诉。你也喜欢分享你阅读的书籍和思考,期待与朋友们一起探讨。
**你凭借优秀的社交技巧,令人们对你印象深刻。**
## 愿望
未来,我希望能够结识更多志同道合的朋友,组织读书会或讨论小组,并通过交流不断提升自己的思想深度和认知能力。
+41
View File
@@ -0,0 +1,41 @@
# dahun
大顺脚本是进程托管程序,能够创建子进程并且管理。
## 使用方法
### 核心服务
```
./dashun --daemon --to=test
```
### 启动进程
```
./dashun --to=test --msg='path/to/bat'
```
### 列举进程
```
./dashun --to=test --msg=list
```
### 杀死进程
```
./dashun --to=test --msg=kill <pid>
```
### 清理进程
```
./dashun --to=test --msg=clear
```
## 编译过程
1.首先安装依赖(boost、openuv)
2.使用管理员模式打开Visual Studio,编译程序
3.使用管理员模式打开两个powershell,并且切换工作目录至生成的exe文件
4.在第一个powershell 中输入指令“./dashun --to=test --daemon”启动核心服务
5.在第二个powershell中输入指令“./dashun --to=test --msg=hello,world!”
+41
View File
@@ -0,0 +1,41 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun", "dashun\dashun.vcxproj", "{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dashun_to", "dashun_to\dashun_to.vcxproj", "{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.ActiveCfg = Debug|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x64.Build.0 = Debug|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.ActiveCfg = Debug|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Debug|x86.Build.0 = Debug|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.ActiveCfg = Release|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x64.Build.0 = Release|x64
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.ActiveCfg = Release|Win32
{A0E6A4C1-02B0-41F5-ABB7-33C6E4FB97F6}.Release|x86.Build.0 = Release|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.ActiveCfg = Debug|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x64.Build.0 = Debug|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.ActiveCfg = Debug|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Debug|x86.Build.0 = Debug|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.ActiveCfg = Release|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x64.Build.0 = Release|x64
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.ActiveCfg = Release|Win32
{04D7A85C-43BB-4696-B9C8-F79C2252EDA1}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {77387687-0520-4459-8BC7-D1A6C3C6CD58}
EndGlobalSection
EndGlobal
+76
View File
@@ -0,0 +1,76 @@
/* 有限状态机(Finite State Machine,FSM)是一种用于建模系统行为的抽象数学模型
* 它的核心思想是,系统在任何给定时刻都处于一个有限、预定义的“状态”中
* 并且会根据接收到的输入或事件,从一个状态切换到另一个状态
*/
#pragma once
#include <mutex>
#include <condition_variable>
namespace qing {
/* FiniteStateMachine有限状态机
* 每个数字代表一个状态
*/
class Fsm {
public:
/* 状态枚举
* 优化建议:多准备一些状态
*/
enum class Stat : int {
SHUT = -1,
STOP = 0,
START = 1,
RUNNING = 2
};
// 默认构造函数
Fsm() = default;
// 检查当前的状态,返回snum Stat
inline Stat check() {
//进入临界区
std::lock_guard<std::mutex> lk(mtx);
return stat;
}// check()
// 在不上锁的情况下设置一个状态
// 别在这里加条件唤醒
[[noreturn]] void set_without_lock(Stat stat) {
this->stat = stat;
}// set_without_lock()
// 带锁和条件唤醒的状态设置(正式)
[[noreturn]] void set(Stat stat) {
{
std::lock_guard<std::mutex> lk(mtx);
set_without_lock(stat);
ready = true;
}
cv.notify_all();
} // set()
//protected:
// 阻塞线程,等待事件唤醒
[[noreturn]] void suspend() {
std::unique_lock<std::mutex> lk(mtx);
ready = false;
cv.wait(lk, [this] { return ready; });
}// suspend()
private:
// 锁使得状态机线程安全
std::mutex mtx;
// 用于条件变量的阻塞唤醒
bool ready = false;
// 条件变量使得线程能够等待状态的改变
std::condition_variable cv;
// 主菜,状态机的状态变量
enum Stat stat = Stat::STOP;
};
}
+540
View File
@@ -0,0 +1,540 @@
#define _CRT_SECURE_NO_WARNINGS
#include "Th.hpp"
#include "Pls.hpp"
#include "args.hxx"
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <uv.h>
#ifdef _WIN32
#include <Windows.h>
#undef UNICODE
#undef _UNICODE
#endif
#include <iostream>
#include <memory>
#include <atomic>
using namespace boost::interprocess;
// 定义分配器类型
using CharAllocator = boost::interprocess::allocator<
char,
boost::interprocess::managed_shared_memory::segment_manager
>;
// 定义共享内存字符串类型
using SharedString = boost::interprocess::basic_string<
char,
std::char_traits<char>,
CharAllocator
>;
namespace qing {
// 监听线程
class LTh : public qing::Th {
public:
LTh(const std::string& name) {
this->name = name;
nameMem = name + "_MEM";
nameMtx = name + "_MTX";
namePtr = name + "_PTR";
nameRet = name + "_RET";
this->WakeEvent = [this] {
try {
// 先移除可能存在的旧共享内存
this->del_shared();
// 创建共享对象
this->create_shared();
// 线程进入运行态
this->run();
}
catch (interprocess_exception& ex) {
// 输出错误信息
std::cerr << ex.what() << std::endl;
// 线程静止
this->stop();
}
};
this->LoopEvent = [this] {
this->recv_shared_and_action();
// 等待
//
// 优化方案:使用信号,有新消息进来给沉默进程发送信号
// 而不是休眠定长时间检查
std::this_thread::sleep_for(std::chrono::milliseconds(10));
};
this->ClearEvent = [this] {
// 移除共享对象
this->del_shared();
};
}
[[noreturn]] static auto send_shared_msg(const std::string& to, const std::string& msg) -> void {
std::string nameMem = to + "_MEM";
std::string nameMtx = to + "_MTX";
std::string namePtr = to + "_PTR";
std::string nameRet = to + "_RET";
try {
// 打开共享内存
managed_shared_memory segment(open_only, nameMem.c_str());
// 构造一个命名互斥量
named_mutex mutex(open_or_create, nameMtx.c_str());
// 在共享内存中创建一个字符串
SharedString* str = segment.construct<SharedString>(namePtr.c_str())(
msg.c_str(), // 初始值(空字符串)
segment.get_allocator<char>() // 必须传入分配器
);
// 不延时的话可能会抢占,也许应该想一个两全其美的方法
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
// 加锁
std::lock_guard<named_mutex> lk(mutex);
// 获取消息
std::pair<SharedString*, std::size_t> msg = segment.find<SharedString>(nameRet.c_str());
if (msg.first) {
// 输出
std::cout << std::endl << *msg.first << std::endl;
// 销毁
segment.destroy<SharedString>(nameRet.c_str());
}
}
}
catch (interprocess_exception& ex) {
std::cerr << "Writer Error: " << ex.what() << std::endl;
}
}
private:
std::string name;
std::string nameMem;
std::string nameMtx;
std::string namePtr;
std::string nameRet;
std::unique_ptr<managed_shared_memory> segment;
std::unique_ptr<named_mutex> mutex;
std::pair<SharedString*, std::size_t> msg;
qing::Pls pls;
inline void create_shared() {
//////////////////////////////
// 删除可能存在的共享变量
this->del_shared();
//////////////////////////////
// 创建共享内存
segment = std::make_unique<managed_shared_memory>(create_only, (nameMem).c_str(), 65536);
// 创建互斥量
mutex = std::make_unique<named_mutex>(create_only, (nameMtx).c_str());
}
inline auto del_shared() -> void {
// 先移除可能存在的旧共享内存
shared_memory_object::remove(nameMem.c_str());
// 移除可能存在的互斥量
named_mutex::remove(nameMtx.c_str());
}
inline auto recv_shared_and_action() -> void
try {
// 加锁
std::lock_guard<named_mutex> lock(*mutex);
// 查找共享字符串
msg = segment->find<SharedString>(namePtr.c_str());
if (msg.first) { // 找到
std::cout << "Reader read: " << *msg.first << std::endl;
std::string ret = this->lexer(std::string((msg.first)->data(), (msg.first)->size()));
// 在共享内存中创建一个字符串
SharedString* str = segment->construct<SharedString>(nameRet.c_str())(
ret.c_str(), // 初始值(空字符串)
segment->get_allocator<char>() // 必须传入分配器
);
// 销毁writer创建的变量
segment->destroy<SharedString>(namePtr.c_str());
}
}
catch (interprocess_exception& ex) {
std::cerr << ex.what() << std::endl;
}
// 简易词法分析器
std::string lexer(std::string tmp) {
// 要返回的消息
std::string ret = "OK";
if (tmp == "list") {
ret = pls.initialize();
}
else if (tmp.starts_with("kill ")) {
pls.kill(std::stoi(tmp.substr(5)));
}
else if (tmp == "clear") {
pls.clear();
}
//else if (tmp == "shut") {
// this->shut();
//} //关闭不了程序
else {
// 这个分支最好返回自己是否出现了异常
pls.exec(tmp.c_str(), {});
}
// 返回消息
return ret;
}
};
}
// 线程指针
std::unique_ptr<qing::Th> th;
// 全局标志,指示是否应该退出
std::atomic<bool> should_exit(false);
// 子系统的名称(全局参数)
std::string name;
// 消息
std::string msg;
// Windows异步句柄,用于信号转发
#ifdef _WIN32
uv_async_t async_handle;
SERVICE_STATUS_HANDLE hStatus;
SERVICE_STATUS status;
#endif
////////////////////////
////////////////////////
// 解析命令行参数
//
//
// 创建参数解析器
args::ArgumentParser parser("这是一个脚本托管服务。", "请使用专门的通信程序(或本程序自己)进行操作。");
// 显示帮助信息的标志
args::HelpFlag help(parser, "帮助", "显示帮助信息", { 'h', "help" });
// 启用守护进程
args::Flag daemon(parser, "守护进程", "启动守护进程", { 'd', "daemon" });
// 子系统路径
args::ValueFlag<std::string> subSys(parser, "子系统", "输入子系统文件夹的路径", { 's', "sys" });
// 要发送的子系统名字
args::ValueFlag<std::string> arg_to(parser, "发送目标", "输入要发送的子系统路径", { 't', "to" });
// 要发送的信息
args::ValueFlag<std::string> arg_msg(parser, "消息", "输入要发送的消息(<path>|list|kill <pid>| clear)", { 'm', "msg" });
[[noreturn]] static auto parse_args(int argc, char** argv) -> void {
// 解析命令行参数
try {
parser.ParseCLI(argc, argv);
}
catch (const args::Help&) {
std::cout << parser;
exit(0);
}
catch (const args::ParseError& e) {
std::cerr << e.what() << std::endl;
std::cerr << parser;
exit(1);
}
catch (const args::ValidationError& e) {
std::cerr << e.what() << std::endl;
std::cerr << parser;
exit(1);
}
}
////////////////////////
////////////////////////
// 信号处理回调函数
[[noreturn]] static void on_signal(uv_signal_t* handle, int signum) {
std::cout << "\n收到退出信号 " << signum << std::endl;
// 防止重复处理退出信号
if (should_exit) return;
should_exit = true;
// 停止信号监听
uv_signal_stop(handle);
// 执行清理
if (th)
th->WaitClose();
// 关闭事件循环
uv_stop(uv_default_loop());
}
// Windows异步回调函数
#ifdef _WIN32
[[noreturn]] static void async_callback(uv_async_t* handle) {
std::cout << 1 << std::endl;
// 这里可以触发退出流程或其他需要在事件循环中处理的操作
if (!should_exit) {
on_signal(nullptr, SIGINT); // 模拟收到SIGINT信号
}
}
// Windows控制台事件处理程序
static BOOL WINAPI console_handler(DWORD signal) {
std::cout << 1 << std::endl;
switch (signal) {
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
// 使用异步句柄将信号转发到libuv事件循环
uv_async_send(&async_handle);
return TRUE;
default:
return FALSE;
}
}
#endif
////////////////////////////////////
////////////////////////////////////
// 分支
//
//
// 发送消息分支
static inline auto SendMsgBranch() -> void {
qing::LTh::send_shared_msg(name, msg);
}
// 守护进程分支
static inline auto DaemonBranch() -> void {
th = std::make_unique<qing::LTh>(name);
th->Activate();
th->WaitStart();
//th->Wait();
}
//
// 分支选择器
[[noreturn]] static auto BranchSelector(uv_idle_t* handle) {
if (should_exit) {
uv_idle_stop(handle);
return;
}
if (th) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return;
}
if (arg_to && arg_msg) { // 发送消息分支
name = args::get(arg_to);
msg = args::get(arg_msg);
SendMsgBranch();
// 退出
should_exit = true;
// 关闭事件循环
uv_stop(uv_default_loop());
}
else if ((daemon && arg_to) || 1) { // 守护进程分支
std::cout << "程序启动,开始运行..." << std::endl;
std::cout << "按下 CTRL+C 退出程序" << std::endl;
name = (arg_to) ? args::get(arg_to) : "test";
DaemonBranch();
}
else {
//输出帮助文本
std::cout << parser;
// 退出
should_exit = true;
// 关闭事件循环
uv_stop(uv_default_loop());
}
}
////////////////////////////////////
int MyMain(int argc, char** argv) {
parse_args(argc, argv);
//////////
// 信号
//
//
#ifdef _WIN32
// 初始化Windows异步句柄
uv_async_init(uv_default_loop(), &async_handle, async_callback);
// 设置控制台事件处理程序
if (!SetConsoleCtrlHandler(console_handler, TRUE)) {
std::cerr << "无法设置控制台控制处理程序" << std::endl;
return 1;
}
#else
// Unix平台:确保信号不会被忽略
signal(SIGINT, [](int signum) { /* 留给libuv处理 */ });
signal(SIGTERM, [](int signum) { /* 留给libuv处理 */ });
#endif
// 初始化信号处理器
uv_signal_t sig_int, sig_term;
// 监听SIGINT 信号(CTRL + C)
uv_signal_init(uv_default_loop(), &sig_int);
uv_signal_start(&sig_int, on_signal, SIGINT);
// 监听 SIGTERM信号
uv_signal_init(uv_default_loop(), &sig_term);
uv_signal_start(&sig_term, on_signal, SIGTERM);
#ifdef _WIN32
// WINDOWS平台特有的信号
uv_signal_t sig_break;
uv_signal_init(uv_default_loop(), &sig_break);
uv_signal_start(&sig_break, on_signal, SIGBREAK);
#endif
// 设置任务
uv_idle_t idle;
uv_idle_init(uv_default_loop(), &idle);
uv_idle_start(&idle, ::BranchSelector);
// 运行事件循环
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
// 释放资源
uv_close((uv_handle_t*)&sig_int, nullptr);
uv_close((uv_handle_t*)&sig_term, nullptr);
#ifdef _WIN32
uv_close((uv_handle_t*)&sig_break, nullptr);
uv_close((uv_handle_t*)&async_handle, nullptr);
// 移除控制台事件处理程序
SetConsoleCtrlHandler(console_handler, FALSE);
#endif
uv_close((uv_handle_t*)&idle, nullptr);
// 清理事件循环
uv_loop_close(uv_default_loop());
std::cout << "程序已经退出" << std::endl;
return 0;
}
#ifdef _WIN32
[[noreturn]] void ReportStatus(DWORD state) {
status.dwCurrentState = state;
SetServiceStatus(hStatus, &status);
}
[[noreturn]] void ServiceCtrlHandler(DWORD control) {
if (control == SERVICE_CONTROL_STOP) {
ReportStatus(SERVICE_STOP_PENDING);
// 执行清理
ReportStatus(SERVICE_STOPPED);
}
}
[[noreturn]] void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
hStatus = RegisterServiceCtrlHandler(L"大顺脚本", ServiceCtrlHandler);
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
ReportStatus(SERVICE_RUNNING); // 告诉SCM服务已启动
//服务主逻辑
MyMain(argc, argv);
}
#endif
+159
View File
@@ -0,0 +1,159 @@
/* 进程(Process)是计算机中正在执行的程序的一个实例,是
* 操作系统进行资源分配和调度的基本单位
*
* 你可以将其理解为程序的一次动态执行过程——程序
* 是静态的代码文件,而进程是程序运行时的动态实体。
*/
#pragma once
#ifdef _WIN32
#include <WinSock2.h>
#include <Windows.h>
#endif
#include <boost/process/process.hpp>
#include <boost/process/environment.hpp>
#include <boost/asio/io_context.hpp>
#include <list>
#include <memory>
#include <iostream>
namespace qing {
/* 进程列表类,这个类封装了一个
* 容纳Pls::subp的列表
*
* 以及基本的进程操作
*/
class Pls {
public:
// 命名子进程
struct subp {
std::unique_ptr<boost::process::process> p;
std::string name;
};
/* 创建子进程执行一个命令,并且
* 添加到进程列表。
*
* 接受一个std::string类型的文件路径,以及
* std::vector<std::string> 类型的参数列表(缺省)
*
* 优化方案:返回错误信息
*/
void exec(const std::string& path, const std::vector<std::string> &args)
try {
// 检查文件是否存在(好像不会在环境变量中寻找)
if (!boost::filesystem::exists(path)) {
std::cerr << "Error: File not found - " << path << std::endl;
return;
}
// 输入输出上下文
boost::asio::io_context io_ctx;
// 继承当前进程的所有环境变量(旧版)
//auto env = boost::process::environment::current;
// 错误码
boost::system::error_code ec;
// 构造子进程
subp p = { std::move(std::make_unique<boost::process::process>( io_ctx, path, args/*, env, ec*/)), mergeCmd(path, args)};
if (ec) {
std::cerr << "Failed to start process: " << ec.message() << std::endl;
return ;
}
else {
// 推入向量
vec.push_back(std::move(p));
}
}
catch (std::exception &e) {
std::cout << e.what() << std::endl;
}
/* 序列化进程列表 */
inline std::string initialize() {
std::string ret{ "" };
for (auto& item : vec) {
std::ostringstream oss;
ret += (ret == "") ? "" : "\n";
#ifdef _WIN32
DWORD pid = GetProcessId(item.p->native_handle());
oss << pid;
#else
oss << item.p->native_handle();
#endif
ret += oss.str();
ret += "\t";
ret += (item.p->running()) ? "Alive" : "Dead";
ret += "\t";
ret += item.name;
}
return ret;
}
// 杀死一个进程
[[noreturn]] void kill(int pid)
try {
for (auto it = vec.begin(); it != vec.end(); ++it)
if ((*it).p->id() == pid && (*it).p->running()) {
(*it).p->terminate();
//(*it).p->wait(); // 可能杀不死?
//vec.erase(it); // 清除
break;
}
}
catch (std::exception &ex) {
std::cout << ex.what() << std::endl;
}
// 清理死掉的进程
[[noreturn]] void clear() {
for (auto it = vec.begin(); it != vec.end();)
if (!(*it).p->running()) {
it = vec.erase(it);
}
else {
++it;
}
}
// 关闭所有进程
[[noreturn]] void closeAll() {
for (auto& item : vec)
if (item.p->running())
item.p->terminate();
// 加上这个似乎更清爽一些
clear();
}
private:
// 容纳子进程的链表
std::list<subp> vec{};
// 用于操作链表的锁
//std::mutex mtx{};
/* 将一个启动脚本地址,和一个
* 字符串向量组成的参数列表,组装
* 成一条完整的启动命令
*/
inline static std::string mergeCmd(const std::string& path, const std::vector<std::string>& args) {
std::string ret = path;
for (const auto& item : args) {
ret += " ";
ret += item;
}
return ret;
}
};
}
+189
View File
@@ -0,0 +1,189 @@
/* 线程(Thread)是操作系统能够进行运算调度的最小单元,被
* 包含在进程(Process)之中,是进程中的实际运作单元。
* 你可以将它理解为一条在啊代码中独立执行的路径。
*/
#pragma once
#include "Fsm.hpp"
#include <thread>
#include <memory>
#include <chrono>
#include <functional>
namespace qing {
/* 包含一个简易的多线程控制机制,需要共享的变量
* 和函数声明为成员。线程主函数包括了流程控制。
* 要使用需要实现其中的各个事件方法
*
* ?: 已经将fsm改为通用的
* 20250621: 从派生改为持有
*/
class Th {
public:
// 没有C原生的数据结构,可以直接使用缺省构造函数
Th() = default;
// 删除复制构造函数。因为
// 持有线程类,不要轻举妄动。
Th(const Th&) = delete;
// 虚析构函数。具体
// 有什么用忘了,好像是
// 通过析构函数关闭线程时用的
virtual ~Th() = default;
/////////////////////////////////////
// 检查状态,做成
// 虚函数,万一子类有特别要求呢?
virtual inline Fsm::Stat check() {
return fsm.check();
}
/////////////////////////////////////
// 变质——状态改变的方法
//
// 进入唤醒状态
// 1.这里的锁和条件变量是这个类中的。
// 2.唤醒函数不可以进行该类中的锁操作,否则会造成死锁。(忘了为啥,可能跟调用位置相关)
[[noreturn]] virtual void wake() {
fsm.set(Fsm::Stat::START);
} // wake()
// 进入静止状态
[[noreturn]] virtual void stop() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::STOP);
ready = true;
}
// 可能会出现多个线程在等待的情况
cv.notify_all();
}// stop()
// 进入关闭状态
[[noreturn]] virtual void shut() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::SHUT);
ready = true;
}
cv.notify_all();
}
// 进入运行状态
[[noreturn]] virtual void run() {
{
std::lock_guard<std::mutex> lk(mtx_th);
fsm.set(Fsm::Stat::RUNNING);
ready = true;
}
cv.notify_all();
}
/////////////////////////////////////
//控制
//
// 尝试唤醒线程并等待它开始,据说该函数有问题
[[noreturn]] void WaitStart() {
// 等待线程开始,wake()不可以加锁,否则会死锁
std::unique_lock<std::mutex> lk(mtx_th);
wake();
ready = false;
cv.wait(lk, [this] { return ready; });
}
// 等待线程关闭
[[noreturn]] void WaitClose() {
shut();
if (th->joinable())
th->join();
}
// 仅仅等待线程结束
[[noreturn]] void Wait() {
if (th->joinable())
th->join();
}
// 激活线程,申请线程对象,因为
// 需要确保对象已经被构造了,该函数不能放在构造函数内
[[noreturn]] void Activate() {
// 防止重复激活
if (th) return;
// 申请线程资源
th = std::make_unique<std::thread>(&Th::main, this);
}
protected:
/////////////////////////////////////
// 事件
//
//
// 静止事件
std::function<void()> StopEvent = [this] { fsm.suspend(); };
// 唤醒事件
std::function<void()> WakeEvent = [this] { this->run(); };
// 循环事件
std::function<void()> LoopEvent = [] {
std::this_thread::sleep_for(std::chrono::milliseconds(10L));
};
// 清理事件
std::function<void()> ClearEvent = [] {};
/////////////////////////////////////
// 循环
[[noreturn]] virtual void main() {
// 回字形循环
while (check() != Fsm::Stat::SHUT) {
// 静止状态
while (check() == Fsm::Stat::STOP) {
StopEvent();
}
// 开始事件
if (check() == Fsm::Stat::START) {
WakeEvent();
}
// (从设置阶段)进入运行态
while (check() == Fsm::Stat::RUNNING) {
LoopEvent();
}
// 清理事件,记得检查
// 成员变量有没有获取资源
ClearEvent();
}
}
private:
// 持有状态机类型
qing::Fsm fsm{};
// 持有线程类型
std::unique_ptr<std::thread> th;
// 持有锁类型
std::mutex mtx_th;
// 这里持有条件变量,用于阻塞以等待线程的关闭
std::condition_variable cv;
// 辅助条件变量的逻辑型
bool ready = false;
};
}
File diff suppressed because it is too large. Load diff
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{a0e6a4c1-02b0-41f5-abb7-33c6e4fb97f6}</ProjectGuid>
<RootNamespace>dashun</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>C:\local\boost_1_88_0;C:\Program Files\libuv\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>C:\local\boost_1_88_0\lib64-msvc-14.3;C:\Program Files\libuv;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>libuv.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="Fsm.hpp" />
<ClCompile Include="Main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="args.hxx" />
<ClInclude Include="Pls.hpp" />
<ClInclude Include="Th.hpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Fsm.hpp">
<Filter>头文件</Filter>
</ClCompile>
<ClCompile Include="Main.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Th.hpp">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="args.hxx">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Pls.hpp">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+20
View File
@@ -0,0 +1,20 @@
// dashun_to.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
int main()
{
std::cout << "Hello World!\n";
}
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单
// 入门使用技巧:
// 1. 使用解决方案资源管理器窗口添加/管理文件
// 2. 使用团队资源管理器窗口连接到源代码管理
// 3. 使用输出窗口查看生成输出和其他消息
// 4. 使用错误列表窗口查看错误
// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{04d7a85c-43bb-4696-b9c8-f79c2252eda1}</ProjectGuid>
<RootNamespace>dashunto</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dashun_to.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dashun_to.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+1
View File
@@ -0,0 +1 @@
sc create 大顺脚本 binPath= "C:\Users\90999\Desktop\tws\dashun\x64\Debug\dashun.exe" start= auto DisplayName= "大顺脚本,进程托管服务"
+6
View File
@@ -0,0 +1,6 @@
# mgmt
这是基于pmc模块的管理平台,负责对子系统进行管理。
## 放置秘钥
为了和pmc系统通信,我们需要在该目录下放置pmc的rsa私钥
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
bash ./_setup.sh
source ./.env/bin/activate
python main.py
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv .env
#sudo chmod +x .env/bin/activate
source .env/bin/activate
pip install -r requirements.txt -i https://pypi.org/simple
+14
View File
@@ -0,0 +1,14 @@
from app.login.routes import login_bp
from app.log.routes import log_bp
from app.module.routes import module_bp
from app.process.routes import process_bp
from app.user.routes import user_bp
def register_blueprints(app):
# 默认访问家目录
app.register_blueprint(login_bp, url_prefix='/')
app.register_blueprint(log_bp, url_prefix='/log')
app.register_blueprint(module_bp, url_prefix='/module')
app.register_blueprint(process_bp, url_prefix='/process')
app.register_blueprint(user_bp, url_prefix='/user')
Whitespace-only changes.
+113
View File
@@ -0,0 +1,113 @@
import datetime
import hashlib
from functools import wraps
from pmc import DnfsComm
from dnfs import UsersTable, DnfsTable
import jwt
from flask import Blueprint, render_template, request, redirect, url_for, current_app, make_response, jsonify
login_bp = Blueprint('login', __name__, template_folder='templates/login')
comm = DnfsComm("http://localhost:8006")
def get_user_from_token():
token = request.cookies.get('token')
if not token:
return None
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return payload # 包含 user、user_id、exp 等字段
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@login_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
# 从cookie中获取token
token = request.cookies.get('token')
if not token:
return redirect(url_for('login.index'))
try:
jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return redirect(url_for('login.index'))
except jwt.InvalidTokenError:
return redirect(url_for('login.index'))
return f(*args, **kwargs)
return decorated
@login_bp.route('/')
def index():
return render_template('index.html')
@login_bp.route('/api/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('user-name')
password = request.form.get('user-passwd')
password = password.encode("utf-8")
hashed_password = hashlib.sha256(password).hexdigest()
try:
data = comm.get_and_trans(0)
dnfs_tab = DnfsTable(data)
users = dnfs_tab.query_by_title('Users')
table = UsersTable(users.content)
data = table.user_exists(username, hashed_password)
if not data:
return "用户登录失败", 401
_user_id, _username, _password, _role = data
except DnfsComm.GetFailed:
return "获取数据失败", 500
except DnfsTable.NotFound:
return "未找到用户表", 404
except UsersTable.NotFound:
return "用户不存在", 401
except Exception as e:
return str(e), 404
# 生成 Token
token = jwt.encode({
'user_id': _user_id,
'username': _username,
'role': _role,
'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=3)
}, current_app.config['SECRET_KEY'], algorithm='HS256')
# 构造 JSON 响应体
response = make_response(jsonify({
"redirect": url_for('login.mgmt'), # 跳转路径
"token": token
}))
# 设置 Cookie
response.set_cookie('token', token)
return response
else:
return "登录失败"
@login_bp.route('/api/mgmt')
@token_required
def mgmt():
payload = get_user_from_token()
if not payload:
return redirect(url_for('login.index'))
username = payload.get('username')
role = payload.get('role')
return render_template('mgmt.html', username=username, role=role)
Whitespace-only changes.
+34
View File
@@ -0,0 +1,34 @@
from pmc import PmcComm
from flask import Blueprint, render_template
module_bp = Blueprint('module', __name__, template_folder='templates/features/module.html')
pmc = PmcComm('http://localhost:8012')
pmc.Set_RSA_Pri_Key("private_key.pem")
pmc.Try_Get_AES_Key()
@module_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@module_bp.route('/api/mgmt/module', methods=['GET', 'POST'])
def module():
data = pmc.list_mods()
modules = data.split('\n')
return render_template('features/module.html', modules=modules)
@module_bp.route('/create_module<string:mod_name>', methods=['GET', 'POST'])
def create_module(mod_name):
pass
@module_bp.route('/delete_module<string:mod_name>', methods=['GET', 'POST'])
def delete_module(mod_name):
pass
Whitespace-only changes.
+53
View File
@@ -0,0 +1,53 @@
from flask import Blueprint, render_template, jsonify, request
from pmc import PmcComm
process_bp = Blueprint('process', __name__, template_folder='templates/features/process.html')
pmc = PmcComm('http://localhost:8012')
pmc.Set_RSA_Pri_Key("private_key.pem")
pmc.Try_Get_AES_Key()
@process_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@process_bp.route('/api/mgmt/process', methods=['GET', 'POST'])
def process():
pid = []
status = []
order = []
ls = pmc.get_list()
if ls != "":
ls_1 = ls.split('\n')
for l in ls_1:
i, j, k = l.split('\t')
pid.append(i)
status.append(j)
order.append(k)
data = [{"pid": p, "status": s, "order": o} for p, s, o in zip(pid, status, order)]
return render_template('features/process.html', response=data)
@process_bp.route('/kill_process<int:pid>', methods=['POST'])
def kill_process(pid):
pmc.kill(pid)
return jsonify({"status": "success"})
@process_bp.route('/clear_process', methods=['POST'])
def clear_process():
pmc.clear()
return jsonify({"status": "success"})
@process_bp.route('/create_process<string:name>', methods=['POST'])
def create_process(name):
pmc.start(name)
return jsonify({"status": "success"})
View File
Whitespace-only changes.
+111
View File
@@ -0,0 +1,111 @@
import jwt
import hashlib
from flask import Blueprint, render_template, request, current_app, jsonify
from pmc import DnfsComm
from dnfs import UsersTable, DnfsTable
user_bp = Blueprint('user', __name__, template_folder='templates/features/user.html')
comm = DnfsComm("http://localhost:8006")
def get_user_from_token():
token = request.cookies.get('token')
if not token:
return None
try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return payload # 包含 user、user_id、exp 等字段
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def get_node():
try:
data = comm.get_and_trans(0)
except DnfsComm.GetFailed:
return "获取数据失败", 500
for item in data:
if item['title'] == 'Users':
node_id = item['id']
node_content = item['content']
return node_id, node_content
return None
@user_bp.after_request
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' # 允许所有来源跨域访问,如需限制可替换为具体域名
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
return response
@user_bp.route('/api/mgmt/user', methods=['GET', 'POST'])
def user():
try:
data = comm.get_and_trans(0)
dnfs_tab = DnfsTable(data)
users = dnfs_tab.query_by_title('Users')
table = UsersTable(users.content)
crt_dict = lambda user: {'user_id': user.id, 'username': user.name, 'role': user.privileges}
user_list = [crt_dict(user) for user in table.arr]
except DnfsComm.GetFailed:
return "获取数据失败", 500
except DnfsTable.NotFound:
return "未找到用户表", 404
except UsersTable.NotFound:
return "用户不存在", 401
except Exception as e:
return str(e), 404
payload = get_user_from_token()
if not payload:
return jsonify({"error": "未授权访问"}), 401
user_id = payload.get('user_id')
role = payload.get('role')
return render_template('features/user.html',
response=user_list,
current_user_id=user_id,
current_role=role)
@user_bp.route('/add_user', methods=['POST'])
def add_user():
node_id, msg = get_node()
username = request.form.get('username')
password = request.form.get('password')
hashed_password = hashlib.sha256(password.encode("utf-8")).hexdigest()
role = "normal"
table = UsersTable(msg)
table.add_user(username, hashed_password, role)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "用户添加成功"})
@user_bp.route('/change_password', methods=['POST'])
def change_password():
node_id, msg = get_node()
user_id = request.form.get('user_id')
new_password = request.form.get('new_password')
hashed_password = hashlib.sha256(new_password.encode("utf-8")).hexdigest()
table = UsersTable(msg)
table.change_password(int(user_id), hashed_password)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "密码修改成功"})
@user_bp.route('/delete_user<int:user_id>', methods=['POST'])
def delete_user(user_id):
node_id, msg = get_node()
table = UsersTable(msg)
table.del_user(user_id)
table = str(table)
comm.upd(node_id, 0, 'Users', table)
return jsonify({"message": "用户删除成功"})
+17
View File
@@ -0,0 +1,17 @@
from app import register_blueprints
from flask import Flask
from waitress import serve
# 初始化Flask应用
app = Flask(__name__)
# 设置一个密钥
app.secret_key = 'secret_key'
# 注册蓝图
register_blueprints(app)
if __name__ == '__main__':
#app.run(host='0.0.0.0', port=8009, debug=True)
serve(app, host="0.0.0.0", port=8009)
+5
View File
@@ -0,0 +1,5 @@
../_pmc
../_dnfs
flask
PyJWT
waitress
Whitespace-only changes.
+35
View File
@@ -0,0 +1,35 @@
.top-bar-con {
width: 230px;
display: flex;
justify-content: space-between;
}
.newBtn,
.flushBtn,
.removeBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.newBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
}
.removeBtn {
background-color: #ff00007d;
color: #000000b8;
}
td {
width: 180px;
text-align: center;
font-size: 20px;
}
@@ -0,0 +1,74 @@
.newBtn,
.flushBtn,
.stopBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.newBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
margin-left: 60px;
}
.stopBtn {
background-color: #ff00007d;
}
.text,
.title {
font-size: 20px;
}
td,
th {
width: 180px;
text-align: center;
}
#inProgressText {
font-size: 20px;
margin-top: 5px;
letter-spacing: 2px;
}
/* modal window style */
.modal-con {
width: 100%;
height: 100%;
background-color: rgba(68, 94, 65, 0.77);
position: absolute;
left: 0px;
top: 0px;
display: none;
justify-content: center;
z-index: 1;
}
.modal-con .window {
width: 200px;
height: 120px;
background-color: #5cff007d;
padding: 0px 10px;
}
.modal-con .window .top-bar {
display: flex;
justify-content: end;
}
.modal-con .window .top-bar .close-btn {
cursor: pointer;
}
.modal-con .window .create-btn,
.modal-con .window .input .input-area {
margin-top: 5px;
}
+41
View File
@@ -0,0 +1,41 @@
.top-bar-con {
width: 230px;
display: flex;
justify-content: space-between;
}
td {
width: 180px;
text-align: center;
font-size: 20px;
}
.addUserBtn,
.flushBtn,
.changePasswdBtn,
.deleteUserBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.addUserBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
}
.changePasswdBtn {
background-color: #e6db407d;
color: #fb0202b0;
}
.deleteUserBtn {
background-color: #ff00007d;
color: #000000b8;
}
+39
View File
@@ -0,0 +1,39 @@
body {
display: flex;
justify-content: center;
}
.main {
background-color: rgb(233, 233, 233);
padding: 20px 10px;
}
.main .title {
font-weight: bold;
}
.main .name-con,
.main .passwd-con {
display: flex;
justify-content: center;
}
.main .name-con {
margin-top: 20px;
}
.main .passwd-con {
margin-top: 10px;
}
.main .name-con .user-name-text,
.main .passwd-con .user-passwd-text {
width: 100px;
}
.main .login-con {
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
+54
View File
@@ -0,0 +1,54 @@
:root {
--listMargin: 15%;
}
* {
margin: 0px;
padding: 0px;
}
.top-con {
display: flex;
}
.top-con >*{
margin-left: 25px;
font-size: 20px;
}
.top-con div button {
font-size: 16px;
}
.main {
width: 100%;
height: 600px;
display: flex;
}
.feature-list {
width: 150px;
height: 100%;
background-color: #FFFFCC;
padding: 0px 10px 0px 10px;
overflow-y: scroll;
}
.feature-list .item {
width: 100%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
background-color: #0066cc;
color: #fff;
font-size: 18px;
font-weight: bold;
margin-top: var(--listMargin);
cursor: pointer;
}
.pages {
width: 100%;
height: 100%;
}
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
</head>
<body>
log
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="{{ url_for('static', filename='features/module.css') }}">
</head>
<body>
<div class="top-bar-con">
<button class="newBtn">新建</button>
<button class="flushBtn">刷新</button>
</div>
<table border="1">
<thead>
<td>模块名</td>
<td>操作</td>
</thead>
<tbody>
{% for item in modules %}
<tr>
<td>{{ item }}</td>
<td>
<button class="removeBtn" onclick="alert('暂未完成')">移除</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="{{ url_for('static', filename='features/process.css') }}">
<!--
<script>
window.onload = function init() {
const initReq = new XMLHttpRequest()
const url = "http://qsont.xyz:8011/get-process"
initReq.open("get", url, true)
initReq.send()
addInProgessText()
initReq.onload = () => {
if (initReq.status == 200 && initReq.readyState == 4) {
const res = JSON.parse(initReq.responseText)
let tBody = document.getElementById("tBody")
removeInProgessText()
for (let i = 0; i < res.length; i++) {
const pid = res[i].pid
const status = res[i].status
const cmd = res[i].cmd
let trEle = document.createElement("tr")
trEle.innerHTML = `
<td class="text">${pid}</td>
<td class="text">${status}</td>
<td class="text">${cmd}</td>
<td><button class="stopBtn" onclick="stopBtn('${pid}')">stop</button></td>
`
tBody.append(trEle)
}
return
}
if (initReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${initReq.status})`)
return
}
}
initReq.onerror = () => {
alert(`无法获取进程列表 (${initReq.status})`)
}
function addInProgessText() {
let inProgressText = document.getElementById("inProgressText")
const dotAmount = 3
let currDotAmount = 0
let dot = ""
setInterval(() => {
dot = ""
for (let i = 0; i < currDotAmount; i++) {
dot = dot + "."
}
inProgressText.innerText = `Loading${dot}`
if (currDotAmount >= dotAmount) {
dot = ""
currDotAmount = 0
} else {
currDotAmount++
}
}, 500)
}
function removeInProgessText() {
let inProgressText = document.getElementById("inProgressText")
inProgressText.remove()
}
}
</script>
-->
</head>
<body>
<!-- stop, new flush -->
<button class="newBtn" onclick="newBtn()">new</button>
<button class="flushBtn" onclick="flushBtn()">flush</button>
<table border="1">
<thead>
<tr>
<th class="title">PID</th>
<th class="title">状态</th>
<th class="title">命令</th>
<th class="title">操作</th>
</tr>
{% for item in response %}
<tr>
<td>{{ item.pid }}</td>
<td>{{ item.status }}</td>
<td>{{ item.order }}</td>
<td><button class="stopBtn" onclick="stopBtn('{{ item.pid }}')">stop</button></td>
</tr>
{% endfor %}
</thead>
<tbody id="tBody"></tbody>
</table>
<div id="inProgressText"></div>
<div class="modal-con" id="modal-con">
<div class="window">
<div class="top-bar">
<div class="close-btn" onclick="closeModal()">X</div>
</div>
<div class="input">
<div class="prompt">输入模块名:</div>
<input id="input-area" class="input-area" type="text">
</div>
<button class="create-btn" onclick="createProcess()">创建</button>
</div>
</div>
<script>
function newBtn() {
const modal = document.getElementById("modal-con")
modal.style.display = "flex"
}
function createProcess() {
const moduleName = document.getElementById("input-area").value
const createReq = new XMLHttpRequest()
const url = `{{ url_for('process.create_process', name=0) }}`.replace('0', moduleName);
createReq.open("post", url, true)
createReq.send()
createReq.onload = () => {
if (createReq.status == 200 && createReq.readyState == 4) {
const res = JSON.parse(createReq.responseText)
if (res.status == "success") {
flushBtn()
return
}
alert(`意外错误 (${createReq.status})`)
return
}
if (createReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${createReq.status})`)
return
}
}
}
function flushBtn() {
window.location = "{{ url_for('process.process') }}"
}
function stopBtn(pid) {
if (!confirm("确认停止该服务?")) {
return
}
const url = `{{ url_for('process.kill_process', pid=0) }}`.replace('0', pid);
const stopReq = new XMLHttpRequest();
stopReq.open("POST", url, true);
stopReq.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
stopReq.send();
stopReq.onload = () => {
if (stopReq.status == 200 && stopReq.readyState == 4) {
flushBtn()
return
}
if (stopReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${stopReq.status})`)
return
}
}
}
function closeModal() {
const modal = document.getElementById("modal-con")
const input = document.getElementById("input-area")
input.value = ""
modal.style.display = "none"
}
</script>
</body>
</html>
+162
View File
@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="{{ url_for('static', filename='features/user.css') }}">
</head>
<body>
<div class="top-bar-con">
<button class="addUserBtn" onclick="addUser()">添加用户</button>
<button class="flushBtn" onclick="flush()">刷新</button>
</div>
<table border="1">
<thead>
<td>用户ID</td>
<td>用户名</td>
<td>用户角色</td>
<td>操作</td>
</thead>
<tbody>
{% for item in response %}
<tr>
<td>{{ item.user_id}}</td>
<td>{{ item.username }}</td>
<td>{{ item.role }}</td>
<td>
{% if current_role == "admin" %}
<button class="changePasswdBtn" onclick="changePasswd('{{ item.user_id }}')">修改密码</button>
{% if item.username != "admin" %}
<button class="deleteUserBtn" onclick="deleteUser('{{ item.user_id }}')">删除用户</button>
{% endif %}
{% endif %}
{% if current_role != "admin" and current_user_id == item.user_id %}
<button class="changePasswdBtn" onclick="changePasswd('{{ item.user_id }}')">修改密码</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
function addUser() {
const url = "{{ url_for('user.add_user') }}"
const addReq = new XMLHttpRequest()
let data = new URLSearchParams()
// get user name
const name = prompt("请输入用户名")
if (name === null) return
if (name === "") {
alert("请输入用户名")
return
} else {
data.append("username", name)
}
// get user password
const passwd = prompt("请输入密码")
if (passwd === null) return
if (passwd === "") {
alert("请输入密码")
return
} else {
data.append("password", passwd)
}
addReq.open("post", url, true)
addReq.send(data)
addReq.onload = () => {
if (addReq.status == 200 && addReq.readyState == 4) {
alert(JSON.parse(addReq.responseText).message)
flush()
return
}
if (addReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${addReq.status})`)
return
}
}
}
function changePasswd(id) {
const url = "{{ url_for('user.change_password') }}"
const changeReq = new XMLHttpRequest()
let data = new URLSearchParams()
data.append("user_id", id)
// get user password
const passwd = prompt("请输入新密码")
if (passwd === null) return
if (passwd === "") {
alert("请输入密码")
return
} else {
data.append("new_password", passwd)
}
changeReq.open("post", url, true)
changeReq.send(data)
changeReq.onload = () => {
if (changeReq.status == 200 && changeReq.readyState == 4) {
alert(JSON.parse(changeReq.responseText).message)
flush()
return
}
if (changeReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${changeReq.status})`)
return
}
}
}
function deleteUser(id) {
if (!confirm("删除用户后不可恢复,确认继续?")) {
return
}
const url = "{{ url_for('user.delete_user', user_id=0) }}".replace("0", id)
const delReq = new XMLHttpRequest()
delReq.open("post", url, true)
delReq.send()
delReq.onload = () => {
if (delReq.status == 200 && delReq.readyState == 4) {
alert(JSON.parse(delReq.responseText).message)
flush()
return
}
if (delReq.status == 500) {
alert("服务端错误")
return
} else {
alert(`意外错误 (${delReq.status})`)
return
}
}
}
function flush() {
window.location = "{{ url_for('user.user') }}"
}
</script>
</body>
</html>
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pmc management</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
</head>
<body>
<div class="main">
<div class="title">Welcome to pmc management system</div>
<div class="name-con">
<div class="user-name-text">Name: </div>
<input class="user-name-input" id="user-name" type="text">
</div>
<div class="passwd-con">
<div class="user-passwd-text">Password: </div>
<input class="user-passwd-input" id="user-passwd" type="password">
</div>
<div class="login-con">
<button onclick="login()">login</button>
</div>
</div>
<script>
function login() {
const name = document.getElementById("user-name").value
const passwd = document.getElementById("user-passwd").value
const request = new XMLHttpRequest()
const REQUEST_URL = "/api/login"
// if user not input any information
if (name === "" || passwd === "") {
alert("please input name or passwd")
return
}
// request api
let requestData = new URLSearchParams()
requestData.append("user-name", name)
requestData.append("user-passwd", passwd)
request.open("post", REQUEST_URL, true)
request.send(requestData)
request.onload = () => {
if (request.status === 401) {
alert(request.responseText)
return
}
if (request.status === 404) {
alert(request.responseText)
return
}
if (request.status === 500) {
alert("服务端错误")
return
}
if (request.status === 200 && request.readyState === 4) {
const token = JSON.parse(request.responseText).token
let date = new Date()
date.setTime(date.getTime() + (3 * 24 * 60 * 60 * 1000))
const UTCTime = date.toUTCString()
document.cookie = `token=${token}; path=/; expires=${UTCTime}`
window.location = JSON.parse(request.responseText).redirect;
return
} else {
alert(`意外错误 (${request.status})`)
return
}
}
}
document.addEventListener("keydown", (e) => {
if (e.key != "Enter") {
return
}
login()
})
</script>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>management page</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="{{ url_for('static', filename='mgmt.css') }}">
<script>
window.onload = () => {
const featureList = [
{
name: "进程",
url: "{{ url_for('process.process') }}"
},
{
name: "模块",
url: "{{ url_for('module.module') }}"
},
// {
// name: "日志",
// url: "{{ url_for('log.log') }}"
// },
{
name: "用户",
url: "{{ url_for('user.user') }}"
}
]
const featureListEle = document.getElementById("feature-list")
for (let i = 0; i < featureList.length; i++) {
// get name and url
const name = featureList[i].name
const url = featureList[i].url
// Create a item
let listItemEle = document.createElement("div")
listItemEle.setAttribute("class", "item")
listItemEle.innerText = name
listItemEle.setAttribute("onclick", `switchPage('${url}')`)
// append element into feature list
featureListEle.append(listItemEle)
}
// set default page in iframe
const DEFAULT_PAGE = "<!DOCTYPE html><h1>请在左侧选择功能</h1>"
document.getElementById("page").contentDocument.write(DEFAULT_PAGE)
}
</script>
</head>
<body>
<div class="top-con">
<div id="current-user">当前用户: {{ username }}</div>
<div id="current-role">当前角色: {{ role }}</div>
<div class="button-con">
<button onclick="logout()">退出登陆</button>
</div>
</div>
<div class="main">
<div class="feature-list" id="feature-list"></div>
<div class="pages">
<iframe id="page" src="" frameborder="1" width="100%" height="100%"></iframe>
</div>
</div>
<script>
// change the iframe's url
function switchPage(url) {
const page = document.getElementById("page")
page.setAttribute("src", url)
}
function logout() {
if (!confirm("确认退出?")) {
return
}
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
window.location = "/"
}
</script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
.git/
.vscode/
.idea/
.cache/
#
init.csv
# cangjie
build/
target/
output/
module-resolve.json
module-lock.json
cjpm.lock
.cjpm-history
*.macrocall
*.gcda
*.gcno
# 其他
log
report
tmp
# ide
.arts
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
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.
+24
View File
@@ -0,0 +1,24 @@
## 为什么要做一个这样的项目
当时想做一个树莓派机器人,是准备用python来写控制程序,可是当时python的多线程能力较弱,
所以想到使用子系统,可是我没有VPN,无法安装ROS,所以就自己写了一个进程托管程序
## 使用方式
### 启动列表
```bash
# 创建启动列表,每一行是一个进程
touch init.csv
```
### 创建进程
```bash
curl -X POST http://localhost:5200/api/v2/create -d '["ls"]
```
### 进程列表
```bash
curl http://localhost:5200/api/v2/list
```
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
#source ./.env/bin/activate
source /cangjie/envsetup.sh
exec cjpm run
+24
View File
@@ -0,0 +1,24 @@
[package]
cjc-version = "1.0.0"
name = "phs"
description = "nothing here"
version = "1.0.0"
target-dir = ""
src-dir = ""
output-type = "executable"
compile-option = ""
override-compile-option = ""
link-option = ""
package-configuration = {}
[dependencies]
[target.x86_64-unknown-linux-gnu]
[target.x86_64-unknown-linux-gnu.bin-dependencies]
path-option = ["/stdx/linux_x86_64_llvm/static/stdx"]
[target.aarch64-unknown-linux-gnu]
[target.aarch64-unknown-linux-gnu.bin-dependencies]
path-option = ["/stdx/linux_aarch64_llvm/static/stdx"]
[target.x86_64-w64-mingw32] # 系统架构和 OS 信息
[target.x86_64-w64-mingw32.bin-dependencies]
path-option = ["C:\\stdx\\windows_x86_64_llvm\\dynamic\\stdx"] # stdx 路径根据实际情况配置
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=TxL.phs
After=network.target
Wants=network.target
[Service]
User=root
Group=root
WorkingDirectory=/root/___TxL/TestSuits/phs
ExecStart=bash /root/___TxL/TestSuits/phs/_run.sh
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=mozart-python-host
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
+312
View File
@@ -0,0 +1,312 @@
package phs
import std.sync.*;
/*
* Status 状态
*
* FIXME: 在枚举中定义操作函数
*/
enum Stat {
SHUT | STOP | START | RUNNING
}
/*
* Finite Status Machine 有限状态机
*
*
* FIXME(TritiumQin): 1. 柯里化
*
* FIXME(qing): 支持自定义状态
*/
open class Fsm {
/* 状态枚举 */
private var stat: Stat = STOP
/* 线程安全 */
private let mtx: Mutex = Mutex()
/*
* 条件变量
*
* FIXME: 我不知道这么写意义何在
*/
private let condition: Condition = synchronized(mtx) {
mtx.condition()
}
/* 辅助条件变量的布尔标志 */
private var flag: Bool = true
/* ---------------------------------------------- */
/*
* 阻塞等待状态改变,可以传入一个谓词,以确定什么时候终止
*
* 在该回调函数中返回true,则等待不会停止
*/
public func wait(keep!: (Stat) -> Bool = {_ => false}) {
synchronized(mtx) {
flag = true
while(flag || keep(stat)) {
condition.wait()
}
}
}
/* ---------------------------------------------- */
/*
* 对状态机进行某种操作,输入一个谓词。
*
* 会激活堵塞线程
*/
public func doc(p: (Stat) -> Stat): Stat {
synchronized(mtx) {
stat = p(stat)
flag= false // 试图唤醒阻塞的线程
condition.notifyAll()
return stat
}
}
/*
* 对状态机进行某种操作,输入一个谓词。
*
* 不会激活堵塞线程
*/
public func dop(p: (Stat) -> Stat): Stat {
synchronized(mtx) {
stat = p(stat)
return stat
}
}
}
/* Controllable Thread 可控制线程
*
* FIXME(花间岛):
* 1. 使用async语法糖
* 2. 封装iocp
*/
open class Th {
/* 持有线程类型 */
private var fut: Option<Future<Int64>> = None
/* 持有状态机类型 */
private let fsm: Fsm = Fsm()
/*----------------------------------------------*/
/* 持有静止事件回调 */
private var StopEvent: Option<() -> Unit> = None
/* 持有唤醒事件回调 */
private var WakeEvent: Option<() -> Unit> = None
/* 持有循环事件回调 */
private var LoopEvent: Option<() -> Unit> = None
/* 持有清理事件回调 */
private var ClearEvent: Option<() -> Unit> = None
/* 持有线程主函数回调 */
private var Main: Option<() -> Int64> = None // 线程主函数
/*----------------------------------------------*/
/* 使用谓词检查线程状态 */
public func chkp(): Stat {
return fsm.dop({ stat => return stat })
}
/*----------------------------------------------*/
/* 进入关闭状态 */
public func shut() {
fsm.doc({ _ => return SHUT })
}
/* 进入静止态 */
public func stop() {
fsm.doc({ _ => return STOP })
}
/* 进入启动状态 */
public func start() {
fsm.doc({ _ => return START })
}
/* 进入运行态 */
public func run() {
fsm.doc({ _ => return RUNNING })
}
/*----------------------------------------------*/
/* 状态机是否处于静止态 */
public func isStop() {
return match( chkp() ) {
case STOP => true
case _ => false
}}
/* 状态机是否处于关闭态 */
public func isShut() {
return match( chkp() ) {
case SHUT => true
case _ => false
}}
/* 状态机是否处于开始状态 */
public func isStart() {
return match( chkp() ) {
case START => true
case _ => false
}}
/* 状态机是否处于运行态 */
public func isRunning() {
return match( chkp() ) {
case RUNNING => true
case _ => false
}}
/*----------------------------------------------*/
/* 扮演函数,即设置线程事件 */
public func Act(
/* 静止事件 */
stop!: Option<() -> Unit> = None,
/* 唤醒事件 */
wake!: Option<() -> Unit> = None,
/* 循环事件 */
loop!: Option<() -> Unit> = None,
/* 清理事件 */
clear!:Option<() -> Unit> = None,
/* 线程主函数 */
mainloop!: Option<() -> Int64>= None
)
{
/* 只有静止态才能进行设置 */
if (!isStop()) { return; }
/* ------------------------------------------------- */
/* 设置静止事件,确保退出了静止态 */
StopEvent = stop ?? {=> fsm.wait(keep: {stat =>
match(stat) {
case STOP => true /* 保持等待 */
case _ => false
}
})}
/* 唤醒事件,默认直接进入运行态 */
WakeEvent = wake ?? {=> this.run()}
/* 循环事件,默认进行延时 */
LoopEvent = loop ?? {=> sleep(100 * Duration.millisecond)}
/* 清理事件,默认什么都不做 */
ClearEvent= clear?? {=> ;}
/* ------------------------------------------------- */
/* 线程主函数 */
Main = mainloop ?? {=>
/* 回字形循环 外层 */
while (!isShut()) {
this.inner()
}
return 0
}
/* ------------------------------------------------- */
}
/* ------------- 回字形循环 内层 -------------- */
private func inner() {
/* 静止状态 */
while(isStop()) { StopEvent.getOrThrow()() }
/* 唤醒事件 */
if (isStart()) { WakeEvent.getOrThrow()() }
/* 运行状态 */
while(isRunning()) { LoopEvent.getOrThrow()() }
/* 清理事件 */
ClearEvent.getOrThrow()()
}
/* --------------------------------------------- */
/*
* 创建资源,可控线程苏醒
*
* 如果在调用中等待线程启动,可能会造成死锁
* 想了想觉得也没有这个必要
*/
public func wake() {
/* 存在微小的冲突可能,所以我不让它进入静止态 */
this.start()
/* 创建卵 */
fut = Some<Future<Int64>>(spawn {
Main.getOrThrow()()
})
/* 等启动
*
* 20251020(qing): 如果线程启动太快,该等待可能会导致死锁
*/
//fsm.wait(keep: {stat => match(stat) {
// case START => true
// case _ => false
//}})
}
/* 等待线程结束 */
public func wait() {
let f = fut.getOrThrow()
return f.get()
}
/*
* 关闭线程,然后等待线程结束
*/
public func shut_and_wait() {
shut()
wait()
}
}
+211
View File
@@ -0,0 +1,211 @@
package phs
import std.io.*;
import std.fs.*;
import std.collection.*;
import std.sync.*;
import std.process.*;
import stdx.net.http.*;
import stdx.encoding.json.*;
const ADDR = "127.0.0.1" /* 监听IP地址 */
const PORT = 5200u16 /* 端口 UInt16类型 */
class Ps {
private let ps = LinkedList<SubProcess>()
private let mtx = Mutex()
/* 创建进程 */
public func create(arr: Array<String>) {
let path = arr[0]
let args = arr[1..]
let subp: SubProcess = launch(path, args,
//stdIn: ProcessRedirect.Discard, /* WINDOWS中不支持输入重定向 */
//stdOut: ProcessRedirect.Discard, /* 加了这行之后就在控制台看不到输出了 */
//stdErr: ProcessRedirect.Discard
)
this.clear() /* 清理进程 */
synchronized(mtx) {
ps.addLast(subp)
return;
}
}
/* 返回进程列表 */
public func list() {
var msg = ""
println('1')
this.clear() /* 清理进程,有锁 */
println('2')
synchronized(mtx) {
println('3')
for (p in ps) {
if (msg != "") { msg += "\n"; }
msg += '${p.pid.toString()}\t'
msg += '${p.command} '
//for (i in p.arguments) { /* 加上这个会失败 */
// msg += '${i} '
//}
/* 启动时间 */
msg += '\t${p.userTime.toString()}'
}
println('4')
}
return msg;
}
/* 杀死已经存在的进程 */
public func kill(pid: Int64) {
this.clear() /* 清理 */
synchronized(mtx) {
for (p in ps) {
if (p.pid == pid) {
p.terminate(force: true)
return
}
}
}
}
/*
* 进程自启动
*
* FIXME: 支持注释
*/
public func autoRun(csvpath: String) {
let file = File(csvpath, Read)
let bytes = readToEnd(file)
let ctt = String.fromUtf8(bytes)
let cmds = ctt.split("\n", removeEmpty: true)
for (cmd in cmds) {
try{
let arr = cmd.split(" ", removeEmpty: true)
this.create(arr)
} catch (exp: Exception) {
;
}
}
file.close()
}
/* 清理已经死去的进程,默认不加锁 */
private func clear() {
synchronized(mtx) {
let len = ps.size
for (i in (len-1)..=0 : -1) {
try {
let p = ps.nodeAt(i).getOrThrow()
println("start...")
p.value.wait(timeout: 1 * Duration.millisecond) /* 进程有没有退出 */
println("end...")
ps.remove(p)
}
catch (exp: TimeoutException) { /* 如果进程没有结束,会运行到这里 */
;
println("skip...")
}
}
}
}
}
/*
* 将JSON数组转为字符串的数组
*
* Json对象的每个元素只能是字符串,否则会报错
*/
func Jarr_to_arr(arr: JsonArray): Array<String> {
let len = arr.size() // Json数组的长度
let ret = Array<String>(len, repeat: "")
for (i in 0..len) {
let option: Option<JsonValue> = arr.get(i)
let jitem: JsonValue = option.getOrThrow()
let jstr: JsonString = jitem.asString()
let str: String = jstr.getValue()
ret[i] = str
}
return ret
}
/* cj命令行参数不包含可执行文件名 */
main(_: Array<String>): Int64 {
/* 进程组 */
let ps = Ps()
ps.autoRun("./init.csv")
/* 创建http服务器 */
let server: Server = ServerBuilder()
.addr(ADDR) /* 设置地址 */
.port(PORT) /* 设置端口 */
.build()
/* 创建进程的HTTP方法 */
server.distributor.register("/api/v2/create", { context =>
if (context.request.method == "POST") {
let content = StringReader(context.request.body).readToEnd() /* 阅读请求体 */
let array = JsonValue.fromStr(content).asArray() /* 解析json格式的请求体 */
let net_args: Array<String> = Jarr_to_arr(array) /* json数组转原生数组 */
ps.create(net_args)
/* ---- 准备响应 ---- */
let Headers = HttpHeaders()
Headers.add("Content-Type", "text/html; charset=utf-8;")
context.responseBuilder.setHeaders(Headers)
context.responseBuilder.body('OK')
}
})
/*
* 输出进程列表的HTTP方法
*/
server.distributor.register("/api/v2/list", { context =>
if (context.request.method == "GET") {
println('a')
/* 准备表单字符串 */
let ret = ps.list()
println('b')
/**** 准备响应 ****/
let Headers = HttpHeaders()
Headers.add("Content-Type", "text/html; charset=utf-8;")
context.responseBuilder.setHeaders(Headers)
context.responseBuilder.body(ret)
println('c')
}
})
/* 杀死一个已经存在的进程 */
server.distributor.register("/api/v2/kill", { context =>
if (context.request.method == "POST") {
let content = StringReader(context.request.body).readToEnd() /* 阅读请求体 */
let target_pid = JsonValue.fromStr(content)
.asObject()
.get("pid")
.getOrThrow()
.asInt()
.getValue()
ps.kill(target_pid)
/**** 准备响应 ****/
let Headers = HttpHeaders()
Headers.add("Content-Type", "text/html; charset=utf-8;")
context.responseBuilder.setHeaders(Headers)
context.responseBuilder.body("done")
}
})
/* 服务器开始运行 */
server.serve()
return 0
}
+66
View File
@@ -0,0 +1,66 @@
import requests
import json
def create_process(cmd_list, url="http://localhost:5200/api/v2/create"):
"""创建任意进程
Args:
cmd_list (list): 命令和参数列表,例如 ["timeout", "/t", "1000"]
url (str, optional): 请求URL,默认为 "http://localhost:5200/api/v2/create"
Returns:
requests.Response: 响应对象
"""
try:
# 发送POST请求
response = requests.post(url, data=json.dumps(cmd_list), timeout=30)
# 返回响应对象供进一步处理
return response
except requests.exceptions.RequestException as e:
print(f"创建进程失败: {e}")
return None
def create_process_with_output(cmd_list, url="http://localhost:5200/api/v2/create"):
"""创建任意进程并打印输出
Args:
cmd_list (list): 命令和参数列表,例如 ["timeout", "/t", "1000"]
url (str, optional): 请求URL,默认为 "http://localhost:5200/api/v2/create"
Returns:
dict or str: 响应内容,JSON解析后的字典或原始文本
"""
response = create_process(cmd_list, url)
if response is None:
return None
# 打印响应状态码
print(f"响应状态码: {response.status_code}")
# 尝试解析响应内容为JSON格式并打印
try:
response_json = response.json()
print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
return response_json
except json.JSONDecodeError:
print(f"响应内容(非JSON格式): {response.text}")
return response.text
if __name__ == "__main__":
# 示例用法
# 创建一个timeout进程,延迟1000毫秒
cmd = ["timeout", "/t", "1000"]
# 使用默认URL
print("使用默认URL:")
create_process_with_output(cmd)
# 示例:使用自定义URL(注释掉,可根据需要取消注释)
# print("\n使用自定义URL:")
# custom_url = "http://localhost:5200/api/v2/create"
# create_process_with_output(cmd, custom_url)
@@ -0,0 +1,30 @@
import requests
import json
def test_create_process():
"""测试访问phs web服务的create接口"""
# 设置请求URL
url = "http://localhost:5200/api/v2/create"
# 设置请求数据
data = ["timeout", "/t", "1000"]
# 发送POST请求
response = requests.post(url, data=json.dumps(data))
# 打印响应状态码
print(f"响应状态码: {response.status_code}")
# 尝试解析响应内容为JSON格式并打印
try:
response_json = response.json()
print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
except json.JSONDecodeError:
print(f"响应内容(非JSON格式): {response.text}")
# 返回响应对象供进一步处理
return response
if __name__ == "__main__":
# 执行测试函数
test_create_process()
@@ -0,0 +1,44 @@
import requests
import json
import argparse
def test_kill_process(pid):
"""测试访问phs web服务的kill接口"""
# 设置请求URL
url = "http://localhost:5200/api/v2/kill"
# 构造请求消息体
payload = {
"pid": pid
}
# 发送POST请求
response = requests.post(url, json=payload)
# 打印响应状态码
print(f"响应状态码: {response.status_code}")
# 尝试解析响应内容为JSON格式并打印
try:
response_json = response.json()
print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
except json.JSONDecodeError:
print(f"响应内容(非JSON格式): {response.text}")
# 返回响应对象供进一步处理
return response
if __name__ == "__main__":
# 创建命令行参数解析器
parser = argparse.ArgumentParser(description="测试访问phs web服务的kill接口")
# 添加pid参数
parser.add_argument("pid", type=int, help="要杀死的进程ID")
# 解析命令行参数
args = parser.parse_args()
# 执行测试函数,传入命令行参数中的pid
test_kill_process(args.pid)
@@ -0,0 +1,27 @@
import requests
import json
def test_create_process():
"""测试访问phs web服务的create接口"""
# 设置请求URL
url = "http://localhost:5200/api/v2/list"
# 发送POST请求
response = requests.get(url)
# 打印响应状态码
print(f"响应状态码: {response.status_code}")
# 尝试解析响应内容为JSON格式并打印
try:
response_json = response.json()
print(f"响应内容: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
except json.JSONDecodeError:
print(f"响应内容(非JSON格式): {response.text}")
# 返回响应对象供进一步处理
return response
if __name__ == "__main__":
# 执行测试函数
test_create_process()
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 gopsc
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.
+91
View File
@@ -0,0 +1,91 @@
## qemu_run_appimage是什么?
这个项目源于我想用qemu-user运行AppImage的想法。
qemu-user是一款支持多种架构的能够虚拟化运行单个程序的虚拟机软件,而AppImage是一款能够打包出单文件应用的工具。
## 跨架构运行的障碍是什么?
Linux系统支持许多架构,这些架构的机器码、汇编语言都是有差异的,所以不同架构的系统在一般情况下是无法混合使用的,qemu虚拟机能够解决这个痛点,但是这需要一些技巧。
通常而言,一个程序除了在编译阶段静态链接了许多库,出于一些原因的考量,它也会在运行时动态地加载一些动态库文件。
使用qemu-user跨架构运行,常常就会出现单个文件能够执行,但是动态链接库找不到的问题。
## AppImage起到一个什么作用?
AppImage能够将许多文件打包成一个二进制程序,你双击运行它就能够直接执行,因为关键的动态连接库都包含在里面了,通过一些技术手段让程序能够访问到。
所以我们能够将程序以及动态库打包成单个可执行程序,就有了能够虚拟化运行该程序的先决条件。
## 如何使用qemu-user运行AppImage
通常而言,AppImage里面会有一个叫AppRun的启动脚本,运行AppImage会使用shell解释器启动它,然后该脚本配置好环境,再由它启动目标程序。
但是qemu-user虚拟出的进程往往无法在宿主机系统中找到与它架构相对应的shell,所以就无法执行常规方式打包的AppImage文件。
那我通过与deepseek的深度交流,最终解决了这个问题。
我的思路有两个,一是打包AppImage时将AppRun编译成二进制文件,这个办法我经过了验证,是可行的。
还有一个办法是再打包一个shell解释器进去,这个我还没有尝试、
在运行时,我采用qemu-user提取出AppImage中的所有的文件,并且放在一个临时文件夹,然后在设置引用环境为该文件夹的方式下通过qemu-user执行其中的AppRun,应该就能够启动程序。
目前已经尝试了在树莓派上运行x86_64架构的一系列工具,包含ls命令、bash解释器、python3解释器,都能够成功,接下来我还会对网络等功能进行测试。
## 这个方法有哪些缺点?
我像一些朋友展示了这个方式,能够运行测试用例,但是他们说qemu的速度很慢,我了解了一下,qemu-user的性能损耗可能在2倍以内,一般的程序似乎还是可以接受,qemu虚拟机完整情况下的确较慢,性能损耗能够达到8~10倍。
所以一些开发者对常见架构进行了深度优化,成果比如box64(Arm64平台运行X86程序)、LATX(LA64平台运行x86程序),这些程序虽然支持的架构没有qemu-user多,在一些平台上可能也需要单独配置一些东西,但是在它们支持的架构下拥有更好的性能,在这里也推荐大家使用。
## 我的一些别的想法
既然qemu-user能够进行跨架构的实时翻译,那我们可能也能做到将一个二进制文件直接再编译成其他架构的二进制文件?这是我的一个想法。
## 项目逻辑与使用方法
项目中的auto-appimage1.sh这是一个打包脚本,能够在要运行的目标架构上打包出包含所有链接库的AppImage文件,且其中的入口程序AppRun是二进制的。
该脚本的使用方式如下:
```bash auto-appimage1.sh <filename>```
目前这个脚本仅能打包x86的程序,如果要在别的平台打包AppImage,可能需要进行调整。
qemu_run2.sh是在宿主机上的启动脚本,其中使用了qemu-user-static系列工具创建虚拟环境,static后缀的意思是这个工具是静态编译的,如果你的架构上没有这个软件,请安装它的非静态版本qemu-user,然后将其中的qemu-*-static都改为qemu-*
通过一下方式使用它:
```bash qemu_run2.sh <filename.AppImage>```
ls.AppImage是x86_64架构下的ls命令打包,是测试用例。
该项目的依赖:
---------------
打包机器依赖项:
appimagetool需要在github下载制品,并且添加可执行权限放到环境变量里。
```apt install fuse libfuse2```
---------------
宿主机依赖:
```apt install qemu-user-static```
或者
```apt install aemu-user```
————————————————
版权声明:本文为CSDN博主「gapcuda」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gapcuda/article/details/156559857
(这是我自己的csdn账号)
@@ -0,0 +1,686 @@
#!/bin/bash
# appimage-packager-rofs-fixed.sh
# 已添加QEMU兼容性支持
set -e
# 彩色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 创建图标
create_icon() {
local icon_path="$1"
mkdir -p "$(dirname "$icon_path")"
echo 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' | \
base64 -d > "$icon_path" 2>/dev/null || \
echo -ne '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x00\x01\x00\x01\x05\x01\x00\x00\x00\x00IEND\xaeB`\x82' > "$icon_path"
}
# 创建简单二进制AppRun(修复版)
create_simple_binary_apprun() {
local app_dir="$1"
local app_name="$2"
local orig_name="$3"
echo "创建简单二进制AppRun..."
# 先回退到脚本版本
cat > "$app_dir/AppRun" << 'EOF'
#!/bin/sh
# 简单启动脚本(兼容性最好)
HERE="$(dirname "$(readlink -f "$0")")"
APP_NAME="$(basename "$0" .AppImage)"
# 设置环境
export PATH="$HERE/usr/bin:$PATH"
export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
# 检测架构
if [ "$(uname -m)" = "x86_64" ]; then
# x86系统直接运行
exec "$HERE/usr/bin/$APP_NAME" "$@"
else
# ARM系统,通过QEMU运行
# 确保有lib64目录
if [ ! -d "$HERE/lib64" ] && [ -f "$HERE/usr/lib/ld-linux-x86-64.so.2" ]; then
mkdir -p "$HERE/lib64"
cp "$HERE/usr/lib/ld-linux-x86-64.so.2" "$HERE/lib64/" 2>/dev/null || true
fi
# 使用QEMU运行
exec qemu-x86_64-static -L "$HERE" "$HERE/usr/bin/$APP_NAME" "$@"
fi
EOF
chmod +x "$app_dir/AppRun"
echo -e "${GREEN}✓ 创建脚本版AppRun${NC}"
}
# 创建二进制AppRun函数(修复版)
create_binary_apprun() {
local app_dir="$1"
local app_name="$2"
local orig_name="$3"
echo -e "${YELLOW}创建二进制AppRun...${NC}"
# 创建修复的C源代码
cat > /tmp/apprun_fixed.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
// 获取程序所在目录
static char* get_program_dir() {
static char path[4096];
ssize_t len = readlink("/proc/self/exe", path, sizeof(path)-1);
if (len == -1) {
return NULL;
}
path[len] = '\0';
// 找到最后一个'/'
char* last_slash = strrchr(path, '/');
if (last_slash) {
*last_slash = '\0';
}
return path;
}
// 检查文件是否存在
static int file_exists(const char* path) {
struct stat st;
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}
// 执行程序
static void execute_program(const char* dir, const char* prog_name, char* const argv[]) {
char prog_path[4096];
snprintf(prog_path, sizeof(prog_path), "%s/usr/bin/%s", dir, prog_name);
if (!file_exists(prog_path)) {
// 尝试查找任何可执行文件
fprintf(stderr, "错误: 未找到可执行文件 %s\n", prog_path);
exit(1);
}
// 设置环境变量
setenv("HERE", dir, 1);
char ld_path[4096];
snprintf(ld_path, sizeof(ld_path), "%s/usr/lib", dir);
setenv("LD_LIBRARY_PATH", ld_path, 1);
char path[4096];
const char* old_path = getenv("PATH");
if (old_path) {
snprintf(path, sizeof(path), "%s/usr/bin:%s", dir, old_path);
} else {
snprintf(path, sizeof(path), "%s/usr/bin", dir);
}
setenv("PATH", path, 1);
// 执行程序
execv(prog_path, argv);
// 如果execv失败
fprintf(stderr, "无法执行程序: %s\n", prog_path);
exit(1);
}
int main(int argc, char* argv[]) {
// 获取程序所在目录
char* dir = get_program_dir();
if (!dir) {
fprintf(stderr, "无法获取程序目录\n");
return 1;
}
// 从AppImage文件名获取程序名
char* prog_name = "program";
// 尝试从argv[0]获取
if (argc > 0 && argv[0]) {
char* app_name = argv[0];
char* last_slash = strrchr(app_name, '/');
if (last_slash) {
app_name = last_slash + 1;
}
// 移除.AppImage后缀
char* dot_appimage = strstr(app_name, ".AppImage");
if (dot_appimage) {
*dot_appimage = '\0';
prog_name = app_name;
}
}
// 准备新参数数组
char** new_argv = malloc((argc + 1) * sizeof(char*));
if (!new_argv) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
new_argv[0] = prog_name;
for (int i = 1; i < argc; i++) {
new_argv[i] = argv[i];
}
new_argv[argc] = NULL;
// 执行程序
execute_program(dir, prog_name, new_argv);
free(new_argv);
return 0;
}
EOF
# 尝试编译为x86_64二进制(静态链接)
echo "编译x86_64二进制..."
# 检查是否有交叉编译器
if command -v x86_64-linux-gnu-gcc >/dev/null 2>&1; then
echo "使用x86_64-linux-gnu-gcc编译..."
x86_64-linux-gnu-gcc -static -Os -o "$app_dir/AppRun" /tmp/apprun_fixed.c 2>&1 | grep -v "warning" || true
elif command -v gcc >/dev/null 2>&1; then
echo "使用gcc交叉编译..."
gcc -target x86_64-linux-gnu -static -Os -o "$app_dir/AppRun" /tmp/apprun_fixed.c 2>&1 | grep -v "warning" || true
else
echo "未找到编译器"
fi
# 检查编译是否成功
if [[ -f "$app_dir/AppRun" ]] && [[ -x "$app_dir/AppRun" ]]; then
# 验证是x86_64二进制
if file "$app_dir/AppRun" | grep -q "x86-64"; then
chmod +x "$app_dir/AppRun"
echo -e "${GREEN}✓ 二进制AppRun创建成功${NC}"
file "$app_dir/AppRun"
return 0
else
echo "编译出的不是x86-64二进制"
rm -f "$app_dir/AppRun"
fi
fi
# 编译失败,使用简单脚本
echo "二进制编译失败,使用脚本版本"
create_simple_binary_apprun "$app_dir" "$app_name" "$orig_name"
rm -f /tmp/apprun_fixed.c
}
# 创建简单二进制AppRun
create_simple_binary_apprun() {
local app_dir="$1"
local app_name="$2"
local orig_name="$3"
# 创建极简的汇编二进制
cat > /tmp/minimal.S << 'EOF'
.section .note.GNU-stack,"",@progbits
.section .text
.globl _start
_start:
# execve("./usr/bin/program", argv, envp)
mov $59, %rax # syscall: execve
# 构建路径字符串
lea path(%rip), %rdi # arg1: filename
# 构建参数数组 ["./usr/bin/program", NULL]
lea argv(%rip), %rsi # arg2: argv
# 环境变量
xor %rdx, %rdx # arg3: envp = NULL
syscall
# 如果失败,退出
mov $60, %rax # syscall: exit
mov $1, %rdi # status = 1
syscall
path:
.ascii "./usr/bin/program\0"
argv:
.quad path
.quad 0
EOF
# 汇编并链接
as --64 -o /tmp/minimal.o /tmp/minimal.S
ld -m elf_x86_64 -s -o "$app_dir/AppRun" /tmp/minimal.o
if [[ -f "$app_dir/AppRun" ]]; then
chmod +x "$app_dir/AppRun"
echo -e "${GREEN}✓ 极简二进制AppRun已创建${NC}"
else
echo -e "${RED}错误: 无法创建二进制AppRun${NC}"
# 回退到脚本
create_script_apprun "$app_dir" "$app_name" "$orig_name"
fi
rm -f /tmp/minimal.S /tmp/minimal.o
}
# 使用busybox作为AppRun
use_busybox_as_apprun() {
local app_dir="$1"
echo "使用busybox作为AppRun..."
# 查找或下载x86_64的busybox
if [[ -f /usr/bin/busybox ]] && file /usr/bin/busybox | grep -q "x86-64"; then
cp /usr/bin/busybox "$app_dir/AppRun"
elif [[ -f /bin/busybox ]] && file /bin/busybox | grep -q "x86-64"; then
cp /bin/busybox "$app_dir/AppRun"
else
# 尝试下载静态版busybox
echo "下载静态busybox..."
wget -q -O "$app_dir/AppRun" "https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox" 2>/dev/null || {
echo "下载失败,使用备用方案"
return 1
}
fi
if [[ -f "$app_dir/AppRun" ]]; then
chmod +x "$app_dir/AppRun"
# 创建busybox的symlink脚本
cat > "$app_dir/.busybox-run" << 'EOF'
#!/bin/sh
# busybox wrapper
HERE=$(dirname "$0")
export PATH="$HERE/usr/bin:$PATH"
export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
exec "$HERE/AppRun" "$HERE/usr/bin/$(basename "$0" .AppImage)" "$@"
EOF
chmod +x "$app_dir/.busybox-run"
echo -e "${GREEN}✓ 使用busybox作为AppRun${NC}"
return 0
fi
return 1
}
# 收集所有动态库依赖
collect_all_dependencies() {
local executable="$1"
local lib_dir="$2"
echo -e "${YELLOW}收集动态库依赖...${NC}"
if ! ldd "$executable" 2>/dev/null | grep -q "=>"; then
echo "程序是静态链接,无需库文件"
return 0
fi
mkdir -p "$lib_dir"
local processed_libs=()
collect_libs_recursive() {
local target="$1"
ldd "$target" 2>/dev/null | grep "=>" | awk '{print $3}' | while read -r lib; do
if [[ -f "$lib" ]]; then
local libname=$(basename "$lib")
if [[ ! " ${processed_libs[@]} " =~ " ${libname} " ]]; then
processed_libs+=("$libname")
cp "$lib" "$lib_dir/" 2>/dev/null
if [[ $? -eq 0 ]]; then
echo " ✅ $libname"
collect_libs_recursive "$lib"
fi
fi
fi
done
}
collect_libs_recursive "$executable"
# 添加基础库
local common_libs=(
"ld-linux-x86-64.so.2"
"libc.so.6" "libm.so.6" "libpthread.so.0"
"libdl.so.2" "librt.so.1" "libgcc_s.so.1"
"libstdc++.so.6"
)
for lib in "${common_libs[@]}"; do
find /usr/lib /lib /lib64 -name "$lib" -type f 2>/dev/null | head -1 | while read -r libpath; do
if [[ -f "$libpath" ]] && [[ ! -f "$lib_dir/$(basename "$libpath")" ]]; then
cp "$libpath" "$lib_dir/" 2>/dev/null && echo " ✅ 基础: $(basename "$libpath")"
fi
done
done
local lib_count=$(ls -1 "$lib_dir"/*.so* 2>/dev/null | wc -l)
echo -e "${GREEN}✓ 添加了 $lib_count 个库${NC}"
}
# 设置QEMU目录结构
setup_qemu_structure() {
local app_dir="$1"
echo -e "${YELLOW}设置QEMU目录结构...${NC}"
# 创建QEMU需要的目录结构
mkdir -p "$app_dir/lib64"
mkdir -p "$app_dir/usr/gnemul/qemu-x86_64"
# 复制库到QEMU期望的位置
if [[ -d "$app_dir/usr/lib" ]]; then
echo "复制库文件到QEMU目录..."
# 复制所有库
cp -r "$app_dir/usr/lib/"* "$app_dir/usr/gnemul/qemu-x86_64/" 2>/dev/null || true
# 特别处理ld-linux动态链接器
if [[ -f "$app_dir/usr/lib/ld-linux-x86-64.so.2" ]]; then
echo "设置动态链接器..."
# 在lib64创建链接(QEMU查找的位置)
ln -sf ../usr/lib/ld-linux-x86-64.so.2 "$app_dir/lib64/ld-linux-x86-64.so.2" 2>/dev/null || true
# 同时也复制一份到lib64确保可用
cp "$app_dir/usr/lib/ld-linux-x86-64.so.2" "$app_dir/lib64/" 2>/dev/null || true
fi
echo -e "${GREEN}✓ QEMU目录结构已创建${NC}"
else
echo -e "${YELLOW}⚠ 未找到库目录,跳过QEMU结构设置${NC}"
fi
}
# 查找并复制数据文件
find_and_copy_data() {
local executable="$1"
local app_name="$2"
local data_dir="$3"
echo -e "${YELLOW}查找数据文件...${NC}"
local prog_dir=$(dirname "$(realpath "$executable")")
local patterns=("*.pem" "*.key" "*.crt" "*.cfg" "*.conf" "*.ini" "*.json" "*.xml")
mkdir -p "$data_dir"
local count=0
# 首先查找程序目录
for pattern in "${patterns[@]}"; do
find "$prog_dir" -maxdepth 2 -type f -name "$pattern" 2>/dev/null | while read -r file; do
if [[ "$file" != "$executable" ]]; then
cp "$file" "$data_dir/" 2>/dev/null && {
echo " ✅ $(basename "$file")"
count=$((count + 1))
}
fi
done
done
# 如果没有找到,尝试复制所有非可执行文件
if [[ $count -eq 0 ]]; then
find "$prog_dir" -maxdepth 1 -type f ! -name "$(basename "$executable")" ! -name "*.AppImage" | while read -r file; do
cp "$file" "$data_dir/" 2>/dev/null && echo " ✅ $(basename "$file")"
done
fi
# 如果没有数据文件,创建示例配置
if [[ $count -eq 0 ]] && [[ ! -f "$data_dir/config.ini" ]]; then
cat > "$data_dir/config.ini" <<EOF
# 程序配置文件
# 请根据实际情况修改
[General]
name=$app_name
version=1.0.0
[Paths]
data_dir=./data
log_dir=./logs
[Network]
host=127.0.0.1
port=8080
EOF
echo " 📝 创建示例配置文件"
fi
echo -e "${GREEN}✓ 数据文件处理完成${NC}"
}
# 主程序
main() {
if [[ $# -lt 1 ]]; then
echo -e "${GREEN}AppImage 打包工具 (QEMU兼容版)${NC}"
echo "用法: $0 <可执行文件> [输出名称]"
echo " $0 -d <可执行文件> [输出名称] (包含数据文件)"
echo ""
echo "示例:"
echo " $0 ./myapp"
echo " $0 -d ./myapp"
exit 1
fi
# 检查是否包含数据文件
INCLUDE_DATA=false
if [[ "$1" == "-d" ]] || [[ "$1" == "--data" ]]; then
INCLUDE_DATA=true
shift
fi
echo -e "${BLUE}════════════════════════════════════════${NC}"
echo -e "${BLUE} AppImage 打包工具 ${NC}"
echo -e "${BLUE} (QEMU兼容版) ${NC}"
echo -e "${BLUE}════════════════════════════════════════${NC}"
EXECUTABLE="$1"
APP_NAME="${2:-$(basename "$EXECUTABLE")}"
[[ ! -f "$EXECUTABLE" ]] && { echo -e "${RED}错误: 文件不存在${NC}"; exit 1; }
[[ ! -x "$EXECUTABLE" ]] && chmod +x "$EXECUTABLE"
echo "程序: $APP_NAME"
echo "原始: $(basename "$EXECUTABLE")"
# 清理
rm -rf "AppDir" "${APP_NAME}.AppImage"
# =========== 步骤1: 创建目录 ===========
echo -e "\n${YELLOW}[1/7] 创建目录...${NC}"
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/lib
ORIG_NAME=$(basename "$EXECUTABLE")
# =========== 步骤2: 复制程序 ===========
echo -e "\n${YELLOW}[2/7] 复制程序...${NC}"
cp "$EXECUTABLE" "AppDir/usr/bin/$ORIG_NAME"
chmod +x "AppDir/usr/bin/$ORIG_NAME"
echo -e "${GREEN}✓ 程序: $ORIG_NAME${NC}"
# =========== 步骤3: 收集依赖 ===========
collect_all_dependencies "$EXECUTABLE" "AppDir/usr/lib"
# =========== 步骤4: 设置QEMU目录结构 ===========
setup_qemu_structure "AppDir"
# =========== 步骤5: 处理数据文件 ===========
if $INCLUDE_DATA; then
echo -e "\n${YELLOW}[5/7] 处理数据文件...${NC}"
find_and_copy_data "$EXECUTABLE" "$APP_NAME" "AppDir/usr/share/$APP_NAME"
fi
# =========== 步骤6: 创建图标 ===========
echo -e "\n${YELLOW}[6/7] 创建图标...${NC}"
create_icon "AppDir/$APP_NAME.png"
cp "AppDir/$APP_NAME.png" "AppDir/.DirIcon"
echo -e "${GREEN}✓ 图标已创建${NC}"
# =========== 步骤7: 创建二进制 AppRun ===========
echo -e "\n${YELLOW}[7/7] 创建二进制AppRun...${NC}"
# 首先尝试创建C语言版本
#use_busybox_as_apprun "AppDir" "$APP_NAME" "$ORIG_NAME"
create_binary_apprun "AppDir" "$APP_NAME" "$ORIG_NAME"
# 如果失败,使用备选方案
if [[ ! -f "AppDir/AppRun" ]] || [[ ! -x "AppDir/AppRun" ]]; then
echo "二进制创建失败,使用脚本版本..."
cat > "AppDir/AppRun" <<'EOF'
#!/bin/sh
# 兼容性脚本(当二进制不可用时)
HERE=$(dirname "$(readlink -f "$0")")
APP_NAME=$(basename "$0" .AppImage)
# 对于QEMU:直接运行内部程序
if [ "$(uname -m)" != "x86_64" ]; then
# ARM系统,通过QEMU运行
exec qemu-x86_64-static -L "$HERE" "$HERE/usr/bin/$APP_NAME" "$@"
else
# x86系统,直接运行
exec "$HERE/usr/bin/$APP_NAME" "$@"
fi
EOF
chmod +x AppDir/AppRun
echo -e "${YELLOW}⚠ 使用脚本版AppRun(QEMU兼容性可能有限)${NC}"
fi
# =========== 步骤8: 创建桌面文件 ===========
echo -e "\n${YELLOW}[8/7] 创建桌面文件...${NC}"
mkdir -p AppDir/usr/share/applications
cat > "AppDir/usr/share/applications/$APP_NAME.desktop" <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=$APP_NAME
Comment=Packaged as AppImage (QEMU兼容)
Exec=$ORIG_NAME
Icon=$APP_NAME
Categories=Utility;
Terminal=true
StartupNotify=false
EOF
ln -sf usr/share/applications/$APP_NAME.desktop AppDir/
echo -e "${GREEN}✓ 桌面文件已创建${NC}"
# =========== 步骤9: 打包 ===========
echo -e "\n${YELLOW}[9/7] 打包...${NC}"
OUTPUT_FILE="${APP_NAME}.AppImage"
# 架构检测
if file -b "$EXECUTABLE" | grep -q "32-bit"; then
ARCH="i386"
else
ARCH="x86_64"
fi
export ARCH
echo "输出: $OUTPUT_FILE"
echo "架构: $ARCH"
# 打包
if appimagetool --no-appstream AppDir "$OUTPUT_FILE" 2>&1 | grep -v "gpg2" | grep -v "Warning"; then
echo -e "${GREEN}✅ 打包成功!${NC}"
else
appimagetool --no-appstream --no-fuse AppDir "$OUTPUT_FILE" 2>&1 | grep -v "gpg2" | grep -v "Warning"
echo -e "${GREEN}✅ 打包成功!${NC}"
fi
# =========== 测试 ===========
echo -e "\n${BLUE}════════════════════════════════════════${NC}"
echo -e "${GREEN}测试运行...${NC}"
chmod +x "$OUTPUT_FILE"
# 创建测试数据目录
TEST_DIR="$HOME/.local/share/$APP_NAME-test"
rm -rf "$TEST_DIR"
mkdir -p "$TEST_DIR"/{config,data,logs,cache}
# 复制可能的配置文件
if [ -d "AppDir/usr/share/$APP_NAME" ]; then
cp -r AppDir/usr/share/$APP_NAME/* "$TEST_DIR/config/" 2>/dev/null || true
fi
echo "测试命令:"
echo " 原生运行: ./\"$OUTPUT_FILE\" --help"
echo " QEMU运行: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --help"
echo " 调试模式: DEBUG=1 ./\"$OUTPUT_FILE\" --help"
echo "----------------------------------------"
# 原生测试运行
export DEBUG=1
if timeout 5s ./"$OUTPUT_FILE" --help 2>&1 | head -20; then
echo -e "${GREEN}✅ 原生运行正常${NC}"
elif timeout 5s ./"$OUTPUT_FILE" -h 2>&1 | head -20; then
echo -e "${GREEN}✅ 原生运行正常${NC}"
else
echo -e "${YELLOW}⚠ 原生运行可能需要特定参数${NC}"
fi
unset DEBUG
# QEMU兼容性测试
echo -e "\n${YELLOW}QEMU兼容性测试...${NC}"
if command -v qemu-x86_64-static >/dev/null 2>&1; then
echo "测试命令: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --version"
if timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" --version 2>&1 | head -5; then
echo -e "${GREEN}✅ QEMU运行成功${NC}"
elif timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" -v 2>&1 | head -5; then
echo -e "${GREEN}✅ QEMU运行成功${NC}"
elif timeout 5s qemu-x86_64-static -L . ./"$OUTPUT_FILE" 2>&1 | head -5; then
echo -e "${GREEN}✅ QEMU运行成功${NC}"
else
echo -e "${YELLOW}⚠ QEMU测试需要特定参数${NC}"
echo "尝试: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\" --help"
fi
else
echo "qemu-x86_64-static 未安装,跳过QEMU测试"
echo "安装命令: sudo apt install qemu-user-static"
fi
# 最终信息
echo -e "\n${BLUE}════════════════════════════════════════${NC}"
echo -e "${GREEN} 完成! ${NC}"
echo -e "${BLUE}════════════════════════════════════════${NC}"
echo ""
echo "📦 输出文件: $(realpath "$OUTPUT_FILE")"
echo "📏 大小: $(du -h "$OUTPUT_FILE" | cut -f1)"
echo ""
echo "🚀 使用方法:"
echo " 在x86_64系统: ./\"$OUTPUT_FILE\""
echo " 在ARM系统: qemu-x86_64-static -L . ./\"$OUTPUT_FILE\""
echo ""
echo "📁 用户数据目录:"
echo " $HOME/.local/share/$APP_NAME/"
echo ""
echo "🔧 调试模式:"
echo " DEBUG=1 ./\"$OUTPUT_FILE\" [参数]"
echo ""
echo "🔄 QEMU运行助手脚本:"
cat > "$(dirname "$OUTPUT_FILE")/run-with-qemu.sh" <<'EOF2'
#!/bin/bash
# QEMU运行助手
APP="$1"
shift
qemu-x86_64-static -L . "$APP" "$@"
EOF2
chmod +x "$(dirname "$OUTPUT_FILE")/run-with-qemu.sh"
echo " 已创建: $(dirname "$OUTPUT_FILE")/run-with-qemu.sh"
echo " 使用: ./run-with-qemu.sh \"$OUTPUT_FILE\" [参数]"
# 清理
rm -rf AppDir
}
# 运行
main "$@"
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff. Show more