Skip to content

Commit 720a7cf

Browse files
fix(generator): fall back to the login link on cp1252 consoles
Redirected stdout on Windows uses the ANSI code page, which cannot encode the half-block characters print_ascii emits, so the generator crashed with UnicodeEncodeError before showing any login path (#150). Render the QR art only when stdout can encode it, otherwise print the tg:// login link, and reconfigure stdio with errors=replace so no other print site (account labels, error text) can abort a login mid-flight.
1 parent a612943 commit 720a7cf

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

session_string_generator.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,32 @@ def _check_installation() -> None:
6767
sys.exit(1)
6868

6969

70+
def _stream_can_encode(text: str, stream) -> bool:
71+
"""True if *stream* can write *text* without raising UnicodeEncodeError."""
72+
encoding = getattr(stream, "encoding", None)
73+
if not encoding:
74+
return True
75+
try:
76+
text.encode(encoding)
77+
except (LookupError, UnicodeEncodeError):
78+
return False
79+
return True
80+
81+
82+
def _harden_stdio() -> None:
83+
"""Replace unencodable output instead of crashing mid-login.
84+
85+
On Windows, redirected stdout falls back to the legacy ANSI code page
86+
(e.g. cp1252), which cannot encode the QR block characters or non-latin
87+
text, so a bare print() raises UnicodeEncodeError and aborts the login.
88+
"""
89+
for stream in (sys.stdout, sys.stderr):
90+
try:
91+
stream.reconfigure(errors="replace")
92+
except (AttributeError, ValueError):
93+
pass
94+
95+
7096
def _render_qr(qr) -> None:
7197
import qrcode
7298

@@ -77,11 +103,18 @@ def _render_qr(qr) -> None:
77103
qr_obj.make(fit=True)
78104
f = io.StringIO()
79105
qr_obj.print_ascii(out=f, invert=True)
80-
print(f.getvalue())
106+
art = f.getvalue()
107+
108+
if _stream_can_encode(art, sys.stdout):
109+
print(art)
110+
print("Scan the QR code above with your Telegram app:")
111+
print(" Open Telegram > Settings > Devices > Link Desktop Device\n")
112+
print(f"Or open this link on a device where you're logged in:\n {qr.url}\n")
113+
else:
114+
encoding = getattr(sys.stdout, "encoding", None) or "unknown"
115+
print(f"This console ({encoding}) cannot draw the QR code.")
116+
print(f"Open this link on a device where you're logged in instead:\n {qr.url}\n")
81117

82-
print("Scan the QR code above with your Telegram app:")
83-
print(" Open Telegram > Settings > Devices > Link Desktop Device\n")
84-
print(f"Or open this link on a device where you're logged in:\n {qr.url}\n")
85118
print(f"Expires at: {qr.expires.strftime('%H:%M:%S')}")
86119
print("Waiting for you to scan...")
87120

@@ -150,6 +183,7 @@ def _phone_login(client: TelegramClient) -> None:
150183

151184

152185
def main() -> None:
186+
_harden_stdio()
153187
args = _parse_args()
154188
_check_installation()
155189

tests/test_session_string_generator.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import io
3+
import sys
24
from datetime import datetime, timedelta, timezone
35

46
import pytest
@@ -162,6 +164,52 @@ def sign_in(self, *args, **kwargs):
162164
assert client.sign_in_calls == ["secret"]
163165

164166

167+
def _bytes_stdout(encoding):
168+
"""A strict TextIOWrapper standing in for a redirected console stream."""
169+
return io.TextIOWrapper(io.BytesIO(), encoding=encoding, errors="strict", write_through=True)
170+
171+
172+
def test_render_qr_prints_ascii_art_when_console_can_encode(monkeypatch):
173+
qr = _FakeQR([])
174+
stdout = _bytes_stdout("utf-8")
175+
monkeypatch.setattr(sys, "stdout", stdout)
176+
177+
session_string_generator._render_qr(qr)
178+
179+
out = stdout.buffer.getvalue().decode("utf-8")
180+
assert "▄" in out # the half-block characters of the QR art
181+
assert qr.url in out
182+
183+
184+
def test_render_qr_falls_back_to_link_when_console_cannot_encode(monkeypatch):
185+
# Redirected stdout on Windows uses the ANSI code page (cp1252), which
186+
# cannot encode the QR block characters and used to crash the generator.
187+
qr = _FakeQR([])
188+
stdout = _bytes_stdout("cp1252")
189+
monkeypatch.setattr(sys, "stdout", stdout)
190+
191+
session_string_generator._render_qr(qr)
192+
193+
out = stdout.buffer.getvalue().decode("cp1252")
194+
assert "cannot draw the QR code" in out
195+
assert qr.url in out
196+
assert "Waiting for you to scan..." in out
197+
198+
199+
def test_harden_stdio_replaces_unencodable_output_instead_of_crashing(monkeypatch):
200+
stdout = _bytes_stdout("cp1252")
201+
stderr = _bytes_stdout("cp1252")
202+
monkeypatch.setattr(sys, "stdout", stdout)
203+
monkeypatch.setattr(sys, "stderr", stderr)
204+
205+
session_string_generator._harden_stdio()
206+
print("session for █Ж█ ready") # raises under strict cp1252
207+
208+
out = stdout.buffer.getvalue().decode("cp1252")
209+
assert "session for" in out
210+
assert "ready" in out
211+
212+
165213
def test_parse_args_qr_selects_qr_login(monkeypatch):
166214
monkeypatch.setattr("sys.argv", ["session_string_generator.py", "--qr"])
167215

0 commit comments

Comments
 (0)