tbtools-cli 1.2.0__py3-none-any.whl
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.
- tbtools_cli/__init__.py +18 -0
- tbtools_cli/auto_commands.py +1149 -0
- tbtools_cli/cli.py +528 -0
- tbtools_cli/cli_load.py +352 -0
- tbtools_cli/cli_rpc.py +303 -0
- tbtools_cli/cli_tools_registry.py +93 -0
- tbtools_cli/cli_top.py +1160 -0
- tbtools_cli/command_metadata.json +4606 -0
- tbtools_cli/command_spec.py +382 -0
- tbtools_cli/config.example.toml +19 -0
- tbtools_cli/config.py +43 -0
- tbtools_cli/core.py +508 -0
- tbtools_cli/errors.py +38 -0
- tbtools_cli/presets.py +100 -0
- tbtools_cli/scenarios.py +92 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AdmixtureCli.java +91 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AmazingMetaCli.java +78 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/AssemblyRecommandCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamIndexCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamSortCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BamStateCli.java +49 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarPlotterCli.java +18 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BarplotCli.java +168 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BatchVizMotifsCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlastXmlConvertCli.java +45 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/BlatExecutorCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalcRepeatCli.java +62 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CalculateSimilarityCli.java +34 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CddMotifCli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircleGeneViewerCli.java +81 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CircosCli.java +76 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ColorSchemeCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CtgGroupCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/CubeHeatmapCli.java +70 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DeHistCli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DegramdomCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DiffExpCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DistanceCli.java +53 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/DualSynCli.java +147 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/EggnogCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ExprCorrCli.java +27 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaMergerCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FastaTableConvertCli.java +38 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileCleanerCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FileSplitCli.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockDualCli.java +71 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindBlockMultipleCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/FindPathCli.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GSEAWrapperCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenBank2FastaCli.java +32 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneDensityCli.java +38 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneLocGffCli.java +140 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GeneStructureCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GenericCli.java +177 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetENALinksCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GetSubNewickTreeCli.java +50 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoEnrichCli.java +42 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GoLevelCli.java +59 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GroupedBarCli.java +95 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GsaDiagCli.java +45 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfFilterCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/GxfSortCli.java +23 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HclustCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HeatmapCli.java +89 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/HmmerSuiteCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/JgblocksCli.java +65 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/KeggEnrichCli.java +31 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/LayoutHeatmapCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXCli.java +80 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MCScanXFastCli.java +41 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MSACli.java +46 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MakeMotifCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerDesignCli.java +62 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MarkerToolsCli.java +57 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Mast2TabCli.java +27 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastCli.java +54 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MastRunCli.java +39 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Meme2TabCli.java +26 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeCli.java +74 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MemeRunCli.java +40 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MgGxfCli.java +35 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MicroSynCli.java +118 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MirIdentifyCli.java +55 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifCli.java +75 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifPatternCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MotifShiftCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MountainPlotCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/MultiSuperHeatCli.java +64 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/NeedlemanWunschCli.java +76 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafGC.java +15 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PafVizCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PeakDistCli.java +58 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Pep2CodonCli.java +26 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PfamMotifCli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PhyloTreeCli.java +92 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PileUpCli.java +55 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PlantCAREResultClassifyCli.java +36 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/PubmedSearchCli.java +25 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrCli.java +69 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrDdctCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QpcrProcCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickGenomeDotCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickProteinAnnoCli.java +34 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickRunIQtreeCli.java +69 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/QuickTrimALCli.java +67 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RNAplotCli.java +129 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionBedToGFF3Cli.java +43 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/RegionDepthCli.java +29 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SamBamCovCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqConverterCli.java +17 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeqLenTrackCli.java +41 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SeveralSpeciesCli.java +132 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleEnricherCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SimpleHmmscanCli.java +32 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SixFrameTranlaterCli.java +35 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/StructAnnoCompareCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SubmitSMARTCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/SuperCircosCli.java +330 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableColManipCli.java +56 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TableCollapseCli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TargetScoreCli.java +47 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TauCalcCli.java +28 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TaxonomyBatchCli.java +72 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeCli.java +75 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TreeRootingCli.java +30 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/TrimMSACli.java +33 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UnrootedTreeCli.java +44 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/UpSetCli.java +90 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn5Cli.java +60 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/Venn6Cli.java +65 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/ViolinCli.java +66 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VisualizeCli.java +57 -0
- tbtools_cli-1.2.0.data/data/tbtools_cli/bridges/VizGFACli.java +39 -0
- tbtools_cli-1.2.0.dist-info/METADATA +504 -0
- tbtools_cli-1.2.0.dist-info/RECORD +139 -0
- tbtools_cli-1.2.0.dist-info/WHEEL +5 -0
- tbtools_cli-1.2.0.dist-info/entry_points.txt +2 -0
- tbtools_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- tbtools_cli-1.2.0.dist-info/top_level.txt +1 -0
tbtools_cli/cli_load.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""cli_load.py — 动态命令注册(架构重构批次 B:从 cli.py 拆分)
|
|
2
|
+
|
|
3
|
+
CATEGORY_MAP/GROUPS 分组映射 + _load_auto_commands/_load_dynamic_commands +
|
|
4
|
+
_make_passthrough(metadata 驱动)+ ToolGroup 兜底注册。
|
|
5
|
+
由 cli.py 在 cli 定义后调用 build_and_load(cli) 完成装配。
|
|
6
|
+
"""
|
|
7
|
+
import difflib
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from tbtools_cli import auto_commands as _ac
|
|
17
|
+
from tbtools_cli.core import ROOT, _, check_input_format, get_pitfall_hint, validate_file
|
|
18
|
+
from tbtools_cli.presets import PRESETS, apply_preset
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---- 动态加载剩余命令 ----
|
|
22
|
+
# ---- 命令分类映射 ----
|
|
23
|
+
CATEGORY_MAP = {
|
|
24
|
+
"sixframe": "seq", # GUI 逆向:六框翻译
|
|
25
|
+
"longestorf": "seq", # GUI 逆向:最长 ORF 预测
|
|
26
|
+
"protparam": "seq", # GUI 逆向:蛋白理化性质
|
|
27
|
+
"genomefilter": "seq", # GUI 逆向:序列长度过滤
|
|
28
|
+
"seqpattern": "seq", # GUI 逆向:序列模式定位
|
|
29
|
+
"bed2gff3": "gxf", # GUI 逆向:BED→GFF3
|
|
30
|
+
"careclassify": "seq", # GUI 逆向:PlantCARE 元件分类
|
|
31
|
+
"protsim": "seq", # GUI 逆向:蛋白相似度矩阵
|
|
32
|
+
"xml2blasttab": "blast", "xml2pairwise": "blast", # GUI 逆向:BLAST XML 转换
|
|
33
|
+
"fa2tab": "seq", "tab2fa": "seq", # GUI 逆向:FASTA↔表
|
|
34
|
+
"muscle": "seq", # GUI 逆向:MUSCLE 比对(系统二进制)
|
|
35
|
+
"trimal": "seq", # GUI 逆向:trimAl 修剪
|
|
36
|
+
"gblocks": "seq", # GUI 逆向:Gblocks 修剪
|
|
37
|
+
"bestid": "blast", # GUI 逆向:最优 ID 转换
|
|
38
|
+
"fasplit": "seq", "famerge": "seq", # GUI 逆向:FASTA 拆/合
|
|
39
|
+
"clearchar": "table", # GUI 逆向:非法字符清理
|
|
40
|
+
"getseqdb": "blast", # GUI 逆向:BLAST 库提序列
|
|
41
|
+
"gb2fa": "seq", # GUI 逆向:GenBank→FASTA
|
|
42
|
+
"findhomolog": "blast", # GUI 逆向:最优同源
|
|
43
|
+
"taxparse": "table", # GUI 逆向:物种分类解析
|
|
44
|
+
"srr2ena": "table", # GUI 逆向:SRR→ENA 链接
|
|
45
|
+
"sraxml2tab": "table", # GUI 逆向:SRA XML→表
|
|
46
|
+
"sranum2info": "table", # GUI 逆向:SRR 信息表
|
|
47
|
+
"blat": "blast", # GUI 逆向:BLAT 比对
|
|
48
|
+
"seqrecommend": "engine", # GUI 逆向:测序量推荐
|
|
49
|
+
"seqfetch": "seq", # GUI 逆向:NCBI 序列下载
|
|
50
|
+
"pubmed": "table", # GUI 逆向:PubMed 检索
|
|
51
|
+
# 序列/结构/域
|
|
52
|
+
"genestructure": "seq", "motif": "seq", "msa": "seq", "seqlentrack": "seq",
|
|
53
|
+
"amazingmeta": "seq", "cddmotif": "seq", "pfammotif": "seq", "memerun": "seq",
|
|
54
|
+
"mastrun": "seq", "mastExtract": "seq", "mast2tab": "seq", "pep2codon": "seq",
|
|
55
|
+
"simplehmmscan": "seq", "gel": "seq", "gfa": "seq", "gfa2fa": "seq",
|
|
56
|
+
# 表达/统计
|
|
57
|
+
"pca": "expr", "hclust": "expr", "qpcr": "expr", "qpcrExp": "expr",
|
|
58
|
+
"groupedbar": "expr", "dehist": "expr", "barplot": "expr", "barplotter": "expr",
|
|
59
|
+
"layoutheatmap": "expr", "cubeheatmap": "expr", "violin": "expr",
|
|
60
|
+
"colorscheme": "expr", "distance": "expr", "mountain": "expr",
|
|
61
|
+
"tauIndex": "expr", "exprCorr": "expr", "groupCol": "expr",
|
|
62
|
+
"kaks": "tree", # GUI 逆向:成对 Ka/Ks
|
|
63
|
+
"subtree": "tree", # GUI 逆向:子树提取
|
|
64
|
+
"iqtree": "tree", # GUI 逆向:IQ-TREE 建树
|
|
65
|
+
# 树/进化
|
|
66
|
+
"phylotree": "tree", "unrooted": "tree", "treeRooting": "tree",
|
|
67
|
+
"onesteptree": "tree", "degramdom": "tree", "findpath": "tree",
|
|
68
|
+
"nwAlign": "tree",
|
|
69
|
+
# 共线性/基因组
|
|
70
|
+
"circos": "syn", "supercircos": "syn", "circlegene": "syn", "dotplot": "syn",
|
|
71
|
+
"microsyn": "syn", "msy": "syn", "multisyn": "syn", "dualsyn": "syn",
|
|
72
|
+
"pafviz": "syn", "pafcomp": "syn", "pafref": "syn",
|
|
73
|
+
"mcscanx": "syn", "collinearRegion": "syn",
|
|
74
|
+
"findblockdual": "syn", "findblockmultiple": "syn", "visualizeblock": "syn",
|
|
75
|
+
"conflictpaf": "syn", "partitionconflict": "syn",
|
|
76
|
+
"microgenome": "syn",
|
|
77
|
+
# 集合/ChIP
|
|
78
|
+
"venn2": "sets", "venn3": "sets", "venn4": "sets",
|
|
79
|
+
"venn5": "sets", "venn6": "sets", "upset": "sets",
|
|
80
|
+
"peaktss": "chipseq", "peakdist": "chipseq", "peakanno": "chipseq",
|
|
81
|
+
"pileup": "chipseq",
|
|
82
|
+
# 组装/注释
|
|
83
|
+
"ctgGroup": "asm", "homoPhase": "asm", "sepChr": "asm",
|
|
84
|
+
"bamMerge": "asm", "bamindex": "asm", "bamsort": "asm", "bamstate": "asm",
|
|
85
|
+
"hicEnzyme": "asm", "virusRecomb": "asm", "preparespecies": "asm",
|
|
86
|
+
"gxfRename": "gxf", "gxfStat": "gxf", "gxfAppend": "gxf", "gxfGenepos": "gxf",
|
|
87
|
+
"gxfSplit": "gxf", "gxfIdAppender": "gxf",
|
|
88
|
+
"gxfRegion": "gxf", "gxfFix": "gxf", "gxfOverlap": "gxf", "gxfRepIDs": "gxf",
|
|
89
|
+
"gxfRepGXF": "gxf", "gxfMatch": "gxf", "gxfRecall": "gxf",
|
|
90
|
+
"regionAnno": "gxf", "annocompare": "gxf", "genedensity": "gxf",
|
|
91
|
+
"genelocation": "gxf", "genelocgff": "gxf", "gxfSort": "gxf",
|
|
92
|
+
# miRNA
|
|
93
|
+
"mirnatarget": "mirna", "mirnaTarget2": "mirna", "mirnaIdentify": "mirna",
|
|
94
|
+
# GO/表格
|
|
95
|
+
"levelGo": "table", "goParse": "table", "batchReplace": "table",
|
|
96
|
+
"goAnno": "table", # GUI 逆向:GO 注释管道
|
|
97
|
+
"goEnrich": "table", "keggEnrich": "table",
|
|
98
|
+
"tableCollapse": "table", "tableColSelect": "table", "tableAppend": "table",
|
|
99
|
+
"tableMelt": "table", "tableColSel": "table", "tableCast": "table",
|
|
100
|
+
"tableUniq": "table", "tableTranspose": "table", "tableSplit": "table",
|
|
101
|
+
"tableMerge": "table",
|
|
102
|
+
# BLAST/比对
|
|
103
|
+
"recipBlast": "blast", "filterCScore": "blast", "quickFamily": "blast",
|
|
104
|
+
"twoSeqBlast": "blast",
|
|
105
|
+
# FASTQ
|
|
106
|
+
"fqTrim": "fastq", "fqfaConv": "fastq", "fastaSubseq": "fastq",
|
|
107
|
+
"fastaExtract": "fastq",
|
|
108
|
+
# HMM
|
|
109
|
+
"hmmExtract": "hmm", "hmmsearch": "hmm",
|
|
110
|
+
"gxfAttr": "gxf", "gdensity": "gxf",
|
|
111
|
+
"notung": "tree",
|
|
112
|
+
"newickRename": "tree",
|
|
113
|
+
"hmmerSearch": "hmm",
|
|
114
|
+
"memeViz": "seq",
|
|
115
|
+
"gsea": "table", "gbar": "expr", "golevel": "table", "sricher": "table",
|
|
116
|
+
"tfbsShift": "seq",
|
|
117
|
+
"kallisto": "expr",
|
|
118
|
+
"mcscanxd": "syn",
|
|
119
|
+
"qdot": "syn",
|
|
120
|
+
"quickAnno": "blast",
|
|
121
|
+
"smart": "seq",
|
|
122
|
+
"fimo": "seq", "meme": "seq", "mast": "seq", "meme2tab": "seq", "makemotif": "seq", "mpattern": "seq",
|
|
123
|
+
# GWAS
|
|
124
|
+
"mimicVqsr": "gwas", "vcfAddID": "gwas",
|
|
125
|
+
# 通用
|
|
126
|
+
"generic": "engine", "efpHeat": "expr", "multiEfp": "expr",
|
|
127
|
+
"plotrna": "seq", "rnaplot": "seq",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# 分组定义
|
|
131
|
+
GROUPS = {
|
|
132
|
+
"seq": "序列/结构/域",
|
|
133
|
+
"expr": "表达/统计",
|
|
134
|
+
"tree": "树/进化",
|
|
135
|
+
"syn": "共线性/基因组",
|
|
136
|
+
"sets": "集合/韦恩",
|
|
137
|
+
"chipseq": "ChIP-seq",
|
|
138
|
+
"asm": "组装/注释",
|
|
139
|
+
"gxf": "GXF/表格",
|
|
140
|
+
"mirna": "miRNA",
|
|
141
|
+
"table": "GO/表格",
|
|
142
|
+
"blast": "BLAST/比对",
|
|
143
|
+
"fastq": "FASTQ/FASTA",
|
|
144
|
+
"hmm": "HMM",
|
|
145
|
+
"gwas": "GWAS",
|
|
146
|
+
"engine": "通用",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# 全局分组表(build_and_load → _ensure_groups(cli) 填充;cli.py/list 等 import 使用)
|
|
150
|
+
_groups: dict[str, click.Group] = {}
|
|
151
|
+
def _load_auto_commands():
|
|
152
|
+
"""从 auto_commands.py 加载所有命令到对应分组(弥补 tbplot.sh 遗漏的命令)"""
|
|
153
|
+
skip = {"seqlogo", "msa", "motif", "genestructure", # seq manual
|
|
154
|
+
"volcano", "heatmap2", "pca", "hclust", "dehist", # expr manual
|
|
155
|
+
"tree", "unrooted", "treeRooting", "onesteptree", # tree manual
|
|
156
|
+
"version", "doctor", "examples", "list", "presets", # top commands
|
|
157
|
+
"seq", "expr", "tool", # group names
|
|
158
|
+
"chipseq", "sets", "syn", "asm", "gxf", "mirna", "table",
|
|
159
|
+
"blast", "fastq", "hmm", "gwas", "engine"}
|
|
160
|
+
for name in sorted(dir(_ac)):
|
|
161
|
+
if name.startswith('_') and name.endswith('_impl') and not name.startswith('__'):
|
|
162
|
+
cmd = name[1:-5]
|
|
163
|
+
if cmd in skip:
|
|
164
|
+
continue
|
|
165
|
+
cat = CATEGORY_MAP.get(cmd, "engine")
|
|
166
|
+
target = _groups.get(cat)
|
|
167
|
+
if target and cmd not in target.commands:
|
|
168
|
+
_make_passthrough(cmd, target)
|
|
169
|
+
|
|
170
|
+
def _load_dynamic_commands():
|
|
171
|
+
"""从 tbplot.sh 动态生成 click 命令,按分类注册到 group"""
|
|
172
|
+
tbplot_sh = os.path.join(ROOT, "bin", "tbplot.sh")
|
|
173
|
+
if not os.path.isfile(tbplot_sh):
|
|
174
|
+
return
|
|
175
|
+
with open(tbplot_sh) as f:
|
|
176
|
+
content = f.read()
|
|
177
|
+
cmds = set(re.findall(r'^ ([a-zA-Z][a-zA-Z0-9]+)\)$', content, re.MULTILINE))
|
|
178
|
+
# 已迁移命令的原始名(不动态转发)——用 tbplot.sh 里的原始命令名
|
|
179
|
+
# 只排除真正有 @xxx.command 手动注册的命令 + group 名
|
|
180
|
+
registered = {"seqlogo", "msa", "motif", "genestructure", # seq
|
|
181
|
+
"volcano", "heatmap2", "pca", "hclust", "dehist", # expr
|
|
182
|
+
"tree", "unrooted", "treeRooting", "onesteptree", # tree
|
|
183
|
+
"version", "doctor", "examples", "seq", "expr", "tool",
|
|
184
|
+
"chipseq", "sets", "syn", "asm", "gxf", "mirna", "table",
|
|
185
|
+
"blast", "fastq", "hmm", "gwas", "engine"}
|
|
186
|
+
for cmd_name in sorted(cmds - registered):
|
|
187
|
+
cat = CATEGORY_MAP.get(cmd_name, "engine")
|
|
188
|
+
_make_passthrough(cmd_name, _groups[cat])
|
|
189
|
+
|
|
190
|
+
# 给所有分组加未知子命令纠错(ToolGroup/rpc 已有自己的 resolve_command)
|
|
191
|
+
def _smart_resolve(self, ctx, args):
|
|
192
|
+
try:
|
|
193
|
+
return click.Group.resolve_command(self, ctx, args)
|
|
194
|
+
except click.UsageError:
|
|
195
|
+
if not args:
|
|
196
|
+
raise
|
|
197
|
+
name = args[0]
|
|
198
|
+
cmds = list(self.commands.keys())
|
|
199
|
+
# 前缀优先(venn2 案例:get_close_matches n=3 按相似度排序会挤掉正确建议)
|
|
200
|
+
prefix_hits = [c for c in cmds if c.startswith(name)]
|
|
201
|
+
close = prefix_hits[:5] or difflib.get_close_matches(name, cmds, n=3, cutoff=0.6)
|
|
202
|
+
if close:
|
|
203
|
+
click.echo(_("❌ '{n}' 不是 '{g}' 分组内的命令", "❌ '{n}' is not a command in group '{g}'").format(n=name, g=self.name), err=True)
|
|
204
|
+
click.echo(f" 你是不是想用: {' / '.join(close)}?", err=True)
|
|
205
|
+
else:
|
|
206
|
+
click.echo(_("❌ '{n}' 不是 '{g}' 分组内的命令", "❌ '{n}' is not a command in group '{g}'").format(n=name, g=self.name), err=True)
|
|
207
|
+
click.echo(f" 查看: tbtools {self.name} --help", err=True)
|
|
208
|
+
ctx.exit(2)
|
|
209
|
+
for gname, g in _groups.items():
|
|
210
|
+
if gname in ("tool", "rpc"):
|
|
211
|
+
continue
|
|
212
|
+
if not hasattr(g, "resolve_command") or g.__class__.__name__ == "Group":
|
|
213
|
+
g.resolve_command = _smart_resolve.__get__(g, type(g))
|
|
214
|
+
|
|
215
|
+
_META_JSON = None
|
|
216
|
+
def _load_meta_json():
|
|
217
|
+
"""懒加载 command_metadata.json(N33: 143 命令完整 help,含 150 可选位)"""
|
|
218
|
+
global _META_JSON
|
|
219
|
+
if _META_JSON is None:
|
|
220
|
+
try:
|
|
221
|
+
import json as _json
|
|
222
|
+
_p = os.path.join(ROOT, "tbtools_cli", "command_metadata.json")
|
|
223
|
+
_META_JSON = _json.load(open(_p, encoding="utf-8")) if os.path.isfile(_p) else {}
|
|
224
|
+
except Exception:
|
|
225
|
+
_META_JSON = {}
|
|
226
|
+
return _META_JSON
|
|
227
|
+
|
|
228
|
+
def _parse_auto_metadata(name):
|
|
229
|
+
"""从 auto_commands.py 解析命令元数据(docstring + 坑位)"""
|
|
230
|
+
impl = _ac._IMPL_REGISTRY.get(name) or getattr(_ac, f'_{name}_impl', None) # 显式注册表优先(第四轮评审)
|
|
231
|
+
doc = (impl.__doc__ or "").strip() if impl else ""
|
|
232
|
+
# N33: 优先 command_metadata.json 完整 help(含可选位;docstring 常被表驱动截断)
|
|
233
|
+
_meta = _load_meta_json().get(name)
|
|
234
|
+
if _meta and _meta.get("help"):
|
|
235
|
+
usage = _meta["help"]
|
|
236
|
+
elif ':' in doc:
|
|
237
|
+
usage = doc.split(':', 1)[1].strip()
|
|
238
|
+
else:
|
|
239
|
+
usage = f"{name} [参数...]"
|
|
240
|
+
pitfall = get_pitfall_hint(name)
|
|
241
|
+
return {'impl': impl, 'usage': usage, 'pitfall': pitfall}
|
|
242
|
+
|
|
243
|
+
def _make_passthrough(name, group=None):
|
|
244
|
+
"""生成元数据驱动的 click 命令(help + 校验 + 预设 + 直调 Java)"""
|
|
245
|
+
meta = _parse_auto_metadata(name)
|
|
246
|
+
impl = meta['impl']
|
|
247
|
+
usage = meta['usage']
|
|
248
|
+
pitfall = meta['pitfall']
|
|
249
|
+
|
|
250
|
+
help_text = usage
|
|
251
|
+
if pitfall:
|
|
252
|
+
help_text += f"\n\n⚠️ {pitfall}"
|
|
253
|
+
|
|
254
|
+
_target = group # 调用方总传 group
|
|
255
|
+
|
|
256
|
+
def _cmd_impl(ctx, verbose, quiet, fmt, preset, height, width, threads):
|
|
257
|
+
args = list(ctx.args)
|
|
258
|
+
|
|
259
|
+
# 输入校验:第一个非选项参数通常是输入文件(mcscanxd/kallisto 首参为工作目录/自定义路径,跳过校验)
|
|
260
|
+
if args and not args[0].startswith('-') and name not in ("mcscanxd", "kallisto", "famerge", "getseqdb", "seqrecommend", "pubmed", "tableMerge", "generic", "preparespecies", "marker", "markertools", "venn5", "venn6"):
|
|
261
|
+
ok, msg = validate_file(args[0], f"{name} 输入文件")
|
|
262
|
+
if not ok:
|
|
263
|
+
print(msg, file=sys.stderr)
|
|
264
|
+
sys.exit(2)
|
|
265
|
+
# C2: 早期格式不匹配警告(不阻断,仅提示)
|
|
266
|
+
warn = check_input_format(name, args[0])
|
|
267
|
+
if warn:
|
|
268
|
+
print(f"⚠️ 格式提醒: {warn}", file=sys.stderr)
|
|
269
|
+
print(" (继续执行;如确认无误可忽略)", file=sys.stderr)
|
|
270
|
+
if preset:
|
|
271
|
+
p = apply_preset(preset, width=width, height=height)
|
|
272
|
+
if not p:
|
|
273
|
+
print(f"❌ 未知预设: {preset}", file=sys.stderr)
|
|
274
|
+
print(f" 可用: {', '.join(PRESETS.keys())}", file=sys.stderr)
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
if 'width' in p and not width:
|
|
277
|
+
width = p['width']
|
|
278
|
+
if 'height' in p and not height:
|
|
279
|
+
height = p['height']
|
|
280
|
+
|
|
281
|
+
# 格式覆盖:替换输出文件扩展名
|
|
282
|
+
if fmt:
|
|
283
|
+
for i in range(len(args) - 1, -1, -1):
|
|
284
|
+
if args[i].endswith(('.svg', '.png', '.pdf')):
|
|
285
|
+
base = os.path.splitext(args[i])[0]
|
|
286
|
+
args[i] = f"{base}.{fmt}"
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
# 追加 width/height 到参数末尾(大多数命令接受 [w] [h] 位置参数)
|
|
290
|
+
if width:
|
|
291
|
+
args.append(str(width))
|
|
292
|
+
if height:
|
|
293
|
+
args.append(str(height))
|
|
294
|
+
|
|
295
|
+
# 调用 impl 或回退到 bash tbplot.sh
|
|
296
|
+
if impl:
|
|
297
|
+
ec = impl(args, verbose=verbose, quiet=quiet)
|
|
298
|
+
else:
|
|
299
|
+
# 兜底走 bash tbplot.sh(Windows 需 Git Bash;无 bash 时报清晰错误)
|
|
300
|
+
bash_bin = shutil.which("bash")
|
|
301
|
+
if bash_bin:
|
|
302
|
+
bash_args = [bash_bin, os.path.join(ROOT, "bin", "tbplot.sh"), name] + list(ctx.args)
|
|
303
|
+
ec = subprocess.run(bash_args).returncode
|
|
304
|
+
else:
|
|
305
|
+
print(f"❌ {name} 需要 bash 兜底(tbplot.sh),但未找到 bash(Windows 请装 Git Bash)", file=sys.stderr)
|
|
306
|
+
ec = 1
|
|
307
|
+
sys.exit(ec)
|
|
308
|
+
|
|
309
|
+
# 先设置 docstring,再装饰
|
|
310
|
+
_cmd_impl.__doc__ = help_text
|
|
311
|
+
_cmd_impl = click.pass_context(_cmd_impl)
|
|
312
|
+
for opt_args, opt_kwargs in [
|
|
313
|
+
(("--verbose", "-V"), {"is_flag": True, "default": False, "help": "显示完整堆栈"}),
|
|
314
|
+
(("--quiet", "-q"), {"is_flag": True, "default": False, "help": "静默模式"}),
|
|
315
|
+
(("--format", "-f", "fmt"), {"default": None, "help": "输出格式: svg|png|pdf"}),
|
|
316
|
+
(("--preset", "-p"), {"default": None, "help": "出版预设: nature|cell|plant_journal|wide|poster"}),
|
|
317
|
+
(("--height", "-H"), {"type": int, "default": None, "help": "画布高度"}),
|
|
318
|
+
(("--width", "-W"), {"type": int, "default": None, "help": "画布宽度"}),
|
|
319
|
+
(("--threads", "-t"), {"type": int, "default": None, "help": "线程数"}),
|
|
320
|
+
]:
|
|
321
|
+
_cmd_impl = click.option(*opt_args, **opt_kwargs)(_cmd_impl) # type: ignore[arg-type]
|
|
322
|
+
|
|
323
|
+
# 提取 click.option 装饰器注册的参数(__click_params__)
|
|
324
|
+
params = getattr(_cmd_impl, '__click_params__', [])
|
|
325
|
+
params = params[::-1] # click 处理顺序是反的
|
|
326
|
+
|
|
327
|
+
cmd = click.Command(name=name, callback=_cmd_impl, params=params,
|
|
328
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
329
|
+
help=help_text)
|
|
330
|
+
_target.add_command(cmd)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def build_and_load(cli):
|
|
334
|
+
"""组装: 建 groups → 动态注册 auto_commands + tbplot.sh 遗留 → 返回 groups dict"""
|
|
335
|
+
_ensure_groups(cli)
|
|
336
|
+
_load_auto_commands()
|
|
337
|
+
_load_dynamic_commands()
|
|
338
|
+
return _groups
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _ensure_groups(cli):
|
|
342
|
+
"""按 GROUPS 定义补齐分组(复用装饰器已创建的;原地填充保持 _groups 引用不变)"""
|
|
343
|
+
global _groups
|
|
344
|
+
_groups.clear()
|
|
345
|
+
for key, desc in GROUPS.items():
|
|
346
|
+
if key in cli.commands:
|
|
347
|
+
_groups[key] = cli.commands[key]
|
|
348
|
+
else:
|
|
349
|
+
g = click.Group(name=key, help=desc)
|
|
350
|
+
cli.add_command(g, name=key)
|
|
351
|
+
_groups[key] = g
|
|
352
|
+
# 子命令纠错绑定在 _load_dynamic_commands 尾部完成(_smart_resolve)
|
tbtools_cli/cli_rpc.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""cli_rpc.py — RPC 服务器管理(架构重构批次 B:从 cli.py 拆分)
|
|
2
|
+
|
|
3
|
+
rpc 分组 + N34/N35 自愈基础设施(pid 文件/健康探针/自动拉起)+ N38 错误兜底。
|
|
4
|
+
由 cli.py 以 `cli_rpc.build_rpc_group()` 注册。
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time as _t
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from tbtools_cli.core import JAR
|
|
16
|
+
from tbtools_cli.core import _ as _tr
|
|
17
|
+
|
|
18
|
+
@click.group('rpc')
|
|
19
|
+
def rpc_group():
|
|
20
|
+
"""RPC 服务器管理(188 方法)"""
|
|
21
|
+
|
|
22
|
+
# ---------- RPC 自愈基础设施(N34/N35 批次 2)----------
|
|
23
|
+
# 症状:引擎进程内存/空闲期自发死亡;代理层把死亡伪装成 502;CLI 无感知、无自愈。
|
|
24
|
+
# 方案:pid 文件 + 健康探针(system.listMethods)+ call/methods 前 ensure 自动拉起。
|
|
25
|
+
# 注意:urllib 必须绕过代理(N41:HTTP_PROXY 注入会让 127.0.0.1 请求走代理转发失败)。
|
|
26
|
+
|
|
27
|
+
def _rpc_state_dir():
|
|
28
|
+
d = os.environ.get(
|
|
29
|
+
"TBTOOLS_RPC_DIR",
|
|
30
|
+
os.path.join(os.path.expanduser("~"), ".config", "tbtools-cli"),
|
|
31
|
+
)
|
|
32
|
+
os.makedirs(d, exist_ok=True)
|
|
33
|
+
return d
|
|
34
|
+
|
|
35
|
+
def _rpc_pid_file(port):
|
|
36
|
+
return os.path.join(_rpc_state_dir(), f"rpc-{port}.pid")
|
|
37
|
+
|
|
38
|
+
def _rpc_log_file(port):
|
|
39
|
+
return os.path.join(_rpc_state_dir(), f"rpc-{port}.log")
|
|
40
|
+
|
|
41
|
+
def _rpc_ping(port, timeout=5):
|
|
42
|
+
"""健康探针:POST system.listMethods。绕过代理。返回 True/False"""
|
|
43
|
+
try:
|
|
44
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
45
|
+
req = urllib.request.Request(
|
|
46
|
+
f"http://127.0.0.1:{port}/rpc",
|
|
47
|
+
data=json.dumps({"jsonrpc": "2.0", "method": "system.listMethods",
|
|
48
|
+
"params": {}, "id": 1}).encode(),
|
|
49
|
+
headers={"Content-Type": "application/json"})
|
|
50
|
+
resp = opener.open(req, timeout=timeout)
|
|
51
|
+
result = json.loads(resp.read())
|
|
52
|
+
return "result" in result
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def _rpc_read_pid(port):
|
|
57
|
+
"""读 pid 文件;进程不存在或与 RPC 无关则清理并返回 None"""
|
|
58
|
+
pid_file = _rpc_pid_file(port)
|
|
59
|
+
try:
|
|
60
|
+
with open(pid_file) as f:
|
|
61
|
+
pid = int(f.read().strip())
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
os.kill(pid, 0)
|
|
66
|
+
except (ProcessLookupError, PermissionError, OverflowError, ValueError):
|
|
67
|
+
_rpc_remove_pid(port)
|
|
68
|
+
return None
|
|
69
|
+
# Linux 下确认 cmdline 是 RPC server(防 pid 复用误杀)
|
|
70
|
+
cmdline = f"/proc/{pid}/cmdline"
|
|
71
|
+
if os.path.isfile(cmdline):
|
|
72
|
+
try:
|
|
73
|
+
with open(cmdline, "rb") as f:
|
|
74
|
+
if b"biocjava.rpc.RpcServer" not in f.read():
|
|
75
|
+
_rpc_remove_pid(port)
|
|
76
|
+
return None
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
return pid
|
|
80
|
+
|
|
81
|
+
def _rpc_write_pid(port, pid):
|
|
82
|
+
with open(_rpc_pid_file(port), "w") as f:
|
|
83
|
+
f.write(str(pid))
|
|
84
|
+
|
|
85
|
+
def _rpc_remove_pid(port):
|
|
86
|
+
try:
|
|
87
|
+
os.remove(_rpc_pid_file(port))
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def _rpc_launch(port, mem):
|
|
92
|
+
"""拉起 RPC 服务器(detached,OOM 崩溃转储,日志落盘)。返回 Popen"""
|
|
93
|
+
if not JAR or not os.path.isfile(JAR):
|
|
94
|
+
raise FileNotFoundError(
|
|
95
|
+
"TBtools jar 未找到。请设置 TBTOOLS_JAR 环境变量或放入常见位置")
|
|
96
|
+
log_path = _rpc_log_file(port)
|
|
97
|
+
log_fh = open(log_path, "ab", buffering=0)
|
|
98
|
+
args = [
|
|
99
|
+
"java", f"-Xmx{mem}",
|
|
100
|
+
# N35: OOM 时宁可崩溃(可自愈拉起)也不要僵尸悬挂;同时留堆转储供排查
|
|
101
|
+
"-XX:+CrashOnOutOfMemoryError",
|
|
102
|
+
"-XX:+HeapDumpOnOutOfMemoryError",
|
|
103
|
+
f"-XX:HeapDumpPath={_rpc_state_dir()}",
|
|
104
|
+
"-cp", JAR, "biocjava.rpc.RpcServer",
|
|
105
|
+
]
|
|
106
|
+
proc = subprocess.Popen(
|
|
107
|
+
args, stdout=log_fh, stderr=log_fh, start_new_session=True)
|
|
108
|
+
_rpc_write_pid(port, proc.pid)
|
|
109
|
+
return proc
|
|
110
|
+
|
|
111
|
+
def _ensure_rpc(port, mem="4g", wait_s=30, quiet=False):
|
|
112
|
+
"""ensure 逻辑(同交付包 run_p*.py 的 ensure_srv):
|
|
113
|
+
健康 → True;不健康/死亡 → 清 stale pid → 拉起 → 轮询健康。"""
|
|
114
|
+
if _rpc_ping(port):
|
|
115
|
+
return True
|
|
116
|
+
old_pid = _rpc_read_pid(port)
|
|
117
|
+
if old_pid:
|
|
118
|
+
if not quiet:
|
|
119
|
+
click.echo(f"⚠️ 检测到旧 RPC 进程 (PID {old_pid}) 无响应,终止后拉起新实例...", err=True)
|
|
120
|
+
try:
|
|
121
|
+
os.kill(old_pid, 15)
|
|
122
|
+
except OSError:
|
|
123
|
+
pass
|
|
124
|
+
_rpc_remove_pid(port)
|
|
125
|
+
_t.sleep(1)
|
|
126
|
+
elif not quiet:
|
|
127
|
+
click.echo(_tr("⚠️ RPC 服务器不可达(端口 {p}),自动拉起...", "⚠️ RPC server unreachable (port {p}) — auto-restarting...").format(p=port), err=True)
|
|
128
|
+
try:
|
|
129
|
+
_rpc_launch(port, mem)
|
|
130
|
+
except FileNotFoundError as e:
|
|
131
|
+
click.echo(f"❌ {e}", err=True)
|
|
132
|
+
return False
|
|
133
|
+
for _ in range(wait_s):
|
|
134
|
+
_t.sleep(1)
|
|
135
|
+
if _rpc_ping(port):
|
|
136
|
+
return True
|
|
137
|
+
click.echo(_tr("❌ RPC 服务器 {s}s 内未就绪,日志: {log}", "❌ RPC server not ready within {s}s, log: {log}").format(s=wait_s, log=_rpc_log_file(port)), err=True)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
@rpc_group.command('start')
|
|
141
|
+
@click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
|
|
142
|
+
@click.option('--mem', '-m', default='4g', help='Java 堆内存')
|
|
143
|
+
@click.option('--force', '-f', is_flag=True, help='强制重启(杀掉已有实例)')
|
|
144
|
+
def rpc_start(port, mem, force):
|
|
145
|
+
"""启动 RPC 服务器(pid 文件 + 健康检查;已在跑则幂等返回)"""
|
|
146
|
+
if _rpc_ping(port):
|
|
147
|
+
if not force:
|
|
148
|
+
pid = _rpc_read_pid(port)
|
|
149
|
+
click.echo(_tr("✅ RPC 服务器已在运行(端口 {p}, PID {pid})", "✅ RPC server already running (port {p}, PID {pid})").format(p=port, pid=pid or '?'))
|
|
150
|
+
return
|
|
151
|
+
old = _rpc_read_pid(port)
|
|
152
|
+
click.echo(f"🔄 --force:终止旧实例 (PID {old or '?'})...")
|
|
153
|
+
if old:
|
|
154
|
+
try:
|
|
155
|
+
os.kill(old, 15)
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
_rpc_remove_pid(port)
|
|
159
|
+
_t.sleep(1)
|
|
160
|
+
else:
|
|
161
|
+
# 清 stale(引擎自发死亡残留)
|
|
162
|
+
old = _rpc_read_pid(port)
|
|
163
|
+
if old:
|
|
164
|
+
click.echo(f"⚠️ 旧 RPC 进程 (PID {old}) 无响应,终止...")
|
|
165
|
+
try:
|
|
166
|
+
os.kill(old, 15)
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
_rpc_remove_pid(port)
|
|
170
|
+
_t.sleep(1)
|
|
171
|
+
click.echo(_tr("🚀 启动 RPC 服务器(端口 {p},堆 {m})...", "🚀 Starting RPC server (port {p}, heap {m})...").format(p=port, m=mem))
|
|
172
|
+
try:
|
|
173
|
+
proc = _rpc_launch(port, mem)
|
|
174
|
+
except FileNotFoundError as e:
|
|
175
|
+
click.echo(f"❌ {e}", err=True)
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
for _ in range(30):
|
|
178
|
+
_t.sleep(1)
|
|
179
|
+
if _rpc_ping(port):
|
|
180
|
+
click.echo(f"✅ RPC 服务器就绪 (PID {proc.pid})")
|
|
181
|
+
click.echo(f" pid 文件: {_rpc_pid_file(port)}")
|
|
182
|
+
click.echo(f" 日志: {_rpc_log_file(port)}")
|
|
183
|
+
click.echo(f" 测试: curl -X POST http://127.0.0.1:{port}/rpc -H 'Content-Type: application/json' -d '{{\"method\":\"system.listMethods\",\"params\":{{}},\"id\":1}}'")
|
|
184
|
+
return
|
|
185
|
+
click.echo(f"❌ 启动超时(30s),日志: {_rpc_log_file(port)}", err=True)
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@rpc_group.command('logs')
|
|
190
|
+
@click.option('--tail', 'n', type=int, default=50, help='显示末尾 N 行(0=全部)')
|
|
191
|
+
@click.option('--port', default=8765, help='RPC 端口')
|
|
192
|
+
def logs(n, port):
|
|
193
|
+
"""查看 RPC 服务器日志(尾部 N 行;0=全部)"""
|
|
194
|
+
import os as _os
|
|
195
|
+
log = _rpc_log_file(port)
|
|
196
|
+
if not _os.path.isfile(log):
|
|
197
|
+
click.echo(f"❌ 无日志文件(服务器未启动过?): {log}", err=True)
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
if n <= 0:
|
|
200
|
+
click.echo(open(log, encoding="utf-8", errors="replace").read())
|
|
201
|
+
else:
|
|
202
|
+
lines = open(log, encoding="utf-8", errors="replace").read().splitlines()
|
|
203
|
+
click.echo("\n".join(lines[-n:]))
|
|
204
|
+
|
|
205
|
+
@rpc_group.command('stop')
|
|
206
|
+
@click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
|
|
207
|
+
def rpc_stop(port):
|
|
208
|
+
"""停止 RPC 服务器"""
|
|
209
|
+
pid = _rpc_read_pid(port)
|
|
210
|
+
if not pid:
|
|
211
|
+
click.echo(f"RPC 服务器未在运行(端口 {port})")
|
|
212
|
+
_rpc_remove_pid(port)
|
|
213
|
+
return
|
|
214
|
+
try:
|
|
215
|
+
os.kill(pid, 15)
|
|
216
|
+
click.echo(_tr("✅ 已发送 SIGTERM (PID {p})", "✅ SIGTERM sent (PID {p})").format(p=pid))
|
|
217
|
+
except OSError as e:
|
|
218
|
+
click.echo(f"❌ 终止失败: {e}", err=True)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
_rpc_remove_pid(port)
|
|
221
|
+
|
|
222
|
+
@rpc_group.command('status')
|
|
223
|
+
@click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
|
|
224
|
+
def rpc_status(port):
|
|
225
|
+
"""查看 RPC 服务器状态"""
|
|
226
|
+
pid = _rpc_read_pid(port)
|
|
227
|
+
healthy = _rpc_ping(port)
|
|
228
|
+
if healthy:
|
|
229
|
+
click.echo(f"✅ 运行中(端口 {port}, PID {pid or '?'})")
|
|
230
|
+
elif pid:
|
|
231
|
+
click.echo(f"⚠️ 进程存在 (PID {pid}) 但健康检查失败(可能正在启动或假死)")
|
|
232
|
+
sys.exit(2)
|
|
233
|
+
else:
|
|
234
|
+
click.echo(f"❌ 未运行(端口 {port})。启动: tbtools rpc start")
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
@rpc_group.command('methods')
|
|
238
|
+
@click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
|
|
239
|
+
@click.option('--mem', '-m', default='4g', help='--autostart 时的 Java 堆内存')
|
|
240
|
+
@click.option('--autostart', is_flag=True, help='服务不可达时自动拉起(默认不启动进程——发现操作应 side-effect free)')
|
|
241
|
+
def rpc_methods(port, mem, autostart):
|
|
242
|
+
"""列出全部 188 RPC 方法(默认静态发现, 不启动服务器; --autostart 时自动拉起)"""
|
|
243
|
+
if autostart and not _ensure_rpc(port, mem):
|
|
244
|
+
click.echo(" 手动启动: tbtools rpc start", err=True)
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
try:
|
|
247
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
248
|
+
req = urllib.request.Request(
|
|
249
|
+
f"http://127.0.0.1:{port}/rpc",
|
|
250
|
+
data=json.dumps({"jsonrpc": "2.0", "method": "system.listMethods", "params": {}, "id": 1}).encode(),
|
|
251
|
+
headers={"Content-Type": "application/json"})
|
|
252
|
+
resp = opener.open(req, timeout=15)
|
|
253
|
+
result = json.loads(resp.read())
|
|
254
|
+
res = result.get('result', [])
|
|
255
|
+
methods = res.get('methods', res) if isinstance(res, dict) else res
|
|
256
|
+
click.echo(f"RPC 方法({len(methods)} 个):")
|
|
257
|
+
for m in methods:
|
|
258
|
+
click.echo(f" {m}")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
click.echo(f"❌ RPC 调用失败: {e}", err=True)
|
|
261
|
+
click.echo(" 引擎可能已死,尝试: tbtools rpc start --force", err=True)
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
|
|
264
|
+
@rpc_group.command('call')
|
|
265
|
+
@click.argument('method')
|
|
266
|
+
@click.argument('params', required=False)
|
|
267
|
+
@click.option('--port', '-p', type=int, default=8765, help='RPC 端口')
|
|
268
|
+
@click.option('--mem', '-m', default='4g', help='自动拉起时的 Java 堆内存')
|
|
269
|
+
@click.option('--timeout', '-t', type=int, default=300, help='调用超时(秒)')
|
|
270
|
+
@click.option('--no-autostart', is_flag=True, help='禁用在不可达时自动拉起')
|
|
271
|
+
def rpc_call(method, params, port, mem, timeout, no_autostart):
|
|
272
|
+
"""调用 RPC 方法(服务不可达时自动拉起)"""
|
|
273
|
+
params_obj = json.loads(params) if params else {}
|
|
274
|
+
if not no_autostart and not _ensure_rpc(port, mem):
|
|
275
|
+
click.echo(" 手动启动: tbtools rpc start", err=True)
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
try:
|
|
278
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
279
|
+
req = urllib.request.Request(
|
|
280
|
+
f"http://127.0.0.1:{port}/rpc",
|
|
281
|
+
data=json.dumps({"jsonrpc": "2.0", "method": method, "params": params_obj, "id": 1}).encode(),
|
|
282
|
+
headers={"Content-Type": "application/json"})
|
|
283
|
+
resp = opener.open(req, timeout=timeout)
|
|
284
|
+
result = json.loads(resp.read())
|
|
285
|
+
if result.get('error'):
|
|
286
|
+
# N38: 引擎错误 message 空/占位符时补友好提示(GffReconstructorBatch 等家族)
|
|
287
|
+
err = result['error']
|
|
288
|
+
msg = str(err.get('data', {}).get('message') if isinstance(err.get('data'), dict) else err.get('data') or err.get('message') or '')
|
|
289
|
+
if not msg.strip() or msg.strip() == '===== See Following Info =====' or msg.startswith('Something Error'):
|
|
290
|
+
msg = '引擎内部错误且未提供消息(N38 家族,GffReconstructorBatch/BestIdConverter/ReciprocalBlast 已知)'
|
|
291
|
+
click.echo(f"❌ RPC 错误 [{err.get('code')}]: {msg}(原始: {json.dumps(err, ensure_ascii=False)[:200]})", err=True)
|
|
292
|
+
else:
|
|
293
|
+
click.echo(f"❌ RPC 错误 [{err.get('code')}]: {msg}", err=True)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
click.echo(json.dumps(result.get('result', ''), indent=2, ensure_ascii=False))
|
|
296
|
+
except Exception as e:
|
|
297
|
+
click.echo(f"❌ RPC 调用失败: {e}", err=True)
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def build_rpc_group():
|
|
302
|
+
"""构建 rpc 分组(由主 CLI 注册)"""
|
|
303
|
+
return rpc_group
|