PLI: return the 7-byte location object, bound every proactive chain
release / release (push) Waiting to run
release / release (push) Waiting to run
The PLI answer used the 5-byte form 13 05 <MCC/MNC> <LAC> with LAC 0000 and no Cell ID. That is the *Network Rejection event download* form (TS 31.111 8.19.1); a PLI location for GERAN is 7 bytes - MCC/MNC, LAC and Cell ID. A card applet that re-reads its location (the Alfa MultiIMSI applet, after a *LOCI status change) treated it as empty and re-requested PLI on every STATUS tick. The default is now 13 07 00F110 0001 0001: PLMN 001/01 (the test network), LAC 0001, Cell ID 0001. _build_tr injects it after the Result; the TR data is pinned by a test. While verifying a live PLI, the manual /api/status-poll chain turned out to be unbounded: it looped on the PLI storm while holding _CARD_LOCK, so the request never returned and every card operation waited. _handle_proactive_chain's max_rounds now defaults to _POLL_CHAIN_MAX (the background poll always passed it; the manual poll, ENVELOPE, menu and TP chains did not), and the manual poll passes it explicitly. A card that re-queues a command can no longer wedge the card lock from any call site. Tests: the PLI default + the built TR, the default chain bound and the manual-poll call pin. 1061 Python + 1069 frontend; sw cache simple-v477.
This commit is contained in:
@@ -43,6 +43,19 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- The **PLI location answer** now uses the 7-byte PLI form (TS 31.111 8.19.1):
|
||||||
|
PLMN 001/01, LAC `0001`, Cell ID `0001`. The former default was the 5-byte
|
||||||
|
*Network Rejection event* form (no Cell ID) with LAC `0000`; a card applet
|
||||||
|
that re-reads its location after a `*LOCI` status change (the Alfa MultiIMSI
|
||||||
|
applet) treated it as empty and re-requested PLI on every STATUS tick — the
|
||||||
|
PLI storm that slowed RAM Explore. The TR data is now
|
||||||
|
`13 07 00F110 0001 0001`.
|
||||||
|
- The **proactive chain is bounded on every call site**: `_handle_proactive_chain`'s
|
||||||
|
`max_rounds` defaults to `_POLL_CHAIN_MAX` (the background poll always passed it;
|
||||||
|
the manual STATUS poll and the ENVELOPE/menu/TP chains did not). A card that
|
||||||
|
re-queues a command (the PLI storm) could keep the manual poll looping forever
|
||||||
|
while it held the card lock — `/api/status-poll` never returned and every card
|
||||||
|
request waited. The manual poll also passes the bound explicitly.
|
||||||
- The load-test loop closes properly on a raised runner error (it used to leave the run
|
- The load-test loop closes properly on a raised runner error (it used to leave the run
|
||||||
`running` forever - the PWA polled a dead run and *Clear report* stayed refused), on a
|
`running` forever - the PWA polled a dead run and *Clear report* stayed refused), on a
|
||||||
card session change inside a suite iteration, and on a member that was stopped; a
|
card session change inside a suite iteration, and on a member that was stopped; a
|
||||||
|
|||||||
+6
-3
@@ -1769,13 +1769,16 @@ Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.
|
|||||||
|
|
||||||
Returns the effective PLI data dictionary as a qualifier-code map: the
|
Returns the effective PLI data dictionary as a qualifier-code map: the
|
||||||
operator's override, else the built-in default (the qualifiers whose objects
|
operator's override, else the built-in default (the qualifiers whose objects
|
||||||
the pinned ETSI specs define - location as the test network 001/01, the test
|
the pinned ETSI specs define - location as the test network 001/01 with test
|
||||||
IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1).
|
LAC 0001 and Cell ID 0001 (the 7-byte PLI form per TS 31.111 8.19.1; the
|
||||||
|
5-byte form without the Cell ID belongs to the Network Rejection event
|
||||||
|
download), the test IMEI/IMEISV, language, access technology, search mode,
|
||||||
|
battery; v3.23.1, the PLI location form fixed in v3.37.0).
|
||||||
The date/time qualifier stays empty - the host clock answers it while no
|
The date/time qualifier stays empty - the host clock answers it while no
|
||||||
override is set.
|
override is set.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"00": "930500F1100000", "0A": "630104", "03": ""}
|
{"00": "130700F11000010001", "0A": "630104", "03": ""}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `POST /api/pli-dict`
|
### `POST /api/pli-dict`
|
||||||
|
|||||||
@@ -540,7 +540,7 @@
|
|||||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а <strong>очистка поля возвращает умолчание</strong>; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Поле, которое сервер не может использовать (значение не hex, нечётное число цифр, код вне списка квалификаторов), указывается под кнопкой «Сохранить», а не отбрасывается молча. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01 с тестовыми LAC 0001 и Cell ID 0001 — 7-байтовая форма PLI, а не короткая форма события Network Rejection; тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а <strong>очистка поля возвращает умолчание</strong>; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Поле, которое сервер не может использовать (значение не hex, нечётное число цифр, код вне списка квалификаторов), указывается под кнопкой «Сохранить», а не отбрасывается молча. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
||||||
|
|
||||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Симуляция сети</h3>
|
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Симуляция сети</h3>
|
||||||
<p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p>
|
<p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p>
|
||||||
|
|||||||
+1
-1
@@ -539,7 +539,7 @@
|
|||||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and <strong>clearing the field restores it</strong>; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. A field the server cannot use (a value that is not hex, an odd number of digits, a code outside the qualifier list) is named under the Save button instead of being silently dropped. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
<p class="text-sm mb-3">Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01 with test LAC 0001 and Cell ID 0001 — the 7-byte PLI form, not the shorter Network Rejection one, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and <strong>clearing the field restores it</strong>; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. A field the server cannot use (a value that is not hex, an odd number of digits, a code outside the qualifier list) is named under the Save button instead of being silently dropped. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
||||||
|
|
||||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Network simulation</h3>
|
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Network simulation</h3>
|
||||||
<p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.</p>
|
<p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.</p>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v476';
|
const CACHE = 'simple-v477';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -3592,9 +3592,12 @@ PLI_QUALIFIER_NAMES = {
|
|||||||
# specs define no contents for them ("reserved for 3GPP/3GPP2") and a
|
# specs define no contents for them ("reserved for 3GPP/3GPP2") and a
|
||||||
# fabricated object would mislead the application more than no answer.
|
# fabricated object would mislead the application more than no answer.
|
||||||
PLI_DEFAULTS = {
|
PLI_DEFAULTS = {
|
||||||
# Location Information (8.19): PLMN 001/01 (the test network, TS 23.003)
|
# Location Information (TS 31.111 8.19.1, the GERAN form a PLI answer
|
||||||
# + LAC 0000
|
# uses): PLMN 001/01 (the test network, TS 23.003) + LAC 0001 + Cell ID
|
||||||
0x00: '1305' + '00F110' + '0000',
|
# 0001. Length 05 (no Cell ID) is the *Network Rejection event* form -
|
||||||
|
# a PLI answer must carry the Cell ID (length 07), otherwise the location
|
||||||
|
# reads as empty and a card applet may re-request it forever.
|
||||||
|
0x00: '1307' + '00F110' + '0001' + '0001',
|
||||||
# IMEI (8.20): the classic test IMEI 490154203237518, nibble-swapped
|
# IMEI (8.20): the classic test IMEI 490154203237518, nibble-swapped
|
||||||
# BCD with the odd 15th digit padded by F (TS 124 008 Mobile Identity)
|
# BCD with the odd 15th digit padded by F (TS 124 008 Mobile Identity)
|
||||||
0x01: '1408' + '94104502237315F8',
|
0x01: '1408' + '94104502237315F8',
|
||||||
@@ -6608,15 +6611,18 @@ def _parse_setup_menu_items(raw):
|
|||||||
return _attach_nai(items, nai)
|
return _attach_nai(items, nai)
|
||||||
|
|
||||||
|
|
||||||
def _handle_proactive_chain(scc, sw91, on_fetch=None, status_poll=True, max_rounds=None):
|
def _handle_proactive_chain(scc, sw91, on_fetch=None, status_poll=True,
|
||||||
|
max_rounds=_POLL_CHAIN_MAX):
|
||||||
"""Run a FETCH/TERMINAL RESPONSE chain; marks the card as busy so that
|
"""Run a FETCH/TERMINAL RESPONSE chain; marks the card as busy so that
|
||||||
terminal-initiated ENVELOPEs (Data available, Channel status, timers) wait.
|
terminal-initiated ENVELOPEs (Data available, Channel status, timers) wait.
|
||||||
``status_poll=False`` skips the trailing STATUS (used by the profile
|
``status_poll=False`` skips the trailing STATUS (used by the profile
|
||||||
switch, where the card is mid-switch and must not be queried further).
|
switch, where the card is mid-switch and must not be queried further).
|
||||||
``max_rounds`` bounds the answers per call (the background poll passes
|
``max_rounds`` bounds the answers per call - the default is
|
||||||
_POLL_CHAIN_MAX so a card that re-queues a command cannot hold the card
|
_POLL_CHAIN_MAX, so a card that re-queues a command (a chatty applet)
|
||||||
lock forever); ``_TEST_RUNNING`` aborts the chain before the next FETCH -
|
cannot hold the card lock forever from *any* call site; a caller may pass
|
||||||
a test run owns the card and the pending command stays for the script."""
|
a different bound. ``_TEST_RUNNING`` aborts the chain before the next
|
||||||
|
FETCH - a test run owns the card and the pending command stays for the
|
||||||
|
script."""
|
||||||
global _PROACTIVE_BUSY
|
global _PROACTIVE_BUSY
|
||||||
_PROACTIVE_BUSY = True
|
_PROACTIVE_BUSY = True
|
||||||
try:
|
try:
|
||||||
@@ -10032,7 +10038,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
sys.stderr.write('STATUS -> %s\n' % st_sw)
|
sys.stderr.write('STATUS -> %s\n' % st_sw)
|
||||||
resp = {'sw': st_sw}
|
resp = {'sw': st_sw}
|
||||||
if st_sw.startswith('91'):
|
if st_sw.startswith('91'):
|
||||||
_handle_proactive_chain(scc, st_sw)
|
_handle_proactive_chain(scc, st_sw, max_rounds=_POLL_CHAIN_MAX)
|
||||||
resp['proactive'] = True
|
resp['proactive'] = True
|
||||||
self._send_json(resp)
|
self._send_json(resp)
|
||||||
self._log_resp(resp)
|
self._log_resp(resp)
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class PliDictHttpTests(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
status, resp = self._req('GET', '/api/pli-dict')
|
status, resp = self._req('GET', '/api/pli-dict')
|
||||||
self.assertEqual(status, 200, resp)
|
self.assertEqual(status, 200, resp)
|
||||||
self.assertEqual(resp['00'], '130500F1100000')
|
self.assertEqual(resp['00'], '130700F11000010001')
|
||||||
self.assertEqual(resp['0A'], '630104')
|
self.assertEqual(resp['0A'], '630104')
|
||||||
self.assertEqual(resp['03'], '') # the host clock answers it
|
self.assertEqual(resp['03'], '') # the host clock answers it
|
||||||
self.assertEqual(resp['02'], '') # no default
|
self.assertEqual(resp['02'], '') # no default
|
||||||
|
|||||||
@@ -1041,7 +1041,10 @@ class TestProactiveDecode(unittest.TestCase):
|
|||||||
# define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23)
|
# define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23)
|
||||||
import pysim_simple_server.server as srv
|
import pysim_simple_server.server as srv
|
||||||
d = srv.PLI_DEFAULTS
|
d = srv.PLI_DEFAULTS
|
||||||
self.assertEqual(d[0x00], '130500F1100000') # 001/01 + LAC 0000
|
# the 7-byte PLI form: 001/01 + LAC 0001 + Cell ID 0001 (length 05
|
||||||
|
# is the Network Rejection event form, TS 31.111 8.19.1)
|
||||||
|
self.assertEqual(d[0x00], '130700F11000010001')
|
||||||
|
self.assertEqual(len(bytes.fromhex(d[0x00])), 9)
|
||||||
self.assertEqual(d[0x01], '140894104502237315F8') # IMEI, F pad
|
self.assertEqual(d[0x01], '140894104502237315F8') # IMEI, F pad
|
||||||
self.assertEqual(d[0x04], '2D02656E') # 'en'
|
self.assertEqual(d[0x04], '2D02656E') # 'en'
|
||||||
self.assertEqual(d[0x05], '2E020000') # Timing Advance
|
self.assertEqual(d[0x05], '2E020000') # Timing Advance
|
||||||
@@ -1073,6 +1076,19 @@ class TestProactiveDecode(unittest.TestCase):
|
|||||||
srv._PLI_DATA.clear()
|
srv._PLI_DATA.clear()
|
||||||
srv._PLI_DATA.update(saved)
|
srv._PLI_DATA.update(saved)
|
||||||
|
|
||||||
|
def test_the_pli_tr_carries_the_7_byte_location(self):
|
||||||
|
# the TR builder injects the location object after the Result; a
|
||||||
|
# 5-byte object (no Cell ID) is the Network Rejection form, not a PLI
|
||||||
|
import pysim_simple_server.server as srv
|
||||||
|
saved = srv._PLI_DATA.pop(0x00, None)
|
||||||
|
try:
|
||||||
|
tr = srv._build_tr(object(), 1, 0x26, 0x81, 0x82, 0x00)
|
||||||
|
finally:
|
||||||
|
if saved is not None:
|
||||||
|
srv._PLI_DATA[0x00] = saved
|
||||||
|
self.assertTrue(tr.startswith(bytes.fromhex('810301260002028281030100')))
|
||||||
|
self.assertTrue(tr.endswith(bytes.fromhex('130700F11000010001')))
|
||||||
|
|
||||||
def test_decode_cmd_empty_raw(self):
|
def test_decode_cmd_empty_raw(self):
|
||||||
self.assertEqual(_decode_cmd(0x26, b'', None), [])
|
self.assertEqual(_decode_cmd(0x26, b'', None), [])
|
||||||
self.assertEqual(_decode_cmd(0x03, None, None), [])
|
self.assertEqual(_decode_cmd(0x03, None, None), [])
|
||||||
|
|||||||
@@ -201,6 +201,20 @@ class TestProactiveChainOwnership(unittest.TestCase):
|
|||||||
S._handle_proactive_chain(scc, '910b', max_rounds=3)
|
S._handle_proactive_chain(scc, '910b', max_rounds=3)
|
||||||
self.assertEqual(len(self._fetches(scc)), 3)
|
self.assertEqual(len(self._fetches(scc)), 3)
|
||||||
|
|
||||||
|
def test_every_chain_call_is_bounded_by_default(self):
|
||||||
|
# v3.37.0: the bound is the default, not only the background poll's
|
||||||
|
# choice - a chatty card must not wedge any call site (the manual
|
||||||
|
# STATUS poll and the ENVELOPE chain used to loop forever)
|
||||||
|
scc = FakeChattyScc()
|
||||||
|
S._handle_proactive_chain(scc, '910b')
|
||||||
|
self.assertEqual(len(self._fetches(scc)), S._POLL_CHAIN_MAX)
|
||||||
|
|
||||||
|
def test_the_manual_status_poll_passes_the_bound(self):
|
||||||
|
import inspect
|
||||||
|
src = inspect.getsource(S.PysimHandler._do_POST)
|
||||||
|
branch = src.split("'/api/status-poll'", 1)[1].split('elif self.path', 1)[0]
|
||||||
|
self.assertIn('max_rounds=_POLL_CHAIN_MAX', branch)
|
||||||
|
|
||||||
def test_the_chain_stops_when_a_run_owns_the_card(self):
|
def test_the_chain_stops_when_a_run_owns_the_card(self):
|
||||||
scc = FakeChattyScc()
|
scc = FakeChattyScc()
|
||||||
S._TEST_RUNNING = True
|
S._TEST_RUNNING = True
|
||||||
|
|||||||
Reference in New Issue
Block a user