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.
@@ -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,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include src/nostop *.sh
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,4 @@
1
+ from nostop.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ nostop = nostop.cli:main