miniyun 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
miniyun-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: miniyun
3
+ Version: 0.1.0
4
+ Summary: Deploy a private MiniYun application market on your own computer
5
+ Author: MiniYun
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://mn.pckee.com/
8
+ Project-URL: Documentation, https://mn.pckee.com/docs/
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: System :: Installation/Setup
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: psutil>=5.9
14
+
15
+ # MiniYun — 在本地建立自己的应用市场
16
+
17
+ 安装后运行两条命令:
18
+
19
+ ```console
20
+ pip install miniyun
21
+ miniyun up
22
+ ```
23
+
24
+ 打开命令输出的 `http://127.0.0.1:17880/workspace`,在页面设置你自己的管理员姓名和密码。新环境不包含线上账号、API Key 或业务数据。
25
+
26
+ PyPI 安装包内附 Windows x64 与 Linux x64 的官方运行包。CLI 选择匹配版本、校验 SHA-256,再启动本机服务;不需要再从演示网站下载。`pip install` 需要联网,完成安装后可以离线初始化和启动。Python 需要 3.10 或更高版本。
27
+
28
+ 首版运行包支持 Windows x64 和 Linux x64,内置工单、请假、报销、组织通讯录四个可信示例。默认是仅监听本机的轻量开发部署。组织、权限、个人 API Key、自定义 H5、应用管理和平台文档都随底座提供。
29
+
30
+ ```console
31
+ miniyun init --dir ./my-market
32
+ miniyun up --dir ./my-market --port 17890
33
+ miniyun status --dir ./my-market
34
+ miniyun logs --dir ./my-market --lines 40
35
+ miniyun down --dir ./my-market
36
+ miniyun doctor
37
+ ```
38
+
39
+ `down` 只停止该目录的进程,保留账号和业务数据。CLI 没有自动删除数据的命令。备份前先停止服务,再备份完整部署目录。
40
+
41
+ Linux 安装并启动 Docker 后,可以用 `miniyun up --docker` 让模块进入 Docker 隔离环境;Windows 轻量版不提供 Linux Docker 模块运行时。不要在本地开发模式中执行来源不可信的应用包。面向远程团队的完整部署另按网站管理员指南配置 Docker、多语言运行时、独立 PostgreSQL、HTTPS 和邮件登录。
42
+
43
+ 默认部署目录是当前目录下的 `miniyun-system`。默认仅绑定 `127.0.0.1`,不会自动开放公网端口、修改防火墙、接入线上数据库或给第三方发送邮件。
@@ -0,0 +1,29 @@
1
+ # MiniYun — 在本地建立自己的应用市场
2
+
3
+ 安装后运行两条命令:
4
+
5
+ ```console
6
+ pip install miniyun
7
+ miniyun up
8
+ ```
9
+
10
+ 打开命令输出的 `http://127.0.0.1:17880/workspace`,在页面设置你自己的管理员姓名和密码。新环境不包含线上账号、API Key 或业务数据。
11
+
12
+ PyPI 安装包内附 Windows x64 与 Linux x64 的官方运行包。CLI 选择匹配版本、校验 SHA-256,再启动本机服务;不需要再从演示网站下载。`pip install` 需要联网,完成安装后可以离线初始化和启动。Python 需要 3.10 或更高版本。
13
+
14
+ 首版运行包支持 Windows x64 和 Linux x64,内置工单、请假、报销、组织通讯录四个可信示例。默认是仅监听本机的轻量开发部署。组织、权限、个人 API Key、自定义 H5、应用管理和平台文档都随底座提供。
15
+
16
+ ```console
17
+ miniyun init --dir ./my-market
18
+ miniyun up --dir ./my-market --port 17890
19
+ miniyun status --dir ./my-market
20
+ miniyun logs --dir ./my-market --lines 40
21
+ miniyun down --dir ./my-market
22
+ miniyun doctor
23
+ ```
24
+
25
+ `down` 只停止该目录的进程,保留账号和业务数据。CLI 没有自动删除数据的命令。备份前先停止服务,再备份完整部署目录。
26
+
27
+ Linux 安装并启动 Docker 后,可以用 `miniyun up --docker` 让模块进入 Docker 隔离环境;Windows 轻量版不提供 Linux Docker 模块运行时。不要在本地开发模式中执行来源不可信的应用包。面向远程团队的完整部署另按网站管理员指南配置 Docker、多语言运行时、独立 PostgreSQL、HTTPS 和邮件登录。
28
+
29
+ 默认部署目录是当前目录下的 `miniyun-system`。默认仅绑定 `127.0.0.1`,不会自动开放公网端口、修改防火墙、接入线上数据库或给第三方发送邮件。
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "miniyun"
7
+ version = "0.1.0"
8
+ description = "Deploy a private MiniYun application market on your own computer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{name = "MiniYun"}]
13
+ dependencies = ["psutil>=5.9"]
14
+ classifiers = ["Programming Language :: Python :: 3", "Topic :: System :: Installation/Setup"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://mn.pckee.com/"
18
+ Documentation = "https://mn.pckee.com/docs/"
19
+
20
+ [project.scripts]
21
+ miniyun = "miniyun.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.setuptools.package-data]
27
+ miniyun = ["runtime/manifest.json", "runtime/*.zip"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,168 @@
1
+ """Small local deployment client. No credentials or business data are bundled."""
2
+ import argparse, hashlib, json, os, pathlib, platform, shutil, stat, subprocess, sys, tempfile, time, urllib.parse, urllib.request, zipfile
3
+ from . import __version__
4
+
5
+ RELEASE = "0.20.23"
6
+ BASE = "https://test.miniyun.com/releases/" + RELEASE + "/"
7
+
8
+ def root_for(args):
9
+ return pathlib.Path(args.directory).expanduser().resolve()
10
+
11
+ def read_json(path):
12
+ return json.loads(path.read_text(encoding="utf-8"))
13
+
14
+ def save(path, value):
15
+ temporary=path.with_suffix(path.suffix+".new")
16
+ temporary.write_text(json.dumps(value,ensure_ascii=False,indent=2),encoding="utf-8")
17
+ temporary.chmod(0o600);temporary.replace(path)
18
+
19
+ def host_key():
20
+ machine=platform.machine().lower();arch={"x86_64":"amd64","amd64":"amd64","aarch64":"arm64","arm64":"arm64"}.get(machine)
21
+ osname={"Windows":"windows","Linux":"linux","Darwin":"darwin"}.get(platform.system())
22
+ if not osname or not arch:raise RuntimeError("当前操作系统或 CPU 暂无运行包")
23
+ return osname+"-"+arch
24
+
25
+ def download(url, destination, maximum):
26
+ with urllib.request.urlopen(url,timeout=45) as response, destination.open("wb") as out:
27
+ count=0
28
+ while True:
29
+ chunk=response.read(1024*1024)
30
+ if not chunk:break
31
+ count+=len(chunk)
32
+ if count>maximum:raise RuntimeError("下载包超过允许大小")
33
+ out.write(chunk)
34
+
35
+ def unpack(archive, destination):
36
+ with zipfile.ZipFile(archive) as z:
37
+ if sum(x.file_size for x in z.infolist())>512*1024*1024:raise RuntimeError("运行包解压大小无效")
38
+ for item in z.infolist():
39
+ target=(destination/item.filename).resolve()
40
+ if not target.is_relative_to(destination.resolve()) or "\\" in item.filename or stat.S_ISLNK(item.external_attr>>16):raise RuntimeError("运行包含不安全路径")
41
+ z.extractall(destination)
42
+ for item in z.infolist():
43
+ p=destination/item.filename
44
+ if p.is_file():p.chmod(0o755 if item.external_attr>>16 & 0o111 else 0o600)
45
+
46
+ def initialize(args):
47
+ root=root_for(args);config=root/"miniyun.json"
48
+ if config.exists():
49
+ value=read_json(config)
50
+ if value.get("release")!=RELEASE:raise RuntimeError("已有其他版本;请先备份,CLI 不会自动覆盖升级")
51
+ return root,value
52
+ if root.exists() and any(root.iterdir()):raise RuntimeError("目标目录非空,且不是已初始化的 MiniYun 目录")
53
+ root.mkdir(parents=True,exist_ok=True,mode=0o700)
54
+ print("正在校验并安装 MiniYun 本地运行包…",flush=True)
55
+ with tempfile.TemporaryDirectory(prefix=".install-",dir=root) as tmp:
56
+ stage=pathlib.Path(tmp);manifest_path=stage/"manifest.json"
57
+ bundled=pathlib.Path(__file__).parent/"runtime"
58
+ if (bundled/"manifest.json").is_file():shutil.copyfile(bundled/"manifest.json",manifest_path)
59
+ else:download(BASE+"manifest.json",manifest_path,1024*1024)
60
+ manifest=read_json(manifest_path)
61
+ if manifest.get("version")!=RELEASE:raise RuntimeError("运行包版本与 CLI 不匹配")
62
+ package=manifest.get("platforms",{}).get(host_key())
63
+ if not package:raise RuntimeError("本版运行包暂不支持 "+host_key())
64
+ name=package["file"]
65
+ if pathlib.PurePosixPath(name).name!=name or not name.endswith(".zip"):raise RuntimeError("运行包地址无效")
66
+ archive=stage/"runtime.zip"
67
+ if (bundled/name).is_file():shutil.copyfile(bundled/name,archive)
68
+ else:download(BASE+urllib.parse.quote(name),archive,256*1024*1024)
69
+ digest=hashlib.sha256()
70
+ with archive.open("rb") as f:
71
+ for chunk in iter(lambda:f.read(1024*1024),b""):digest.update(chunk)
72
+ if digest.hexdigest()!=package["sha256"]:raise RuntimeError("SHA-256 校验失败,未运行下载内容")
73
+ unpacked=stage/"unpacked";unpacked.mkdir();unpack(archive,unpacked)
74
+ if not (unpacked/("platform.exe" if os.name=="nt" else "platform")).is_file():raise RuntimeError("运行包缺少平台程序")
75
+ unpacked.rename(root/"runtime")
76
+ for folder in ["data","logs","ipc"]:(root/folder).mkdir(mode=0o700)
77
+ modules=root/"runtime/modules";registry=[]
78
+ if modules.exists():
79
+ for p in sorted(modules.glob("*/manifest.json")):
80
+ m=read_json(p);registry.append({"id":m["id"],"capabilities":m["capabilities"]})
81
+ save(root/"data/registry.json",registry)
82
+ value={"release":RELEASE,"platform":host_key(),"port":17880};save(config,value)
83
+ return root,value
84
+
85
+ def process(root):
86
+ import psutil
87
+ receipt=root/"process.json"
88
+ if not receipt.exists():return None
89
+ value=read_json(receipt)
90
+ try:
91
+ p=psutil.Process(value["pid"])
92
+ expected=root/"runtime"/("platform.exe" if os.name=="nt" else "platform")
93
+ if pathlib.Path(p.exe()).resolve()!=expected.resolve() or abs(p.create_time()-value["created"])>0.1:raise RuntimeError("进程标识不匹配,拒绝操作其他程序")
94
+ if p.status()==psutil.STATUS_ZOMBIE:return None
95
+ return p
96
+ except psutil.NoSuchProcess:return None
97
+
98
+ def request(port, path="/healthz"):
99
+ with urllib.request.urlopen("http://127.0.0.1:"+str(port)+path,timeout=2) as r:return json.load(r)
100
+
101
+ def up(args):
102
+ import psutil
103
+ root,value=initialize(args)
104
+ if process(root):print("已运行: http://127.0.0.1:"+str(value['port'])+"/workspace");return
105
+ port=args.port or value['port']
106
+ if not 1024<=port<=65535:raise RuntimeError("端口必须在 1024–65535 之间")
107
+ import socket
108
+ with socket.socket() as s:
109
+ if os.name!="nt":s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
110
+ s.bind(("127.0.0.1",port))
111
+ env={k:v for k,v in os.environ.items() if not k.startswith("OA_") and k not in ["RESEND_API_KEY","OA_MAIL_FROM"]}
112
+ env["OA_MODULE_RUNTIME"]="native"
113
+ if args.docker:
114
+ if platform.system()!="Linux":raise RuntimeError("Docker 模块运行时目前要求 Linux;Windows 可使用默认轻量版")
115
+ subprocess.run(["docker","info"],check=True,stdout=subprocess.DEVNULL)
116
+ with tempfile.TemporaryDirectory() as tmp:
117
+ pathlib.Path(tmp,"Dockerfile").write_text("FROM scratch\n",encoding="utf-8")
118
+ subprocess.run(["docker","build","-q","-t","oa-module-runtime:1",tmp],check=True)
119
+ env.update(OA_MODULE_RUNTIME="docker",OA_DOCKER_IPC_DIR=str(root/"ipc"))
120
+ binary=root/"runtime"/("platform.exe" if os.name=="nt" else "platform")
121
+ command=[str(binary),"-addr","127.0.0.1:"+str(port),"-data-dir",str(root/"data"),"-module-dir",str(root/"runtime/modules"),"-log-dir",str(root/"logs")]
122
+ with (root/"logs/launcher.log").open("ab") as log:
123
+ child=subprocess.Popen(command,cwd=root,env=env,stdin=subprocess.DEVNULL,stdout=log,stderr=log,start_new_session=os.name!="nt",creationflags=subprocess.CREATE_NO_WINDOW if os.name=="nt" else 0)
124
+ p=psutil.Process(child.pid);save(root/"process.json",{"pid":child.pid,"created":p.create_time()});value['port']=port;save(root/"miniyun.json",value)
125
+ for _ in range(100):
126
+ if child.poll() is not None:raise RuntimeError("启动失败,请运行 miniyun logs 查看原因")
127
+ try:
128
+ if request(port).get("version")==RELEASE:
129
+ print("MiniYun 已启动:http://127.0.0.1:"+str(port)+"/workspace\n首次进入请在页面初始化管理员。数据目录:"+str(root));return
130
+ except (OSError,ValueError):pass
131
+ time.sleep(0.2)
132
+ raise RuntimeError("启动尚未就绪,请运行 miniyun status 和 miniyun logs 检查;未自动重复启动")
133
+
134
+ def main(argv=None):
135
+ p=argparse.ArgumentParser(prog="miniyun",description="在本机建立自己的 MiniYun 应用市场")
136
+ p.add_argument("--version",action="version",version="miniyun "+__version__)
137
+ sub=p.add_subparsers(dest="operation",required=True)
138
+ for name in ["init","up","down","status","logs","doctor"]:
139
+ cmd=sub.add_parser(name);cmd.add_argument("--dir",dest="directory",default="./miniyun-system")
140
+ if name=="up":cmd.add_argument("--port",type=int);cmd.add_argument("--docker",action="store_true")
141
+ if name=="logs":cmd.add_argument("--lines",type=int,default=40)
142
+ args=p.parse_args(argv)
143
+ try:
144
+ if args.operation=="init":root,value=initialize(args);print("已初始化:"+str(root)+"\n运行 miniyun up 启动。")
145
+ elif args.operation=="up":up(args)
146
+ elif args.operation=="doctor":print(json.dumps({"cli":__version__,"python":platform.python_version(),"platform":host_key(),"docker_cli":bool(shutil.which("docker")),"release":RELEASE},ensure_ascii=False,indent=2))
147
+ elif args.operation=="logs":
148
+ from collections import deque
149
+ root=root_for(args);path=root/"logs/platform.log"
150
+ if not path.exists():path=root/"logs/launcher.log"
151
+ if not 1<=args.lines<=1000:raise RuntimeError("日志行数必须为 1–1000")
152
+ with path.open(encoding="utf-8",errors="replace") as f:print(''.join(deque(f,maxlen=args.lines)),end='')
153
+ else:
154
+ root=root_for(args);value=read_json(root/"miniyun.json");proc=process(root)
155
+ if args.operation=="down":
156
+ if proc:
157
+ import psutil
158
+ proc.terminate()
159
+ try:proc.wait(25)
160
+ except psutil.TimeoutExpired:raise RuntimeError("仍在停止;未强制杀进程,请检查日志")
161
+ print("已停止;账号和业务数据保留。")
162
+ elif proc:print(json.dumps({"running":True,"url":"http://127.0.0.1:"+str(value['port'])+"/workspace","health":request(value['port']),"ready":request(value['port'],'/readyz')},ensure_ascii=False,indent=2))
163
+ else:print(json.dumps({"running":False,"data_preserved":True}))
164
+ return 0
165
+ except (OSError,ValueError,RuntimeError,subprocess.SubprocessError) as error:
166
+ print("MiniYun: "+str(error),file=sys.stderr);return 1
167
+
168
+ if __name__=="__main__":sys.exit(main())
@@ -0,0 +1,27 @@
1
+ {
2
+ "version": "0.20.23",
3
+ "platforms": {
4
+ "windows-amd64": {
5
+ "file": "miniyun-0.20.23-windows-amd64.zip",
6
+ "sha256": "32cf5dc42f56e1d89b30dc8d4abfd4a01c55c2dd02df2f7b58f5fe9626f7b80d",
7
+ "bytes": 26297898,
8
+ "examples": [
9
+ "workorder",
10
+ "leave",
11
+ "reimbursement",
12
+ "directory"
13
+ ]
14
+ },
15
+ "linux-amd64": {
16
+ "file": "miniyun-0.20.23-linux-amd64.zip",
17
+ "sha256": "4fbf2e515c0591652fb6e38669b79e3e54ff486d5fefede91c2d3b9e6aa97e9c",
18
+ "bytes": 25549108,
19
+ "examples": [
20
+ "workorder",
21
+ "leave",
22
+ "reimbursement",
23
+ "directory"
24
+ ]
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: miniyun
3
+ Version: 0.1.0
4
+ Summary: Deploy a private MiniYun application market on your own computer
5
+ Author: MiniYun
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://mn.pckee.com/
8
+ Project-URL: Documentation, https://mn.pckee.com/docs/
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: System :: Installation/Setup
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: psutil>=5.9
14
+
15
+ # MiniYun — 在本地建立自己的应用市场
16
+
17
+ 安装后运行两条命令:
18
+
19
+ ```console
20
+ pip install miniyun
21
+ miniyun up
22
+ ```
23
+
24
+ 打开命令输出的 `http://127.0.0.1:17880/workspace`,在页面设置你自己的管理员姓名和密码。新环境不包含线上账号、API Key 或业务数据。
25
+
26
+ PyPI 安装包内附 Windows x64 与 Linux x64 的官方运行包。CLI 选择匹配版本、校验 SHA-256,再启动本机服务;不需要再从演示网站下载。`pip install` 需要联网,完成安装后可以离线初始化和启动。Python 需要 3.10 或更高版本。
27
+
28
+ 首版运行包支持 Windows x64 和 Linux x64,内置工单、请假、报销、组织通讯录四个可信示例。默认是仅监听本机的轻量开发部署。组织、权限、个人 API Key、自定义 H5、应用管理和平台文档都随底座提供。
29
+
30
+ ```console
31
+ miniyun init --dir ./my-market
32
+ miniyun up --dir ./my-market --port 17890
33
+ miniyun status --dir ./my-market
34
+ miniyun logs --dir ./my-market --lines 40
35
+ miniyun down --dir ./my-market
36
+ miniyun doctor
37
+ ```
38
+
39
+ `down` 只停止该目录的进程,保留账号和业务数据。CLI 没有自动删除数据的命令。备份前先停止服务,再备份完整部署目录。
40
+
41
+ Linux 安装并启动 Docker 后,可以用 `miniyun up --docker` 让模块进入 Docker 隔离环境;Windows 轻量版不提供 Linux Docker 模块运行时。不要在本地开发模式中执行来源不可信的应用包。面向远程团队的完整部署另按网站管理员指南配置 Docker、多语言运行时、独立 PostgreSQL、HTTPS 和邮件登录。
42
+
43
+ 默认部署目录是当前目录下的 `miniyun-system`。默认仅绑定 `127.0.0.1`,不会自动开放公网端口、修改防火墙、接入线上数据库或给第三方发送邮件。
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/miniyun/__init__.py
4
+ src/miniyun/cli.py
5
+ src/miniyun.egg-info/PKG-INFO
6
+ src/miniyun.egg-info/SOURCES.txt
7
+ src/miniyun.egg-info/dependency_links.txt
8
+ src/miniyun.egg-info/entry_points.txt
9
+ src/miniyun.egg-info/requires.txt
10
+ src/miniyun.egg-info/top_level.txt
11
+ src/miniyun/runtime/manifest.json
12
+ src/miniyun/runtime/miniyun-0.20.23-linux-amd64.zip
13
+ src/miniyun/runtime/miniyun-0.20.23-windows-amd64.zip
14
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ miniyun = miniyun.cli:main
@@ -0,0 +1 @@
1
+ psutil>=5.9
@@ -0,0 +1 @@
1
+ miniyun
@@ -0,0 +1,35 @@
1
+ import argparse,hashlib,json,pathlib,sys,tempfile,unittest,zipfile
2
+ from unittest import mock
3
+ sys.path.insert(0,str(pathlib.Path(__file__).resolve().parents[1]/'src'))
4
+ from miniyun import cli
5
+
6
+ class InstallationTests(unittest.TestCase):
7
+ def test_zip_rejects_escape_and_symlink(self):
8
+ for name,mode in [('../outside',0o100644),('link',0o120777)]:
9
+ with tempfile.TemporaryDirectory() as tmp:
10
+ root=pathlib.Path(tmp);archive=root/'bad.zip'
11
+ with zipfile.ZipFile(archive,'w') as z:
12
+ info=zipfile.ZipInfo(name);info.external_attr=mode<<16;z.writestr(info,'outside')
13
+ with self.assertRaises(RuntimeError):cli.unpack(archive,root/'target')
14
+ self.assertFalse((root.parent/'outside').exists())
15
+ def test_checksum_failure_never_installs_or_runs(self):
16
+ with tempfile.TemporaryDirectory() as tmp:
17
+ root=pathlib.Path(tmp)/'market';args=argparse.Namespace(directory=str(root))
18
+ def fake(url,path,maximum):
19
+ if url.endswith('manifest.json'):path.write_text(json.dumps({'version':cli.RELEASE,'platforms':{cli.host_key():{'file':'runtime.zip','sha256':'0'*64}}}))
20
+ else:path.write_bytes(b'corrupted runtime')
21
+ with mock.patch.object(cli,'__file__',str(pathlib.Path(tmp)/'unbundled/cli.py')),mock.patch.object(cli,'download',side_effect=fake),mock.patch.object(cli.subprocess,'Popen') as launch:
22
+ with self.assertRaisesRegex(RuntimeError,'SHA-256'):cli.initialize(args)
23
+ launch.assert_not_called();self.assertFalse((root/'runtime').exists());self.assertFalse((root/'miniyun.json').exists())
24
+ def test_existing_directory_data_is_preserved(self):
25
+ with tempfile.TemporaryDirectory() as tmp:
26
+ root=pathlib.Path(tmp);(root/'business-data').write_text('preserve')
27
+ with self.assertRaises(RuntimeError):cli.initialize(argparse.Namespace(directory=str(root)))
28
+ self.assertEqual((root/'business-data').read_text(),'preserve')
29
+ def test_foreign_process_is_not_stopped(self):
30
+ with tempfile.TemporaryDirectory() as tmp:
31
+ import psutil,os
32
+ root=pathlib.Path(tmp);p=psutil.Process(os.getpid());cli.save(root/'process.json',{'pid':p.pid,'created':p.create_time()})
33
+ with self.assertRaisesRegex(RuntimeError,'进程标识'):cli.process(root)
34
+
35
+ if __name__=='__main__':unittest.main()