nostop-cli 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.
- nostop_cli-0.1.0/LICENSE +21 -0
- nostop_cli-0.1.0/MANIFEST.in +3 -0
- nostop_cli-0.1.0/PKG-INFO +275 -0
- nostop_cli-0.1.0/README.md +258 -0
- nostop_cli-0.1.0/pyproject.toml +34 -0
- nostop_cli-0.1.0/setup.cfg +4 -0
- nostop_cli-0.1.0/src/nostop/__init__.py +0 -0
- nostop_cli-0.1.0/src/nostop/__main__.py +4 -0
- nostop_cli-0.1.0/src/nostop/cli.py +26 -0
- nostop_cli-0.1.0/src/nostop/nostop.sh +343 -0
- nostop_cli-0.1.0/src/nostop_cli.egg-info/PKG-INFO +275 -0
- nostop_cli-0.1.0/src/nostop_cli.egg-info/SOURCES.txt +13 -0
- nostop_cli-0.1.0/src/nostop_cli.egg-info/dependency_links.txt +1 -0
- nostop_cli-0.1.0/src/nostop_cli.egg-info/entry_points.txt +2 -0
- nostop_cli-0.1.0/src/nostop_cli.egg-info/top_level.txt +1 -0
nostop_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nostop contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nostop-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Resume-capable command wrapper for Linux: rerun interrupted commands and skip completed steps.
|
|
5
|
+
Author: nostop
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://example.com/nostop
|
|
8
|
+
Keywords: cli,resume,checkpoint,pipeline,bioinformatics
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Topic :: System :: Shells
|
|
12
|
+
Classifier: Programming Language :: Unix Shell
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# nostop
|
|
19
|
+
|
|
20
|
+
一个极小的 Linux 命令行工具,用于**包装并执行任意命令**,并在命令因断网、断电、
|
|
21
|
+
崩溃等任何原因中断后,能够**从中断处继续**,避免从头重跑浪费时间。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
nostop <命令> # 运行 / 续跑一条命令
|
|
25
|
+
nostop stats # 查看此前中断的命令(命令内容 + 中断时的工作目录)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 它解决什么问题
|
|
31
|
+
|
|
32
|
+
- 跑了 6 个小时的生信流程,第 5 步时断电 / 断网 / 节点被回收,重跑又要 6 小时。
|
|
33
|
+
- 写了一个多步骤的 shell 流程,中途失败,希望**已完成的步骤不要再跑**。
|
|
34
|
+
|
|
35
|
+
nostop 的价值:
|
|
36
|
+
|
|
37
|
+
1. **通用**:`nostop <任意命令>` 的行为与直接执行该命令完全一致(输出、退出码、环境都透传)。
|
|
38
|
+
2. **可续跑**:中断后再次运行**完全相同**的 `nostop <命令>`(同一工作目录),工具会识别已完成的
|
|
39
|
+
进度并跳过,从断点继续。
|
|
40
|
+
3. **可查询**:`nostop stats` 列出此前中断的命令,含命令内容与其**中断时所在路径(cwd)**,
|
|
41
|
+
方便你回到原目录重新启动。
|
|
42
|
+
4. **简约**:单个 bash 脚本,无编译、无依赖、无守护进程、无数据库;体积几 KB,运行时几乎零开销。
|
|
43
|
+
5. **支持广**:既支持“多步骤 shell 脚本”(用 checkpoint API 精确跳过步骤),也支持“单个长时间
|
|
44
|
+
运行的程序”(如 megahit 等自带断点续跑能力的二进制,nostop 负责用完全相同的方式重新拉起它)。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 安装
|
|
49
|
+
|
|
50
|
+
### 方式一:pip 安装(推荐)
|
|
51
|
+
|
|
52
|
+
本工具已打包为 Python 项目,可直接用 pip 从源码或你们内部的 PyPI 源安装:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 从本地源码目录安装
|
|
56
|
+
pip install .
|
|
57
|
+
|
|
58
|
+
# 或打包后安装(生成 dist/nostop-cli-*.whl / tar.gz)
|
|
59
|
+
pip install build
|
|
60
|
+
python -m build
|
|
61
|
+
pip install dist/nostop-cli-*.whl
|
|
62
|
+
|
|
63
|
+
# 如需发布到你们内部的 PyPI 或公开 PyPI:
|
|
64
|
+
twine upload dist/*
|
|
65
|
+
# 之后他人即可:pip install nostop-cli (命令名仍是 nostop)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
安装后 `nostop` 命令即可用(其实质是一个极薄的 Python 入口,启动后立刻 exec
|
|
69
|
+
到内置的 bash 脚本,运行时不依赖 Python)。也可用 `python -m nostop ...` 运行。
|
|
70
|
+
|
|
71
|
+
### 方式二:单文件拷贝(零依赖,连 Python 都不需要)
|
|
72
|
+
|
|
73
|
+
如果你只想用一个文件、连 Python 环境都不想依赖,直接复制内置脚本即可
|
|
74
|
+
(依赖系统自带的 bash 与 coreutils,CentOS / Ubuntu 等主流发行版默认都有):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sudo cp src/nostop/nostop.sh /usr/local/bin/nostop
|
|
78
|
+
sudo chmod +x /usr/local/bin/nostop
|
|
79
|
+
|
|
80
|
+
# 可选:自定义状态目录(默认 ~/.nostop)
|
|
81
|
+
# export NOSTOP_HOME=/data/.nostop
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
验证:`nostop help`
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 快速开始
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 像平常一样运行命令,只是前面加 nostop
|
|
92
|
+
nostop "bash my_pipeline.sh"
|
|
93
|
+
|
|
94
|
+
# 中断后,回到同一个目录,原样再敲一次即可续跑
|
|
95
|
+
nostop "bash my_pipeline.sh"
|
|
96
|
+
|
|
97
|
+
# 查看哪些命令之前中断了(含命令内容与工作目录)
|
|
98
|
+
nostop stats
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 场景一:多步骤 shell 流程(精确到步骤的跳过)
|
|
104
|
+
|
|
105
|
+
对“脚本里有多个步骤”的情况,使用 nostop 提供的 **checkpoint(检查点)API**。
|
|
106
|
+
在脚本里把每个步骤包成 `nostop step <名字> -- <命令>`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
#!/bin/bash
|
|
110
|
+
# my_pipeline.sh
|
|
111
|
+
nostop step prepare -- "echo 准备数据 && sleep 2"
|
|
112
|
+
nostop step align -- "echo 比对 && sleep 2"
|
|
113
|
+
nostop step call -- "echo 变异检测 && sleep 2"
|
|
114
|
+
nostop step report -- "echo 出报告"
|
|
115
|
+
echo "全部完成"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
运行:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
nostop "bash my_pipeline.sh"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- 第一次正常运行,每完成一步就记录一个检查点。
|
|
125
|
+
- 中途断电/被杀:再次 `nostop "bash my_pipeline.sh"` 时,已完成的步骤会被**自动跳过**
|
|
126
|
+
(输出 `[nostop] skip step 'prepare' (already done)`),直接从断点继续。
|
|
127
|
+
- 步骤失败(退出码非 0)不会被标记为完成,下次会自动重跑该步骤。
|
|
128
|
+
|
|
129
|
+
> 如果你的步骤本身是一条带管道 / 重定向的复杂命令,可以写成单个字符串:
|
|
130
|
+
> `nostop step build -- "make && make test | tee build.log"`。
|
|
131
|
+
|
|
132
|
+
手动控制检查点(更灵活):
|
|
133
|
+
|
|
134
|
+
| 写法 | 作用 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `nostop step <名字> -- <命令>` | 运行命令;成功则标记 `<名字>` 完成,下次跳过 |
|
|
137
|
+
| `nostop done <名字>` | 退出码 0 表示该检查点已完成,否则 1(用于 `if` 判断) |
|
|
138
|
+
| `nostop mark <名字>` | 直接把 `<名字>` 标记为完成 |
|
|
139
|
+
|
|
140
|
+
例如在脚本里手写分支:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
if nostop done align; then
|
|
144
|
+
echo "比对已完成,跳过"
|
|
145
|
+
else
|
|
146
|
+
do_align && nostop mark align
|
|
147
|
+
fi
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> 注意:checkpoint API 只在 `nostop <命令>` 内部才记录进度;直接 `bash my_pipeline.sh`
|
|
151
|
+
> 运行时它会退化为“直接执行”,不影响结果,只是不记录进度。
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 场景二:单个长时间运行的程序(如 megahit)
|
|
156
|
+
|
|
157
|
+
很多生信程序(megahit、Trinity、SPAdes 等)**自身就支持断点续跑**——只要用相同的输出目录
|
|
158
|
+
重新运行,它们会接着上次的产物继续。nostop 在这里的作用是:**用完全相同的方式重新拉起命令,
|
|
159
|
+
并替你记录状态 / 方便重跑**。
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 第一次运行
|
|
163
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
164
|
+
|
|
165
|
+
# 中断后,回到同一目录,原样再敲一次;megahit 会基于 mega.out 里已有的中间文件续跑
|
|
166
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
nostop 本身对“单个二进制内部进度”无感知(那是程序自己的事),但它保证:
|
|
170
|
+
- 用**完全相同的命令字符串与工作目录**重启;
|
|
171
|
+
- 把这次运行登记到状态库,供 `nostop stats` 查看;
|
|
172
|
+
- 程序成功跑完(退出码 0)后记为 `DONE`,之后原样再跑会被直接跳过,不会重复计算。
|
|
173
|
+
|
|
174
|
+
> 如果程序需要显式续跑参数(例如某些工具要用 `--continue` / `-c`),把它写进命令里即可,
|
|
175
|
+
> 因为 nostop 是“原样重放”。
|
|
176
|
+
|
|
177
|
+
## 场景三:批处理(nostop batch)
|
|
178
|
+
|
|
179
|
+
当同一道命令要对多组输入分别跑一遍(例如多个样本分别做 fastp / spades),用 `nostop batch`
|
|
180
|
+
一次性提交。它把每条输入展开到模板里,并把**每条输入当作一个检查点**——中断后续跑时,
|
|
181
|
+
已完成的样本会被自动跳过。
|
|
182
|
+
|
|
183
|
+
模板占位符:
|
|
184
|
+
|
|
185
|
+
- `{N}`:第 N 个字段(从 1 起),输入按 `:` 切分。例 `s1_1.fq:s1_2.fq` → `{1}=s1_1.fq`、`{2}=s1_2.fq`。
|
|
186
|
+
- `{}`:整条输入原文(含 `:`)。
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# 双端配对({1}/{2} 占位)
|
|
190
|
+
nostop batch "fastp -i {1} -I {2} -o {1}.clean.fq -O {2}.clean.fq" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
191
|
+
|
|
192
|
+
# 工具无续跑参数时,结合幂等写法最稳(重跑时已完成的组装直接跳过)
|
|
193
|
+
nostop batch "[ -s asm_{}/final.contigs.fa ] || spades.py -1 {1} -2 {2} -o asm_{}" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- 每条输入即 `nostop step <输入>` 检查点,状态同样出现在 `nostop stats`。
|
|
197
|
+
- 某个样本失败,batch 会停下(不再跑后续),修好后重跑即可;已完成样本自动跳过。
|
|
198
|
+
- `{}` 是整条输入原文(如 `s1_1.fq:s1_2.fq`),故 `asm_{}` 目录名会带冒号(Linux 下合法);
|
|
199
|
+
想要干净目录名时,把样本名单独作为条目传入、用 `{1}/{2}` 拼文件路径即可。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 子命令
|
|
204
|
+
|
|
205
|
+
| 命令 | 说明 |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `nostop <命令>` | 运行 / 续跑一条命令 |
|
|
208
|
+
| `nostop batch <模板> <条目>...` | 对每条条目展开模板并分别运行,已完成的跳过 |
|
|
209
|
+
| `nostop stats [--all]` | 列出**中断 / 失败 / 正在运行**的命令;`--all` 同时显示已完成的 |
|
|
210
|
+
| `nostop show <id>` | 查看某条运行的详情(命令、目录、开始时间、尾部输出) |
|
|
211
|
+
| `nostop resume <id>` | 在原始工作目录下,重新运行该命令(不用再手敲整条命令) |
|
|
212
|
+
| `nostop clean [--all|<id>]` | 清除某条(或全部)运行的状态;用于需要强制重跑时 |
|
|
213
|
+
| `nostop step/mark/done <名字>` | 见上文 checkpoint API |
|
|
214
|
+
| `nostop help` | 帮助 |
|
|
215
|
+
|
|
216
|
+
`stats` 示例输出:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
ID STATUS COMMAND
|
|
220
|
+
97ecb77e INTERRUPTED bash my_pipeline.sh
|
|
221
|
+
cwd: /home/user/project
|
|
222
|
+
last checkpoint: align
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `ID` 是状态库里的短标识,可直接用于 `nostop show <id>` / `nostop resume <id>` / `nostop clean <id>`。
|
|
226
|
+
- `STATUS` 取值:`RUNNING`(仍在跑) / `INTERRUPTED`(被中断,最常见:断电、被杀、崩溃) /
|
|
227
|
+
`FAILED`(命令自身非零退出) / `DONE`(已成功完成)。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 工作原理(如何检测“中断”)
|
|
232
|
+
|
|
233
|
+
状态保存在 `$NOSTOP_HOME/runs/<hash>/`,其中 `<hash>` 由 **命令字符串 + 当前工作目录** 计算。
|
|
234
|
+
每次运行写入 `meta` 文件,记录 `status`(初始 `running`)、`pid`、`boot_id`(来自
|
|
235
|
+
`/proc/sys/kernel/random/boot_id`)、命令、目录等。
|
|
236
|
+
|
|
237
|
+
- 命令**成功退出** -> `status=done`;**非零退出** -> `status=failed`。
|
|
238
|
+
- 若进程被 **SIGINT/SIGTERM** 终止 -> 捕获后记为 `interrupted`。
|
|
239
|
+
- 若机器**断电 / 崩溃**(nostop 来不及响应)-> `meta` 仍停留在 `running`,
|
|
240
|
+
且记录的 `pid` 已不存在(或 `boot_id` 与本次启动不一致)-> 查询时判定为 `INTERRUPTED`。
|
|
241
|
+
|
|
242
|
+
因此 `nostop stats` 能可靠区分“正在跑”、“被中断”、“已成功”、“已失败”。
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 环境变量
|
|
247
|
+
|
|
248
|
+
| 变量 | 默认值 | 说明 |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `NOSTOP_HOME` | `~/.nostop` | 状态库根目录,可指向大容量磁盘 |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 兼容性与限制
|
|
255
|
+
|
|
256
|
+
- **兼容性**:纯 bash + coreutils(`sha256sum`/`date`/`kill`/`tee`),CentOS 7/8、Ubuntu 20.04/22.04
|
|
257
|
+
等主流发行版开箱即用;仅依赖 Linux 的 `/proc/sys/kernel/random/boot_id`(缺失时退化为仅按 PID 判断)。
|
|
258
|
+
- **限制**:
|
|
259
|
+
- 对“单个不自带续跑能力的黑盒二进制”,nostop 只能保证原样重放,无法替它在内部续跑;
|
|
260
|
+
这类场景请用脚本 + `nostop step` 拆成可重入的步骤,或选用自带续跑的程序。
|
|
261
|
+
- 检查点跳过依赖**步骤本身是可重入 / 幂等**的;请确保重试某步不会造成破坏。
|
|
262
|
+
- 同一“命令 + 目录”被视为同一次运行;若想以不同参数再跑一份,请换目录或改命令。
|
|
263
|
+
- **已知命令已 DONE 时**再原样运行会被直接跳过(输出提示)。需要强制重跑请先 `nostop clean <id>`。
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 文件结构
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
nostop # 工具本体(单文件,拷贝即用)
|
|
271
|
+
README.md # 本文档
|
|
272
|
+
examples/ # 示例脚本
|
|
273
|
+
pipeline.sh # 场景一:带 checkpoint 的多步骤流程
|
|
274
|
+
megahit.sh # 场景二:单个长时间程序续跑示例
|
|
275
|
+
```
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# nostop
|
|
2
|
+
|
|
3
|
+
一个极小的 Linux 命令行工具,用于**包装并执行任意命令**,并在命令因断网、断电、
|
|
4
|
+
崩溃等任何原因中断后,能够**从中断处继续**,避免从头重跑浪费时间。
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
nostop <命令> # 运行 / 续跑一条命令
|
|
8
|
+
nostop stats # 查看此前中断的命令(命令内容 + 中断时的工作目录)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 它解决什么问题
|
|
14
|
+
|
|
15
|
+
- 跑了 6 个小时的生信流程,第 5 步时断电 / 断网 / 节点被回收,重跑又要 6 小时。
|
|
16
|
+
- 写了一个多步骤的 shell 流程,中途失败,希望**已完成的步骤不要再跑**。
|
|
17
|
+
|
|
18
|
+
nostop 的价值:
|
|
19
|
+
|
|
20
|
+
1. **通用**:`nostop <任意命令>` 的行为与直接执行该命令完全一致(输出、退出码、环境都透传)。
|
|
21
|
+
2. **可续跑**:中断后再次运行**完全相同**的 `nostop <命令>`(同一工作目录),工具会识别已完成的
|
|
22
|
+
进度并跳过,从断点继续。
|
|
23
|
+
3. **可查询**:`nostop stats` 列出此前中断的命令,含命令内容与其**中断时所在路径(cwd)**,
|
|
24
|
+
方便你回到原目录重新启动。
|
|
25
|
+
4. **简约**:单个 bash 脚本,无编译、无依赖、无守护进程、无数据库;体积几 KB,运行时几乎零开销。
|
|
26
|
+
5. **支持广**:既支持“多步骤 shell 脚本”(用 checkpoint API 精确跳过步骤),也支持“单个长时间
|
|
27
|
+
运行的程序”(如 megahit 等自带断点续跑能力的二进制,nostop 负责用完全相同的方式重新拉起它)。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
### 方式一:pip 安装(推荐)
|
|
34
|
+
|
|
35
|
+
本工具已打包为 Python 项目,可直接用 pip 从源码或你们内部的 PyPI 源安装:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 从本地源码目录安装
|
|
39
|
+
pip install .
|
|
40
|
+
|
|
41
|
+
# 或打包后安装(生成 dist/nostop-cli-*.whl / tar.gz)
|
|
42
|
+
pip install build
|
|
43
|
+
python -m build
|
|
44
|
+
pip install dist/nostop-cli-*.whl
|
|
45
|
+
|
|
46
|
+
# 如需发布到你们内部的 PyPI 或公开 PyPI:
|
|
47
|
+
twine upload dist/*
|
|
48
|
+
# 之后他人即可:pip install nostop-cli (命令名仍是 nostop)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
安装后 `nostop` 命令即可用(其实质是一个极薄的 Python 入口,启动后立刻 exec
|
|
52
|
+
到内置的 bash 脚本,运行时不依赖 Python)。也可用 `python -m nostop ...` 运行。
|
|
53
|
+
|
|
54
|
+
### 方式二:单文件拷贝(零依赖,连 Python 都不需要)
|
|
55
|
+
|
|
56
|
+
如果你只想用一个文件、连 Python 环境都不想依赖,直接复制内置脚本即可
|
|
57
|
+
(依赖系统自带的 bash 与 coreutils,CentOS / Ubuntu 等主流发行版默认都有):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
sudo cp src/nostop/nostop.sh /usr/local/bin/nostop
|
|
61
|
+
sudo chmod +x /usr/local/bin/nostop
|
|
62
|
+
|
|
63
|
+
# 可选:自定义状态目录(默认 ~/.nostop)
|
|
64
|
+
# export NOSTOP_HOME=/data/.nostop
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
验证:`nostop help`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 快速开始
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# 像平常一样运行命令,只是前面加 nostop
|
|
75
|
+
nostop "bash my_pipeline.sh"
|
|
76
|
+
|
|
77
|
+
# 中断后,回到同一个目录,原样再敲一次即可续跑
|
|
78
|
+
nostop "bash my_pipeline.sh"
|
|
79
|
+
|
|
80
|
+
# 查看哪些命令之前中断了(含命令内容与工作目录)
|
|
81
|
+
nostop stats
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 场景一:多步骤 shell 流程(精确到步骤的跳过)
|
|
87
|
+
|
|
88
|
+
对“脚本里有多个步骤”的情况,使用 nostop 提供的 **checkpoint(检查点)API**。
|
|
89
|
+
在脚本里把每个步骤包成 `nostop step <名字> -- <命令>`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
#!/bin/bash
|
|
93
|
+
# my_pipeline.sh
|
|
94
|
+
nostop step prepare -- "echo 准备数据 && sleep 2"
|
|
95
|
+
nostop step align -- "echo 比对 && sleep 2"
|
|
96
|
+
nostop step call -- "echo 变异检测 && sleep 2"
|
|
97
|
+
nostop step report -- "echo 出报告"
|
|
98
|
+
echo "全部完成"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
运行:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
nostop "bash my_pipeline.sh"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- 第一次正常运行,每完成一步就记录一个检查点。
|
|
108
|
+
- 中途断电/被杀:再次 `nostop "bash my_pipeline.sh"` 时,已完成的步骤会被**自动跳过**
|
|
109
|
+
(输出 `[nostop] skip step 'prepare' (already done)`),直接从断点继续。
|
|
110
|
+
- 步骤失败(退出码非 0)不会被标记为完成,下次会自动重跑该步骤。
|
|
111
|
+
|
|
112
|
+
> 如果你的步骤本身是一条带管道 / 重定向的复杂命令,可以写成单个字符串:
|
|
113
|
+
> `nostop step build -- "make && make test | tee build.log"`。
|
|
114
|
+
|
|
115
|
+
手动控制检查点(更灵活):
|
|
116
|
+
|
|
117
|
+
| 写法 | 作用 |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `nostop step <名字> -- <命令>` | 运行命令;成功则标记 `<名字>` 完成,下次跳过 |
|
|
120
|
+
| `nostop done <名字>` | 退出码 0 表示该检查点已完成,否则 1(用于 `if` 判断) |
|
|
121
|
+
| `nostop mark <名字>` | 直接把 `<名字>` 标记为完成 |
|
|
122
|
+
|
|
123
|
+
例如在脚本里手写分支:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
if nostop done align; then
|
|
127
|
+
echo "比对已完成,跳过"
|
|
128
|
+
else
|
|
129
|
+
do_align && nostop mark align
|
|
130
|
+
fi
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> 注意:checkpoint API 只在 `nostop <命令>` 内部才记录进度;直接 `bash my_pipeline.sh`
|
|
134
|
+
> 运行时它会退化为“直接执行”,不影响结果,只是不记录进度。
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 场景二:单个长时间运行的程序(如 megahit)
|
|
139
|
+
|
|
140
|
+
很多生信程序(megahit、Trinity、SPAdes 等)**自身就支持断点续跑**——只要用相同的输出目录
|
|
141
|
+
重新运行,它们会接着上次的产物继续。nostop 在这里的作用是:**用完全相同的方式重新拉起命令,
|
|
142
|
+
并替你记录状态 / 方便重跑**。
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 第一次运行
|
|
146
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
147
|
+
|
|
148
|
+
# 中断后,回到同一目录,原样再敲一次;megahit 会基于 mega.out 里已有的中间文件续跑
|
|
149
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
nostop 本身对“单个二进制内部进度”无感知(那是程序自己的事),但它保证:
|
|
153
|
+
- 用**完全相同的命令字符串与工作目录**重启;
|
|
154
|
+
- 把这次运行登记到状态库,供 `nostop stats` 查看;
|
|
155
|
+
- 程序成功跑完(退出码 0)后记为 `DONE`,之后原样再跑会被直接跳过,不会重复计算。
|
|
156
|
+
|
|
157
|
+
> 如果程序需要显式续跑参数(例如某些工具要用 `--continue` / `-c`),把它写进命令里即可,
|
|
158
|
+
> 因为 nostop 是“原样重放”。
|
|
159
|
+
|
|
160
|
+
## 场景三:批处理(nostop batch)
|
|
161
|
+
|
|
162
|
+
当同一道命令要对多组输入分别跑一遍(例如多个样本分别做 fastp / spades),用 `nostop batch`
|
|
163
|
+
一次性提交。它把每条输入展开到模板里,并把**每条输入当作一个检查点**——中断后续跑时,
|
|
164
|
+
已完成的样本会被自动跳过。
|
|
165
|
+
|
|
166
|
+
模板占位符:
|
|
167
|
+
|
|
168
|
+
- `{N}`:第 N 个字段(从 1 起),输入按 `:` 切分。例 `s1_1.fq:s1_2.fq` → `{1}=s1_1.fq`、`{2}=s1_2.fq`。
|
|
169
|
+
- `{}`:整条输入原文(含 `:`)。
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# 双端配对({1}/{2} 占位)
|
|
173
|
+
nostop batch "fastp -i {1} -I {2} -o {1}.clean.fq -O {2}.clean.fq" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
174
|
+
|
|
175
|
+
# 工具无续跑参数时,结合幂等写法最稳(重跑时已完成的组装直接跳过)
|
|
176
|
+
nostop batch "[ -s asm_{}/final.contigs.fa ] || spades.py -1 {1} -2 {2} -o asm_{}" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- 每条输入即 `nostop step <输入>` 检查点,状态同样出现在 `nostop stats`。
|
|
180
|
+
- 某个样本失败,batch 会停下(不再跑后续),修好后重跑即可;已完成样本自动跳过。
|
|
181
|
+
- `{}` 是整条输入原文(如 `s1_1.fq:s1_2.fq`),故 `asm_{}` 目录名会带冒号(Linux 下合法);
|
|
182
|
+
想要干净目录名时,把样本名单独作为条目传入、用 `{1}/{2}` 拼文件路径即可。
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 子命令
|
|
187
|
+
|
|
188
|
+
| 命令 | 说明 |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `nostop <命令>` | 运行 / 续跑一条命令 |
|
|
191
|
+
| `nostop batch <模板> <条目>...` | 对每条条目展开模板并分别运行,已完成的跳过 |
|
|
192
|
+
| `nostop stats [--all]` | 列出**中断 / 失败 / 正在运行**的命令;`--all` 同时显示已完成的 |
|
|
193
|
+
| `nostop show <id>` | 查看某条运行的详情(命令、目录、开始时间、尾部输出) |
|
|
194
|
+
| `nostop resume <id>` | 在原始工作目录下,重新运行该命令(不用再手敲整条命令) |
|
|
195
|
+
| `nostop clean [--all|<id>]` | 清除某条(或全部)运行的状态;用于需要强制重跑时 |
|
|
196
|
+
| `nostop step/mark/done <名字>` | 见上文 checkpoint API |
|
|
197
|
+
| `nostop help` | 帮助 |
|
|
198
|
+
|
|
199
|
+
`stats` 示例输出:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
ID STATUS COMMAND
|
|
203
|
+
97ecb77e INTERRUPTED bash my_pipeline.sh
|
|
204
|
+
cwd: /home/user/project
|
|
205
|
+
last checkpoint: align
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- `ID` 是状态库里的短标识,可直接用于 `nostop show <id>` / `nostop resume <id>` / `nostop clean <id>`。
|
|
209
|
+
- `STATUS` 取值:`RUNNING`(仍在跑) / `INTERRUPTED`(被中断,最常见:断电、被杀、崩溃) /
|
|
210
|
+
`FAILED`(命令自身非零退出) / `DONE`(已成功完成)。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 工作原理(如何检测“中断”)
|
|
215
|
+
|
|
216
|
+
状态保存在 `$NOSTOP_HOME/runs/<hash>/`,其中 `<hash>` 由 **命令字符串 + 当前工作目录** 计算。
|
|
217
|
+
每次运行写入 `meta` 文件,记录 `status`(初始 `running`)、`pid`、`boot_id`(来自
|
|
218
|
+
`/proc/sys/kernel/random/boot_id`)、命令、目录等。
|
|
219
|
+
|
|
220
|
+
- 命令**成功退出** -> `status=done`;**非零退出** -> `status=failed`。
|
|
221
|
+
- 若进程被 **SIGINT/SIGTERM** 终止 -> 捕获后记为 `interrupted`。
|
|
222
|
+
- 若机器**断电 / 崩溃**(nostop 来不及响应)-> `meta` 仍停留在 `running`,
|
|
223
|
+
且记录的 `pid` 已不存在(或 `boot_id` 与本次启动不一致)-> 查询时判定为 `INTERRUPTED`。
|
|
224
|
+
|
|
225
|
+
因此 `nostop stats` 能可靠区分“正在跑”、“被中断”、“已成功”、“已失败”。
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 环境变量
|
|
230
|
+
|
|
231
|
+
| 变量 | 默认值 | 说明 |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `NOSTOP_HOME` | `~/.nostop` | 状态库根目录,可指向大容量磁盘 |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 兼容性与限制
|
|
238
|
+
|
|
239
|
+
- **兼容性**:纯 bash + coreutils(`sha256sum`/`date`/`kill`/`tee`),CentOS 7/8、Ubuntu 20.04/22.04
|
|
240
|
+
等主流发行版开箱即用;仅依赖 Linux 的 `/proc/sys/kernel/random/boot_id`(缺失时退化为仅按 PID 判断)。
|
|
241
|
+
- **限制**:
|
|
242
|
+
- 对“单个不自带续跑能力的黑盒二进制”,nostop 只能保证原样重放,无法替它在内部续跑;
|
|
243
|
+
这类场景请用脚本 + `nostop step` 拆成可重入的步骤,或选用自带续跑的程序。
|
|
244
|
+
- 检查点跳过依赖**步骤本身是可重入 / 幂等**的;请确保重试某步不会造成破坏。
|
|
245
|
+
- 同一“命令 + 目录”被视为同一次运行;若想以不同参数再跑一份,请换目录或改命令。
|
|
246
|
+
- **已知命令已 DONE 时**再原样运行会被直接跳过(输出提示)。需要强制重跑请先 `nostop clean <id>`。
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 文件结构
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
nostop # 工具本体(单文件,拷贝即用)
|
|
254
|
+
README.md # 本文档
|
|
255
|
+
examples/ # 示例脚本
|
|
256
|
+
pipeline.sh # 场景一:带 checkpoint 的多步骤流程
|
|
257
|
+
megahit.sh # 场景二:单个长时间程序续跑示例
|
|
258
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nostop-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Resume-capable command wrapper for Linux: rerun interrupted commands and skip completed steps."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.6"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["cli", "resume", "checkpoint", "pipeline", "bioinformatics"]
|
|
13
|
+
authors = [{ name = "nostop" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Topic :: System :: Shells",
|
|
18
|
+
"Programming Language :: Unix Shell",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://example.com/nostop"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
nostop = "nostop.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = { "" = "src" }
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
nostop = ["nostop.sh"]
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Console-script entry point for the nostop bash tool.
|
|
2
|
+
|
|
3
|
+
This thin wrapper just exec's bash on the bundled ``nostop.sh`` so the real
|
|
4
|
+
implementation stays a single portable shell script (no Python at runtime
|
|
5
|
+
beyond launching it). exec replaces this process, so argv, environment
|
|
6
|
+
(NOSTOP_RUN_DIR, NOSTOP_HOME, ...) and the exit code all pass through
|
|
7
|
+
unchanged, and nested ``nostop step`` calls from inside a wrapped script
|
|
8
|
+
resolve back to this same wrapper on PATH.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nostop.sh")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv=None):
|
|
19
|
+
if argv is None:
|
|
20
|
+
argv = sys.argv[1:]
|
|
21
|
+
bash = shutil.which("bash") or "/bin/bash"
|
|
22
|
+
os.execv(bash, [bash, _SCRIPT] + list(argv))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# nostop - a tiny resume-capable command wrapper for Linux.
|
|
3
|
+
#
|
|
4
|
+
# Usage: nostop <command> # run / continue a command
|
|
5
|
+
# nostop stats [--all] # list interrupted commands
|
|
6
|
+
# nostop show <id> # details + tail of output for a run
|
|
7
|
+
# nostop resume <id> # re-run a stored command in its dir
|
|
8
|
+
# nostop clean [--all|<id>] # forget run state
|
|
9
|
+
# nostop step <name> -- <c> # (inside a script) run c once, mark name
|
|
10
|
+
# nostop mark <name> # mark checkpoint 'name' done
|
|
11
|
+
# nostop done <name> # exit 0 if 'name' already done
|
|
12
|
+
#
|
|
13
|
+
# State lives in ${NOSTOP_HOME:-$HOME/.nostop}. No daemon, no DB, just files.
|
|
14
|
+
set -u
|
|
15
|
+
|
|
16
|
+
NOSTOP_HOME="${NOSTOP_HOME:-$HOME/.nostop}"
|
|
17
|
+
RUNS_DIR="$NOSTOP_HOME/runs"
|
|
18
|
+
mkdir -p "$RUNS_DIR" 2>/dev/null || {
|
|
19
|
+
echo "[nostop] cannot create state dir $RUNS_DIR" >&2; exit 1; }
|
|
20
|
+
|
|
21
|
+
SELF="$(readlink -f "$0" 2>/dev/null || command -v nostop 2>/dev/null || echo "$0")"
|
|
22
|
+
|
|
23
|
+
usage() {
|
|
24
|
+
cat <<'EOF'
|
|
25
|
+
nostop - wrap a command so it can resume after an interruption
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
nostop <command> Run (or continue) COMMAND. On rerun of the
|
|
29
|
+
exact same command in the same directory,
|
|
30
|
+
completed checkpoints are skipped.
|
|
31
|
+
nostop batch <tpl> <e1> [<e2>...] Run TEMPLATE for each entry; {1},{2},... are the
|
|
32
|
+
colon-separated fields, {} the whole entry.
|
|
33
|
+
Completed entries are skipped on resume.
|
|
34
|
+
nostop stats [--all] List interrupted commands (cmd + working dir).
|
|
35
|
+
nostop show <id> Show a run's details and tail of its output.
|
|
36
|
+
nostop resume <id> Re-run a stored command in its original dir.
|
|
37
|
+
nostop clean [--all|<id>] Forget run state (one run or everything).
|
|
38
|
+
nostop step <name> -- <cmd...> Inside a script: run cmd once; mark 'name' done.
|
|
39
|
+
nostop mark <name> Mark checkpoint 'name' complete for this run.
|
|
40
|
+
nostop done <name> Exit 0 if 'name' already done, else 1.
|
|
41
|
+
nostop help Show this help.
|
|
42
|
+
|
|
43
|
+
Env:
|
|
44
|
+
NOSTOP_HOME override state directory (default ~/.nostop)
|
|
45
|
+
|
|
46
|
+
See README.md for the checkpoint pattern and examples (megahit, pipelines).
|
|
47
|
+
EOF
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ---------- helpers ----------
|
|
51
|
+
now_iso() { date '+%Y-%m-%d %H:%M:%S'; }
|
|
52
|
+
|
|
53
|
+
boot_id() {
|
|
54
|
+
if [ -r /proc/sys/kernel/random/boot_id ]; then
|
|
55
|
+
cat /proc/sys/kernel/random/boot_id
|
|
56
|
+
else
|
|
57
|
+
echo "unknown"
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cmd_hash() { # cmd cwd -> sha256
|
|
62
|
+
printf '%s\0%s' "$1" "$2" | sha256sum | cut -d' ' -f1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# run a list of tokens the same way bash would: a single token is a shell
|
|
66
|
+
# string (supports pipes/redirects); many tokens keep their quoting.
|
|
67
|
+
run_args() {
|
|
68
|
+
if [ "$#" -eq 1 ]; then bash -c "$1"; else bash -c '"$@"' nostop "$@"; fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_get_meta() { # file key
|
|
72
|
+
local file="$1" key="$2" line
|
|
73
|
+
[ -f "$file" ] || return 1
|
|
74
|
+
while IFS= read -r line; do
|
|
75
|
+
case "$line" in "$key="*) printf '%s' "${line#$key=}"; return 0 ;; esac
|
|
76
|
+
done < "$file"
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_set_meta() { # file key value
|
|
81
|
+
local file="$1" key="$2" val="$3" tmp line
|
|
82
|
+
tmp="$file.tmp"
|
|
83
|
+
: > "$tmp"
|
|
84
|
+
if [ -f "$file" ]; then
|
|
85
|
+
while IFS= read -r line; do
|
|
86
|
+
case "$line" in "$key="*) ;; *) printf '%s\n' "$line" >> "$tmp" ;; esac
|
|
87
|
+
done < "$file"
|
|
88
|
+
fi
|
|
89
|
+
printf '%s=%s\n' "$key" "$val" >> "$tmp"
|
|
90
|
+
mv "$tmp" "$file"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
compute_status() { # st pid bid
|
|
94
|
+
local st="$1" pid="$2" bid="$3" cur
|
|
95
|
+
cur="$(boot_id)"
|
|
96
|
+
case "$st" in
|
|
97
|
+
done) echo DONE ;;
|
|
98
|
+
failed) echo FAILED ;;
|
|
99
|
+
interrupted) echo INTERRUPTED ;;
|
|
100
|
+
running)
|
|
101
|
+
if [ "$bid" != "$cur" ]; then echo INTERRUPTED
|
|
102
|
+
elif kill -0 "$pid" 2>/dev/null; then echo RUNNING
|
|
103
|
+
else echo INTERRUPTED; fi ;;
|
|
104
|
+
*) echo "$st" ;;
|
|
105
|
+
esac
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ---------- run setup (shared by run_command and batch_cmd) ----------
|
|
109
|
+
# Sets up the run directory / meta / traps for a given command identity and
|
|
110
|
+
# exports NOSTOP_RUN_DIR etc. RUN_DIR and META become globals for the caller.
|
|
111
|
+
begin_run() {
|
|
112
|
+
local cmd="$1"
|
|
113
|
+
local cwd; cwd="$(pwd)"
|
|
114
|
+
local id; id="$(cmd_hash "$cmd" "$cwd")"
|
|
115
|
+
RUN_DIR="$RUNS_DIR/$id"
|
|
116
|
+
mkdir -p "$RUN_DIR"
|
|
117
|
+
META="$RUN_DIR/meta"
|
|
118
|
+
touch "$RUN_DIR/checkpoints"
|
|
119
|
+
|
|
120
|
+
# Refuse to launch a second instance of a still-running command.
|
|
121
|
+
if [ -f "$META" ]; then
|
|
122
|
+
local st pid
|
|
123
|
+
st="$(_get_meta "$META" status || true)"
|
|
124
|
+
pid="$(_get_meta "$META" pid || true)"
|
|
125
|
+
if [ "$st" = "done" ]; then
|
|
126
|
+
echo "[nostop] already completed successfully (run ${id:0:8}). Use 'nostop clean ${id:0:8}' to rerun." >&2
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
if [ "$st" = "running" ] && kill -0 "${pid:-}" 2>/dev/null; then
|
|
130
|
+
echo "[nostop] still running (pid $pid). Refusing to start a duplicate." >&2
|
|
131
|
+
exit 1
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
echo "id=$id"
|
|
137
|
+
echo "command=$cmd"
|
|
138
|
+
echo "cwd=$cwd"
|
|
139
|
+
echo "started=$(now_iso)"
|
|
140
|
+
echo "pid=$$"
|
|
141
|
+
echo "boot_id=$(boot_id)"
|
|
142
|
+
echo "status=running"
|
|
143
|
+
echo "argv_b64=${ORIG_ARGV_B64:-}"
|
|
144
|
+
} > "$META"
|
|
145
|
+
|
|
146
|
+
export NOSTOP_RUN_DIR="$RUN_DIR"
|
|
147
|
+
export NOSTOP_RUN_ID="$id"
|
|
148
|
+
export PATH="$(dirname "$SELF"):$PATH"
|
|
149
|
+
|
|
150
|
+
_on_exit() {
|
|
151
|
+
local s; s="$(_get_meta "$META" status 2>/dev/null || true)"
|
|
152
|
+
[ "$s" = "running" ] && _set_meta "$META" status interrupted
|
|
153
|
+
}
|
|
154
|
+
_on_sig() { _set_meta "$META" status interrupted; exit 130; }
|
|
155
|
+
trap '_on_exit' EXIT
|
|
156
|
+
trap '_on_sig' INT
|
|
157
|
+
trap '_on_sig' TERM
|
|
158
|
+
|
|
159
|
+
echo "[nostop] run ${id:0:8} | cwd=$cwd" >&2
|
|
160
|
+
echo "[nostop] cmd: $cmd" >&2
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
run_command() {
|
|
164
|
+
local cmd="$*"
|
|
165
|
+
[ -z "$cmd" ] && { usage; exit 2; }
|
|
166
|
+
begin_run "$cmd"
|
|
167
|
+
local rc=0
|
|
168
|
+
run_args "$@" 2>&1 | tee -a "$RUN_DIR/run.log"
|
|
169
|
+
rc=${PIPESTATUS[0]}
|
|
170
|
+
if [ "$rc" -eq 0 ]; then
|
|
171
|
+
_set_meta "$META" status done
|
|
172
|
+
echo "[nostop] finished successfully." >&2
|
|
173
|
+
else
|
|
174
|
+
_set_meta "$META" status failed
|
|
175
|
+
echo "[nostop] exited with code $rc." >&2
|
|
176
|
+
fi
|
|
177
|
+
exit "$rc"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ---------- batch ----------
|
|
181
|
+
# Expand a template against one entry. Entry is split on ':' into fields.
|
|
182
|
+
# {N} -> N-th field (1-indexed); {} -> the whole entry.
|
|
183
|
+
expand_template() {
|
|
184
|
+
local tpl="$1" entry="$2"
|
|
185
|
+
local IFS=':'
|
|
186
|
+
local -a fields=($entry)
|
|
187
|
+
local result="$tpl" i f n=${#fields[@]}
|
|
188
|
+
for ((i=1; i<=n; i++)); do
|
|
189
|
+
f="${fields[$((i-1))]}"
|
|
190
|
+
result="${result//\{$i\}/$f}"
|
|
191
|
+
done
|
|
192
|
+
result="${result//\{\}/$entry}"
|
|
193
|
+
printf '%s' "$result"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
batch_cmd() {
|
|
197
|
+
if [ "$#" -eq 0 ]; then echo "usage: nostop batch <template> <entry> [<entry>...]" >&2; exit 2; fi
|
|
198
|
+
local template="$1"; shift
|
|
199
|
+
[ -z "$template" ] && { echo "usage: nostop batch <template> <entry> [<entry>...]" >&2; exit 2; }
|
|
200
|
+
[ "$#" -eq 0 ] && { echo "nostop batch: no entries given" >&2; exit 2; }
|
|
201
|
+
local entries=("$@")
|
|
202
|
+
local CMD="batch $template${entries[*]:+ ${entries[*]}}"
|
|
203
|
+
begin_run "$CMD"
|
|
204
|
+
# Mirror all output (incl. nested step output) into the run log.
|
|
205
|
+
exec > >(tee -a "$RUN_DIR/run.log") 2>&1
|
|
206
|
+
local entry key concrete rc
|
|
207
|
+
for entry in "${entries[@]}"; do
|
|
208
|
+
key="$entry"
|
|
209
|
+
concrete="$(expand_template "$template" "$entry")"
|
|
210
|
+
echo "[nostop] === entry: $key ===" >&2
|
|
211
|
+
echo "[nostop] cmd : $concrete" >&2
|
|
212
|
+
# Each entry is one checkpoint: completed entries are skipped on rerun.
|
|
213
|
+
bash "$SELF" step "$key" -- "$concrete"; rc=$?
|
|
214
|
+
if [ "$rc" -ne 0 ]; then
|
|
215
|
+
echo "[nostop] entry '$key' failed (exit $rc); stopping batch so you can resume." >&2
|
|
216
|
+
_set_meta "$META" status failed
|
|
217
|
+
exit "$rc"
|
|
218
|
+
fi
|
|
219
|
+
done
|
|
220
|
+
_set_meta "$META" status done
|
|
221
|
+
echo "[nostop] batch finished successfully (${#entries[@]} entries)." >&2
|
|
222
|
+
exit 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# ---------- step / mark / done (checkpoint API) ----------
|
|
226
|
+
step_cmd() {
|
|
227
|
+
local name="$1"; shift
|
|
228
|
+
[ "$1" = "--" ] && shift
|
|
229
|
+
[ -z "$name" ] && { echo "usage: nostop step <name> -- <command...>" >&2; exit 2; }
|
|
230
|
+
local rd="${NOSTOP_RUN_DIR:-}"
|
|
231
|
+
if [ -z "$rd" ]; then
|
|
232
|
+
run_args "$@"; exit "$?" # standalone: just run, no tracking
|
|
233
|
+
fi
|
|
234
|
+
if grep -qxF "$name" "$rd/checkpoints" 2>/dev/null; then
|
|
235
|
+
echo "[nostop] skip step '$name' (already done)" >&2
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
echo "[nostop] run step '$name' ..." >&2
|
|
239
|
+
run_args "$@"; local rc=$?
|
|
240
|
+
if [ "$rc" -eq 0 ]; then
|
|
241
|
+
printf '%s\n' "$name" >> "$rd/checkpoints"
|
|
242
|
+
echo "[nostop] step '$name' done" >&2
|
|
243
|
+
else
|
|
244
|
+
echo "[nostop] step '$name' failed (exit $rc), not marked" >&2
|
|
245
|
+
fi
|
|
246
|
+
exit "$rc"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
mark_cmd() {
|
|
250
|
+
local rd="${NOSTOP_RUN_DIR:-}"
|
|
251
|
+
[ -z "$rd" ] && { echo "nostop mark: not inside a nostop run" >&2; exit 1; }
|
|
252
|
+
printf '%s\n' "$1" >> "$rd/checkpoints"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
done_cmd() {
|
|
256
|
+
local rd="${NOSTOP_RUN_DIR:-}"
|
|
257
|
+
[ -z "$rd" ] && exit 1
|
|
258
|
+
grep -qxF "$1" "$rd/checkpoints" 2>/dev/null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ---------- stats / show / resume / clean ----------
|
|
262
|
+
stats_cmd() {
|
|
263
|
+
local all=0
|
|
264
|
+
[ "${1:-}" = "--all" ] && all=1
|
|
265
|
+
local any=0
|
|
266
|
+
printf '%-10s %-12s %s\n' "ID" "STATUS" "COMMAND"
|
|
267
|
+
local m
|
|
268
|
+
for m in "$RUNS_DIR"/*/meta; do
|
|
269
|
+
[ -f "$m" ] || continue
|
|
270
|
+
local id cmd cwd st pid bid disp lastck
|
|
271
|
+
id="$(_get_meta "$m" id || true)"
|
|
272
|
+
cmd="$(_get_meta "$m" command || true)"
|
|
273
|
+
cwd="$(_get_meta "$m" cwd || true)"
|
|
274
|
+
st="$(_get_meta "$m" status || true)"
|
|
275
|
+
pid="$(_get_meta "$m" pid || true)"
|
|
276
|
+
bid="$(_get_meta "$m" boot_id || true)"
|
|
277
|
+
disp="$(compute_status "$st" "$pid" "$bid")"
|
|
278
|
+
[ "$all" -eq 0 ] && [ "$disp" = "DONE" ] && continue
|
|
279
|
+
any=1
|
|
280
|
+
lastck="$(tail -n1 "${m%/meta}/checkpoints" 2>/dev/null)"
|
|
281
|
+
printf '%-10s %-12s %s\n' "${id:0:8}" "$disp" "$cmd"
|
|
282
|
+
printf '%-10s %-12s cwd: %s\n' "" "" "$cwd"
|
|
283
|
+
[ -n "$lastck" ] && printf '%-10s %-12s last checkpoint: %s\n' "" "" "$lastck"
|
|
284
|
+
done
|
|
285
|
+
[ "$any" -eq 0 ] && echo "(no runs found)"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
show_cmd() {
|
|
289
|
+
local id="$1"; [ -z "$id" ] && { echo "usage: nostop show <id>" >&2; exit 2; }
|
|
290
|
+
local meta; meta="$(ls -d "$RUNS_DIR"/"$id"*/meta 2>/dev/null | head -1)"
|
|
291
|
+
[ -f "$meta" ] || { echo "no run matching '$id'" >&2; exit 1; }
|
|
292
|
+
cat "$meta"
|
|
293
|
+
echo "---- last output (tail) ----"
|
|
294
|
+
tail -n 20 "${meta%/meta}/run.log" 2>/dev/null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
resume_cmd() {
|
|
298
|
+
local id="$1"; [ -z "$id" ] && { echo "usage: nostop resume <id>" >&2; exit 2; }
|
|
299
|
+
local meta; meta="$(ls -d "$RUNS_DIR"/"$id"*/meta 2>/dev/null | head -1)"
|
|
300
|
+
[ -f "$meta" ] || { echo "no run matching '$id'" >&2; exit 1; }
|
|
301
|
+
local cmd cwd argv_b64 a argv
|
|
302
|
+
cmd="$(_get_meta "$meta" command || true)"
|
|
303
|
+
cwd="$(_get_meta "$meta" cwd || true)"
|
|
304
|
+
argv_b64="$(_get_meta "$meta" argv_b64 || true)"
|
|
305
|
+
cd "$cwd" 2>/dev/null || { echo "cannot cd to '$cwd'" >&2; exit 1; }
|
|
306
|
+
# Replay the exact argv if available (preserves spaces-in-args),
|
|
307
|
+
# otherwise fall back to the joined command string.
|
|
308
|
+
if [ -n "$argv_b64" ]; then
|
|
309
|
+
argv=()
|
|
310
|
+
while IFS= read -r -d '' a; do argv+=("$a"); done < <(printf '%s' "$argv_b64" | base64 -d)
|
|
311
|
+
exec "$SELF" "${argv[@]}"
|
|
312
|
+
else
|
|
313
|
+
exec "$SELF" "$cmd"
|
|
314
|
+
fi
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clean_cmd() {
|
|
318
|
+
if [ "${1:-}" = "--all" ]; then rm -rf "$RUNS_DIR"/*; echo "cleared all runs"; return; fi
|
|
319
|
+
local id="$1"; [ -z "$id" ] && { echo "usage: nostop clean [--all|<id>]" >&2; exit 2; }
|
|
320
|
+
local d; d="$(ls -d "$RUNS_DIR"/"$id"* 2>/dev/null | head -1)"
|
|
321
|
+
[ -d "$d" ] || { echo "no run matching '$id'" >&2; exit 1; }
|
|
322
|
+
rm -rf "$d"; echo "removed $d"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# ---------- dispatch ----------
|
|
326
|
+
main() {
|
|
327
|
+
ORIG_ARGV_B64="$(printf '%s\0' "$@" | base64 | tr -d '\n')"
|
|
328
|
+
case "${1:-}" in
|
|
329
|
+
""|-h|--help|help) usage; exit 0 ;;
|
|
330
|
+
stats) shift; stats_cmd "$@" ;;
|
|
331
|
+
show) shift; show_cmd "$@" ;;
|
|
332
|
+
resume) shift; resume_cmd "$@" ;;
|
|
333
|
+
clean) shift; clean_cmd "$@" ;;
|
|
334
|
+
batch) shift; batch_cmd "$@" ;;
|
|
335
|
+
step) shift; step_cmd "$@" ;;
|
|
336
|
+
mark) shift; mark_cmd "$@" ;;
|
|
337
|
+
done) shift; done_cmd "$@" ;;
|
|
338
|
+
--) shift; run_command "$@" ;;
|
|
339
|
+
*) run_command "$@" ;;
|
|
340
|
+
esac
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main "$@"
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nostop-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Resume-capable command wrapper for Linux: rerun interrupted commands and skip completed steps.
|
|
5
|
+
Author: nostop
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://example.com/nostop
|
|
8
|
+
Keywords: cli,resume,checkpoint,pipeline,bioinformatics
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Topic :: System :: Shells
|
|
12
|
+
Classifier: Programming Language :: Unix Shell
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# nostop
|
|
19
|
+
|
|
20
|
+
一个极小的 Linux 命令行工具,用于**包装并执行任意命令**,并在命令因断网、断电、
|
|
21
|
+
崩溃等任何原因中断后,能够**从中断处继续**,避免从头重跑浪费时间。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
nostop <命令> # 运行 / 续跑一条命令
|
|
25
|
+
nostop stats # 查看此前中断的命令(命令内容 + 中断时的工作目录)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 它解决什么问题
|
|
31
|
+
|
|
32
|
+
- 跑了 6 个小时的生信流程,第 5 步时断电 / 断网 / 节点被回收,重跑又要 6 小时。
|
|
33
|
+
- 写了一个多步骤的 shell 流程,中途失败,希望**已完成的步骤不要再跑**。
|
|
34
|
+
|
|
35
|
+
nostop 的价值:
|
|
36
|
+
|
|
37
|
+
1. **通用**:`nostop <任意命令>` 的行为与直接执行该命令完全一致(输出、退出码、环境都透传)。
|
|
38
|
+
2. **可续跑**:中断后再次运行**完全相同**的 `nostop <命令>`(同一工作目录),工具会识别已完成的
|
|
39
|
+
进度并跳过,从断点继续。
|
|
40
|
+
3. **可查询**:`nostop stats` 列出此前中断的命令,含命令内容与其**中断时所在路径(cwd)**,
|
|
41
|
+
方便你回到原目录重新启动。
|
|
42
|
+
4. **简约**:单个 bash 脚本,无编译、无依赖、无守护进程、无数据库;体积几 KB,运行时几乎零开销。
|
|
43
|
+
5. **支持广**:既支持“多步骤 shell 脚本”(用 checkpoint API 精确跳过步骤),也支持“单个长时间
|
|
44
|
+
运行的程序”(如 megahit 等自带断点续跑能力的二进制,nostop 负责用完全相同的方式重新拉起它)。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 安装
|
|
49
|
+
|
|
50
|
+
### 方式一:pip 安装(推荐)
|
|
51
|
+
|
|
52
|
+
本工具已打包为 Python 项目,可直接用 pip 从源码或你们内部的 PyPI 源安装:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 从本地源码目录安装
|
|
56
|
+
pip install .
|
|
57
|
+
|
|
58
|
+
# 或打包后安装(生成 dist/nostop-cli-*.whl / tar.gz)
|
|
59
|
+
pip install build
|
|
60
|
+
python -m build
|
|
61
|
+
pip install dist/nostop-cli-*.whl
|
|
62
|
+
|
|
63
|
+
# 如需发布到你们内部的 PyPI 或公开 PyPI:
|
|
64
|
+
twine upload dist/*
|
|
65
|
+
# 之后他人即可:pip install nostop-cli (命令名仍是 nostop)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
安装后 `nostop` 命令即可用(其实质是一个极薄的 Python 入口,启动后立刻 exec
|
|
69
|
+
到内置的 bash 脚本,运行时不依赖 Python)。也可用 `python -m nostop ...` 运行。
|
|
70
|
+
|
|
71
|
+
### 方式二:单文件拷贝(零依赖,连 Python 都不需要)
|
|
72
|
+
|
|
73
|
+
如果你只想用一个文件、连 Python 环境都不想依赖,直接复制内置脚本即可
|
|
74
|
+
(依赖系统自带的 bash 与 coreutils,CentOS / Ubuntu 等主流发行版默认都有):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sudo cp src/nostop/nostop.sh /usr/local/bin/nostop
|
|
78
|
+
sudo chmod +x /usr/local/bin/nostop
|
|
79
|
+
|
|
80
|
+
# 可选:自定义状态目录(默认 ~/.nostop)
|
|
81
|
+
# export NOSTOP_HOME=/data/.nostop
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
验证:`nostop help`
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 快速开始
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 像平常一样运行命令,只是前面加 nostop
|
|
92
|
+
nostop "bash my_pipeline.sh"
|
|
93
|
+
|
|
94
|
+
# 中断后,回到同一个目录,原样再敲一次即可续跑
|
|
95
|
+
nostop "bash my_pipeline.sh"
|
|
96
|
+
|
|
97
|
+
# 查看哪些命令之前中断了(含命令内容与工作目录)
|
|
98
|
+
nostop stats
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 场景一:多步骤 shell 流程(精确到步骤的跳过)
|
|
104
|
+
|
|
105
|
+
对“脚本里有多个步骤”的情况,使用 nostop 提供的 **checkpoint(检查点)API**。
|
|
106
|
+
在脚本里把每个步骤包成 `nostop step <名字> -- <命令>`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
#!/bin/bash
|
|
110
|
+
# my_pipeline.sh
|
|
111
|
+
nostop step prepare -- "echo 准备数据 && sleep 2"
|
|
112
|
+
nostop step align -- "echo 比对 && sleep 2"
|
|
113
|
+
nostop step call -- "echo 变异检测 && sleep 2"
|
|
114
|
+
nostop step report -- "echo 出报告"
|
|
115
|
+
echo "全部完成"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
运行:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
nostop "bash my_pipeline.sh"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- 第一次正常运行,每完成一步就记录一个检查点。
|
|
125
|
+
- 中途断电/被杀:再次 `nostop "bash my_pipeline.sh"` 时,已完成的步骤会被**自动跳过**
|
|
126
|
+
(输出 `[nostop] skip step 'prepare' (already done)`),直接从断点继续。
|
|
127
|
+
- 步骤失败(退出码非 0)不会被标记为完成,下次会自动重跑该步骤。
|
|
128
|
+
|
|
129
|
+
> 如果你的步骤本身是一条带管道 / 重定向的复杂命令,可以写成单个字符串:
|
|
130
|
+
> `nostop step build -- "make && make test | tee build.log"`。
|
|
131
|
+
|
|
132
|
+
手动控制检查点(更灵活):
|
|
133
|
+
|
|
134
|
+
| 写法 | 作用 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `nostop step <名字> -- <命令>` | 运行命令;成功则标记 `<名字>` 完成,下次跳过 |
|
|
137
|
+
| `nostop done <名字>` | 退出码 0 表示该检查点已完成,否则 1(用于 `if` 判断) |
|
|
138
|
+
| `nostop mark <名字>` | 直接把 `<名字>` 标记为完成 |
|
|
139
|
+
|
|
140
|
+
例如在脚本里手写分支:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
if nostop done align; then
|
|
144
|
+
echo "比对已完成,跳过"
|
|
145
|
+
else
|
|
146
|
+
do_align && nostop mark align
|
|
147
|
+
fi
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> 注意:checkpoint API 只在 `nostop <命令>` 内部才记录进度;直接 `bash my_pipeline.sh`
|
|
151
|
+
> 运行时它会退化为“直接执行”,不影响结果,只是不记录进度。
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 场景二:单个长时间运行的程序(如 megahit)
|
|
156
|
+
|
|
157
|
+
很多生信程序(megahit、Trinity、SPAdes 等)**自身就支持断点续跑**——只要用相同的输出目录
|
|
158
|
+
重新运行,它们会接着上次的产物继续。nostop 在这里的作用是:**用完全相同的方式重新拉起命令,
|
|
159
|
+
并替你记录状态 / 方便重跑**。
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 第一次运行
|
|
163
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
164
|
+
|
|
165
|
+
# 中断后,回到同一目录,原样再敲一次;megahit 会基于 mega.out 里已有的中间文件续跑
|
|
166
|
+
nostop "megahit -1 read1.fq -2 read2.fq -o mega.out"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
nostop 本身对“单个二进制内部进度”无感知(那是程序自己的事),但它保证:
|
|
170
|
+
- 用**完全相同的命令字符串与工作目录**重启;
|
|
171
|
+
- 把这次运行登记到状态库,供 `nostop stats` 查看;
|
|
172
|
+
- 程序成功跑完(退出码 0)后记为 `DONE`,之后原样再跑会被直接跳过,不会重复计算。
|
|
173
|
+
|
|
174
|
+
> 如果程序需要显式续跑参数(例如某些工具要用 `--continue` / `-c`),把它写进命令里即可,
|
|
175
|
+
> 因为 nostop 是“原样重放”。
|
|
176
|
+
|
|
177
|
+
## 场景三:批处理(nostop batch)
|
|
178
|
+
|
|
179
|
+
当同一道命令要对多组输入分别跑一遍(例如多个样本分别做 fastp / spades),用 `nostop batch`
|
|
180
|
+
一次性提交。它把每条输入展开到模板里,并把**每条输入当作一个检查点**——中断后续跑时,
|
|
181
|
+
已完成的样本会被自动跳过。
|
|
182
|
+
|
|
183
|
+
模板占位符:
|
|
184
|
+
|
|
185
|
+
- `{N}`:第 N 个字段(从 1 起),输入按 `:` 切分。例 `s1_1.fq:s1_2.fq` → `{1}=s1_1.fq`、`{2}=s1_2.fq`。
|
|
186
|
+
- `{}`:整条输入原文(含 `:`)。
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# 双端配对({1}/{2} 占位)
|
|
190
|
+
nostop batch "fastp -i {1} -I {2} -o {1}.clean.fq -O {2}.clean.fq" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
191
|
+
|
|
192
|
+
# 工具无续跑参数时,结合幂等写法最稳(重跑时已完成的组装直接跳过)
|
|
193
|
+
nostop batch "[ -s asm_{}/final.contigs.fa ] || spades.py -1 {1} -2 {2} -o asm_{}" s1_1.fq:s1_2.fq s2_1.fq:s2_2.fq
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- 每条输入即 `nostop step <输入>` 检查点,状态同样出现在 `nostop stats`。
|
|
197
|
+
- 某个样本失败,batch 会停下(不再跑后续),修好后重跑即可;已完成样本自动跳过。
|
|
198
|
+
- `{}` 是整条输入原文(如 `s1_1.fq:s1_2.fq`),故 `asm_{}` 目录名会带冒号(Linux 下合法);
|
|
199
|
+
想要干净目录名时,把样本名单独作为条目传入、用 `{1}/{2}` 拼文件路径即可。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 子命令
|
|
204
|
+
|
|
205
|
+
| 命令 | 说明 |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `nostop <命令>` | 运行 / 续跑一条命令 |
|
|
208
|
+
| `nostop batch <模板> <条目>...` | 对每条条目展开模板并分别运行,已完成的跳过 |
|
|
209
|
+
| `nostop stats [--all]` | 列出**中断 / 失败 / 正在运行**的命令;`--all` 同时显示已完成的 |
|
|
210
|
+
| `nostop show <id>` | 查看某条运行的详情(命令、目录、开始时间、尾部输出) |
|
|
211
|
+
| `nostop resume <id>` | 在原始工作目录下,重新运行该命令(不用再手敲整条命令) |
|
|
212
|
+
| `nostop clean [--all|<id>]` | 清除某条(或全部)运行的状态;用于需要强制重跑时 |
|
|
213
|
+
| `nostop step/mark/done <名字>` | 见上文 checkpoint API |
|
|
214
|
+
| `nostop help` | 帮助 |
|
|
215
|
+
|
|
216
|
+
`stats` 示例输出:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
ID STATUS COMMAND
|
|
220
|
+
97ecb77e INTERRUPTED bash my_pipeline.sh
|
|
221
|
+
cwd: /home/user/project
|
|
222
|
+
last checkpoint: align
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `ID` 是状态库里的短标识,可直接用于 `nostop show <id>` / `nostop resume <id>` / `nostop clean <id>`。
|
|
226
|
+
- `STATUS` 取值:`RUNNING`(仍在跑) / `INTERRUPTED`(被中断,最常见:断电、被杀、崩溃) /
|
|
227
|
+
`FAILED`(命令自身非零退出) / `DONE`(已成功完成)。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 工作原理(如何检测“中断”)
|
|
232
|
+
|
|
233
|
+
状态保存在 `$NOSTOP_HOME/runs/<hash>/`,其中 `<hash>` 由 **命令字符串 + 当前工作目录** 计算。
|
|
234
|
+
每次运行写入 `meta` 文件,记录 `status`(初始 `running`)、`pid`、`boot_id`(来自
|
|
235
|
+
`/proc/sys/kernel/random/boot_id`)、命令、目录等。
|
|
236
|
+
|
|
237
|
+
- 命令**成功退出** -> `status=done`;**非零退出** -> `status=failed`。
|
|
238
|
+
- 若进程被 **SIGINT/SIGTERM** 终止 -> 捕获后记为 `interrupted`。
|
|
239
|
+
- 若机器**断电 / 崩溃**(nostop 来不及响应)-> `meta` 仍停留在 `running`,
|
|
240
|
+
且记录的 `pid` 已不存在(或 `boot_id` 与本次启动不一致)-> 查询时判定为 `INTERRUPTED`。
|
|
241
|
+
|
|
242
|
+
因此 `nostop stats` 能可靠区分“正在跑”、“被中断”、“已成功”、“已失败”。
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 环境变量
|
|
247
|
+
|
|
248
|
+
| 变量 | 默认值 | 说明 |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `NOSTOP_HOME` | `~/.nostop` | 状态库根目录,可指向大容量磁盘 |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 兼容性与限制
|
|
255
|
+
|
|
256
|
+
- **兼容性**:纯 bash + coreutils(`sha256sum`/`date`/`kill`/`tee`),CentOS 7/8、Ubuntu 20.04/22.04
|
|
257
|
+
等主流发行版开箱即用;仅依赖 Linux 的 `/proc/sys/kernel/random/boot_id`(缺失时退化为仅按 PID 判断)。
|
|
258
|
+
- **限制**:
|
|
259
|
+
- 对“单个不自带续跑能力的黑盒二进制”,nostop 只能保证原样重放,无法替它在内部续跑;
|
|
260
|
+
这类场景请用脚本 + `nostop step` 拆成可重入的步骤,或选用自带续跑的程序。
|
|
261
|
+
- 检查点跳过依赖**步骤本身是可重入 / 幂等**的;请确保重试某步不会造成破坏。
|
|
262
|
+
- 同一“命令 + 目录”被视为同一次运行;若想以不同参数再跑一份,请换目录或改命令。
|
|
263
|
+
- **已知命令已 DONE 时**再原样运行会被直接跳过(输出提示)。需要强制重跑请先 `nostop clean <id>`。
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 文件结构
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
nostop # 工具本体(单文件,拷贝即用)
|
|
271
|
+
README.md # 本文档
|
|
272
|
+
examples/ # 示例脚本
|
|
273
|
+
pipeline.sh # 场景一:带 checkpoint 的多步骤流程
|
|
274
|
+
megahit.sh # 场景二:单个长时间程序续跑示例
|
|
275
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/nostop/__init__.py
|
|
6
|
+
src/nostop/__main__.py
|
|
7
|
+
src/nostop/cli.py
|
|
8
|
+
src/nostop/nostop.sh
|
|
9
|
+
src/nostop_cli.egg-info/PKG-INFO
|
|
10
|
+
src/nostop_cli.egg-info/SOURCES.txt
|
|
11
|
+
src/nostop_cli.egg-info/dependency_links.txt
|
|
12
|
+
src/nostop_cli.egg-info/entry_points.txt
|
|
13
|
+
src/nostop_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nostop
|