mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-10-23 13:33:03 +00:00
fix(geocaching): retain staged edits during SD contention
This commit is contained in:
1 parent
8dbb112f73
commit
d953528584
7 files changed
+59
-13
No files matched your search
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
由 scripts/audit_nvs_access.mjs 扫描 ESP、共享 UI 和仓库 third_party 的 C/C++ 文件。只记录 NVS 函数、namespace/key/handle 实参和位置,不读取设备、不记录值/默认值/缓冲/分区名。不把最近的 nvs_open 自动推断为后续调用的 namespace;跨作用域句柄、宏和条件编译仍需人工核对。
|
||||
|
||||
扫描 1522 个源文件,找到 106 个候选调用,分布于 8 个文件。显式打开 15,键访问 45,其余为提交/关闭/清域。调用数不是独立配置数。
|
||||
扫描 1527 个源文件,找到 106 个候选调用,分布于 8 个文件。显式打开 15,键访问 45,其余为提交/关闭/清域。调用数不是独立配置数。
|
||||
|
||||
这是源码入口清单,不是全部配置或硬件生效证明。系统 SDK、构建环境依赖、其他平台、函数指针/包装器实参以及复杂原始字符串不由此表自动覆盖。依赖库可单独调用 collectNvsAccesses 指定根目录审计;输出应单独记录其版本,不能写进与机器无关的仓库清单。
|
||||
|
||||
|
||||
+15
-6
@@ -19,10 +19,15 @@ On 2026-10-14 the user reserved the remaining Wio upload opportunity for final
|
||||
release acceptance and became unavailable for manual device operations. Complete
|
||||
remaining development and host-side verification before using it. Do not flash
|
||||
intermediate or diagnostic candidates, or reset the device just to resume tests.
|
||||
The last flashed source is `4be4f572`; retain the original identity, configuration
|
||||
and pending publication fixture. An upload is permitted only when a release-ready
|
||||
candidate actually needs the final physical acceptance run. Do not infer that an
|
||||
available serial port is authorization to consume this reserved opportunity.
|
||||
That reserved opportunity was subsequently consumed by combined TMAP firmware
|
||||
`fbcdd8bb`, with application SHA-256
|
||||
`6ab701588dca89739c4a796c39f366fc49f234f08b2bf449ed8d553460734ddc`.
|
||||
The original author/RF identities and all 309 configuration fields were verified.
|
||||
The later navigation/Map/POI candidate built successfully but remains unflashed;
|
||||
Tracker title/card changes and the staged-draft SD contention fix are newer still.
|
||||
Retain the original identities, configuration and publication fixtures. A further
|
||||
upload requires a new device opportunity; an available application port is not
|
||||
permission to reset or replace the running firmware.
|
||||
|
||||
The existing browser/native-directory track lifecycle has an opt-in physical
|
||||
author mode. It requires an explicit application port, serial Python executable,
|
||||
@@ -32,8 +37,12 @@ clients and retains the exact public confirmation checkpoint before writing.
|
||||
Its default synthetic mode and synthetic API-bridge mode both pass locally with
|
||||
two real native Reticulum directories, three signed versions, KML/attachment
|
||||
reads and archived history after restart. The API-bridge check does not prove
|
||||
physical USB signatures or device durability. The physical mode remains unused
|
||||
until the reserved final acceptance run.
|
||||
physical USB signatures or device durability. Physical mode has verified trail v1
|
||||
signing, publication, directory proof and durable device confirmation. Subsequent
|
||||
versions remain blocked by retained upload-slot capacity; the original checkpoint
|
||||
must be preserved. Physical Geocaching v1 confirmation also passed. The same-record
|
||||
update attempt returned a storage failure during draft save, so update/archive is
|
||||
still unaccepted.
|
||||
|
||||
## Requested release deliverables
|
||||
|
||||
|
||||
@@ -1124,6 +1124,24 @@ int main(int argc, char** argv)
|
||||
};
|
||||
require(staged(staged_id, 7) == rt::WebReadResult::AlreadySaved, "web file was not saved through the existing editor transaction");
|
||||
require(permission(staged_id.data(), 1, router.identity.public_key.data()) == rt::WebReadResult::Ready, "web file save generation was not persisted");
|
||||
// Ordinary SD contention during any selected-input operation must
|
||||
// preserve the staged edit and resume at its last complete slice.
|
||||
for (unsigned operation = 0; operation < 4; ++operation)
|
||||
{
|
||||
staged_id.fill(static_cast<uint8_t>(20 + operation));
|
||||
test::file_busy_cycles = 1;
|
||||
test::file_busy_remaining.fill(1);
|
||||
test::file_busy_remaining[operation] = 0;
|
||||
const auto hits = test::file_busy_hits[operation];
|
||||
require(rt::queueWebDraftFile(staged_id.data(), static_cast<uint32_t>(edit_size), 0, edit_hash.data(), router.identity.public_key.data(), 7) == rt::WebReadResult::Pending,
|
||||
"busy file test could not queue its edit");
|
||||
until([&]
|
||||
{ return test::file_busy_hits[operation] > hits; },
|
||||
"staged input did not exercise SD contention");
|
||||
test::file_busy_cycles = 0;
|
||||
require(staged(staged_id, 7) == rt::WebReadResult::AlreadySaved, "SD contention rejected the staged web edit");
|
||||
require(permission(staged_id.data(), 1, router.identity.public_key.data()) == rt::WebReadResult::Ready, "busy web edit was not persisted");
|
||||
}
|
||||
staged_id.fill(10);
|
||||
test::files[rt::kWebDraftCandidatePath][0] ^= 1;
|
||||
require(staged(staged_id, 7) == rt::WebReadResult::HashMismatch, "corrupted web candidate was committed");
|
||||
|
||||
@@ -2945,6 +2945,13 @@ static bool advanceWebDraftFile(Session& s)
|
||||
if (!file.open(kWebDraftCandidatePath, "r") || file.size() != total || !file.seek(job.size) ||
|
||||
file.read(job.bytes + job.size, count) != static_cast<int>(count))
|
||||
{
|
||||
// Map/other SD readers can own the filesystem briefly. Keep the edit
|
||||
// and retry this same slice; only a real read failure rejects it.
|
||||
if (file.read_busy())
|
||||
{
|
||||
next_step.store(millis() + 20);
|
||||
return true;
|
||||
}
|
||||
failWebDraftSave(job, WebReadResult::Unavailable);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {GeocachingHardwareExchange} from '../../tools/hostlink/geocaching_hardwa
|
||||
const args=process.argv.slice(2),option=name=>args[args.indexOf(name)+1],hardwarePort=args.includes('--hardware-port')?option('--hardware-port'):null;
|
||||
if(hardwarePort&&(!/^COM\d+$/.test(hardwarePort)||!args.includes('--serial-python')||!args.includes('--expected-author')||!/^[a-f0-9]{64}$/.test(option('--expected-author'))))throw Error('Hardware mode requires --hardware-port COMxx --serial-python <pyserial Python> --expected-author <fingerprint>');
|
||||
const resumeDraftId=args.includes('--resume-draft')?option('--resume-draft'):null;
|
||||
const resumeConfirmed=args.includes('--resume-confirmed-record');
|
||||
if(resumeConfirmed&&(!resumeDraftId||!args.includes('--resume-confirmed-receipt')))throw Error('Confirmed resume requires the exact draft, signed v1 record and directory receipt');
|
||||
if(resumeDraftId&&(!hardwarePort||!args.includes('--resume-name')||!/^Wio网页验收-[a-z0-9]+$/.test(option('--resume-name'))||!/^[a-f0-9]{32}$/.test(resumeDraftId)))throw Error('Resume requires explicit hardware, isolated acceptance name and ID');
|
||||
const testName=hardwarePort?(resumeDraftId?option('--resume-name'):'Wio网页验收-'+Date.now().toString(36)):'真实网络网页藏宝点',updatedName=hardwarePort?testName+'-v2':'第二版网络藏宝点';
|
||||
const python=process.env.TRAIL_MATE_TRACK_PYTHON??path.resolve('.codex-build/reticulum-directory/venv/'+(process.platform==='win32'?'Scripts/python.exe':'bin/python'));
|
||||
@@ -26,6 +28,14 @@ async function freePort(){const socket=net.createServer();await new Promise(reso
|
||||
const server=http.createServer(async(req,res)=>{try{const name=new URL(req.url,'http://localhost').pathname;if(name==='/trail-mate/geocaching/network.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify({endpoint,discoverySeeds:seeds}));return;}if(!name.startsWith('/trail-mate/'))throw Error('prefix');const file=path.resolve(root,name.slice(12)+(name.endsWith('/')?'index.html':''));if(!file.startsWith(root+path.sep))throw Error('path');res.setHeader('Content-Type',({'.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.png':'image/png','.svg':'image/svg+xml','.woff2':'font/woff2','.ttf':'font/ttf'})[path.extname(file)]??'application/octet-stream');res.end(await fs.readFile(file));}catch{res.writeHead(404).end();}});await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));const origin='http://127.0.0.1:'+server.address().port;
|
||||
const author=await Identity.generate(),publicKey=await author.getPublicKey(),nameHash=(await Identity.fullHash(new TextEncoder().encode('lxmf.delivery'))).slice(0,10),destination=await Identity.truncatedHash(Uint8Array.from([...nameHash,...author.identityHash])),info=[publicKey,author.identityHash,destination,7];
|
||||
const drafts=new Map(),transfers=new Map(),signatures=new Map(),confirmations=new Map(),signedVersions=[],calls=[];let catalog=0,signCount=0,confirmCount=0;
|
||||
let resumeBaseHash=null;
|
||||
if(resumeConfirmed){
|
||||
const signed=JSON.parse(await fs.readFile(option('--resume-confirmed-record'),'utf8')).map(value=>Uint8Array.from(value)),verified=await verifySigned(signed),record=verified.record;
|
||||
assert.ok(equal(record[2],Uint8Array.from(Buffer.from(resumeDraftId,'hex'))));assert.equal(record[3],1);assert.equal(record[5],0);assert.equal(record[8],testName);assert.equal(record[6],310000000);assert.equal(record[7],1210000000);
|
||||
assert.equal(Buffer.from(await crypto.subtle.digest('SHA-256',record[1])).toString('hex'),option('--expected-author'));
|
||||
const receipt=Object.fromEntries(Object.entries(JSON.parse(await fs.readFile(option('--resume-confirmed-receipt'),'utf8'))).map(([key,value])=>[key,Uint8Array.from(value)]));await verifyPublicationReceipt([receipt.raw,receipt.signature],signed,receipt);
|
||||
resumeBaseHash=verified.summary[2].slice();signedVersions.push(signed);
|
||||
}
|
||||
const countSignatures=()=>hardware?.signCount??signCount,countConfirmations=()=>hardware?.confirmCount??confirmCount;
|
||||
const hex=bytes=>Buffer.from(bytes).toString('hex');
|
||||
async function usbReply(raw){const frame=new FrameDecoder().push(Uint8Array.from(raw))[0];assert.ok(frame);if(frame.type===FRAME.HELLO)return [...encodeFrame(FRAME.HELLO_ACK,frame.sequence,new Uint8Array([1,0,0,2,0,4,0,0,6,...new TextEncoder().encode('T-Deck'),4,...new TextEncoder().encode('test')]))];const request=decodeManagement(frame.payload),body=request.body;calls.push(request.code);let response;
|
||||
@@ -48,18 +58,18 @@ async function usbReply(raw){const frame=new FrameDecoder().push(Uint8Array.from
|
||||
else assert.fail('Unexpected USB operation '+request.code);
|
||||
return [...encodeFrame(FRAME.MANAGEMENT_RESPONSE,frame.sequence,encodeManagement(request.session,request.request,0,response))];}
|
||||
try{
|
||||
if(hardwarePort)hardware=new GeocachingHardwareExchange({port:hardwarePort,python:option('--serial-python'),fingerprint:option('--expected-author'),namePrefix:testName,resumeDraftId,calls,signedVersions,checkpointPath:path.join(work,'signature-recovery.json')});
|
||||
if(hardwarePort)hardware=new GeocachingHardwareExchange({port:hardwarePort,python:option('--serial-python'),fingerprint:option('--expected-author'),namePrefix:testName,resumeDraftId,resumeBaseHash,calls,signedVersions,checkpointPath:path.join(work,'signature-recovery.json')});
|
||||
await fs.rm(path.join(work,'result.json'),{force:true});
|
||||
await fs.rm(path.join(state,'stop'),{force:true});const tcp=await freePort();service=start('directory',['tools/geocaching/browser_test_service.py','--state',state,'--port',String(tcp),'--empty','--startup-delay','0','--run-seconds','600']);const directory=await ready(service,'ready');seeds=[directory.discovery];bridge=start('bridge',['tools/geocaching/packet_bridge.py','--port','0','--tcp-port',String(tcp),'--origin',origin,'--run-seconds','600']);endpoint='ws://127.0.0.1:'+(await ready(bridge,'bridge_ready')).port;
|
||||
browser=await chromium.launch({channel:process.env.TRAIL_MATE_TEST_BROWSER_CHANNEL,headless:true});const context=await browser.newContext({viewport:{width:1440,height:960}});await context.exposeFunction('deviceReply',hardware?raw=>hardware.exchange(raw):usbReply);await context.addInitScript(()=>{let input;const port={async open(){this.readable=new ReadableStream({start(controller){input=controller;}});this.writable=new WritableStream({async write(bytes){input.enqueue(Uint8Array.from(await window.deviceReply([...bytes])));}});},async setSignals(){},async close(){}};Object.defineProperty(navigator,'serial',{configurable:true,value:{requestPort:async()=>port}});});await context.route('https://tile.openstreetmap.org/**',route=>route.fulfill({contentType:'image/svg+xml',body:'<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><rect width="256" height="256" fill="#dceae4"/></svg>'}));
|
||||
let page=await context.newPage();const errors=[];page.on('pageerror',error=>errors.push(String(error)));await page.goto(origin+'/trail-mate/geocaching/?lang=zh-Hans');await page.waitForFunction(()=>document.querySelector('#search')&&!document.querySelector('#search').disabled,{},{timeout:60000});await page.getByRole('button',{name:'草稿',exact:true}).click();await page.getByRole('button',{name:'连接设备',exact:true}).click();await page.getByText('USB 作者已验证',{exact:true}).waitFor();
|
||||
if(resumeDraftId){await page.getByRole('button',{name:'刷新设备列表',exact:true}).click();await openTestDraft(testName);assert.ok(hardware.drafts.has(resumeDraftId));}
|
||||
if(resumeDraftId){if(resumeConfirmed)await page.getByRole('button',{name:'我的藏宝点',exact:true}).click();await page.getByRole('button',{name:'刷新设备列表',exact:true}).click();await openTestDraft(testName);assert.ok(hardware.drafts.has(resumeDraftId));}
|
||||
else{await page.getByRole('button',{name:'新建草稿',exact:true}).click();await page.getByLabel('名称',{exact:true}).fill(testName);await page.getByLabel('正文',{exact:true}).fill('网页中文正文 <script>window.__geoNetworkInjection=true</script>\n'.repeat(25));await page.getByLabel('提示',{exact:true}).fill('&< >'.repeat(80));await page.getByLabel('纬度',{exact:true}).fill('31');await page.getByLabel('经度',{exact:true}).fill('121');await page.getByRole('button',{name:'保存草稿到设备',exact:true}).click();await page.getByText('草稿已保存到设备。刷新列表可更新发布状态。',{exact:true}).waitFor({timeout:45000});}
|
||||
async function publishAndSync(){await page.getByRole('button',{name:'刷新 IP 目录',exact:true}).click();await page.getByRole('button',{name:'检查并确认发布',exact:true}).click();assert.equal(await page.locator('dialog[open] script').count(),0);await page.locator('dialog[open]').getByRole('button',{name:'通过 IP 网络发布',exact:true}).click();await page.locator('.geo-publication-task').getByText('发布内容与目录签名证明已归档到设备。',{exact:true}).waitFor({timeout:60000});}
|
||||
async function openTestDraft(name){for(let index=0;index<128;index++){await page.waitForFunction(()=>[...document.querySelectorAll('button')].some(button=>button.textContent==='刷新设备列表'&&!button.disabled),{},{timeout:45000});const row=page.locator('.geo-draft-row').filter({hasText:name});if(await row.count()){await row.click();await page.getByLabel('名称',{exact:true}).waitFor();return;}const more=page.getByRole('button',{name:'加载更多设备草稿',exact:true});if(!await more.count())throw Error('Isolated acceptance draft not present in current device catalogue');await more.click();}throw Error('Device catalogue acceptance search exceeded bounded page count');}
|
||||
await publishAndSync();assert.equal(countSignatures(),1);assert.equal(countConfirmations(),1);await page.getByRole('button',{name:'我的藏宝点',exact:true}).click();await openTestDraft(testName);await page.getByLabel('名称',{exact:true}).fill(updatedName);await page.getByRole('button',{name:'保存草稿到设备',exact:true}).click();await page.getByText('草稿已保存到设备。刷新列表可更新发布状态。',{exact:true}).waitFor();await publishAndSync();await openTestDraft(updatedName);await page.getByLabel('状态',{exact:true}).selectOption('2');await page.getByRole('button',{name:'保存草稿到设备',exact:true}).click();await page.getByText('草稿已保存到设备。刷新列表可更新发布状态。',{exact:true}).waitFor();await publishAndSync();assert.equal(countSignatures(),3);assert.equal(countConfirmations(),3);assert.equal((await verifySigned(signedVersions[2])).record[5],2);if(hardware)await page.locator('.geo-publication-panel').screenshot({path:'docs/locales/zh-Hans/web-outdoor-management/images/geocaching-wio-network-archive.png'});else await page.screenshot({path:'docs/locales/zh-Hans/web-outdoor-management/images/geocaching-real-network-archive.png',fullPage:true});
|
||||
if(!resumeConfirmed){await publishAndSync();assert.equal(countSignatures(),1);assert.equal(countConfirmations(),1);}await page.getByRole('button',{name:'我的藏宝点',exact:true}).click();await openTestDraft(testName);await page.getByLabel('名称',{exact:true}).fill(updatedName);await page.getByRole('button',{name:'保存草稿到设备',exact:true}).click();await page.getByText('草稿已保存到设备。刷新列表可更新发布状态。',{exact:true}).waitFor();await publishAndSync();await openTestDraft(updatedName);await page.getByLabel('状态',{exact:true}).selectOption('2');await page.getByRole('button',{name:'保存草稿到设备',exact:true}).click();await page.getByText('草稿已保存到设备。刷新列表可更新发布状态。',{exact:true}).waitFor();await publishAndSync();assert.equal(countSignatures(),resumeConfirmed?2:3);assert.equal(countConfirmations(),resumeConfirmed?2:3);assert.equal((await verifySigned(signedVersions[2])).record[5],2);if(hardware)await page.locator('.geo-publication-panel').screenshot({path:'docs/locales/zh-Hans/web-outdoor-management/images/geocaching-wio-network-archive.png'});else await page.screenshot({path:'docs/locales/zh-Hans/web-outdoor-management/images/geocaching-real-network-archive.png',fullPage:true});
|
||||
await context.grantPermissions(['clipboard-read','clipboard-write'],{origin});const expectedId=(await verifySigned(signedVersions[2])).cacheId;await page.getByRole('button',{name:'复制藏宝点 ID',exact:true}).click();assert.equal(await page.evaluate(()=>navigator.clipboard.readText()),expectedId);
|
||||
const priorCalls=calls.length;for(const {id} of locales){await page.locator('.management-language select').selectOption(id);await page.locator('.geo-publication-task').getByText(geocachingIpPublicationText(id).confirmed,{exact:true}).waitFor();await page.locator('.geo-publication-task').getByText(geocachingDeviceSyncText(id).confirmed,{exact:true}).waitFor();await page.setViewportSize({width:390,height:844});assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1),false);assert.equal(calls.length,priorCalls);}await page.locator('.management-language select').selectOption('zh-Hans');await page.setViewportSize({width:1440,height:960});
|
||||
const native=spawnSync(python,['tools/geocaching/verify_browser_publication.py','--database',path.join(state,'service','directory.sqlite')],{input:JSON.stringify(signedVersions.map(item=>item.map(part=>[...part]))),windowsHide:true,maxBuffer:1048576});assert.equal(native.status,0,String(native.stderr));const nativeCheck=JSON.parse(native.stdout);assert.equal(nativeCheck.transportDiffersFromAuthor,true);
|
||||
await page.reload();await page.getByRole('button',{name:'我的藏宝点',exact:true}).click();await page.getByRole('button',{name:'连接设备',exact:true}).click();await page.getByText('USB 作者已验证',{exact:true}).waitFor();await page.locator('.geo-publication-task').getByText('发布内容与目录签名证明已归档到设备。',{exact:true}).waitFor();assert.equal(countSignatures(),3);assert.equal(countConfirmations(),3);assert.equal(await page.evaluate(()=>window.__geoNetworkInjection),undefined);assert.ok(signedVersions.every(item=>item[0].length>512));assert.ok(!calls.some(code=>[7,16,17,20,21,67,68,70].includes(code)));assert.deepEqual(errors,[]);await fs.writeFile(path.join(work,'result.json'),JSON.stringify({passed:true,actualPage:true,actualWorker:true,nativeNetwork:true,nativeWebSerial:false,usb:hardware?'physical Wio L2 via restricted browser serial API test bridge':'synthetic independent identity with real signatures',author:hardware?{fingerprint:hardware.author.fingerprint,generation:hardware.author.generation}:undefined,cacheId:expectedId,created:true,updated:true,archived:true,directoryProof:true,deviceConfirmation:true,refreshRecovery:true,signatures:countSignatures(),confirmations:countConfirmations(),rfRequests:0,configWrites:0,keyExports:0,locales:locales.length,mobileWidth:390,clipboardId:true,resourceSizedContent:true,nativeCheck},null,2));console.log('Actual Geocaching page + Worker + native network: create/update/archive/proof/device confirmation and refresh passed. USB: '+(hardware?'physical Wio L2':'synthetic'));
|
||||
await page.reload();await page.getByRole('button',{name:'我的藏宝点',exact:true}).click();await page.getByRole('button',{name:'连接设备',exact:true}).click();await page.getByText('USB 作者已验证',{exact:true}).waitFor();await page.locator('.geo-publication-task').getByText('发布内容与目录签名证明已归档到设备。',{exact:true}).waitFor();assert.equal(countSignatures(),resumeConfirmed?2:3);assert.equal(countConfirmations(),resumeConfirmed?2:3);assert.equal(await page.evaluate(()=>window.__geoNetworkInjection),undefined);assert.ok(signedVersions.every(item=>item[0].length>512));assert.ok(!calls.some(code=>[7,16,17,20,21,67,68,70].includes(code)));assert.deepEqual(errors,[]);await fs.writeFile(path.join(work,'result.json'),JSON.stringify({passed:true,actualPage:true,actualWorker:true,nativeNetwork:true,nativeWebSerial:false,usb:hardware?'physical Wio L2 via restricted browser serial API test bridge':'synthetic independent identity with real signatures',author:hardware?{fingerprint:hardware.author.fingerprint,generation:hardware.author.generation}:undefined,cacheId:expectedId,created:true,resumedVerifiedV1:resumeConfirmed,updated:true,archived:true,directoryProof:true,deviceConfirmation:true,refreshRecovery:true,signatures:countSignatures(),confirmations:countConfirmations(),rfRequests:0,configWrites:0,keyExports:0,locales:locales.length,mobileWidth:390,clipboardId:true,resourceSizedContent:true,nativeCheck},null,2));console.log('Actual Geocaching page + Worker + native network: create/update/archive/proof/device confirmation and refresh passed. USB: '+(hardware?'physical Wio L2':'synthetic'));
|
||||
}finally{try{if(browser){const pages=browser.contexts().flatMap(context=>context.pages());for(const page of pages){await fs.writeFile(path.join(work,'page-state.json'),JSON.stringify({signatures:countSignatures(),confirmations:countConfirmations(),operations:calls,responses:hardware?.responses,status:await page.locator('.geo-maintenance-list p').first().textContent().catch(()=>null),publication:await page.locator('.geo-publication-panel').textContent().catch(()=>null)},null,2));if(hardware)await page.locator('.geo-publication-panel').screenshot({path:path.join(work,'page.png')}).catch(()=>{});else await page.screenshot({path:path.join(work,'page.png'),fullPage:true}).catch(()=>{});}}}finally{await browser?.close();try{await hardware?.close();}finally{if(service&&service.child.exitCode===null){await fs.writeFile(path.join(state,'stop'),'stop');await Promise.race([service.done,sleep(15000)]);}for(const entry of children){if(entry.child.exitCode===null)entry.child.kill();await entry.done;await fs.writeFile(path.join(work,entry.label+'.log'),entry.output);}await new Promise(resolve=>server.close(resolve));}}}
|
||||
@@ -6,10 +6,12 @@ import path from 'node:path';
|
||||
import {FrameDecoder,FRAME} from '../../site/device/src/hostlink.js';import {decodeManagement} from '../../site/device/src/management-protocol.js';import {decode,verifySigned,equal} from '../../site/geocaching/src/protocol.js';import {validateAuthorInfo} from '../../site/device/src/author-identity.js';
|
||||
const allowed=new Set([0,1,2,3,32,33,34,35,36,37,38,64,65,66,71,72,73,74,75,76,77]),hex=value=>Buffer.from(value).toString('hex');
|
||||
export class GeocachingHardwareExchange{
|
||||
constructor({port,python,fingerprint,namePrefix,resumeDraftId=null,calls=[],signedVersions=[],checkpointPath=null}){
|
||||
constructor({port,python,fingerprint,namePrefix,resumeDraftId=null,resumeBaseHash=null,calls=[],signedVersions=[],checkpointPath=null}){
|
||||
if(!/^COM\d+$/.test(port)||!/^[a-f0-9]{64}$/.test(fingerprint)||!namePrefix)throw Error('Explicit port, expected author and isolated test-name prefix required');
|
||||
this.fingerprint=fingerprint;this.namePrefix=namePrefix;this.calls=calls;this.signedVersions=signedVersions;this.signatures=new Set();this.confirmations=new Set();this.confirmed=new Set();this.drafts=new Set();this.transfers=new Map();this.outputs=new Map();this.completed=new Set();this.pending=null;this.queue=Promise.resolve();this.broken=null;
|
||||
if(resumeDraftId!==null&&(!/^[a-f0-9]{32}$/.test(resumeDraftId)||!/^Wio网页验收-[a-z0-9]+$/.test(namePrefix)))throw Error('Resume requires an explicit isolated acceptance ID and name');this.resumeDraftId=resumeDraftId;
|
||||
if(resumeBaseHash!==null)assert.ok(resumeDraftId&&resumeBaseHash instanceof Uint8Array&&resumeBaseHash.length===32,'Confirmed resume requires an exact signed base');
|
||||
this.resumeBaseHash=resumeBaseHash?.slice()??null;
|
||||
this.checkpointPath=checkpointPath;
|
||||
this.child=spawn(python,[fileURLToPath(new URL('./serial_transport.py',import.meta.url)),port],{windowsHide:true,stdio:['pipe','pipe','pipe']});this.stderr='';this.child.stderr.on('data',bytes=>{this.stderr=(this.stderr+bytes).slice(-4096);});
|
||||
this.done=new Promise((resolve,reject)=>{this.child.on('error',error=>{this.broken=error;this.pending?.reject(error);this.pending=null;reject(error);});this.child.on('exit',code=>{if(this.pending){this.pending.reject(Error('Hardware serial process exited: '+code));this.pending=null;}resolve(code);});});this.done.catch(()=>{});
|
||||
@@ -61,7 +63,7 @@ export class GeocachingHardwareExchange{
|
||||
if(code===33){const row=this.transfers.get(hex(body[0]));row.content.set(body[2],body[1]);row.offset=Math.max(row.offset,body[1]+body[2].length);}
|
||||
if(code===35){const row=this.transfers.get(hex(body[0]));assert.equal(row.offset,row.total);assert.ok(equal(new Uint8Array(await crypto.subtle.digest('SHA-256',row.content)),row.hash));row.sealed=true;}
|
||||
if(code===66)this.drafts.add(hex(body[1]));
|
||||
if(code===64&&value[3]&&hex(value[3][0])===this.resumeDraftId&&!this.drafts.has(this.resumeDraftId)){const row=value[3];assert.equal(row[3],this.namePrefix);assert.equal(row[5],310000000);assert.equal(row[6],1210000000);assert.equal(row[4],0);assert.equal(row[8][0],0);assert.ok(row[2]===null||equal(row[2],this.author.publicKey));this.drafts.add(this.resumeDraftId);}
|
||||
if(code===64&&value[3]&&hex(value[3][0])===this.resumeDraftId&&!this.drafts.has(this.resumeDraftId)){const row=value[3];assert.equal(row[3],this.namePrefix);assert.equal(row[5],310000000);assert.equal(row[6],1210000000);assert.equal(row[4],0);assert.equal(row[8][0],this.resumeBaseHash?1:0);if(this.resumeBaseHash){assert.equal(row[8][1],1);assert.equal(row[8][2]&7,0);assert.ok(equal(row[7],this.resumeBaseHash));}assert.ok(row[2]===null||equal(row[2],this.author.publicKey));this.drafts.add(this.resumeDraftId);}
|
||||
if(code===71)this.signatures.add(hex(body[4]));
|
||||
if(code===75)this.confirmations.add(hex(body[0]));
|
||||
if([75,76].includes(code)&&value[3]===1&&value[7]===1)this.confirmed.add(hex(body[0]));
|
||||
|
||||
Reference in new issue
Block a user