mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
用户这一轮报了两个问题,都修了。
== 一、Win7 上语音播报不能用 ==
用户原话:"我 win7 上的语音播报好像不能用,你再试一下别的版本能不能用"
**原来只有一条路**:
powershell -NoProfile -WindowStyle Hidden -Command
"Add-Type -AssemblyName System.Speech; $s = New-Object ...; $s.Speak('...')"
这在 Win7 上有三个坑,任何一个都能让它彻底不出声:
1. **`powershell` 只按名字找**
Win7 的 powershell.exe 在 `%SystemRoot%\System32\WindowsPowerShell\v1.0\`。
精简版系统、或从服务/计划任务拉起时 PATH 里可能没有 ——
CreateProcess 按名字找不到时是**静默失败**的。
2. **Win7 默认 PowerShell 2.0,它按 ANSI 代码页解析 `-Command`**
PS 3.0 起才按 UTF-16 处理。中文经 `-Command` 传进去容易乱码 ——
表现是念出来是乱码,或者干脆不念。
3. **`System.Speech` 需要 .NET Framework 3.0+**
而系统自带的 **SAPI5** 从 Windows XP 就有,**根本不需要 .NET**。
**改成多方案降级**,按"依赖越少越优先"排:
1. cscript + VBScript + SAPI5 <- Win98 起就有,不用装任何东西
2. wscript + VBScript + SAPI5 <- 上一条被安全软件拦时的备选
3. powershell + SAPI5 (COM) <- 绕开 .NET
4. powershell + System.Speech <- 原来的做法,改用绝对路径
5. powershell(PATH) + System.Speech <- 最原始的,兜底
一条通了**记住它**,以后直接用(不用每次从头试)。
**顺带修掉一个"骗人"的行为**:原来 `say()` 用 `Popen` 不等进程结束,
失败了也返回"已播报" —— 明明没出声却报成功。
现在等退出码,**全失败会明确报错并列出试过哪几条**。
代价是同步等待(念多久等多久),但"报成功其实没出声"比"慢一点"糟糕得多。
还加了 `_build/diag_tts.py`:在一台机器上把 5 条方案全试一遍并报告
哪条能用 —— 下次再有"某台机器上不行",跑一下就知道。
VBScript 的细节:**必须用 ANSI(mbcs) 写**,写成 UTF-8 的话
cscript 读出来中文是乱码(VBScript 的老规矩,很隐蔽)。
长文本还要分句 —— VBScript 单行有长度上限。
== 二、快捷栏按钮太小 + 4 格都要能换 ==
用户原话:"屏幕最上面这个按钮有些按不到,按钮太小了,做大一点,
然后呢,这个每每个四格算换一下吧,第一个第二个第三个
第四个都能换东西"
* **按钮做大**:30dp -> 46dp(接近 Android 建议的最小可点区域 48),
字号 11 -> 14。30dp 在表盘上手指按上去一半在栏外。
**栏高做成常量 QUICK_BAR_DP,栏高 / 面板 topMargin / addView
三处用同一个值** —— 分三处写的话改一个忘一个,
表现是"内容被盖住"或"上面空一条"。
* **4 格全部可自定义**。用户最早说"其中有一个按钮不能改,
功能就是一键返回桌面",第一版就按"第 4 格写死 desktop"做的。
现在他改主意了 —— 所以 desktop 变成**普通可选动作**,
默认仍放第 4 格(习惯不用改),但想换也能换。
可选动作从 7 个变成 8 个。
**教训(写进测试注释了)**:别把"用户当时的一句话"当成永久约束
焊进结构里;真是约束的话,注释里要写清是什么时候的要求,
下次他改主意时才知道该动哪儿。
== 三、修一个"测试自己没擦干净"造成的假 bug ==
全量回归报:
[FAIL] 配对码正确写入 实际 '(旧的残留值)'
而 `(旧的残留值)` 是**上一次测试遗留**的配对码 —— 数据目录
`C:\ProgramData\ykxmz\data` 在卸载时**故意不删**(那里存着用户的
中转地址、配对码等,卸载不该删),所以机器上会留着上一轮的
settings.json。测试装的时候虽然传了 `/CODE=517284`,但环境没擦干净时
就可能读到旧值 —— 表现成"安装包有 bug"。
**修法**:`test_installer_cycle.py` 加一步"装前先清残留"
(正规卸载 -> 杀进程 -> 删安装目录 -> **删数据目录**)。
数据目录那一步是关键 —— 卸载器不删它。
== 四、其他 ==
* `docs/项目规矩.md` 第一节改成**按预案执行**:
预案一 = 做完测试没问题就上传;预案二 = 做完测试但不上传,直接给用户。
用户没说预案几时默认按预案一,并在交付时说明。
* 顺手把那份文档里的老域名 `relay.xxmxxs.top` 改成现在的
`yk.xxmxxs.top`(老服务器早就退役了)。
* 修掉自己造的重复定义:往 actions.py 插降级链时,
`list_voices` / `say` 各插了两份(pyflakes 报 redefinition)。
* 一次危险的误操作:`Path.write_text(..., newline='\n')` 会**先截断文件
再校验参数**,参数非法时文件已经空了 —— 我用它把
`_tests/test_quickbar.py` 清空了。已重写。
**以后不给 write_text 传 newline。**
== 验证 ==
全量回归(21 关)全部 PASS
新增关卡:3a13 手表快捷栏 / 3a14 语音播报降级
安装包周期 23 项全过(装 -> 验 -> 卸 -> 再验)
Java 编译通过(android-34, --release 8)
本机实测语音播报:cscript 那条通了(2.4 秒)
全部产物重建,Win7 兼容校验通过
385 lines
16 KiB
Python
385 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''把语音播报改成**多条方案降级**,修 Win7 上用不了的问题。
|
||
|
||
== 用户反馈 ==
|
||
|
||
"我 win7 上的语音播报好像不能用,你再试一下别的版本能不能用"
|
||
|
||
== 原来只有一条路,而且有几个 Win7 特有的坑 ==
|
||
|
||
powershell -NoProfile -WindowStyle Hidden -Command
|
||
"Add-Type -AssemblyName System.Speech; $s = New-Object ...; $s.Speak('...')"
|
||
|
||
坑一:**`powershell` 只按名字找**
|
||
Win7 的 powershell.exe 在 `%SystemRoot%\System32\WindowsPowerShell\v1.0\`。
|
||
精简版系统、或者从服务/计划任务拉起时 PATH 里可能没有 ——
|
||
找不到就**静默失败**(Popen 不报错,因为进程起来了,只是在里面失败)。
|
||
|
||
坑二:**Win7 默认 PowerShell 2.0,它按 ANSI 代码页解析 `-Command`**
|
||
PS 3.0 起才按 UTF-16 处理。所以中文经 `-Command` 传进去在 PS 2.0 上
|
||
很容易乱码 —— 表现是"念出来是乱码"或者干脆不念。
|
||
|
||
坑三:**`System.Speech` 需要 .NET Framework 3.0+**
|
||
而系统自带的 **SAPI5**(`SAPI.SpVoice`)从 Windows XP 就有,
|
||
**根本不需要 .NET**。绕开 .NET 更稳。
|
||
|
||
== 现在的做法:降级链 ==
|
||
|
||
按"依赖越少越优先"排:
|
||
|
||
1. **cscript + VBScript + SAPI.SpVoice**
|
||
VBScript 从 Windows 98 就有,SAPI5 是系统组件 ——
|
||
两个都不需要装东西,也不受 PowerShell 版本 / PATH / .NET 影响。
|
||
**这是最可能在 Win7 上通的一条。**
|
||
|
||
2. **wscript + VBScript + SAPI.SpVoice**
|
||
同上,换宿主(cscript 万一被安全软件拦了)。
|
||
|
||
3. **powershell(绝对路径)+ SAPI.SpVoice COM**
|
||
绕开 .NET,但仍要 PowerShell。
|
||
|
||
4. **powershell(绝对路径)+ System.Speech**
|
||
原来的做法,只是改用绝对路径。
|
||
|
||
5. **powershell(按 PATH)+ System.Speech**
|
||
最原始的做法,留作最后兜底。
|
||
|
||
**一条通了就记住它**(模块级缓存),后面都直接走那条 ——
|
||
不然每播一次都要把上面几条全试一遍,慢且难看。
|
||
|
||
== 顺手改进 ==
|
||
|
||
* `say()` 原来用 `Popen` 不等待 —— **失败了也返回"已播报"**。
|
||
现在**等它结束并看退出码**,真失败会明确报错,不再骗人。
|
||
代价是播报变成了同步的(要等念完)—— 但"报成功其实没出声"
|
||
比"慢一点"糟糕得多。
|
||
* VBScript 文件用 **mbcs(ANSI)** 编码写。写成 UTF-8 的话
|
||
cscript 读出来是乱码 —— 这是 VBScript 的老规矩。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
ACTIONS = ROOT / "windows" / "lanremote" / "actions.py"
|
||
|
||
# ---------------------------------------------------------------- 要替换的旧代码
|
||
OLD_VOICES = ''' def list_voices(self) -> list[str]:
|
||
"""列出系统可用的 TTS 音色。"""
|
||
if not IS_WINDOWS:
|
||
return []
|
||
script = (
|
||
"Add-Type -AssemblyName System.Speech;"
|
||
"(New-Object System.Speech.Synthesis.SpeechSynthesizer)"
|
||
".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }"
|
||
)
|
||
try:
|
||
proc = subprocess.run(
|
||
["powershell", "-NoProfile", "-Command", script],
|
||
capture_output=True, timeout=20,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
encoding="mbcs", errors="replace")
|
||
except Exception:
|
||
return []
|
||
return [line.strip() for line in (proc.stdout or "").splitlines() if line.strip()]'''
|
||
|
||
OLD_SAY = ''' safe = text.replace("'", "''")
|
||
parts = [
|
||
"Add-Type -AssemblyName System.Speech;",
|
||
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;",
|
||
]
|
||
if chosen:
|
||
parts.append(f"try {{ $s.SelectVoice('{chosen.replace(chr(39), chr(39) * 2)}') }} "
|
||
"catch { };")
|
||
if speed:
|
||
parts.append(f"$s.Rate = {speed};")
|
||
parts.append(f"$s.Speak('{safe}')")
|
||
script = "".join(parts)
|
||
|
||
try:
|
||
subprocess.Popen(
|
||
["powershell", "-NoProfile", "-WindowStyle", "Hidden",
|
||
"-Command", script],
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||
except Exception as exc:
|
||
raise ActionError(f"TTS 启动失败: {exc}") from exc
|
||
return f"已播报({chosen or '默认音色'})"'''
|
||
|
||
NEW = ''' # ------------------------------------------------------------------
|
||
# 语音播报(系统自带 TTS)
|
||
# ------------------------------------------------------------------
|
||
#
|
||
# ⚠ 这里有一套**降级链**,不是随便写的。原因见文件末尾的说明,
|
||
# 简单说:Win7 上的 PowerShell 2.0 + 中文 + PATH 三样都可能出问题,
|
||
# 而 cscript + SAPI5 是最老最稳的组合(从 XP 就有)。
|
||
#
|
||
# 顺序:cscript -> wscript -> ps(绝对路径)+SAPI COM
|
||
# -> ps(绝对路径)+System.Speech -> ps(PATH)+System.Speech
|
||
|
||
def _powershell_path(self) -> str:
|
||
"""找 powershell.exe。**先按绝对路径**,PATH 只作兜底。
|
||
|
||
为什么:Win7 上 PATH 里不一定有,而 CreateProcess 按名字找不到时
|
||
是**静默失败**的(进程根本没起来,但 Popen 不报错)。
|
||
"""
|
||
import shutil as _sh
|
||
cand = Path(os.environ.get("SystemRoot", r"C:\\Windows")) / \\
|
||
"System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe"
|
||
if os.name == "nt" and cand.is_file():
|
||
return str(cand)
|
||
return _sh.which("powershell") or ""
|
||
|
||
def _write_vbs(self, text: str, voice: str, rate: int) -> Path:
|
||
"""生成一个播报用的 .vbs。
|
||
|
||
⚠ 必须用 **mbcs(系统 ANSI 代码页)** 写 ——
|
||
cscript 读 .vbs 是按 ANSI 读的,写成 UTF-8 中文就是乱码。
|
||
"""
|
||
import tempfile
|
||
lines = ['Dim v', 'Set v = CreateObject("SAPI.SpVoice")']
|
||
if voice:
|
||
# 按描述匹配音色。找不到就 silently 用默认 ——
|
||
# 音色选不上不该让整条播报失败。
|
||
lines.append('Dim i')
|
||
lines.append('For i = 0 To v.GetVoices().Count - 1')
|
||
lines.append(' If InStr(v.GetVoices().Item(i).GetDescription(), "%s") > 0 Then'
|
||
% voice.replace('"', '""'))
|
||
lines.append(' Set v.Voice = v.GetVoices().Item(i)')
|
||
lines.append(' End If')
|
||
lines.append('Next')
|
||
if rate:
|
||
# SAPI 的 Rate 范围是 -10..10,跟我们的设置一致
|
||
lines.append("v.Rate = %d" % max(-10, min(10, rate)))
|
||
# 长文本 VBScript 单行有长度限制,所以**分句拼接**
|
||
chunk = text.replace('"', '""').replace("\\r", " ").replace("\\n", " ")
|
||
while chunk:
|
||
piece, chunk = chunk[:400], chunk[400:]
|
||
lines.append('v.Speak "%s"' % piece)
|
||
path = Path(tempfile.gettempdir()) / "ykxmz_say.vbs"
|
||
path.write_bytes(("\\r\\n".join(lines) + "\\r\\n").encode("mbcs",
|
||
errors="replace"))
|
||
return path
|
||
|
||
def _tts_commands(self, text: str, voice: str, rate: int) -> list:
|
||
"""按"依赖越少越优先"排出所有可行方案,返回 [(名字, 命令行)]。"""
|
||
cmds = []
|
||
vbs = None
|
||
try:
|
||
vbs = self._write_vbs(text, voice, rate)
|
||
except Exception: # noqa: BLE001
|
||
vbs = None
|
||
if vbs is not None:
|
||
# 1 / 2:完全不依赖 PowerShell 和 .NET
|
||
cmds.append(("cscript", ["cscript", "//nologo", str(vbs)]))
|
||
cmds.append(("wscript", ["wscript", "//nologo", str(vbs)]))
|
||
|
||
ps = self._powershell_path()
|
||
esc = text.replace("'", "''")
|
||
if ps:
|
||
# 3:绕开 .NET,只用系统原生 SAPI5
|
||
sapi = ["$v = New-Object -ComObject SAPI.SpVoice;"]
|
||
if voice:
|
||
sapi.append("foreach ($o in $v.GetVoices()) { "
|
||
"if ($o.GetDescription() -like '*%s*') { "
|
||
"$v.Voice = $o } };" % voice.replace("'", "''"))
|
||
if rate:
|
||
sapi.append("$v.Rate = %d;" % max(-10, min(10, rate)))
|
||
sapi.append("$v.Speak('%s')" % esc)
|
||
cmds.append(("ps-sapi", [ps, "-NoProfile", "-WindowStyle", "Hidden",
|
||
"-Command", "".join(sapi)]))
|
||
|
||
# 4:原来的做法,但用绝对路径
|
||
sp = ["Add-Type -AssemblyName System.Speech;",
|
||
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;"]
|
||
if voice:
|
||
sp.append("try { $s.SelectVoice('%s') } catch { };"
|
||
% voice.replace("'", "''"))
|
||
if rate:
|
||
sp.append("$s.Rate = %d;" % max(-10, min(10, rate)))
|
||
sp.append("$s.Speak('%s')" % esc)
|
||
cmds.append(("ps-speech", [ps, "-NoProfile", "-WindowStyle",
|
||
"Hidden", "-Command", "".join(sp)]))
|
||
|
||
# 5:最原始的(PATH + System.Speech),兜底
|
||
import shutil as _sh
|
||
if _sh.which("powershell"):
|
||
sp = ["Add-Type -AssemblyName System.Speech;",
|
||
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;"]
|
||
if voice:
|
||
sp.append("try { $s.SelectVoice('%s') } catch { };"
|
||
% voice.replace("'", "''"))
|
||
if rate:
|
||
sp.append("$s.Rate = %d;" % max(-10, min(10, rate)))
|
||
sp.append("$s.Speak('%s')" % esc)
|
||
cmds.append(("ps-path", ["powershell", "-NoProfile",
|
||
"-WindowStyle", "Hidden",
|
||
"-Command", "".join(sp)]))
|
||
return cmds
|
||
|
||
def list_voices(self) -> list[str]:
|
||
"""列出系统可用的 TTS 音色。
|
||
|
||
用 SAPI5 COM 列(不依赖 .NET)—— Win7 上没有 .NET 3.0 的话
|
||
System.Speech 那条路是列不出来的,而 SAPI5 一定有。
|
||
"""
|
||
if not IS_WINDOWS:
|
||
return []
|
||
out = []
|
||
ps = self._powershell_path()
|
||
if ps:
|
||
script = ("$v = New-Object -ComObject SAPI.SpVoice;"
|
||
"$v.GetVoices() | ForEach-Object { $_.GetDescription() }")
|
||
try:
|
||
proc = subprocess.run(
|
||
[ps, "-NoProfile", "-Command", script],
|
||
capture_output=True, timeout=20,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
encoding="mbcs", errors="replace")
|
||
out = [ln.strip() for ln in (proc.stdout or "").splitlines()
|
||
if ln.strip()]
|
||
except Exception: # noqa: BLE001
|
||
out = []
|
||
if not out:
|
||
# 退到 System.Speech 再列一次
|
||
if ps:
|
||
script = ("Add-Type -AssemblyName System.Speech;"
|
||
"(New-Object System.Speech.Synthesis"
|
||
".SpeechSynthesizer).GetInstalledVoices()"
|
||
" | ForEach-Object { $_.VoiceInfo.Name }")
|
||
try:
|
||
proc = subprocess.run(
|
||
[ps, "-NoProfile", "-Command", script],
|
||
capture_output=True, timeout=20,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
encoding="mbcs", errors="replace")
|
||
out = [ln.strip() for ln in (proc.stdout or "").splitlines()
|
||
if ln.strip()]
|
||
except Exception: # noqa: BLE001
|
||
out = []
|
||
return out
|
||
|
||
def say(self, text: str, voice: str = "", rate: int = 0) -> str:
|
||
"""语音播报。voice 为空则用系统默认(或设置里的默认音色)。
|
||
|
||
⚠ 会**等播报结束**并看退出码,失败就抛错。
|
||
原来用 Popen 不等待,失败了也返回"已播报"—— 那是骗人。
|
||
代价是同步等待(念多久等多久),但"报成功其实没出声"
|
||
比"慢一点"糟糕得多。
|
||
"""
|
||
if not text.strip():
|
||
raise ActionError("播报内容为空")
|
||
if not IS_WINDOWS:
|
||
raise ActionError("语音播报目前仅支持 Windows")
|
||
|
||
chosen = (voice or "").strip() or str(
|
||
self.store.get("tts_voice") or "").strip()
|
||
try:
|
||
speed = int(rate) if rate else int(self.store.get("tts_rate", 0) or 0)
|
||
except (TypeError, ValueError):
|
||
speed = 0
|
||
speed = max(-10, min(10, speed))
|
||
|
||
cmds = self._tts_commands(text, chosen, speed)
|
||
if not cmds:
|
||
raise ActionError("这台电脑上找不到可用的语音播报方式"
|
||
"(没有 cscript 也没有 powershell?)")
|
||
|
||
# 上次成功的那条优先 —— 不然每播一次都要把几条全试一遍
|
||
cached = globals().get("_TTS_LAST_OK")
|
||
if cached:
|
||
cmds.sort(key=lambda c: 0 if c[0] == cached else 1)
|
||
|
||
tried = []
|
||
for name, cmd in cmds:
|
||
try:
|
||
proc = subprocess.run(
|
||
cmd, capture_output=True, timeout=180,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
encoding="mbcs", errors="replace")
|
||
except FileNotFoundError:
|
||
tried.append("%s(没有这个程序)" % name)
|
||
continue
|
||
except subprocess.TimeoutExpired:
|
||
tried.append("%s(超时)" % name)
|
||
continue
|
||
except Exception as exc: # noqa: BLE001
|
||
tried.append("%s(%s)" % (name, type(exc).__name__))
|
||
continue
|
||
if proc.returncode == 0:
|
||
globals()["_TTS_LAST_OK"] = name
|
||
return "已播报(%s,用 %s)" % (chosen or "默认音色", name)
|
||
err = ((proc.stderr or "") + (proc.stdout or "")).strip()
|
||
tried.append("%s(退出码%d %s)" % (name, proc.returncode, err[:40]))
|
||
|
||
raise ActionError("语音播报失败,试过:" + ";".join(tried))'''
|
||
|
||
NEW_VOICES = ''' def list_voices(self) -> list[str]:
|
||
"""列出系统可用的 TTS 音色。
|
||
|
||
用 SAPI5 COM 列(不依赖 .NET)—— Win7 上没装 .NET 3.0 的话
|
||
System.Speech 那条路列不出来,而 SAPI5 一定有。
|
||
"""
|
||
if not IS_WINDOWS:
|
||
return []
|
||
ps = self._powershell_path()
|
||
if not ps:
|
||
return []
|
||
out = []
|
||
for script in (
|
||
# 优先 SAPI5 COM
|
||
"$v = New-Object -ComObject SAPI.SpVoice;"
|
||
"$v.GetVoices() | ForEach-Object { $_.GetDescription() }",
|
||
# 退到 System.Speech
|
||
"Add-Type -AssemblyName System.Speech;"
|
||
"(New-Object System.Speech.Synthesis.SpeechSynthesizer)"
|
||
".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }",
|
||
):
|
||
try:
|
||
proc = subprocess.run(
|
||
[ps, "-NoProfile", "-Command", script],
|
||
capture_output=True, timeout=20,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||
encoding="mbcs", errors="replace")
|
||
out = [ln.strip() for ln in (proc.stdout or "").splitlines()
|
||
if ln.strip()]
|
||
except Exception: # noqa: BLE001
|
||
out = []
|
||
if out:
|
||
return out
|
||
return []'''
|
||
|
||
|
||
def main() -> int:
|
||
print("=" * 58)
|
||
print(" 语音播报:改成多条方案降级")
|
||
print("=" * 58)
|
||
print()
|
||
|
||
raw = ACTIONS.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
t = raw.decode("utf-8-sig")
|
||
|
||
if "_tts_commands" in t:
|
||
print(" 已经改过了")
|
||
return 0
|
||
|
||
missing = [k for k, v in {"list_voices": OLD_VOICES, "say": OLD_SAY}.items()
|
||
if v not in t]
|
||
if missing:
|
||
print(" ✗ 找不到:%s" % ", ".join(missing))
|
||
return 1
|
||
print(" ✓ 锚点就位")
|
||
|
||
t = t.replace(OLD_VOICES, NEW_VOICES, 1)
|
||
t = t.replace(OLD_SAY, NEW, 1)
|
||
|
||
ACTIONS.write_bytes((b"\xef\xbb\xbf" if bom else b"") + t.encode("utf-8"))
|
||
print(" ✓ list_voices 改用 SAPI5 COM(+ System.Speech 兜底)")
|
||
print(" ✓ say 改成降级链 + 等待退出码 + 记住可用的那条")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|