diff --git a/docs/reports/.gitignore b/docs/reports/.gitignore new file mode 100644 index 0000000..9c4e5d5 --- /dev/null +++ b/docs/reports/.gitignore @@ -0,0 +1,21 @@ +# prod +dist/ + +# deps +node_modules/ +.wrangler +worker-configuration.d.ts + +# env +.env +.env.production +.dev.vars + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* diff --git a/docs/reports/.prettierrc.toml b/docs/reports/.prettierrc.toml new file mode 100644 index 0000000..1808fff --- /dev/null +++ b/docs/reports/.prettierrc.toml @@ -0,0 +1,12 @@ +printWidth = 100 +tabWidth = 2 +useTabs = false +semi = true +singleQuote = true +quoteProps = "as-needed" +jsxSingleQuote = false +trailingComma = "all" +bracketSpacing = true +bracketSameLine = false +arrowParens = "avoid" +endOfLine = "lf" diff --git a/docs/reports/ant.lockb b/docs/reports/ant.lockb new file mode 100644 index 0000000000000000000000000000000000000000..b05cf82ab53b27745eb6486093dd877d12c73eee GIT binary patch literal 14232 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 statement-breakpoint +CREATE TABLE `reports` ( + `id` text PRIMARY KEY NOT NULL, + `runtime` text NOT NULL, + `version` text NOT NULL, + `trace` text NOT NULL, + `kind` text NOT NULL, + `reason` text NOT NULL, + `code` text NOT NULL, + `target` text NOT NULL, + `os` text NOT NULL, + `arch` text NOT NULL, + `fault_address` text NOT NULL, + `elapsed_ms` integer, + `peak_rss` integer, + `first_seen_at` text NOT NULL, + `last_seen_at` text NOT NULL, + `hit_count` integer NOT NULL, + `expires_at` text NOT NULL +); diff --git a/docs/reports/migrations/meta/0000_snapshot.json b/docs/reports/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..3957500 --- /dev/null +++ b/docs/reports/migrations/meta/0000_snapshot.json @@ -0,0 +1,200 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "74f454aa-26ab-4ce9-816d-b14eec7662d2", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "report_frames": { + "name": "report_frames", + "columns": { + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "frame_index": { + "name": "frame_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "frame": { + "name": "frame", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "report_frames_report_id_reports_id_fk": { + "name": "report_frames_report_id_reports_id_fk", + "tableFrom": "report_frames", + "tableTo": "reports", + "columnsFrom": [ + "report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "report_frames_report_id_frame_index_pk": { + "columns": [ + "report_id", + "frame_index" + ], + "name": "report_frames_report_id_frame_index_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trace": { + "name": "trace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fault_address": { + "name": "fault_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "elapsed_ms": { + "name": "elapsed_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "peak_rss": { + "name": "peak_rss", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hit_count": { + "name": "hit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/docs/reports/migrations/meta/_journal.json b/docs/reports/migrations/meta/_journal.json new file mode 100644 index 0000000..262e343 --- /dev/null +++ b/docs/reports/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1777152332594, + "tag": "0000_conscious_vampiro", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/docs/reports/package.json b/docs/reports/package.json new file mode 100644 index 0000000..601998d --- /dev/null +++ b/docs/reports/package.json @@ -0,0 +1,30 @@ +{ + "name": "js.report", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy --minify", + "db:generate": "drizzle-kit generate --config drizzle.config.ts", + "db:migrate": "wrangler d1 migrations apply js_report --remote", + "cf-typegen": "wrangler types --env-interface CloudflareBindings" + }, + "dependencies": { + "hono": "^4.12.15", + "drizzle-orm": "^0.45.2", + "short-uuid": "^6.0.3", + "zod": "^4.3.6" + }, + "devDependencies": { + "wrangler": "^4.4.0", + "drizzle-kit": "^0.31.10" + }, + "trustedDependencies": [ + "workerd", + "sharp", + "sha3", + "keccak", + "core-js", + "secp256k1", + "scrypt" + ] +} diff --git a/docs/reports/public/assets/ant.png b/docs/reports/public/assets/ant.png new file mode 100644 index 0000000000000000000000000000000000000000..df3534ffd8a07b84a92e2ac28456cd8dad2f787f GIT binary patch literal 40977 zcmeAS@N?(olHy`uVBq!ia0y~yV0Z??9Bd2>3jV&tzbMs;^>z01qf1Ob29Eq~_!%mgE;H1Qg{f80%RuFfg!8U;&#T!oa|=VF4r5 zWJU&t1x#?$o-JTTut5eJ85o&a8JJiZnJO3>S(#W`8Co(_u9yHakF&rdvY3H^TNs2H z8D`CqU|?Y0kQowD65;D(m7Jfemza{Dl&V*eTL3bN!KT8hBDWwnwIorYA~z?m*s8)- z39P~@uhiX@M6ib6egm+a%`ew0!zY*RT`p zyq@a5YOj?!IS*zUM9er+ap0KQJeBB4rmI7Wug*UH{{QXgv5`H6J+WGz-^1>Hf4BSa z&gXA7|4)Cv`MlllintxEA{?3mK?+M8JQ^o3DY80owG2LisZNabOH-cQKXp9+&natt zsguw1JdPx-&#jKzbT=da{jYnMyPsZ)+5On}=%affi?7a{w_5M)x!UaCXRfKw%g{Re zZvWK03fmh1nttFijr6}I`U?cVhA-S=|Jj83MQPxRUH zXyc6_Be}VqM<2ES$xGS&zP$XmVSV`STa`{N_8&H`jb5$2k?D|4tH_T!k&e3kleOR8 znWpRii~F~EOw@X_bF0HvWv~9~mXejH7qv1xdE<(drPqCb#i?ljw78O0daZQ#J=Uzk zI-6In^+pptW9E4rG28!3RQzdNeeUY1v)J~gQZ{x0HZTIhXOY&y= zx@CTc&4t|3{X0Uk|Gj?j$48Y%snMyk=Wp=>Kjw+n^W=m*_DvhQ@y@# zeYOAR#q(2YO4au7X1f3LT=_DguT^>zzqAGM{?|F59ee3r$@)oO?#O)oyK%;Q|6dnR z2*;lPpSAh!-{)P=-~M{M|Ig{2@BP&O$Ob*A2z=1O^sHO)QttKm`98fW*GqQ4)RtO% z?Q7iT;?y&r+ZS()P!sF5yZTMJ{`$MJ{nK9b+kblV(0=aSU01oU-WGR$@nPolDNO3N z2K@`C{XaPW_fFHRK5wGdU#m#oe6wb0kMm-wy3C4;CzS7cJn?>-^|$ZZy4Y&FuNRzo zwzfojG(L7~6h5T&J1Fq^>wf!vKYH_Rzr9!;U;A=$pnhOpSyXG)TC+>U^_33<f{xvuTE>j zE~ zo@d`&`EsYV?z+A4WNn#U??2|-m(;9Msmbiqa|KDG=-p|vz&-+~0vR>VL z>{;68(pv%bg%7@bJ-Xg*S91P}OKdG%`=?K3nwVO9o2&Mw`nFtcSv#ZOIlFJ}(HHDL zD!lQ8c=$0*bz2;zvxNWX zcAOd;f*N1?->G{2;+5%9r!AMheV3hie%qdF0V-13Rr}&S zs^-plUwt!g_q-Pzvo0#LXA8gIy;UT0`*UmA{9D;u_)irX&-PS#rf&DK^Ah*U0ps&&vzpOO!)Hsbe$gC0CxaG;A>&Dhh-#adiF_}}jA%~L;?d7oSVvt--l zyzkp@-wj!QyR+Q-Q&nzxQc+C5%M-=hllI3hxtr_y$HRN>vnX{*>!1&NnPb=gzEfZQ ze!ZR1^&H!|)vs>G7VZ7rsBrfBrMG?yqd!{Jxn4ebX|Btr4@CktKE_K9a4$U2#k}mf zt>j$I{`hM%y<(n=cenO6XzI(JRd}iLq&obot^D zT+Y^d*>vu<{m0&yng5JSU-$U8xagmEe((2eepmm0nQ#BPxwEUkG2c(;ig23uJ}_~Q z@XKwi73&p0{&eDyfcy8^l`91*Q*-+C&gu6)O*%QFAiRH@75Asyx!)H3YwTFEAm-J) zT$f#!Kb9@O^!8Vs_9D&rx92uo&&|)+X1%;pHf>r(>k4^E(PMs>3r;+h-Cr8F=H`#- zGxt52`~LsZ_^MyY^M9V%!*R=U+KQn5-Gch7iuYX)IdnXZyWg2hN1ZiH#vt@Zn7Zt4 z^|>B`mz{>D#gYx1_DEc3XXS*McDp>u|rhhz0`<^1(Hw&dxWBOhYb8O|i``tJ(9l&(4q3Ro7c{RXcoY_E-7*uvf~Po`o5= z{!wSSasHT}b?$P9yF34VI?cVh?Q_0w`X~99r=NV@>}@Rn_wo6^m)q+Cem(p-fk|JT zHEhw5FaJL9|KG6A>HgQnB7dg&*K&1Qt^QlL@Mq)w>hslh+jr?#U5wfM@0v5a`h77? z-F?^nk7-|x-EFtFIxT2tp3S@C>_HnV;?`BpEm~{*ckv$k8nYz_`kfj@InT&1b-4dN zawTi*`~H6!ZrAq}z3w%?7q1yUz2sf^r;|UvR^H$5oxG^K_I`?_EHy8Vq%ST==%sVOFt3Fv;bjxcU zSC@ zwd-cDdV>}R?a#k0>_27y@0}mYJsO`bI-tpUrb_+Rx4#~-kLSsrRg>)ex#_=Kvbei) z-SVLI_PejVy|oX{uwBl_>gSoDCdBFQ$IcYDWa$q3*_U31Sk3jBcR9x- z?RD?3>8ITLa_mbZ6m(vCEdiMwaY#L&j*<;BY+y2jZWgn94uPCtQ z$f9|Z!fvMM*~)bv>DrlNHScU%@mxRkAidD}*=AY)f4sDQe`mjtw$_=;D_t9d1!olf z@SHR6^pZ_GbIdN~Y~7o+wQNI(ZtddWyxPxKR{h(sMSCjKav6jBC1*{K`j{1eO!)6$ zB-dZL^it5?ODk$O3wQoIynO$ssVApK1t#wE(*;FJ%>Jsctje`V!;d}M_#|oNqtI1b z^G#Q6)j#TWwc3B}Dt_-=9k!*%lhK57Kr7mm|eq%ecaW3_Ac-i;e4UQAM)e?{fxsjJiDcfRhvy{%R2t>@`?>V60Pv_iug zdUEu1R9;WqSZcWb@2(V+f18=^Urn_X(wV9qxB2RqTSW(hACv{BfBYjUE&cV<%q>j$ zuJSostpD%5^Tuw@jJa#?{#eLwDtcDIgln$Fo|>SiUzRW|yYM)F?u-t1_C138WB2Gp z`|Q=7d~%=B#GbYJ+BP|R7Yb}_2n-71X49OrIce>0!SnsU>=msAuD3rWx~R&WM=%+JC><{L_0^t`FGb zBvjzCde#q#N*RIH7_U|Qo{qP_9-bR~pfr9}uF5irH@~Ktnrvlj<3}k47B&CEH}_8_{DZ+MUQjWu{_h*e6zi+OHY0C+WF^e z)XGny^>szJ|E2Sv`;v5RUdC&+PgYHE@|>>x2d<9= z_1BfQDqZwge)-k5thK*FHhx`w?dkRV!lUwbw2D+zY7{szex5vkkELAZ znpopmQZ>>YyNXhNZt|>V7r+Ij!&Y z%IldStsFj^Kc;T|y0wH;E7wHovkCLj16?e=#~;t_TmLR=vuWhIuz53P&-r7#e(sLf zz1#1ddX@Z8Ea-vi(gU2#XRPgNe>-fyQjuylxh~c&zGkzSY}C>d%qO=wbWRn0z-Zj& zvWO|1C2$4rnzE_`Pq*)yu<6Vz$;kBGOqy(p?;O`Zm0YW(wo>zJ*d<|yNnt+66xTB7 zFF3aI-Zkq}Q>)&t{2BE0{d2YF|B6okzPEk)mczPB4tTpZ3Ny_pzw__m#|WM8pIe=N zSskt08zK0Pjm02-p}Oji6oadMR#M(a&QvalTU+g2q?Ry0I%3j{)vv3LO!l64Jzl3` zr;n6qs6uG`mbV8oSy<d{I`E%c<`aci(`=5Va`TFU?&tZ&@YgVpkUYmA0pDV}1HrTuR ztoPce&&!sj?@gb7`?2lj>i+Gqzc-(M`}v!U&6BtDZ65iaJ5$l^t-3yLI)CGVo{1c$ zGep17I@@;r$_B5^OW!YBvdSS}%jnhm9*0;J-Q&CFKR3Cf)N4K~+`h!%)p0BP!h=Ws z>+5cBGw>+kST((cNjoa(-pAkjRuuoPH+Q6jFxFz723ghi3 zSEhWw)98F}bM-f_mg0MRRF*MWtd7sjzF4K{xy#Y>jH$Boy<;B>^D`FQpM1K1$x5M@ zD-T&+$X|JQ#g-MDR~-AeqLNcp#qjF&D?8Hf1nWfUPd$0kQt_td)mPSiGIpO1{&M)@ z9ldSOb9O!T|C6pn%s8Rx*eJ?<=DeSe$Rp>S=HF9|_AjeAlfHWXj4ZQRYI~zTJ;>bp zBG`!M(acl7?#%mpZ+rQ>Z}Y5eUdqm!{rS|^S=Td6d`=b0&t9~)dS=KaX$_G*Ta^#r z%Um5+dD!_c)7&1rKaBx*wdTy$TAgsgHom-BSxe>g}?zosEqw*xa-0)Y}GJG#>j#*o^ zq_J9V*^Mb}E>S_g*^_pBt>WFh`deH0V_BKkc2X?w@@HQ3(8^n%@BKf_IE?4WPN&4J zJWic?YCk`0UH^aOQT>{S+zYo~=zen~o((SFI}wPVIjGQGM^*-~1I_UJp!J?98rQsIhy$HhTLx@1;Ta z<}7)!ul6^Kon+go)b(1OUH5b1&7SjCxlW!GRd-%zvqamE&56Q+8gd732v1I#`g@*A znwF$^#>?r~_Vyj%`ZRG#Gjpx|-I@Bfm*#i<-MQ&N&eQ^K%@2z-HY{rJjG6aZPa<}< z_tJl_9pXQ3+SL%wsj}4JvcXDK=AHu$S7ry=e|q@Vde8AP`}9{jLX*};=u}OQDeC-w z>-xTHyZ`-qZOdBvO-#l0{r>-TYvX^r-@EK@pIdw7Z}8=N4-Ot?igv0!zi0cERUScR ztR8LltiGAso|P;R=48D4Cu`}YEKk~Q*l%K&6IXVT1MLYY0|yh)^mSdF27Uv z+rC|EZK(T?k4hRFCWS=)-=-VgwkPb*_2X{r_iqY0c&U1tMMh}{*=tWKFl`hr4N+9j zm3Xo;wtCO+`S%iq0=StXKTeEUUw!xW9`ij9dG8$+kIzwX`|4U1SNHSjJ$NcmYrs$x$;eJ+3tpoj=p%f1YU2S!>S2^R#Z_!p=)U zkqQgXFTeY%o~M7uLf+nY)xndd`Jec;i)8|fT!Z6${mByp-B!JRc({GE4E->Cb?s4Km+duDDP5kg|0@I=c zT@z-=|NoJD(&c7-sL8B~c(ut^t7F#vn96l&)1#?NPhFcZtw<{1hUaZ@70>d_h$l0f z*_Zm6ov|02I+-_Lw#>oO+FHA`{MfJQUkq=0Tgbln{qeZ`_J3;U6DA(5Ojzr1`UdCY znLlnn3h(BQ-`%}@f)wj9n_QjX&sHBLE}8CW%@BDsY1*@0>Zaxsz0!BDJ-6qM5MR!& zFPmx?UTHb?@Z0-^2MVXp2x9%~FZ<(e;QIZ)+kA}bCh9ESmsUK(>*axtD^recNtZk- zWo^CxiR_G-X0LVJ6dX7$B;FnPwX6PrU2v%B?I$a?Jejd~|Ni^8cNQ;iw4JbN#mR^w z>9@I5a;nX%^gR{5kRa-&-Dv({?D`J-dJ7?cC=bW&ut? z(^NHIMrUy_b8FOk%UpeV-}l`4N_R0a)vYn#c^g=*pSo!&88#Pxu?|dUR8w&4b#>iZ z=-tSp{eF>>%#_om`8fp#mZV?F_Uy}={NhNd|6Z@J+JX5~*%`Ope6`}grhKez|B^GO z9xBWhU9mcHy<)=h;+?)5BR)-)zBcDuM-XQtr|#jmticZ7jz8X>*y^jgSUTzBi>HUK zW%XF|G$woqoKmvB=T+3@8H@HYYD|kbCM~|?nUd?ow8kw@X3I{Hco3qqME$UGgUD)~ zthY~|ELkesFfD(He8X`85p(V}ZSGU}tmoOEyLiAeC2d2*t}6RgrmMbCsTf>al~iF^ z#mb#?{ztHDR@R?MhCVa)s~9NkoRGnw&5&BaW6?Y}JLikw+VxsnMIW$zztto3_t;VX z^l~bJidY4~eqtNHKbA%Xler9{0eA{3HzeudC~RJKgkIKkegFCz~RzAa!ril?$iNs(iuVG1tT46GjlLK&s37jUbnnLLn^4mwx)YDeYlKWWb;XFK$Et@>_IcJhl}n`6$Qt>={g z?Fvf%C~!#PN>_%SdTpF!tHJI^*53ane)9M}OR8^~%HAuDhD+=3eJDuu*>1bq_Kp+R z9tAEL^Mre4x4&|(?48c(_FU_g!#xhmg*lN){KysIs?Kk!G+;gbK=%HY7a(*E+idlwvXWAX5ePoMj?V&gU`#<#k$ z?nT?K9$)jzFX_>2ZjHdX(vx19KD8I_f4X?%jawB*;uq(%99R-`=q+pDgB^^QqjlDF zuI-9bU6HfG{ArwergJ^-{D>KQnxy5Le@^mYbMZX3>lokZSBE1TbQyU|MFTc5pGf{a zo#A6d0@I0PRn038cb)$!x6&ir==vok+n7(c_BcPD`NdRt){2{br(QZ#yCl!bo!K~x zH->qxR`x5MzdpXQ%u6~;w8Xytt#fsa_}-ND_{r{*^JmQXVE<(yf93w)yZ1})jCy)| zW7bst(>vx{*H|d*-d>lp=6B1w+&Qyf+bzkDeShc2lgY99NlZ(RM*i@#yY=USkgR#a zx>F+O#o5kfUY-5d^y}3R?+$WBPx{5x94@Kk_{(5|zMKEn;A`i%9IU@87Rc5&brz#W z|I`^%WInKp-FWc(#^>Yr`uo?ks=Qp9%eYI#zs2)%8q1>hHxFuDF>+__^exU?kl%T$ z*LwcWS0C5jKbv-}`^e*$B}RsI$7O{(b)(hhRA1s>J8AOsN9lIIX7BV_WKnTF{*UXw zTgB&3o!2?b!@ni^XiT!5xctfGA{ExRz9!#gew$@{IVa@%k!bG48;=AnyB+akdGF!P z)^>jm9`)xt{ycAI>Y}1g0Z&_}KH0(W`ECAwz1zLj(@z~<9{+f9ooDUoRUr|RZJ5#% zI*-coDHt^~>&uugy!rDmn~;#hrG6e2^#op(WCg?KeX2o|dp{qPo1;9%!^>`SgTbTi zUwU74?_ZsX&#Ppfh>@~*D>diieO{`c{A|IMwlr`{6&>X-iePtyl$ zlPwjS)*BbcG+17cja+;~?)kP^Z|6vQO9*YWo3*4*XZ_WNovNA}>*Qp;Lv)SGBpcpG z6tHFK9(Zgc-N3VtIg?Xir`#RqsD?=kCoYj;{jt{6UneSVw|Iwad)e;(>e4lBaao&- zFFI%KT5xH_Ru}t(n&7`lUwb93I*&_h9-Vscg1v#8{pTf2Y2k`4|FddpEbgtE8!RKW z%VvIG{50?7qNz%oyaKrumz@x~ny7wAXO7oeC9X>bLE5&wp`}-*1o}?Cpxn76%I*Gj zxsyvDT@>u)%@4{gzqPZ(J<)Y_Z&Yr6?v`&Mt1th%cHQo~`F(+X<)N2fYW{THt?8;# zJt^ln>x|BlCq5^)UcOT9BW3WEpIc&L--?EVHXdyrQLhd!3|ckysQWn;Sp`lN6^6$W z2}NhNCU75Mmypy;WM1&cZrk~j!a8PCCSLE4Wxvo6@H{p^|IIM}DkKV5ip##dNa$9aF!uc-Ndf4C@4mbmfZ{LVA3 z(%iN7GtZ^yO^e#)cFT29V7^M!S2vN|<$VkNOb+a{U)RZWMZWH=o6n1rjI%oDn4Wp9 zHP=*MYvPqFa@TLXN}PMaPBLXa+9G4FMDZ*8}!algd+eENBRA=6d>5d-nwrd5d$@XV7gvhye zC9Mv6SsSaW`yo55Y@8M(f|N60PEu&z8+wx~ZpBs&~#DMk@j*y>Ju)@ z)!oxxg>5s=b&JjZvmi9|aLNv|M@wD5IIoWPl?X7c^yZe;pRIj-iClM5U}(6K^5a`k z`m=YMMCZis4Ow@u{{D0O?@wzI9~>-RzxP`U|NBjE4lJo(Iq|Tuh>PXxD-n#rTJpNu z|28ye+$faD_nx{mh*2`gdk;7FZZF{lOaY%{J$$u95?HOjUv7$aeZ9}2itYNuX{|Bm zRMal^dsA$>~hTN)VlUGv;K6{17TT-Co=M7y3%>~>I z$IYb`679du_V%RODU)o7nLSb552 z%icEqWg>@9HV95^^w=|HLRv$~2?N(NOStEF?mMfc<2FT*WuITiW#dz?9c<>wgyt-o zSfR6ZmF)X@cV$1n|J&{V=icA_1+Vk>uVGxCU;qF2<>hnBuembU*5-SuKDvHn&%0$| zivx2qHaX6HsO#x+ta$C6Rpu&77w`V0&)bw|#hd7RAm_}^MD?2a7qv82pY*vnVbf)c z!|XQt-rH6lYEjuLdSK~ArO?G$qBE~O&;E16LHl`Mnet4JZM&UMmMmKkddT?owH{He zuRrT|ygvW$&BB`{RsUQ}L~7NiOLyT2{$9<{RVFI_ z{Tq+o^Lf63Yx#cn*DnngYP}8BxF6=pBsR(T#)_Ov%NFfyx4ki8T3Ou6)JdBD6MwuB zD%RP_7SMC>OYZSGNvp)`=WO=nNzL3PHeZcl|82boBEd-;Q^UHBv}Q6-=2~c}&R+9% z@|5-4!cROe@j0V-EJC$)sboj=#93aRt2X|MlbH5Hm;H~6UVOylb7^j+m)m@lwr-m% zomKPCcz)%F^tzYRr^VF8UGo3GdH&yueH&in`YjR4_jmhiUUvKJ)gq=&p~yvRjb`dP z_$hANv(WO#>N~atyBuzPd8#Oux{yE4R9`b|;gT6X5eH(YUU-n?GC6w5u{Ph{uSG^; zSNC{0|E;{LxiD6$l;y#si}Jgo1KzbUthQOPpEF)XXyp~li6K=HCa=8nJdO(oKW%@% zanX-YI?Z>cM#pf$Dye{aqddorD-t|K!wb*JUwdZLwUbxEL($Ok zuiqhep{Ag-uLHYexcwYBC2rni3N>X&D>$e3s`;9%_nU@acFnDeCSPGnJ1>zfmf*lP zd7~lwBlOrT`HF7@7T(6 z^`kS}%sGp@mCtM4tnIJ~6t$K1+4M#0 z_hFlGnoY^gM$KgZdY!pvJdIp)VuEyageOc?C|RA56LM&pg!IegzqX-#e9vBaM@R^) zS)?O(gzZNk>+_8Vco=^ERZ+dpz_L(>QT6h+fb}Psx5-X;ekH>&T7mJa7t4a!E7fcvp!9V;p6x5t2E}4IJT+Yaz^kZcV{=YicfD(IY#+0DF+74 zy61LBd7h`fQ0%@n6J|!PzY%_hXRX|BuS8Sh#mYx_TEE-z_}z4;RACL}^k+qnFYfr> zmZI`c&hW%eZsW~n*6ikC`1nW@lO_{VMvp>{D?6qUNt>U9~O; zEa-Mh3a$^eW#%l+kWO9J?>jf)CtHV1_*2iR7cPXv#)a!l?O=1^kmzRDIb@VGb;2=c z^W&yWvyQ2A&eD9c*Js~{u8got#<$Lv%<9$ea=S0q>(_B%(P@64{dec*mR~;WcAGJ{ zC6Bvh#RP^c2Uc=9dPUZ|-v45*J*#w*r}rVwIi6c5RorH|794CFy0>rR+#@^;(lN%z z1SD2QXx-^?tV&z5{`}G3g2@UybBkw`pE+WjC2?b_adin4=S6Pz}=J_1Hy<=6qjlA!lu3!7if8PD} zYUSbJxX8q~4O=E@A6l+6HTm9|JA2I68?BAE&U>J(8@k3$XH&C4dU5Jk2L_+%>enxX ztqQX5H)~>A@^|N_;N>gc{qX&KZqk*TOJ0cH5T9_u!*kkBh8sG+<_66d{qX2PW1@zV zz;>&1Cl^~7Y}jTWzQgyS&v~iO%m4qHJpa!V@%)`n#hx$yl*GA}=e(}9f$&|AWp^{x zOky6*E_CsE&1385#N;M%bL);(9*rz*D?C~=O*V@$&h<)sr*es7g5umJSq3)i=Qo&M zmg;B(_OdStS|=yBN#TOU>CbVuTLi8|Hne{+<6QB5i)hL%DZWn`XO>%+$oHhIntI@@ z+r5dKOw0}~^_(*EX65IcsI?zdGkMuBbSyaan)TTQ1$Jfj0QE_wcc0ES(AuG0)RcZI zNIH|{Qdf9NVVlXe3+6ZKzg~)YI(@6ut@@o$r}^*ASXTXb^|}2DcXRH}IFXW}H4Eg?`zGEV(I+UxT_HM^E^o?>WmU{e|P=TiR_4&#qvad!^u_!ow&2 zv#xBte=YQT{+7o-XB_LfFyY1g>#S#{t^9Fr--Uo=cea|lFM4vN+&oW9d)Eg1n(GlX zvuTR|%9~aEwGSI7USvF4{VTIRr{ZzsTaCPl<+o0UeK@{}+4$N^6_r`Y(e5283(RaIC#19-h;sjbCgMOJ^MWG&>k}s& zuS@Q{Yq0Iq>+1%b(%)a2?%j5K@8oOqO3nnWuFFf%d^YE)yxK*>_hyrAe8`Dydiz6yZdORsR|nxBZq+E z!-SG6wZG0W>ZU&v^O<%1>0a0R$wsrLv`pIj>%r{(>vrg_I=0@aR)hE5=Ip*ZB6o9N zO?H--imh2XGh;!tZ`Pu<26a8N&X-<~@VFaVyGT^nP=4_&`IWPeO0R8AFK$gzo9Y>F zV`KLte*d>t>qp`H3s2j#tdH)zf11x{qGyX3Lty;TD-rRo=93M6Wv*QiY|W@@a#dZ( z!#pv?V@0ZrwA{D0rA=~IqEvki(-tq>sAp&`y`WG__0ri;8K0KZ@()xl7{9-5VD_vx z^R3XYRR$t!_0L@Xy}kMbdvB}svA}r~n`}K#lm}n;;C|e^G9WMEoRpY~_OZS+>qVg} zr!P6BH)CVVj4fy0s^ow14oqyGzALRYKBzlX?pT56{5pxhvG@9o+boSel+I|zsvdeb zzp!jud|}tUZxgm3Vd<58YxApVn}6MpX8UE0fA0tE)6)$1Zna!_GDAc54!5gxn`YFe z4x{X2Jf&9Y$>$d>*UyZbb}II&sZ*boz^knd8@JCsKBeKwF1ZVrFPiQ!Ke57i6X$`f z$_Kns-cssbhitC}tZMAJb}`^ZK2yxQ%$ZYCY9x0(tj<2af-lKr$0GUO>}@l<(?9Lx zW;6QvzvQ`Wygp z=_PKeOq2L_tZ)tK;&puC9+uqa8W}Xx;H-&Z1BU{y$0~*y_u7)CEVI<}c=f^egwTxY zuNM}SAJb}G6{79Ebk?giU;0;_zb5cv`z4kc6<-#Z&7NYC@Mwpg+SFaFCX+udkt{B{ zx#jD@2=B>t0@8+={N8tO*T3ipKl60!^S*t@L#*#IH!qS4;#;zOPo|I3fs!B34n6sx z{`2wNDgC|ACHbGqdHTmYvo_Dod(?P+Ug^1i*76kx?xvLgd(dzHV{ZKZgdO!C{_cAG zswi`%fxY$f%M4ETv&*g*|5Mn)J(+tIlje1v8CtWu^0RDi@dOzJe9R2V4A{Lz>sCey zQ@NXzsoAMl2RtWd@b+XpIK?kec-eWbsow?T9q+czK4&}W`PpEfpUIP&HwRA03;*le z+t{VYFsUxqVW{K+Vn0z4bMccJmZOqv# zlf)vq`?^%in&o*fOkp|BG%e4;)jU3C-l7M)rnVjVaq-^C&3~@U|25%4d+$E&zgPU` zJQU{fk$N;eL*XqeZ|sy+QU%kG%$hgvQh;Nbj`aJO*gG2-SN_bmD*ONR|F8Lrug={0 ze}PPFXq3iN(TC^VHydzjW*!ze5h%dbbFlISgT|{}eiL&~&At+|OX0!+o|qE%fRC|; zQ)Fd7te;!OBPG)lr6rQvf3?3c^n9bjZvX2Sr=3}#c8GJ;EO~7yQ-f<0)zw3VPkDzd zOVOC&J6$%H^~ADin-wNEh|F@Vwlwo7c(!iV$tU+qkIVc1xi{^*&l(HyeW}xDh|jD0 zwc=mSyU*d%mnnx^ul+PjxL^ zrKrv^`9l&Dk9mWrsM&14St8e0rA#^$Re9^!jX9b1fyZXow(~`=wq0>yrbzqtgELH? zIDVWUQ=lF+!SlwJcXO1VO^~$uBGcpFyuiBj&a|#GllWZ93l6i_vaQ zoL&95MtLRE&Zp7VA-g?KyYKNLKbPzsE40+NUbr)_bk+S2yu1wO7aE?td$Ir6m&6*Q$~sr>l<2+?V!DuRqqOOw;LQcn(eH0J28di`yufL8?sB!5n__}4mh*1S2A#JUU=yuSAq3x!)5b2 z%VMox-s3HfHa;(Vt9R=f#RpZsf9-#r-nT@3qP)cYm`RQ~3KQ>0ymvFtlaaZzw7qV@ zm7cTd=et!FnXh;#by=v%kg03uvB0OoAI{uhe#{(FdwKg3L!-3^8xGh+zHDXco-6MA z!RNauzvmPKDVD3(w3lsC-o>C-E4_9FPv6_ijJGbjObNQxv?}9?)W*J$4b#6Y zR1|Arwad7X^(gqYbS#5{W{~)@t1AK<=3P;b*4g;tJ1<|~N!?Gr&O47C?n>E_e)`(f z$Ff`A9OzL==H7SzV#m&f3yyc%o=iORTtmOAtMXtfgU`8$(3e}H&GNar7i0x(ZnTM? zwqu8l+lG5g3~dHEwmy$edd)p}+a-8%&Wxi`v9gE1Pc%y_3thvXe30Xs*bX+wsTs%Q z8&)Rse&mRF^Jm$yyA0+*y>*EPObm2-*&8CSNGSBOo)h$tm}lsj$>Xl5s>-M`0*^2O;{{1!g zGXu8Y|M#yXeP3~`&CFd(wrrUY6C@Lty~ZcHaMdo!GcMj+_^(77r@Wkd?fg5>ye!^b zC9F~V0uN5DT`b#hprg2C@tr>A2OAaU$e}mHsR0ILSLRF&6QV}|Nb>G z4?XKs_u{SdYPIb`*3W0;KAM%C^fGk$ey?ArT0*XD{JFF8t9D@t$ARY|5zz-ch2%qT zCOv&M*TXlX?n=V4J&O!42soOn2vwSu&Uu{RzVxc$(xri^D-Qo@@=dZbNa}pX#yp=d zXV?69=3C`91=PKYd+N@#C@d%Q*(HB%A z{y>E^SVg@Lx>A4Mmp0_r0GVvC8`T(%EwB)rm&~nc9Lrotj;wBJd$Pe`?AFfeBJ` zCxryYeCJ@)@+_Fv^6HrF>tO#^tjE0=?YCzJ$M251E#!av{EVwNR_$c<+EtQJaB79@ zGPk5l)7g7?+nh{}G;;aRmnogXTXpHij-|>~LKoM4u{*KE?DrLRUWM6HozrVgBhoCd zTDo&i4NZ7%mv((c(3`sN4__Sc5q`zCA+mW@x8Lf(d0%2&7RI00pXv8U>}=Go@VWWY zS;42guA3e)+~sUEXJz1T!HE=#<(U-X1e_VcC3Wjo#+xH4t#&ax$Yqc1nF4r2Vx zD8SRJ$nfk~M#HI)MiK|MWi=W-icC>B?w-qM}=@C7=;lcy@2V_Zemj&oFSPEZA4ld(&dlhg0Xv zbIRiN?_Uty)pe}PBuUh5uG7;Ft)@07DzZ-qkZ`%P_=*bEq~ zzAP|YDib*K@5VdVmjqsD_#jrfWk!qIDLa!k<^}p27l}RoJda5zb;-Z|mQi`OHOG$} zIWw*EvAjv{&BmRfvTx#dee^xw!RY{E9C=bjl2hW39v41X=yWZ4$BdhN-pGR&{k9QS_Fvt4APxKnes zl0gnZjrlnqFprNyVBmuV{?S7mO8Iptl!bKGa^bQ$ia*GLwdJ2cU8eH+c&PW z{;qUnRk)?3X>Fx$I{SkBlJ+B$-)~}AXSdX$Z|UUwVjukvY%bE~XB3z+@6)pvY_${a zOt4(R`EGOd!uGNy^81yw7H3@(ZLOZdcYUeKjm=Z$PhPj@o0zTp&AZQDefoLqq*Pe8 zz=~i`x2djWcIIho-A*;m`hE5IaSjdNuCq^j-LCeU9~b71%2xaDuD!C5Wu=M^)g61My5uFw+J z{Qc*hib2$bty^s`+!WrM-mB^%+_usFIM2cAx6xBQUwdi${AU&AXM0*F#T)aB!@cR% znIq|Kz7xu(q^8wf6ub8H>XuW>jI`%BG~A7Bm}(y-^Y+x(9IO?LjlI=jlaPe$1cnrEY#r z%w=xx?nzxi@tU*Ct}l0~EzSHScP`rA@4c%0g}L?&Nwsbvd52RLynQiy7em6Wi#$AA zL^_Is!cBHqd0*VAe#2w_=PO1Iml|W2-#K`$a-KrGNyD8ZrWt&_H#0d-8lMiI(583W zc9~`8wr_Gm7grdhB)j z{@gyd`2xlJ7pbjVck1Y=D}JnD!6v-Q(?ViBC1kTd$fa?->iU($?~wH%NVhzr!!;*4 zrFQYAMP?8Gx$RLfyX1Ig*9!&%u9pWi4nAALuwO@aX`|F>3$+JNWPZAs_I5KGe3|hi zgmtn+Lt@eUJ!|GX>F(m6cR0y&-h&j0D-yF-so&@N$o$x&(%7L$x`~1F@?%Bs^MAaj zvU^?nz5TWBSL;NRKVI6V*UBz>rE?jrJyDWg_3@gYZNRspPD9o2Rh|dyVpv|<8;N=S z}^uyImhT=86Mu2XV-Q~cN5_@u2v>eL0M8!kw9Obk7cQXDol_*Cw@ zjtq`XF0b>Li}>$Olb2F?eK^QcZ7H*->;)Z(C?o^NR^F0r8tB+`f~0e0`31&==GBYI`4Vl@;&&`Q+T(uc!5Q z6?ll&EY(`4A0Qms`z!eA%BHnJ3%J;rt{Q0RgtT4ji@rNOxoGQFgWX#n@Wsd+mbjLs zy?5P?OIgjouCX>R&N?3SWYSv$;Wvw7d6F8gZ2rQ3QutDX*tDloGwPNFNUIr6?21u- z#Bh1{Gt+r5EZkKOML6H`h^sxEpO$gS@YD58sSY8MJaekd9hCW}sef{-yW+qg#K<}I ztf&8;{5uTReYYc)oW1EF6vMV*$%4Jww>sb3{XH~sXN39R$FJD;c?WW^pP0|+Uz^Bd zZINMMKgrO$*zbC^ltjytN4qRnPLp-7c-mRRHj8U(!I~*s9?NljWjJbZD=6>R!?514 z@R)$uLS~bT@fTKnlK6Tm(joP^D~~&S+oF_@!oF{2otwR73FEcB9vRQgICwKfc7(>V zT%9d>d&P@;zgKwAaV(jAcY4s1nwPe{Dc7wl?jCjCZMXle;NBNMa*n$uIT`vM|Fexr z=l!W=mzuNginLC!YvgdP?fhva)aY?`29wPt=PQ|#Eh`MJ#2tS9S>&?ofhDUL&;L3$ zL&3R0;)IEuA&Ya2pcwNdQ|6_=Dz;p?7QX39BJaG=C^i;#-N4BiPuCi$HqI12x$cMc zGlR2BVrPhOYUZo$yg&7deevYVvx1Bt(|y_wvb|pYX`zup$;=D4rK--Y*|qCX5D!;! zrcY)NyKh7K*L-IYHD4L#SFL*6PemmJIR0(PG`zQ>UfNUbud(UQyxe`ZMN2N{?hD#B z-LyjK;&c;Vhwqp8k9B2Ei}-tN=Yf@Nd>mDsxqKRXaz`!f`M|qBnsJs z)~);1VUDU)*ij*I`;2MRXK7sKI+C1W8g%r_IgZ}x=dxdP{|RGx;dO0#;isCNHgPfU z@BdjbskUuP|M9Xt4KJ@Qx|nV>@!j=FGoJ)(oV~l@=hSsaMSfn*ySY*0k=7jMS50^%?G~>?1X{!QwI5j2feddOq?&h*K-k+ddx3Fug%1MvVulZu>63diN zzUEqVL1NePl?7ieAbS=Z|3oKh_O*W9f zwD-^?+_`kodtrL`>wOpAcU-bq*|&&c zhw6k^cFQ#_F3h;wo#NSB;u-T&azll9rZb1$&Eak-GKF*{yE%{yag(VLyc^w6K&+RFi z;FTo6+$SX!qS=?2w@qRB9{JE2H9NmYuIqbtdd5YEmBG7G?zzA6K2+1vl5%QR>-7my zzV|v5#IGB0WooJUyEtp+o?Z1)e1Du0|GYY>2@Tt}l`N=x%;Ok;qsPmZVMmOG)UMK` zD=${&Wh{_5=ajnDK6Q@$cl`iG->onCvJEuF&qtU$FEGt*oxC@F$MpFoHYR4x3}fqJ*~HU$2yPWhcA8g7fx)Ssl@tt z=PafP0&!cPSj`AqX~4Ll^l-S=z53^9%kO}p-M6)TBmM#JgdDF0q4G*XyBPzv zN7zdzeLa_WrjhN5@6PRZ+k=lQtMQ7JMNOZaPWU5+q z%Pxo5^;dGI{QJs%``=WJAE)=pnukuC^lkmU9s7$ip2uGFOHWv^euJOe0-33Tf!cae z2L)GYTsw4S;fw22;ze{#oMC4HtT?2aeVpl5aPNv4aZ5kxoPVg!S@=|Gee#U?TtTsy zlDZW$gl@0sxzhQ|;&@(G`a@}3g=rU6pM@EzIJG|f#4f?;5LT>T=)>Oe<@U5wpH6T8 zb0}-iW#4(nn!Nud${aZSnaB5lP2tfhw@DY)8!zsZXxEJMdEK>wKdm+}boS&H$WuKZl@KECqBc+X?Zqs^QhSDv6L&(EyV zYin5MW3=L$OZd{G?`JG=@w}*JRF@LK`1o9>=&y5OPSu{dJIyCP|B>r)cC(&MuIBU; zQ-wc1N!uFtSW2fqLRE_SM!S~KtggtPoW%ueKxzalG=b@~PGTic>7X5RWOodnW&DdbEO8$~dhQvJy zzXuF0PguEEuJryDc8Q5Wc>Ym_U%btNR>2XWkJpXHYpwaUydVEA@1LP<_vP-tV|#V>t%*M_I%CS4==-RG zF8JjhPPaFAy`y$;Zd8en5@i?o%#`G39P#V39+TtzzGBNoTYhr6?^L*M=2`J+Mac(Q z)!Ze@35m=5k6B)ccxH2I=b>jUUD-!M?(kPkVO{$0U(B*Z3$uC0_lx^7h)P~}Xe-J& z;j-nBCG*#wwubL+i~=3kk5yYPoA}W7*%Srm{>fsqUa1xsCAWV6d~p873CXff$-3!@ zFJ}n1o^xE+5Et{HGa=Zs#3NZzUq*ZPE7n;ZVP^ksR$sf?TKA%1!t^VeTb^&a+Wzjj z?Xo!Oxf=Qp%6fUXJhXe^9zX9lgX%4&+Qd^wE8iVlKC`6qm=t$ofBqH)DdszzAD1i= zT==lW?!v`&F--kuo+f?&e|cMV|1MLur!_nG_gfoiKB(=t397z3b-m3C^(5b;<+W31 zC^m*Ts5vov8R;pnK`c;)$DgGKuete$at>o*VkPp|I0OUa$uvU%#o33Gy6 zd8aTPSnzCVUw>c4Y7@ztnv@VhxwbulzFhKh5?|PU|Ju8+Ch)07g)=krJ*m6fqpY_? zu9?zPd9u0LD1EE!(&?K^n0_6o5Q=Gj$?QAL{mu4y$uYBbInG@k@Xm%a>-lM}Q^J0m zisrJ-$d-xz`E%Jb7r#2^WagE5kE{)TFr0BnN~(NXsiM4qWr<4Gt9|dBUrC=|u|}fh zg24iFVQGn?Mf>GH%wDs(N+&8dr)2-n^X2!84;bz4kw3n*zcRlUqKGS6?DTy4Wv(H6U=yBnF1GrJv?> zd7M5q`vFJoEAE15Rt}pL1|ebxwjZqAx@gw)G~-aNN?+OFIrAPi+DURfnssRFgR|Ln zseuVgOG_trn{r0|b-T1V?9L9ZHs&{b+>cM<$Y2!Z4ZU#PvDW3#Q6?HtD<(MyGajI-A6UuiJK zqHOaShI8*%Jd(b?-uUaEnYPusN^){mf(!?Lf9)0I+BHe>lIeQM-7PK8)lAh^EURjW zH<`3(U!&8(MY{45n_XCYFHY+7I%L50YS+8vbqge)DxS|;q$7Lpd0)x&zmxe@6a%gn z|JwG8b&A#Dcm>na$l2#Ka{py0yp0rCXVsaN(dM>JvV2`f%zICd^$VvSS{yh>H^jd1 z>_(3x$4n<@ZWh{ee^Tr*{uetZZ9DP)&!^jMn`iF(t{U|5``NSlDQPCAE0@&Cc<)zM zjCmojQio~T%92;xBlkOrURU`gRsU;S*wj-DOMV%bNCkvE4Y3hjG;NaQR37sS_ZOc( zxW{c-|JTlk?riLPA0AvJaWFgo{#Ny>^UnOy^EdfVO8+eR+IP{TfCJe+T_+Do>IC}V zyK#d3b=HkD;!_>hHk=ieot$EFAwelBr^zOH+wnX6KbX4p3TuTtulKWTXk=p) zcH-Vsp5v;!_wb*Xuf68{y$pT)5~djv5}#g%f2lNXdSRPBZ(H2s_1^auDo=Kc|In7= zILX<#EhvJww~fVpkmUS_^+_d3|QvL8Cn5s*Hdt-mR0imS#=r zoH9K;e2IUr%xRk)7Yrgxqc1Jfbj)u3^rh?X-*%T<_O@4E)MPwzZD9#zjQg@JNhNJP z%iR8&U+b6I6coQsXO%8xMG7jPxu$TAN62{Qft^zN;?F!zdCn0F(9Lt& zKXHMq@8Uy05{g+vJ43=YFnUO^W@{d3`B^BV;9)Se^_7)IXlaO|k*M?XFs;z}H+R;b zU$vKA{kQ2I0oO-oGj2A$-Tl4tb$Gv&1B3L$PZJm{IB(2Y(06u)VrHgxEN9J~%j#1% z`ycRl^|xWhnvXI*&U%H@`oE|+Ek3q^E${;avzv&i+Ly&&`%5h~ruyn#H;yQOSw1UF z(duG9%MzawwO7`!9?aSpIC+|d!*EPxN^Hr+g^%2h?jYa-IRGD_6$qiPJ7Oc zv#Z?x$>G^aRw2R6>{_d*%9BJo7`LfJg-;99J*ItL-qgV=ZI>{rvEq=BC`@6Pj)3k3fsV=*}?Axo`VtP(S zuj@h*q$=+&J?4M;(<5JfO?PIgN;jq?Ue~=DTQ4m-D!KG|7A`MqI`A{N)3c%a$Z^PG;cpU18eU(;1xPpzdkdp`a^k!N}UM zglVPBU+LuN_z;If6B->^Os72LUnIaI_IuC5g>_#~{<5li|E_iEC-LvHcAK)Y@(O#e zIA$(0=$R4FX=wQK#F;tUt5Qxl=v~xioA4_m$gy?7qtKHL%|B=Itd01^#F(Yh_hgA< zh?UB+NUga78~+@NIoS3}(nI=}_Y_B+obnk_8A6V!taF&=f8DsCdCioD-eX)_MNYcA zu&ankKbSFXdrfj>e^|lwXelaJP?^aRs7aY{_nX9R2Xv)ss=Z@W^duY%<{$W zRDN`c;m@n)eF@>4820{JyIH*8a?jH%_gW|!u(15!Q5wXMU%!OVBqv9U~uo^{URF8HP>wEuRp7vh5UQ_?l1eL z&7ZmVTW;SnBREFZ^niqB#>jkN zU*D(AZmUAGRgz{+ot#s8?bXNQx)W1>MRW>%&bVm*VPpTLfDm32#1 z>y=f;rkj&}RfGE<9c@ZH990$kc)~QLOEP+Ak*@*!cZHqUWm0Xajb&%_q2y)t*}M zxHwrW*}?kgjurQo|9Kpd7$pBCqdY8g?q$PEB2(vVjs11yMndmZr*+GC4N4dfZ;D}_ zBop^M^+I=@3Y&hqV_8S>zt0izhSZb`%aUp@F^DW)qSO0HyuA_ZCQ4Ik+jPd z`}-c=Dtp#(O<`&^)v{SJSKrn<%Bsh=Lz#JOI`?SKZO za8|3%SIy13XTQZ?nXz<9O38Gsm)%7oKW^Qxc{|%We7W#-ldP|=pJs*6m>vJ6v%*iR z!HN6*E7=x(&zEylCNJT=XV2FoiDH@~XKvuF0E523~@L!1)2Qtoe#eXWyp zKWxGy*|jInOn;L6XH|(_X{g=woqKNUt_j}1Nn@+o?JGvF&dfN#ki_HayJ{LEN0uEYb|h0}UpU#rR%{Tutbxq{#B*N*LfckWG_ z%ktuQUdnYtwU{?oqM7;)Pf7_SU0crY{$FD1#4iz?vp1d$^n4O(6tQyS#!rVntT@qA zEhLeXmF2Zr-D}ylOAV@vBYYD}j@}HtcUCZtS6p3Fdcjnkm!2lpx$?*ErTBmMoc7Ye zLF9qKf`Z<DqPdzR>duoS$SXRwq zX#iMX58k+4-|c(aAm-zh&F5zpTWRst1|N`}m~v=MAAfHB z{I(Fbwj)1^3MXG#x%i(4*QKCjA1S#v->jSUH;pZL=}p{X16dmi_wF$k?ZnqV<4aLsW86KAa_i6s>mqL-US_(ku&cKXHlEh-;3XL<0+ z%)7F|&-KhZh64fu*B%&OU%$09gpYekR#?xLJ^dg3u6(S!d)P4eV1~d2h6ecrh7Hp! zKV=-R3Sz&;uu5U$is(&pFFmRsO9?#xBfmpc%VUqJ7=ymnN&b!rFSxS|mdsu8ppQxI zYt?&}Ckwj6i}p|Se(#{Aqd4*#tdnZm|S-b5;y;a_6 zvjZ}L`yy354@{L3l$<-WQf+Zb+@{do`_=vLmp*D?N}EvLJ!5aFw&SvWi^AT#i&?y{ zY|V^`>vOA$_uRg#@X$z4;i>7h<3Wcvag=&*+AkYcGuw7)*6WRKng2e{EzMs)FLkZU zZM~gOCRN;Dwea&Fzb|{+)6?pnJv=`D8=v22F2n8_Q>M+^Xk*hP5qD!l+MVd7cFH=3 z)(EM&@BSEa_VmI%4U6)$j99Ye57X+n@28ur?&x2w$T?!2eK@n|y?155{qD=VKWBfh*j*qp zp-cAqgSwzgch;Qf=6lmqYZT+WLx{&C=NgAcjX}NeYm<8pkE3P@BnvqEmhj(MlD$a9 zz^|vU_n$S-diz3KA@*HMOw2eqCOnza*jl?m?en(>kvbPjtTRj|Oj=@Sx9L>dinOZ> z5=BeH@}Kq|i81@i_wLFv@mY)3Z>(lahaJnH-ap z&Ga4eci`%6OQv}>4cs6+s-dNSuxb`&ygj|9`1wcSFXS{m!q<=hgmVTX}T#6@S0# z{+#>oY9GE^UwyykOW@=J=OXDdoC+RMACuS5f8M}3L!#xm)cfK$tE*KeCZ1zmx!Wf! zUL|hgI;E+BGb0_PrpjpYW!$KkC-TE=!maIB;#B%1eMQ!;U$k>2)3&uQ)N-4OHg`!J zvpSaI*`yfxYk&Iux@8SAo7XjLVwzHQy}CALZ^v(DgChR-r9zYUY`7`=E?~acg~*qa zlV@A4$TwZT%3I~@#!SoH0}Lx)>MfIszLnJxBIdh(a&g=m3D1@N8+vYYY8uu{)K+!f_8?yoWA7D;cs7>Z%pu7(JRez@8Lp5{;3~d&fUEthFSDlvc=LxSEs*R zKhJQ7U)CInh%;VR9-9u_<-0m5Qdj48dspSHS6A;X>wV3-X2$oovga$l+x^jf|9aA) z$-%~1CQnu>J?2ex%K5?CaM@k-`R+fAOkaPi)IRRI_+rM5O*!9h{_*pyIr{tRk5x%x znTO2(2Ads_cyld0#A*s-C!5HFqSxO!bxnMFl$oW{E-i3fAa*(9;ttU(2ED5T7uxqs z+vvRIpmb-!e96Z7|CV|BzHq1uG;~y4f3a`N6NVXVS!*1_o`3POKE5|*b8nl=aX02u z3)fseu;WTYeg5X>8@*Q?TwoI>=IO&295qdUapJxiXBMB*yIppP@$-G>ozm-fsx9|> zdH($Qsrhqe&EEa$qwM)3&z@dQ{+PFPUSdw<+PXWb;;~O+cl>;F^Ww#a`YW#Sg=&4h zy6VCFR1Kz1kNcDBM0{UmSx%U*mvFB8V7h7LEHAkW5}H|x(rg+Qk9Ib*@BM6^YqUY| zBCk8kui1J3mOrcO3hchQN9?7n5$E0YiF22izy0>_+q(5LPVH(?pAr(FvuTOD%U6X< z3~Si`TWUV9yHLUKt>)m$=>|GIoHrN*w6tua8(LEiD>n6RdE;zeY(M|o^E!(S9$%B6 zv4^Gcc$a#n9g}#hwK|948B>S!f}YZ6a_%>TRa6BPB<|0dQno4atd!*CJ5@{B+J$9EHzOp5>)A(Eqn+z)C;Vdz5e<5>p`7JX{$OR}M18yR ziOL%;Y_s{PQEmEEmEp-}9d72`|E33PUu73oDBSiyZkqIg6{f5Hy?0oc@oM{-)Mc-d zAI`Zp!8fu`G3U!+N9Nx9llCkSo4H_)XO~^kr|UmV-Y3qv)MnMtBW3ZAVawjxE)K!( zYZ!EPyMK4&e%H*GBe6N7<;D*iu^Bd}k8S=Fy{pQ)@<82+V_LE6ra5s`%yuBW5(W6PZt>)$wkTECRP>c@#K-=BPwl~O@zKat;oVL*$FLzB|u7rT6Ew?eX&*gN=>v=}Gn$cr_Fz z)I8$e*d_6J;)>gjACf)94CW}yd(APE2#NQs{JUZ1)TzvdiUG$Ol7qLN`E=#$v?W2e z0vKzg3Ut4B@08rWG~Ua(*|pSS_T<)x*Hg4gmWefo-i%RkxU@PWjk{N1mF^Z*O)0ex zj0tBxC?zmTNHuTGn)AZ?wEE3oRW=X)RqUMHwJUZx>kK&s%k%l~7WYhxcUkfG!EEL$ z`L)spj2dUBU)H#gKX*&$p}l9je57u|SHdzyB3=kFqy! zp1fRtN|w3oJj+kBR=)hH$m^~0v?Sr&yV>V&Xl%DTTdpH^Ycn^usOP~u6JKinZ>ZYY zJ*g{wP5k1zt1Ff`d0x92S;*%sac=5{n_iLjX`bD;Lsp447TmgBonT*Kn6|J#c=|4` z(~Kw9%$V=mHSNfU73Y8K&~n^yQR3nfE_+>8i4%qDXL?R8(-17nE||M|MMZ>eVOcuI zvUn4TYdbRB6#8D*bw5&IP&ylU_~vCJ1Ic3P{ktz{JUhJRpGs=RLMiSU7Eg@RJeO4c z;bPG6ZFM}s@c36DS7KJsW;J&nHBo~rCi+#Z*OI%`BQwK-nitdsyX^D1xwJv%HTP=s zUg7-2$0vMSo}HLF{q=>zzs}rA@py9JVt2V<+~uVU;_sF%dTH+5$=MlqNcY#X>wA`+ z??0}uA3m*aRZQG2AFop{cYlBTdiI=o!3AHoHK?|Q#CuG;^6cgEwf0-;YW~#un*7@x zpR_*@H7ju3sRAtNz;d`^%ZAnv>T2GCU{In;!Srrd; zc$XZPp0&zsW$+4{7HJcHN&wDoCoLmI(@LYt6nSpTT^$Q`EwTA#m>?%+-pPr#mb+(d#?8zPw-)* zm4VX5vH}*2BA0H2IMiOg&wVUrVw=Ymg|O_{&o{(^1ZF6oJvqOuL@58eZPdY9vZuQfC z+psMx=oGha$t*pO3m%b^9Jj?3ee=6}AoC2B8<(>YuDOD%@!>SX#*W>LK#CC0o z(%tX8I$-~{n00&%9Bs}k_D(!#_tyFC)#Sxf&!*4WnSOZJ*6+(7P24Z4@%~=%hlG9i zzSgDf3)C2MzP2=XD=_?El_!!}s@3{#VUC6gWpwd)`0O zgV9n_ln5HxYYO5?;ou-!0&8Q{*we|KpnSw7zvWj_JN>;9to zEC15QMyH?8c^cB^2B@vk_bPUXGTPX%h3C$lHM|VWe&0_oeSXQrmAe$vw1%1ej!PRv+(sCsc!SaRgXO>6XvkH*Y9pS^CUTA7=ol|}H% z|65|t#JR>?9HfESM2te z|8G2O)U-!Ws?$4RhvgKPobV;h*PK>KPU@1d(#P#za!iTMa&QU?%-VAzwfNZ zGMgD}1!pR=WvB3;I=I)7Ibo*q_B}m?;ZcWD@A!EoBt6ia=9RoO>hUVC&A;BAbavO9 zH*vRAi@cmh)%smO{#^QXb#?g?qnoxfV}2Rh%hme${@Ok{+w5J{?`gVkXXo#;ob31G zy5^ZDyG6^AQ!)b=h%iU$gnRP;3SZp(ck`wf|DBTyuFC&;w0rxRdmmnf=XV@?T)A~i zfxPKj-sx5wmtVVfIz)WFGFqRDn*{-Cxe8WC%u=Sx>r<+sC?R8;`zJ^~)`%w-yCSIm~wuPu$^TocgMn`RHD) zbg$%DM`!-L`RB?TpC=!#Y}x+)f}r$=uk!<(viV z$$OZ3VhvY3bJ)G;W#q4kEfI$4HL>ctTLj;o(0sov>Q+kj!9%yi1YLXjO42XCRk9Wm z{%$cfYUZI!F(*Sr9TgT!S`?p}{wHjzc}U`S2mfD5A1*T6FyGjDFFP;h(UbP_7w1oT zSypbWWe7O+Y?4asyRB#Q`066cTx+-1AMuDh_BMrOna!!}$KG|%&n~Tc{G9)db;-tQ z#@~~;;vKzf{#mAPed721`plY#{=uK0-fe&Ma%JcJfBNooC+F{e`|F8^N#V>_54M-T zyT5(bO^K3O8B=FZawuq0dS>c={MbCfpnrL%r|E8bQE+-q41;^oq**&kqnApbF1Z!O zyKuI@OYzR^?YX&?x3}H>esXqx-3j@Zw_m7F5>LCl^Q@HAO_lvG6Ai-`-p*TU^4IfO zaJ;{pbd>hyCEN8q^!z-h`E@%k+hXwk%e#}zYv206-&uA2-{1WeFD@kVrfd0}IM(~N z_?*St^X6+Jzg}^uzc2dt=$mMh?vzric^=DH-F$z1dnU5|#1aM>-+f=(?<~H!FsDN5 zp$cOI=S=n8n)mN=zoWcLs)Z z)~A-X&g_9gk!xF(gpc|Doi2Db+J4`y9=9DICkEeh|HtC~moe@4%f7FNPc8Y~)LX=Q zUcoif$2W_6gHA{GqbajXdw=inJLSIpp5pT@HO+N93tue09{;R)GT)5+y3e!OgUT5Y$Sx;amH$6e+F;X`aE?io2sUGyZ78Q@wXdvZ{@vZjXiG6H9san;k<>%gI;^&tqjtptgGFaaidh=al%V>GwZ6Bf~P;t zj*4{LYAAo4c|`iSx}3j)?E7rx#l1`J$QiWltp8?SxZ!8}_2isI6BSOcn0n*Tw^>eS zyzl>g$Dwti{?mmcPwG$k_?AAOkmA)i$9<`rd-d+hXKi2EwemjKznMLwLNh3-vWh!a z+GKUv-ITOfcYnLP8RTZnkbWk13WB(=G ztC$?NURD0i=I0M04?f_xy8cB(Fe67%;LqGT`y1Yk*ODq?WgXNOx}Iw=Ozx?$J~!(2F1(F$Tqw6FwCY& z|I68>9UcE4%{vqtDD(VTQ}GAp`_HyNE6-8bVO#uv`?Qp8Q}XBie{lNm&so{)0(Gy} zvgWjg_w@OfJWH7NAVp(Bsleil+$i%2-A5jq%bz>*FVAr4lZZ*za!D@MHDg zc79G)esTHc$*x5y0jJAcN=vq=e=Aize4Afx&K=|blBX(ldAc6A>Tq>suCEK?J7N~} z@3VB*p%*QwxgWX=73GpDZ*hz9?OBlb%kAdl*ZtwEvtDhxeSkBq{OYQyyA_w8om8B$ zK;_=u4;Swr@l*20nm?)cO%}QB=25rp{aI}mRq@5b_oePnWIZt@ z`qb%tVFH(9Py5tpn9u%wBPX4BfoVc-rT?WjK}Zi9@n1yD-l6@W{oBh{~S1u_$F<GkDA}~o#N>3z66RLsqEC8EjYob?v78wsd{NcHk5%M($35N_47E-;Tw}()U@iI9ra$H_mx6S<_1*$ld3OVdH{5 z^7}r|cZf{VwC_@jNHA<(rKK-9^NLOJ&ilD5^vl=lzI%VFzowwLtnAmT!|v+WM7S(P zI2Jh0eODT{nP~p>aI5b(mW>sHd|j;f62xz^?|0^NewR?es#6?% zC+<$?&-ZVRRRmac{C=HxeOA)pmsPK?z1_Mp>Z`}jsCo&H>C1UevggDpPg7xF+rwbG zd7?6hFfVsdw^n4t4&PM%iIep&?x_1Hy}E1Sq>G;OMW*e)w9eBdD<~$&TFLPA>CeZ` z)STP6DSqDZ`?FUZ{TSfsEIIjJsDXasX^6bar?|01W{h)T}?l>W__QhcQFOT8{p4(C#gQ263)YveUi)UddWHR^3D2L+`@ZXS+n=3h zj{II&@Z*DzS^2*gF1HTPon6EyE*g2m$Yi6z$^(a&*P2N1{jm`FW;@NRcDKcK&&SM6 z&CCuCovUgW&Ny)Cq0-YyjTbZ%Kk8SgRY6Z7z)jMzLc{!p*4_bj@i^<>c=98|MoinP1g#BqjT$lzWO=(}XUg zUXF=fCd<_X`bw>&H!0m?o%+Q3o#VD?MOu+@jNEr-%Q-O_DxT*$BErF+a`>mn%MJsX zA0>Pb%WuTYJl(;0m?h{_%%9}=Ket^1=j8|6{hphny;E+{B9`4^$Net0T$!OWw$>S|0mdcUvvKAP(^v`j+-oJKHh6g3Ho_Td-sCxKN75rOXZx_gk;IIM#%`D zyRkndI@(Z->s^awl6%&?MaypH@2-C=xiskGT;t7~)0Zz_-u$KD;i3mjf_do)3pKn`-TDD4kVeII-KKB+HzWmX1^x3ydPW#XFs=2H&D|Eg$`SCO3 z1mzbRVy7pboo(J-y{xVMtJg}aNR8~pDe0_Bg_LvnPROlZ&>>;}H)*C}BZo)7GMgdy zbq9@$KO~qm8xAgM@|`*PyO@q1JJS=vPxBpu@+8cny^lPw)kyzT)5pA6?05@Lr`^ZR zy$&>B+jjKAk&9>Vl`gR<8fO-CTEP@gax4uH_M-DwYw3N->?2ytu88k{0)g3&PA~ zT>Mn-HDB?oU`jmmr^oU0`V1NQiR~}lUvP@BvM$$b_t2FnSCW>t_*=YD@? z{&jHnf&)KvejWXM!{CAJn`;(Rm!B^^RLuIR=_aFi$jPi_vg@zyNLzY;TIt3$UlL~j zt^cL`zPEDkMsBa0kLqnpo}75y5aF~`pRx17_O2Vp@)oaOo^8Kr$^FZfw{CuK+NF2n zZQh=bZu_Tl#4mZuEpKaMb<23(OefEhSzH>oZ|J3pZrG4=p6&PVo_FW^;(s(Z{=T}L z|IqD(mu#orE`7v4z58yAk4{>cj^P@`+a7zrY*jB32{{`SYMz_DCcfopuWNVt#9c?W zJaSn5Bl7LrV(ImX(-NE3Bsy$UoSSjrbp@}u;eWx09*nMeiZ_%qC#M9ey3OU-*}sfe zf&Kfu$_&MM(>=7NI5RM)`Zn%yNV#Zl$k^*8qjtzm-^tRYVsl{k-7V{dQg5=X?&($+ z;1D<};jvXSO<*?5R+03F{QHW(D=zsc*zYc%Tytpa3}FSu6$<-4rv|&#v0ZQ2ocpfp zz0Ea;wB6ztH*&Bz2$}O_M)@9IaO+UnDMhtAZ})7nE>ruozb>i9sQBQK6sgHgoDF*? zc^oiNI3Z)sz4KGzlY2W~-^yMed!}kvZQ5HOoxan3X=>(ve*W59{u}-j)l{3DoH;@F z<-B7ShfQnu&RlT*yYP-YIghtD44*4*spz|N;;{Ob?isFnQFWM+zf~`2NU3L=bmfG`lS4~Aa~v1X$yD4B-ZtB5)9&hvf1GrWT>KdFyJX{rqfsK) zg%(b5Wt=VDyi4-YQCIIJin{`$y|0B?++dD#3OihAro@ubZU<{YSL4W8i6saf9f;ZN?r3%S#-y16jyX`CV> zEwDLp|B>~J_?31$R46I^+R~qVuTYM8*Uz~O2hYFZI-#}B_(FITXKUP|aDnCPFU)rp z-tJT$xh(G`^KDrlo~B8$mbRw9_Z=?S@$8(A7{h`UP7gQN6&zrY{@rl>`EKnk@3tHl zzi;(*rN8ReoPWl`mpA8IT3PkQY}qwKXOZbsueKn=tL&P`+qPz(`Bw2Geben9sb|Yi z7s@iQC_XxKZmpD$&9)pi5$!k&mYJV6z1xv|b=At5ZoXeNr8tGe&GOH;vOj6u=Un7} zzP{o_@1!MN7j+j{oGt#qy>+F@%ny<*$(`lX0uxR|YCI`sUBPpxn{}c@?Bj#^XIszZ z9g#5L*tFuz%p*tRTX(K(3lIJ$GDXnCGtek&i_C31SZaCF-D={tzyj!iJ0&%#r@Hyn>A$|eYYIj;tCI07&>V>?i9+ZcS}|g(5diVc`Mei8r!S~zN;-9D z{fr{D?(asa=?sNa?S4rA_Ap&|Q}kBUop6^K1$+$d&rUD>E%W)orbk;fRof-j=c*jM_>|Iz$9HT)EtrB!%!@ zXBNDAPojeVoAQ`2M%({l($+54pLY z91O|Y{As^k_1E}A#?OAG$K8-Wb76N6VQVDM>powTcYj}Ow^~wr^2QIw zdp%A%ZkwbTc+T?kmX!rIRkQue>SujtS!(QP;uxEIJF)nqdHwU zS(qE(VYTgd!<>*+8{1ZNrDxCb{b9+Z&X=*jRmQ_CY{w_Yc}+&l2j7(6X4AB3?C4l9 z$wg^jPKv6ukIJ_5!o}Kk<&1`j=Z~-{J~_j7$S84(gObg8nMaAH#ft@W%+{%HRsSlM zSYmPBE#=QW2ak=04|X^>>99E-{OS8ZZJ%P;~KG>ifJi|Gw?=mDXtgvwCSoT^LWR>k;?Q|5k>0 z-4ytgC$&DsZu|G6e`S+j@|=lGIBuXBBXY=uDeL5d_;*j5ng1S}_3vKn)ugZIoXzz@ zWK4g@{HZH=c4Dz@*7frXDt<`>IkX+(FngEdzvT99;eHm~pTF;K5DwAb_on}6US-Pv z#0rsG{++GYn9QEH3y3DYTzCK3s?CS{x64Y+zrXXzI;oCW?a9^(i#RH0G)>9)RxRAf z5uR4}T`3^?+byZvYWp2r7Ui!?T%^X8aQsPR%3tpZmrwq?{br5t*$u~}i(O1Mn_m60 zcIln7?_F5_6iV(96A`{;TwoU>aZpvj8VcJ%3n#FOnTd0`=HIpa_UMFx%kpp| z>D$}i>(qRBTW@AH`?OT*j6FLlHnE;}Jo7<6dHww#)n7KgS#yDR`@JZaTT||8Z2Ne* z!lcJaZyJlZ)B0&w@0{#>a_F}xudLU?%nb``? zrES|wHtpKd$;4^Nu;g~*Ve8beIQP_vx&CRL90vQCCVD*2kraKNwC7v;rnN3M|JsbD zPPmKBklm=Cr`1%H6fAW8VU*Igvs|C&umy3M-R+%V{n*p^MO>Jni_H)DAI*a6|4ln9 z_G?0#k(H_KY6F#yHWundj>sOBV~u*KF$07Qf8<@=(mu zPmSVyeJgx=6-_qmn6`;OG9rs7^MqRUwfonDo&U{`|M}-vt@qEX`((DXwZ#a%(stvy zJ7txJeUMJb3_<^ydS|q%zr7K36w@z%D_?jgXu`JhtD@T{Y+QFJy;0I&cIWfE+04myn$e|TPbOA2+kx`F&?JuW9(C{C0Kx(#6`>do#?HA01h7Dqly`J6y)1WJ$h|sY?q_ z(@AC3gdWx28)pREzt%=a@7|Yrc2;xO{oi}fUpeeP>+haP-?P5j9I?J;^`NZzRX{@j z!AA#QTCp{|?Vnefcyhw}@JsLIt(Hrio)Nfyira+%5?^+TR_4lCu6-8IJ+8yXT$OgdiU+%&79O%I6qI+ z=J_os95=a^@8NEFBMojj!A^+}I&(xq_IrFfV_>Aj`Zlnn^YJl{RbDPhPTBnH7SGRP zot-@OGb7i@KW;qi9PLW$8t27Xy9r;PVC(9{VHl?*DmdwsepV-|(IeyIXMPJFoRQdg z!c%u`?z4x6G0naQ-2#IAst+%0>X_}#!T6O!?7ULI3&y=u>NyIYoV$8y+pWmR?-5%I z^eSc-A2`HvGOxgWnNrinDi71l5|zo;g1bIGKKOU$8P#rv6_tA*7A^dcrt;7#J4uUK zKIoL@k|{SD8ZAmR7w>H8;!xY+#Ns7)aA*IFqJ-(ya@K#m(o(Wkz2c7g$MHjdqw$3= z56mXS-rO`v?yS6v{O0vb=7(4N)mvpvvsxpx>G<>$j0$2#5Bghv9WqGKd*%JveedER z-?}~9SAXvL%g%RdNx%{r70HhYE$iCDs+GdrS+jC9OW&`V(!So{lenWHXN`bJ+Z*|n z%O+gxkeDI7@MBA7i278g_@wllKbcmt4o91P*uE7#HvLljwC#jf#*!JK_t=lKeqxv@ zal^00nXAOHHga3jl{G6Xb2qKt(^jP7xyqvLqu@geyMJy%x^|Cu`(_%Z&C&JAkGTy0X8bN&D#fz5e(MUWeS9TH?<7yop>3)n z^4r9wl^F){>YXbvNhwjc=2M>fbbsvksnTxGOTTv6Wo=$2)wPppfdfa+vNJB187{q& zURQDUdGxgj>whv=*~x6X_29(Ise4_6vO?Z{+OqrZ-gmopHY82b_0PRIiL<4bU9tba zrc&m$uv4i$S5;rFDA~3(+Q2k*qNDBGVA0sV<7|$-N}H~DZ0$UHF6f2tD&ny2hw5Uq=7|1+4a&VW9$;lg^7Oy>__*M9gp~-;| z2F3LgJw()QoO^bx;oK|{;pI0tZa5ePdFCmYZA#kw;Nvm1Lx&}LC5#29eKL8wv1b|Q zf>Rbd+9F*)*>pbNeLdUH_TSUDw-s5p>{}akWY3pNYd}PtEIVYrRi0)Tumpb@|Jw%;XhId{n+vEUaKS?ef#> ziR;pb(%n04ujTQ6`|7Lu=;ZH3j&JJ>7q2hUYf9vwb3^w?ic*zixA41dB`-f;=UHqk zwrSFfcS{#0t^XwFJ$GO7alX&B?`zTzre958u_Iw$^vb-o?sL+l&6YcVewt$ODco*< z>WjQ9Uwpolmj1na<41~g&cUWN3#Kh+`~5sQ&-6#M~VGlQV^ZFN=bD00&*mPz|LXOZ5xkcyp zF)Qn3oNSzNVh)3ne@nVWph>ant9kv^d8ZUfaV@SkXf40)^Yr2RJlVK6rfqf6 zm%~((Cf(Y)Ggdrg%PsZGmxJ$~IICqm-zQxu?U0E<*M=q0C8mEqZ#iwuw`9xq7p@;J z8A~pmZP>t;Ra>x-^+DP7yEAo^e_sB4G2y}U@Vbwy)lDQ% z#4g{pH&4`~b1D1J4$o?y!`ZW{L|*wSJ@Ya(>QL2Lk*VjCCa^*>%08?-++cFzsivP@ z>aSnlZsV5U_2JO(LyY%BWj9tR*-kFJUJ}E6)kGv&dD-@F(JL?S-EQ`}nlB`G(_Y znHAY8U0{1}b~O7)k&|ckqWptV%uPJJi|6Z2^_bS0)3~H*NvV_D*6F%iUmYo#Ez5ey z$%k>*xtTd0s;%rof>Coit1lnx;hmRP(K92lPWefp)?wa{1$T|=lCGZG?)~e`!S#8+ zZ!)PadaU zviaSGE==>TyKTvPmBs5Z!<|p^-HaHCo^{uKHbmSoyw3AwR>#&m!aOU>Pu@KmtIkz@ zLPv{9JD^wi?p*TB!xw^o+*LS9b z#9B{yQWiLS?UOriSM)oj@@YGUzh1E~Rl=@--_y6ObkP*Wv)1RA1zoSHTzhM$b<$4z z=2=S57ap1+KP#<#V-wc}7o|g!&T}0J3~f8}bHmStYTR@09$aX!$2;SK^!Me*rU#lY zdLwVgq9;IwrSHzPLHL!;Sx7dhzp~ zr?zrIyY%+|VGi8?uBv)zPF!1wV(o%>k1ssaxbN8oKGA-9?(|$)&Rq%r-$;6@Ju2V6 zz5LGCm)XJUI{p=*XZExieGd3H!Q?{mRJY;{WpM%59X zU0VO%zFxcZ>$`?oJ493!|5?fPuQ~mkTkDZ+uGwV6$rl#AniczMZjowa%m=PJb3e@Q zXsmANU)po({!0f3mSt!38;ybc zHv}7J2dB&kyRf|Xz&FEx%MMKqm{GJKFzqL60mHQ8uV=T`J=~tU_uJteYWL%B{;FS| z|IjzwGdi^6=grf4+)kz6AI3QTKN5JQKZVOxYU4_d7cbtda7_M{bD+fj*Vg(Q-!^go zDHe}qIr1&hzBXx zpPBPx;w6b&;;ml%daimRiS3(tu5Q|L?qd4i5QeoU@@!sykFG7M?c$!a$s|eQX^QPd ze(A?dtOE1%IZn^AW0qLz)Xb8>cPwjHK!Zs)%j&+~tsW70RglNY)rw0VZh zsa;F=OGX`Cy?ohnCy%cOd-4-}bq=0uQJyHir}4tO8CiC}ize)rG4ns+Er0HunBC0N zPp>~yh;CYaFvaO5vuBT)xs}S6qY2Wl-^#k~Ix3$vt;YSHd~K-N>80=bXR%D!qa&H! zcYANfmxD*SBR9vqjY!y%x{f31+w#k4aWBstaAfi`S=O$`x8cLy4;CIv+S=U2fB#Z* z%zChXxxh9V-H8pY`4cs?`yPC7-1*KcVGZ}8ISn(O&2@I(ue~Pr_Z-Qf_v}e~j=m4n z=h&jI-*Noa+zC%(TWthHyp-4&H|%ke6`d8ho{>u|FZ9kD2L5}Kz8~rCIAyqDmPzgW z&I>MnDW4CV?6Gom%~G)0bZ_z(u5Vj1)dU^H{@%EH#3OU1!=l@Au_lr4Sw(iTsmQYa zt#dP9vp3+#+lyMq^7g;65ArMt|KRiaX};mHtDn+C71XBM?%KU4ZhdsP$mgTRYHG_{ zwdS6Z)PK^iccrK2N6l`AANT)PDeZs4$FATl+|FO0u`TSEOIli*z`vfxz4Wn_d(*G(9dDV`Yc6=M2yp0WV3~4y zML}lD4Q|E7ap5KxT~n_oEQ zKG${$5eHq})ZD;|jt+L0ln>J$s9BhIF{QKa&t^(c&h~who1{N+M$F_@>y}C~Gpm`l zFm-U;NQivtt8)0ChMdR5fN8f+F-Y%b8xi+}ItpV9Mqd)jM<_O)R@&d-cv_cdypzuk-8fuYY0u zNHmtUCFfQ}r+3Q=&)y|D*Roc)v7i3RwKc^k(>3PTn=RMF84P)2~!3z9?ua3o^8DDyQ~(FMD~V=4lt-{_67Yg>76ngDt*g&o$+&BE} zEx#7S_08^QZ|Vixi8H>M-DJ1*e97s;#eFI_JQ)46ZlvU{lrUPL@aS*mx`f92$|tJM zzq+H7$8ujY!2Lp@MrlBe=Yc7r+be4iv+}Q$h&kcIA0KvdZnIP7nF(t+e>nDJY_d#} zxaAlgDydzu>FC8t+qYOwUdhAD9~^6EZhgFRU+Blnp6+L^m|l1J^mFF*^wm4}J86jd zC|gFW_Ipk^(Ddzl$!?{_2f~c5l{RWW)u%PxI&S}Mq4^mx?(6y3|~nWr|nY&me3ZB5l#?o({%f+t@4QqnrF`s0GGJqISqPVdwZ;_+paeDZy! zQ`~(8{&y|2?>eTm%s*&nox5Xcp^I`#Z&mKKO--5k0q3qhiE_QhHC0{x_14_meJ>UF z?UixJE0kB4HdIYnA#L#G*3T7HTRHDpzDuhM(&A2fsBwCJ_=3P{uTPr7*Y8wpE_-`z z?Ptd)`wUBCYwj1j+gVJVZM!}~gYCg@tuvxAB`Li-zP*sDl{=E*7jU+86byZI6S`Nv*@nI4rc%?((WD ztS}Kbk1*F&bIx~Y)bJ9!+$VPNVV-iSOOa(2o6gf68+Vw0l>L^HwdLn5&!yXZzU^X< zpD=}snd6o~jO>&Yi4?mMWwC*)CH}4hZGOth33pH)U^;FKJ_4D>#5eZ`a zX35&QH$qP?Z>sgO3dhHe&ADz19LN_x#cx z5gu0=D6sU*c{htowmQ{8P%oWz3zHrn>*lCOEA8KpU-f(%=in|j-Rlh&JAEh|i_3jlOk32Rp%N^R% z*0b{^_}V@M@oFC58N(}Qv9#go?8YFr9wu(a%m;7Iv;P_) z`cu-he#&aCRmtUPuPx)gcL{F3IJLrisf(|;)C=`WhFc-ur|DbDK(uLmQrw$%mvT@n|<#K&2b&WeT}!o#AIvwz1O+Unzc>A_sp%X>dZ@Spy@ z@>$TTxb2axR&vb@RYIatGdj1jN;kH&?npXY_1XGfU{&IEW#J2cU4J{e#C1OCo!D{m z%G2rHR%&N=Gc)?AB`>SV{Ljesz<8%ma@B7IDW@Hx~8>@0-|d zsrmNPY4g?Vs@~>KXpf86m=aMF8sME4psanF)jBzSQ|+njmHZiPPB%C0Jh)rqKg;U$ zQis;Ummzxpp9gDr-CojX!#%}m&yLrZwz2od847%`E4lsQ!z80uMGOpPm$_mdb#aI- zD-3v`x;Sx#ZOTd={A+=$Gm3yrJEA) z-}P$po06kR++{c4T&(qs)!(;2Mx!<-Cn;!A;M^4rrfR=bwAEEh9Vr&T+i@%Gu*?V0vG4)$$AR!CzOh{V`lP<)a>V zbj6*8Zk{dg{ct_4Ag_1-zDR8}-8dnjVM;=OPlk%cqi~faoHvti2ZV84Fp+Ht zdh0Z8kCbp>tH4A4`*AlC9zS)_*>wKO7V+>G*NX4{;`ttEDly}u<4x5E4>nB*J;?KH z%O$2GbDXkRMLi$quDCh<@NtI#&8p~yE0kNSH(QFzYl8dm!~|Blw`KZu8-E|2 z_*eXy*U|34^GX_A3(Quiin4@!X{Z<8y_zS?NPtUfVnN24*WYZuH1B>NH*t$x_w&=@ zf3GH%zqO5b?s?8!Ao@K~=JBNS6^TpM*tQq{T%djH$-H@m^-DjLgoi(ue!%#oRjumK z^N(_lvS(RyOb)Fyng8VCi37LZxG66a{IH}xYiHEQYuQoZ;Tm2?WMVHmnp;iJex~v0 zaoU2=cgJ_^DQR;z(7y7DMf$0q@V7HH92+-3pOSqq{?UUbm(Ob@lC|F(ZlApE)3VBw zGkV%;ckjAASyIK*Y?qaF%xgCG-pl4|WVsbiZp!h{VKI4r-Xs3j$1gu_EBwx3^Dd5B z7$73%Tm8Yo>){A{*d*1%RAF~M>emu2ok6}e0W2}YR4rcYme)@ZLP?Ek)vEK=~A6d_zY`dMey0Up;^+blM>iF{K`xi{Pr>ijY`_m;GTRS={ z(hUAeo%yXJtt9of^^d#Q4yNZUXEz9y-o0ydR%er(&%u)ly^}tsTD3SV&N)5vM!`wm zz~2Ea%w1ut=O%i%xo3TTHrXj8?(Lj8FMg^&efUjo)-0?4Z+!22vMg#}{I>J+2H*9r zVOx_tzW$s2r2CIS>1W>H&+-`tucS3ZK8vRpZ_f8SpYpfoWpaUzbctJ%&_QnQYtu4S z9#o|(aD2PD;P`)sPY;fCZJzM?_$`M+(-a>4`E6zwExPniKy>sU%hg%4?7Zi-CWP&I zx$WUksl?Ntu4FHJy58hzv4B@bKkMHmYT|2hA|2BXMOX-A%}m>-_j`F$+}jQDALp_D zzbVo6#!0awhvPw8M%g0?Nygw6Q_eRCA65H$#`yb=vNsU|jGI|({BAm{vl^DazqdBN z;2%UjDn{g)N`-NE{n;rH8I^(!1^%S`gI@HoNBW-1<=Ix#7vLvx4u@>!C+ ziRW{;+YU9G?sPhOC0oqUpG*6+<+6O$ro!*P=iS?#9JKl0)u}Ij{S@E7^R4BZN8C63 zGBOu;7}aO*i+I;6+!S^EiAi)Y_vw?&_S$#03zTFZOsv(n@tmij)j09l&j0i7^|w!H zX=dE6utWV|YM$K+=`}iira}p)PiS3h+ZVOT?VOa{I>promhfB_YF@cgXTePu7k18M z$*1utj5<^QerS<6k|ulL#q+mX>(@%!X^Irvaj?2t^M!Uhx!Q&5{n6C2JL>&`VIhy_ zm94t-QsOtB4Bl}>(8XwnQ0~T@6RY#wZ(K|^&$-=Zp|f1KsiE{+{hM_@HsPn8{&EU# z{>cAAJ?7Lzm!6K!KU++WeOr(e!Ek-OMXFb#)bmK_-{t8K z`aUcVXquDxN_us{mi^ymWvgvCJiowdhHF7&Xngc0>3J`N^V=Hwk1*$-e%jT%pM(%OpNdbd-0iNlRPg=%-6J7bWA}jH6%ql zlX+d#Qu7op_upZ>>z96yyxQ?|SIQ=Nqo?zqeA+1yd&a^s#(_!GF!4}uhgRtt19`3o zTU2jvR*Zk}ylahZ{bgTsSz)(mu1mp>(!x%>o#(bFZgS)^597{>E?!zK4;)!kyx7%LL_u^Rtr}Oh0`}`n6koB*mhK*v}Sp+8SQnFpiyg!~V;vhs_ms3-xAvpL6cGtANAZlY%OTPdO$$k85Wu z=Ct5HG}&&mhw&k=iiICbcBO{A{Pz33{=$$$E3Ut`Jfd~&fX~5GX56>CdL`xt#B$o9g|=p%ag?W(r2G2>WsQknvG7i(oaUG$xfy zsW-1(ZN2|**S%ZCl*%Bn;Pn=J2F5FMWSpPgHeuQ^acT3HOUq^{osjO9=a#>zQnLR^ z>2@ANOSY4fmJ6I~|8vk$WUAwl?gEnqlNq-s{yA)IGkM0!z02d)eoZrCk2;oBT(dT7 zwV2`iJt;S?)Xr;i{~C41_|_beAfHpyru0hq^a|xJd(~O*UjDo8h|{{YOm|k+$zM^F z;@6lb7{a;w{LV|a7A>nV`E@2Nki~q-ujpf^J{=RaweRiu#6ID<=IKdwGVLAbRgxYZ zecPN^=OH!CW!~#`yeG}r4UhJ0eZhZ?r43bx? zckh~U>8-lg)}q&6-fq8KzUkn!tr-)Rf5_c*EZOn~?|07=uHNUGyX5t%u6=o+=23XC zrC#e(#CDTOhu1tWE;woZ=yRny6Jy962BUKuHyzSROHaQ3`f9-1xbEj(TTf`(@akKC z3H3I#OnMUYYpZ+EzDv_O>gD;~v431uvQ<6y?%um>HQAT<+S-;ocz$@g#lMO#P z^T6-N1GY?yid<%tx_7eG4OKy*HeKnR3nwtSc83&QO};4^Wqw9<|K(L;bKdVruKw4x zaH0EMA+4J`)|iA?El>VhqRi#U?cQAdm+^mW+Bz5YLUv96gq05ef7yuWx{LHL_bm(m z%d*JVQt`E*_sqV_2GO|(%mb%ez5iIvx1em9p0dN_pD8Po4Gd+(F1oIY%bKWqa*|3w z$dkOvw_8JdXRv%PZ*Mo|m{51a+K%t*zFi03e*N;R*-O^4WFF^N(fJ;KAAjg#bJ+iF z*`FK2FQ>jqJStozW_GxD!sEX_Sr6P!8qJ#jVtv;k`&oH2z0>yDUpxJ&zDaY~8o9GURUG`r6BM03zY1Gz-uJZrfofIE*N&5h`I{$yH(EAP zf4QzOOWXVElxND~@g@ES5?11NCk&_j3Yd`0uUF7@_43U?uHS#FdCq=4V|4eOXu0vi z%%!D`2NpM)JylQ7i}}9r@M2-X8K)Ppf9cRloS3mh&ie>I+uz3Lhc{V%>$0|M^|MhG z_$<=&T+=+HBYS_R>!KAkPhYi^hcYF`7i9BH*=23@y}5Od?T)kutzq^eu9IJ{leStg z{qX&}SMJC6w>-|*5G(f7_bO|AyDjPA37wjS?*s3=iw~>5wl3n?;V}E}e|~eu-uc6P z?shEP?S7MAzw-`I%yHPxBiqV7FMxTSdRN*HNtXEY{ZDgD3+_12HfNtWXYKt- zXUuqgwq1X3_~6v+aMhOuIcKHBEM{@9$p88v>{A_I(mItA|AqTD9@x9nw)#nqLB`ya z6r0us&v>pjHZU`?Gx4#=a99WwC_HdDFuVm0ylOe{pWSA+z``tFfkO-o3=E#GelF{r G5}E)R@y=lY literal 0 HcmV?d00001 diff --git a/docs/reports/public/assets/report.css b/docs/reports/public/assets/report.css new file mode 100644 index 0000000..d609e7d --- /dev/null +++ b/docs/reports/public/assets/report.css @@ -0,0 +1,103 @@ +* { + margin: 0; + padding: 0; +} + +html, +code { + font: + 15px/22px arial, + sans-serif; +} + +html { + background: #fff; + color: #222; + padding: 15px; +} + +body { + margin: 7% auto 0; + max-width: 400px; + min-height: 180px; + padding: 30px 0 15px; +} + +* > body { + padding-right: 205px; +} + +p { + margin: 11px 0 22px; + overflow: hidden; +} + +ins { + color: #777; + text-decoration: none; +} + +a { + color: #15c; +} + +code { + background: #f8f8f8; + border: 1px solid #eee; + border-radius: 2px; + padding: 0 3px; +} + +ol { + margin: 11px 0 22px 22px; +} + +li { + margin: 3px 0; + overflow-wrap: anywhere; +} + +#logo { + width: 32px; + height: 32px; + margin-bottom: 11px; + display: inline-block; + user-select: none; + pointer-events: none; +} + +#logo img { + width: 100%; + height: 100%; +} + +.meta { + margin: 11px 0 22px; +} + +.meta p { + margin: 0; +} + +.label { + color: #777; +} + +.frames code { + background: transparent; + border: 0; + padding: 0; +} + +.url { + overflow-wrap: anywhere; +} + +@media screen and (max-width: 772px) { + body { + background: none; + margin-top: 0; + max-width: none; + padding-right: 0; + } +} diff --git a/docs/reports/public/favicon.ico b/docs/reports/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bee5c73bf6dc52d82e0c653ec0bf66f0eaf5cb9e GIT binary patch literal 4286 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 literal 0 HcmV?d00001 diff --git a/docs/reports/src/index.ts b/docs/reports/src/index.ts new file mode 100644 index 0000000..ae872a6 --- /dev/null +++ b/docs/reports/src/index.ts @@ -0,0 +1,135 @@ +import { Hono } from 'hono'; +import { asc, eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/d1'; +import { renderBlank, renderReport } from './view'; +import { generate as generateShortUuid } from 'short-uuid'; +import { CrashReportSchema, reportFrames, reports, type CrashReport } from './schema'; + +type Bindings = { DB: D1Database }; +const app = new Hono<{ Bindings: Bindings }>(); + +app.get('/', c => c.html(renderBlank(), 404)); + +async function reportTrace(report: CrashReport): Promise { + const traceInput = JSON.stringify({ + runtime: report.runtime, + version: report.version, + target: report.target, + os: report.os, + arch: report.arch, + code: report.code, + reason: report.reason, + addr: report.addr, + frames: report.frames, + }); + + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(traceInput)); + return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join(''); +} + +function publicUrl(requestUrl: string, id: string): string { + const url = new URL(requestUrl); + return `${url.origin}/${id}`; +} + +const reportFromRows = ( + row: typeof reports.$inferSelect, + frames: (typeof reportFrames.$inferSelect)[], +): CrashReport => ({ + schema: 1, + runtime: 'ant', + version: row.version, + target: row.target, + os: row.os, + arch: row.arch, + kind: row.kind, + code: row.code, + reason: row.reason, + addr: row.faultAddress, + elapsedMs: row.elapsedMs, + peakRss: row.peakRss, + frames: frames.map(frame => frame.frame), +}); + +async function insertReportFrames( + db: ReturnType, + reportId: string, + frames: string[], +): Promise { + if (!frames.length) return; + await db + .insert(reportFrames) + .values(frames.map((frame, index) => ({ frame, reportId, frameIndex: index }))); +} + +async function getReportFrames( + db: ReturnType, + reportId: string, +): Promise<(typeof reportFrames.$inferSelect)[]> { + return db + .select() + .from(reportFrames) + .where(eq(reportFrames.reportId, reportId)) + .orderBy(asc(reportFrames.frameIndex)); +} + +app.get('/:id', async c => { + const db = drizzle(c.env.DB); + + const [row] = await db + .select() + .from(reports) + .where(eq(reports.id, c.req.param('id'))) + .limit(1); + + if (!row) return c.html(renderBlank(), 404); + const frames = await getReportFrames(db, row.id); + + return c.html(renderReport(reportFromRows(row, frames), publicUrl(c.req.url, row.id))); +}); + +app.post('/report', async c => { + let rawReport: unknown; + try { + rawReport = await c.req.json(); + } catch { + return c.json({ error: 'invalid payload' }, 400); + } + + const parsedReport = CrashReportSchema.safeParse(rawReport); + if (!parsedReport.success) return c.json({ error: 'invalid report' }, 400); + + const report = parsedReport.data; + const trace = await reportTrace(report); + + const db = drizzle(c.env.DB); + const id = generateShortUuid(); + + const now = new Date(); + const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); + + await db.insert(reports).values({ + id, + trace, + hitCount: 1, + runtime: report.runtime, + version: report.version, + kind: report.kind, + reason: report.reason, + code: report.code, + target: report.target, + os: report.os, + arch: report.arch, + faultAddress: report.addr, + elapsedMs: report.elapsedMs, + peakRss: report.peakRss, + firstSeenAt: now.toISOString(), + lastSeenAt: now.toISOString(), + expiresAt: expires.toISOString(), + }); + + await insertReportFrames(db, id, report.frames); + return c.text(publicUrl(c.req.url, id), 200); +}); + +export default app; diff --git a/docs/reports/src/schema.ts b/docs/reports/src/schema.ts new file mode 100644 index 0000000..ede851f --- /dev/null +++ b/docs/reports/src/schema.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +const boundedString = z + .string() + .min(1) + .transform(value => value.slice(0, 512)); + +const boundedFrameString = z.string().transform(value => value.slice(0, 512)); +const reportNumber = z.number().int().nonnegative().nullable(); + +export const CrashReportSchema = z + .object({ + schema: z.literal(1), + runtime: z.literal('ant'), + version: boundedString, + target: boundedString, + os: boundedString, + arch: boundedString, + kind: boundedString, + code: boundedString, + reason: boundedString, + addr: boundedString, + elapsedMs: reportNumber, + peakRss: reportNumber, + frames: z.array(boundedFrameString).transform(value => value.slice(0, 48)), + }) + .strict(); + +export const reports = sqliteTable('reports', { + id: text('id').primaryKey(), + runtime: text('runtime').notNull(), + version: text('version').notNull(), + trace: text('trace').notNull(), + kind: text('kind').notNull(), + reason: text('reason').notNull(), + code: text('code').notNull(), + target: text('target').notNull(), + os: text('os').notNull(), + arch: text('arch').notNull(), + faultAddress: text('fault_address').notNull(), + elapsedMs: integer('elapsed_ms'), + peakRss: integer('peak_rss'), + firstSeenAt: text('first_seen_at').notNull(), + lastSeenAt: text('last_seen_at').notNull(), + hitCount: integer('hit_count').notNull(), + expiresAt: text('expires_at').notNull(), +}); + +export const reportFrames = sqliteTable( + 'report_frames', + { + reportId: text('report_id') + .notNull() + .references(() => reports.id, { onDelete: 'cascade' }), + frameIndex: integer('frame_index').notNull(), + frame: text('frame').notNull(), + }, + table => [primaryKey({ columns: [table.reportId, table.frameIndex] })], +); + +export type CrashReport = z.infer; diff --git a/docs/reports/src/view.tsx b/docs/reports/src/view.tsx new file mode 100644 index 0000000..f370f94 --- /dev/null +++ b/docs/reports/src/view.tsx @@ -0,0 +1,99 @@ +import type { Child } from 'hono/jsx'; +import type { CrashReport } from './schema'; + +function formatBytes(value: number | null): string { + if (!value || !Number.isFinite(value)) return 'unknown'; + if (value >= 1024 * 1024) return `${Math.round(value / 1024 / 1024)}mb`; + if (value >= 1024) return `${Math.round(value / 1024)}kb`; + return `${value}b`; +} + +const Shell = ({ title, children }: { title: string; children: Child }) => ( + + + + + + + {title} + + {children} + +); + +const Logo = () => ( + +); + +const BlankPage = () => ( + + +

+ 404. That's an error. +

+

If you were given a report link, check the URL and try again.

+
+); + +const ReportPage = ({ report, url }: { report: CrashReport; url: string }) => ( + + +

+ {report.reason}.{' '} + + {report.code} at {report.addr} + +
+ Ant crashed and sent a redacted report. +

+ +
+

+ Runtime: Ant {report.version} +

+

+ Platform: {report.os} {report.arch} +

+

+ Target: {report.target} +

+

+ Elapsed: {report.elapsedMs ?? 'unknown'}ms +

+

+ Peak RSS: {formatBytes(report.peakRss)} +

+
+ +
+ Native backtrace: +
    + {report.frames.length ? ( + report.frames.map(frame => ( +
  1. + {frame} +
  2. + )) + ) : ( +
  3. + No native frames were captured. +
  4. + )} +
+
+ +

+ This report URL: {url} +

+
+); + +export function renderBlank(): string { + return `${()}`; +} + +export function renderReport(report: CrashReport, url: string): string { + return `${()}`; +} diff --git a/docs/reports/tsconfig.json b/docs/reports/tsconfig.json new file mode 100644 index 0000000..5e1f025 --- /dev/null +++ b/docs/reports/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} diff --git a/docs/reports/wrangler.toml b/docs/reports/wrangler.toml new file mode 100644 index 0000000..d00dad9 --- /dev/null +++ b/docs/reports/wrangler.toml @@ -0,0 +1,23 @@ +name = "js-report" +main = "src/index.ts" + +workers_dev = false +compatibility_date = "2026-04-25" +compatibility_flags = ["nodejs_compat"] + +[assets] +directory = "./public" +binding = "ASSETS" + +[observability] +enabled = true + +[[d1_databases]] +binding = "DB" +database_name = "js_report" +database_id = "aed7ba56-b1f0-41f0-ad77-e4ef981aad4f" +remote = true + +[[routes]] +pattern = "js.report" +custom_domain = true diff --git a/include/crash.h b/include/crash.h index 788fd48..3f5a812 100644 --- a/include/crash.h +++ b/include/crash.h @@ -1,6 +1,13 @@ #ifndef ANT_CRASH_H #define ANT_CRASH_H -void ant_crash_init(void); +#include +#include "types.h" + +void ant_crash_init(int argc, char **argv); +void ant_crash_suppress_reporting(void); + +int ant_crash_run_internal_report(ant_t *js); +bool ant_crash_is_internal_report(int argc, char **argv); #endif diff --git a/include/modules/fetch.h b/include/modules/fetch.h index 3217d89..62c0db7 100644 --- a/include/modules/fetch.h +++ b/include/modules/fetch.h @@ -1,7 +1,10 @@ #ifndef FETCH_H #define FETCH_H +#include "types.h" + void init_fetch_module(void); int has_pending_fetches(void); +ant_value_t ant_fetch(ant_params_t); #endif diff --git a/src/crash.c b/src/crash.c index 3cfcc96..7e4dd44 100644 --- a/src/crash.c +++ b/src/crash.c @@ -1,41 +1,912 @@ #include // IWYU pragma: keep -#include "crash.h" -#include +#include #include +#include +#include #include #include +#include +#include -#ifndef _WIN32 -#include +#include "ant.h" +#include "crash.h" +#include "internal.h" +#include "reactor.h" + +#include "silver/engine.h" +#include "modules/assert.h" +#include "modules/fetch.h" +#include "modules/json.h" + +#define ANT_CRASH_FRAME_MAX 24 +#define ANT_CRASH_ALT_STACK_SIZE 65536 +#define ANT_CRASH_ARGV_MAX 1024 +#define ANT_CRASH_EXE_PATH_MAX 1024 +#define ANT_CRASH_PAYLOAD_MAX 32768 + +static char crash_argv[ANT_CRASH_ARGV_MAX] = ""; +static char crash_exe_path[ANT_CRASH_EXE_PATH_MAX] = ""; + +static bool crash_print_trace = false; +static bool crash_reporting_enabled = true; +static bool crash_report_status_printed = false; +static bool crash_report_status_inline = false; + +static uint64_t crash_start_ms = 0; +static volatile sig_atomic_t crash_reporting_suppressed = 0; + +static bool should_upload_report(void) { + return + crash_reporting_enabled && + crash_reporting_suppressed == 0; +} + +bool ant_crash_is_internal_report(int argc, char **argv) { + return + argc >= 2 && argv && + argv[1] && strcmp(argv[1], "__internal-crash-report") == 0; +} + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include #include +#include +#include -#define ANT_CRASH_ALT_STACK_SIZE (64 * 1024) +#ifdef __APPLE__ +#include +#endif #if defined(__APPLE__) || defined(__linux__) || defined(__GLIBC__) #define ANT_CRASH_HAVE_EXECINFO 1 #include #endif +#endif -static void as_write(int fd, const char *s) { - size_t len = 0; - while (s[len]) len++; - ssize_t _ = write(fd, s, len); - (void)_; +typedef struct { + char *buf; + size_t cap; + size_t len; +} crash_buf_t; + +static void cb_putc(crash_buf_t *b, char c) { + if (b->len + 1 >= b->cap) return; + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void cb_puts(crash_buf_t *b, const char *s) { + if (!s) return; + while (*s && b->len + 1 < b->cap) b->buf[b->len++] = *s++; + if (b->cap) b->buf[b->len] = '\0'; } -static void as_write_uint(int fd, unsigned long v) { - char buf[32]; - int i = (int)sizeof(buf); - buf[--i] = '\0'; - if (v == 0) buf[--i] = '0'; +static void cb_put_uint(crash_buf_t *b, unsigned long long v) { + char tmp[32]; + int i = (int)sizeof(tmp); + tmp[--i] = '\0'; + if (v == 0) tmp[--i] = '0'; while (v && i > 0) { - buf[--i] = (char)('0' + (v % 10)); + tmp[--i] = (char)('0' + (v % 10)); v /= 10; } - as_write(fd, &buf[i]); + cb_puts(b, &tmp[i]); +} + +static void cb_put_hex(crash_buf_t *b, uint64_t v) { + char tmp[32]; + int i = (int)sizeof(tmp); + tmp[--i] = '\0'; + if (v == 0) tmp[--i] = '0'; + while (v && i > 0) { + unsigned d = (unsigned)(v & 0xf); + tmp[--i] = (char)(d < 10 ? '0' + d : 'a' + d - 10); + v >>= 4; + } + cb_puts(b, "0x"); + cb_puts(b, &tmp[i]); +} + +static void cb_put_json_string(crash_buf_t *b, const char *s) { + static const char hexdigits[] = "0123456789abcdef"; + cb_putc(b, '"'); + + if (s) for (const unsigned char *p = (const unsigned char *)s; *p; p++) { + switch (*p) { + case '"': cb_puts(b, "\\\""); break; + case '\\': cb_puts(b, "\\\\"); break; + case '\b': cb_puts(b, "\\b"); break; + case '\f': cb_puts(b, "\\f"); break; + case '\n': cb_puts(b, "\\n"); break; + case '\r': cb_puts(b, "\\r"); break; + case '\t': cb_puts(b, "\\t"); break; + default: if (*p < 0x20) { + cb_puts(b, "\\u00"); + cb_putc(b, hexdigits[*p >> 4]); + cb_putc(b, hexdigits[*p & 0xf]); + } else cb_putc(b, (char)*p); + }} + + cb_putc(b, '"'); +} + +static bool env_bool(const char *value, bool default_value) { + if (!value || !*value) return default_value; + if ( + strcmp(value, "0") == 0 || + strcmp(value, "false") == 0 || + strcmp(value, "FALSE") == 0 || + strcmp(value, "off") == 0 || + strcmp(value, "OFF") == 0 || + strcmp(value, "no") == 0 || + strcmp(value, "NO") == 0 + ) return false; + return true; +} + +static const char *path_basename_const(const char *path) { + if (!path) return ""; + const char *base = path; + for (const char *p = path; *p; p++) { + if (*p == '/' || *p == '\\') base = p + 1; + } + return base; +} + +static void init_argv_strings(int argc, char **argv) { + crash_buf_t payload = { crash_argv, sizeof(crash_argv), 0 }; + int limit = argc < 8 ? argc : 8; + for (int i = 0; i < limit; i++) { + if (i) cb_putc(&payload, ','); + cb_put_json_string(&payload, argv[i] ? path_basename_const(argv[i]) : ""); + } + if (argc > limit) { + if (limit > 0) cb_putc(&payload, ','); + cb_put_json_string(&payload, "..."); + } +} + +static void init_report_controls() { + crash_print_trace = env_bool(getenv("ANT_CRASH_TRACE"), false); + const char *enabled = getenv("ANT_ENABLE_CRASH_REPORTING"); + if (enabled) crash_reporting_enabled = env_bool(enabled, true); + else crash_reporting_enabled = !env_bool(getenv("DO_NOT_TRACK"), false); + if (getenv("ANT_CRASH_REPORT_URL") && !enabled) crash_reporting_enabled = true; } +static void init_exe_path(int argc, char **argv) { +#ifdef _WIN32 + DWORD len = GetModuleFileNameA(NULL, crash_exe_path, (DWORD)sizeof(crash_exe_path)); + if (len > 0 && len < sizeof(crash_exe_path)) return; +#else +#ifdef __APPLE__ + uint32_t size = (uint32_t)sizeof(crash_exe_path); + char tmp[ANT_CRASH_EXE_PATH_MAX]; + if (_NSGetExecutablePath(tmp, &size) == 0) { + char *resolved = realpath(tmp, crash_exe_path); + if (resolved) return; + strncpy(crash_exe_path, tmp, sizeof(crash_exe_path) - 1); + crash_exe_path[sizeof(crash_exe_path) - 1] = '\0'; + return; + } +#elif defined(__linux__) + ssize_t len = readlink("/proc/self/exe", crash_exe_path, sizeof(crash_exe_path) - 1); + if (len > 0) { + crash_exe_path[len] = '\0'; + return; + } +#endif +#endif + if (argc > 0 && argv && argv[0]) { + strncpy(crash_exe_path, argv[0], sizeof(crash_exe_path) - 1); + crash_exe_path[sizeof(crash_exe_path) - 1] = '\0'; + } +} + +static const char *os_name(void) { +#ifdef _WIN32 + return "Windows"; +#elif defined(__APPLE__) + return "macOS"; +#elif defined(__linux__) + return "Linux"; +#else + return "unknown-os"; +#endif +} + +static const char *arch_name(void) { +#if defined(__aarch64__) || defined(_M_ARM64) + return "arm64"; +#elif defined(__x86_64__) || defined(_M_X64) + return "x64"; +#elif defined(__i386__) || defined(_M_IX86) + return "x86"; +#else + return "unknown-arch"; +#endif +} + +static uint64_t now_ms(void) { +#ifdef _WIN32 + return GetTickCount64(); +#else + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0; + return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; +#endif +} + +static unsigned long long peak_rss_bytes(void) { +#ifdef _WIN32 + return 0; +#else + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; +#ifdef __APPLE__ + return (unsigned long long)ru.ru_maxrss; +#else + return (unsigned long long)ru.ru_maxrss * 1024ULL; +#endif +#endif +} + +static unsigned long long crash_process_id(void) { +#ifdef _WIN32 + return (unsigned long long)GetCurrentProcessId(); +#else + return (unsigned long long)getpid(); +#endif +} + +static bool crash_streq_len(const char *s, size_t len, const char *literal) { + size_t literal_len = strlen(literal); + return len == literal_len && strncmp(s, literal, literal_len) == 0; +} + +static const char *crash_code_detail(const char *code, size_t len) { + if (crash_streq_len(code, len, "SIGSEGV")) return "invalid memory access"; + if (crash_streq_len(code, len, "SIGBUS")) return "bus error"; + if (crash_streq_len(code, len, "SIGFPE")) return "floating point exception"; + if (crash_streq_len(code, len, "SIGILL")) return "illegal instruction"; + if (crash_streq_len(code, len, "SIGABRT")) return "abort"; + + if (crash_streq_len(code, len, "EXCEPTION_ACCESS_VIOLATION")) return "invalid memory access"; + if (crash_streq_len(code, len, "EXCEPTION_STACK_OVERFLOW")) return "stack overflow"; + if (crash_streq_len(code, len, "EXCEPTION_ILLEGAL_INSTRUCTION")) return "illegal instruction"; + + return "fatal error"; +} + +static int crash_code_signal_number(const char *code, size_t len) { +#ifdef SIGSEGV + if (crash_streq_len(code, len, "SIGSEGV")) return SIGSEGV; +#endif +#ifdef SIGBUS + if (crash_streq_len(code, len, "SIGBUS")) return SIGBUS; +#endif +#ifdef SIGFPE + if (crash_streq_len(code, len, "SIGFPE")) return SIGFPE; +#endif +#ifdef SIGILL + if (crash_streq_len(code, len, "SIGILL")) return SIGILL; +#endif +#ifdef SIGABRT + if (crash_streq_len(code, len, "SIGABRT")) return SIGABRT; +#endif + return 0; +} + +static const char *posix_signal_reason(int sig) { +switch (sig) { + case SIGSEGV: return "Segmentation fault"; +#ifdef SIGBUS + case SIGBUS: return "Bus error"; +#endif + case SIGFPE: return "Floating point exception"; + case SIGILL: return "Illegal instruction"; + case SIGABRT: return "Abort"; + default: return "Fatal signal"; +}} + +static const char *posix_signal_code(int sig) { +switch (sig) { + case SIGSEGV: return "SIGSEGV"; +#ifdef SIGBUS + case SIGBUS: return "SIGBUS"; +#endif + case SIGFPE: return "SIGFPE"; + case SIGILL: return "SIGILL"; + case SIGABRT: return "SIGABRT"; + default: return "SIGNAL"; +}} + +static void format_native_frame(char *out, size_t out_cap, uintptr_t addr) { + if (out_cap == 0) return; + out[0] = '\0'; + +#ifdef _WIN32 + HANDLE process = GetCurrentProcess(); + DWORD64 displacement = 0; + DWORD64 base = SymGetModuleBase64(process, (DWORD64)addr); + + char module_path[MAX_PATH] = ""; + const char *image = "unknown"; + if (base && GetModuleFileNameA((HMODULE)(uintptr_t)base, module_path, (DWORD)sizeof(module_path)) > 0) + image = path_basename_const(module_path); + + // TODO: make dry + union { + SYMBOL_INFO info; + char storage[sizeof(SYMBOL_INFO) + MAX_SYM_NAME]; + } symbol_buf; + + SYMBOL_INFO *symbol = &symbol_buf.info; + memset(&symbol_buf, 0, sizeof(symbol_buf)); + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = MAX_SYM_NAME; + + if (SymFromAddr(process, (DWORD64)addr, &displacement, symbol)) { + snprintf( + out, out_cap, "%s 0x%016llx %s + %llu", image, + (unsigned long long)addr, symbol->Name, (unsigned long long)displacement); + return; + } + + snprintf(out, out_cap, "%s 0x%016llx", image, (unsigned long long)addr); +#else + Dl_info info; + memset(&info, 0, sizeof(info)); + + if (dladdr((void *)addr, &info) && info.dli_fname) { + const char *image = path_basename_const(info.dli_fname); + if (info.dli_sname && info.dli_saddr) { + unsigned long long offset = (unsigned long long)(addr - (uintptr_t)info.dli_saddr); + snprintf( + out, out_cap, "%s 0x%016llx %s + %llu", + image, (unsigned long long)addr, info.dli_sname, offset); + return; + } + if (info.dli_fbase) { + unsigned long long offset = (unsigned long long)(addr - (uintptr_t)info.dli_fbase); + snprintf(out, out_cap, "%s 0x%016llx + %llu", image, (unsigned long long)addr, offset); + return; + } + snprintf(out, out_cap, "%s 0x%016llx", image, (unsigned long long)addr); + return; + } + + snprintf(out, out_cap, "0x%016llx", (unsigned long long)addr); +#endif +} + +static size_t build_report_payload( + char *payload_buf, size_t payload_cap, + const char *kind, const char *code, const char *reason, + uint64_t fault_addr, const uintptr_t *frames, int frame_count +) { + crash_buf_t p = { payload_buf, payload_cap, 0 }; + cb_puts(&p, "{\"upload\":"); + cb_puts(&p, should_upload_report() ? "true" : "false"); + cb_puts(&p, ",\"trace\":"); + cb_puts(&p, crash_print_trace ? "true" : "false"); + cb_puts(&p, ",\"pid\":"); + cb_put_uint(&p, crash_process_id()); + cb_puts(&p, ",\"argv\":["); + cb_puts(&p, crash_argv); + cb_puts(&p, "],\"report\":{\"schema\":1,\"runtime\":\"ant\",\"version\":"); + cb_put_json_string(&p, ANT_VERSION); + cb_puts(&p, ",\"target\":"); + cb_put_json_string(&p, ANT_TARGET_TRIPLE); + cb_puts(&p, ",\"os\":"); + cb_put_json_string(&p, os_name()); + cb_puts(&p, ",\"arch\":"); + cb_put_json_string(&p, arch_name()); + cb_puts(&p, ",\"kind\":"); + cb_put_json_string(&p, kind); + cb_puts(&p, ",\"code\":"); + cb_put_json_string(&p, code); + cb_puts(&p, ",\"reason\":"); + cb_put_json_string(&p, reason); + cb_puts(&p, ",\"addr\":"); + cb_putc(&p, '"'); + cb_put_hex(&p, fault_addr); + cb_putc(&p, '"'); + cb_puts(&p, ",\"elapsedMs\":"); + cb_put_uint(&p, now_ms() - crash_start_ms); + cb_puts(&p, ",\"peakRss\":"); + cb_put_uint(&p, peak_rss_bytes()); + cb_puts(&p, ",\"frames\":["); + for (int i = 0; i < frame_count && i < ANT_CRASH_FRAME_MAX; i++) { + if (i) cb_putc(&p, ','); + char frame_text[384]; + format_native_frame(frame_text, sizeof(frame_text), frames[i]); + cb_put_json_string(&p, frame_text); + } + cb_puts(&p, "]}}"); + + return p.len; +} + +static bool crash_stderr_is_tty(void) { +#ifdef _WIN32 + return _isatty(_fileno(stderr)) != 0; +#else + return isatty(fileno(stderr)); +#endif +} + +#ifndef _WIN32 +static void write_all_fd(int fd, const char *data, size_t len) { +while (len > 0) { + ssize_t n = write(fd, data, len); + if (n <= 0) return; + data += n; + len -= (size_t)n; +}} + +static void spawn_reporter(const char *payload, size_t payload_len) { + int stdin_pipe[2]; + if (pipe(stdin_pipe) != 0) return; + + pid_t pid = fork(); + if (pid != 0) { + close(stdin_pipe[0]); + if (pid > 0) write_all_fd(stdin_pipe[1], payload, payload_len); + close(stdin_pipe[1]); + if (pid > 0) { + int status = 0; + (void)waitpid(pid, &status, 0); + } + return; + } + + close(stdin_pipe[1]); + dup2(stdin_pipe[0], STDIN_FILENO); + if (stdin_pipe[0] > STDERR_FILENO) close(stdin_pipe[0]); + + int null_out = open("/dev/null", O_WRONLY); + if (null_out >= 0) { + dup2(null_out, STDOUT_FILENO); + if (null_out > STDERR_FILENO) close(null_out); + } + + const char *exe = crash_exe_path[0] ? crash_exe_path : "ant"; + char *const reporter_argv[] = { + (char *)exe, + "__internal-crash-report", + NULL + }; + + execv(exe, reporter_argv); + execvp(exe, reporter_argv); + _exit(0); +} +#else +static void spawn_reporter(const char *payload, size_t payload_len) { + if (!crash_exe_path[0]) return; + + SECURITY_ATTRIBUTES sa; + memset(&sa, 0, sizeof(sa)); + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE read_pipe = NULL; + HANDLE write_pipe = NULL; + if (!CreatePipe(&read_pipe, &write_pipe, &sa, 0)) return; + SetHandleInformation(write_pipe, HANDLE_FLAG_INHERIT, 0); + + HANDLE null_out = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_WRITE, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + char cmd[ANT_CRASH_EXE_PATH_MAX + 64] = ""; + snprintf(cmd, sizeof(cmd), "\"%s\" __internal-crash-report", crash_exe_path); + + STARTUPINFOA si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = read_pipe; + si.hStdOutput = null_out != INVALID_HANDLE_VALUE ? null_out : GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + + if (CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { + CloseHandle(read_pipe); + DWORD written = 0; + WriteFile(write_pipe, payload, (DWORD)payload_len, &written, NULL); + CloseHandle(write_pipe); + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } else { + CloseHandle(read_pipe); + CloseHandle(write_pipe); + } + if (null_out != INVALID_HANDLE_VALUE) CloseHandle(null_out); +} +#endif + +static ant_value_t crash_report_noop(ant_t *js, ant_value_t *args, int nargs) { + if (crash_report_status_printed) return js_mkundef(); + crash_report_status_printed = true; + + // TODO: make dry + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + + if (args && nargs > 0) { + const char *message = js_str(js, args[0]); + if (message && *message) { + fputs(message, stderr); + fputc('\n', stderr); + }} + + return js_mkundef(); +} + +static ant_value_t crash_report_print_url(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkundef(); + + size_t len = 0; + const char *s = vtype(args[0]) == T_STR ? js_getstr(js, args[0], &len) : NULL; + + if (!s || len == 0) { + if (!crash_report_status_printed) { + crash_report_status_printed = true; + // TODO: make dry + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + } + return js_mkundef(); + } + + crash_report_status_printed = true; + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K %.*s\n\n", (int)len, s); + else crfprintf(stderr, "\n %.*s\n\n", (int)len, s); + + return args[0]; +} + +static ant_value_t crash_report_response_text(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkundef(); + + ant_value_t text_fn = js_getprop_fallback(js, args[0], "text"); + if (!is_callable(text_fn)) { + if (!crash_report_status_printed) { + crash_report_status_printed = true; + // TODO: make dry + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + fputs("Response.text is not available.\n", stderr); + } + return js_mkundef(); + } + + ant_value_t text_promise = sv_vm_call(js->vm, js, text_fn, args[0], NULL, 0, NULL, false); + if (is_err(text_promise)) return text_promise; + + ant_value_t print_promise = js_promise_then( + js, text_promise, + js_mkfun(crash_report_print_url), + js_mkfun(crash_report_noop) + ); + + promise_mark_handled(text_promise); + promise_mark_handled(print_promise); + + return js_mkundef(); +} + +static void crash_report_url(char *out, size_t out_cap) { + const char *base = getenv("ANT_CRASH_REPORT_URL"); + if (!base || !*base) base = "https://js.report"; + + size_t n = strlen(base); + while (n > 0 && base[n - 1] == '/') n--; + if (n >= out_cap) n = out_cap - 1; + + memcpy(out, base, n); + out[n] = '\0'; + strncat(out, "/report", out_cap - strlen(out) - 1); +} + +static char *crash_read_stdin(size_t *len) { + size_t cap = 4096; + *len = 0; + char *buf = malloc(cap); + if (!buf) return NULL; + + size_t n = 0; + while ((n = fread(buf + *len, 1, cap - *len, stdin)) > 0) { + *len += n; + if (*len == cap) { + cap *= 2; + char *next = realloc(buf, cap); + if (!next) { + free(buf); + return NULL; + } + buf = next; + }} + + buf[*len] = '\0'; + return buf; +} + +static ant_value_t crash_get(ant_t *js, ant_value_t obj, const char *key) { + if (!is_object_type(obj)) return js_mkundef(); + return js_get(js, obj, key); +} + +static const char *crash_get_string(ant_t *js, ant_value_t obj, const char *key, size_t *len, const char *fallback) { + ant_value_t value = crash_get(js, obj, key); + if (vtype(value) == T_STR) { + const char *s = js_getstr(js, value, len); + if (s) return s; + } + *len = strlen(fallback); + return fallback; +} + +static unsigned long long crash_get_uint(ant_t *js, ant_value_t obj, const char *key) { + ant_value_t value = crash_get(js, obj, key); + if (vtype(value) != T_NUM) return 0; + double n = js_getnum(value); + if (n <= 0) return 0; + return (unsigned long long)n; +} + +static void crash_format_bytes(char *out, size_t out_cap, unsigned long long bytes) { + if (bytes == 0) { + snprintf(out, out_cap, "unknown"); + return; + } + + if (bytes >= 1024ULL * 1024ULL) + snprintf(out, out_cap, "%lluMB", bytes / (1024ULL * 1024ULL)); + else if (bytes >= 1024ULL) + snprintf(out, out_cap, "%lluKB", bytes / 1024ULL); + else + snprintf(out, out_cap, "%lluB", bytes); +} + +static void crash_print_quoted_value(ant_t *js, ant_value_t value) { + size_t len = 0; + const char *s = NULL; + + if (vtype(value) == T_STR) s = js_getstr(js, value, &len); + if (!s) { + s = ""; + len = strlen(s); + } + + fputc('"', stderr); + for (size_t i = 0; i < len; i++) { + char c = s[i]; + if (c == '"' || c == '\\') fputc('\\', stderr); + if (c == '\n' || c == '\r') c = ' '; + fputc(c, stderr); + } + + fputc('"', stderr); +} + +static void crash_print_string_value(ant_t *js, ant_value_t value) { + size_t len = 0; + const char *s = NULL; + + if (vtype(value) == T_STR) s = js_getstr(js, value, &len); + if (!s) { + s = ""; + len = strlen(s); + } + + for (size_t i = 0; i < len; i++) { + char c = s[i]; + if (c == '\n' || c == '\r') c = ' '; + fputc(c, stderr); + } +} + +static void crash_print_args(ant_t *js, ant_value_t argv) { + crfprintf(stderr, "Args:"); + if (vtype(argv) != T_ARR || js_arr_len(js, argv) == 0) { + fputs(" \"ant\"\n", stderr); + return; + } + + ant_offset_t len = js_arr_len(js, argv); + for (ant_offset_t i = 0; i < len; i++) { + fputc(' ', stderr); + crash_print_quoted_value(js, js_arr_get(js, argv, i)); + } + fputc('\n', stderr); +} + +static void crash_print_frames(ant_t *js, ant_value_t report) { + ant_value_t frames = crash_get(js, report, "frames"); + crfprintf(stderr, "Native backtrace:\n"); + if (vtype(frames) != T_ARR || js_arr_len(js, frames) == 0) { + crfprintf(stderr, " (no native frames were captured)\n\n"); + return; + } + + ant_offset_t len = js_arr_len(js, frames); + for (ant_offset_t i = 0; i < len; i++) { + fputs(" ", stderr); + fprintf(stderr, "%-2lld ", (long long)i); + crash_print_string_value(js, js_arr_get(js, frames, i)); + fputc('\n', stderr); + } + fputc('\n', stderr); +} + +static void crash_print_report_summary(ant_t *js, ant_value_t report, ant_value_t argv, bool upload, bool trace, unsigned long long pid) { + crprintf_var("version", ANT_VERSION); + crprintf_var("target", ANT_TARGET_TRIPLE); + crprintf_var("git_hash", ANT_GIT_HASH); + + crprintf_var("os_name", os_name()); + crprintf_var("os_version", "os_version"); + + size_t code_len = 0, addr_len = 0, reason_len = 0; + + const char *code = crash_get_string(js, report, "code", &code_len, "SIGNAL"); + const char *addr = crash_get_string(js, report, "addr", &addr_len, "0x0"); + const char *reason = crash_get_string(js, report, "reason", &reason_len, "Fatal signal"); + + unsigned long long elapsed = crash_get_uint(js, report, "elapsedMs"); + unsigned long long peak_rss = crash_get_uint(js, report, "peakRss"); + + const char *detail = crash_code_detail(code, code_len); + int signal_number = crash_code_signal_number(code, code_len); + + char peak_rss_text[32]; + crash_format_bytes(peak_rss_text, sizeof(peak_rss_text), peak_rss); + + fprintf(stderr, "=== (%llu) ===================================================\n", pid); + + // TODO: Ant v () + crfprintf(stderr, "Ant {version} {git_hash} {target}\n"); + crfprintf(stderr, "{os_name} {os_version}\n"); + + crash_print_args(js, argv); + + crfprintf(stderr, "Summary: %s (%.*s) with signal %d\n", detail, code_len, code, signal_number); + crfprintf(stderr, "Elapsed: %llums | RSS Peak: %s\n\n", elapsed, peak_rss_text); + crfprintf(stderr, "panic(main thread): %.*s at address %.*s \n", reason_len, reason, addr_len, addr); + + crfprintf(stderr, "oh no: Ant has crashed. This indicates a bug in Ant, not your code.\n\n"); + if (trace) crash_print_frames(js, report); + + if (upload) { + crfprintf(stderr, "To send a redacted crash report to Ant's team,\n"); + crfprintf(stderr, "please file a GitHub issue using the link below:\n\n"); + if (crash_report_status_inline) crfprintf(stderr, " uploading..."); + } + else crfprintf(stderr, "Crash reporting is disabled for this process.\n\n"); +} + +int ant_crash_run_internal_report(ant_t *js) { + if (!js) return EXIT_FAILURE; + + size_t payload_len = 0; + char *payload = crash_read_stdin(&payload_len); + if (!payload) return EXIT_FAILURE; + + crash_report_status_printed = false; + + ant_value_t wrapper_json = js_mkstr(js, payload, payload_len); + ant_value_t wrapper = json_parse_value(js, wrapper_json); + + if (is_err(wrapper) || !is_object_type(wrapper)) { + // TODO: make this dry + crfprintf(stderr, "Crash report upload failed.\n"); + free(payload); + return EXIT_FAILURE; + } + + ant_value_t report = crash_get(js, wrapper, "report"); + if (!is_object_type(report)) { + crfprintf(stderr, "Crash report upload failed.\n"); + free(payload); + return EXIT_FAILURE; + } + + bool upload = js_truthy(js, crash_get(js, wrapper, "upload")); + bool trace = js_truthy(js, crash_get(js, wrapper, "trace")); + + unsigned long long pid = crash_get_uint(js, wrapper, "pid"); + ant_value_t argv = crash_get(js, wrapper, "argv"); + + crash_report_status_inline = upload && crash_stderr_is_tty(); + crash_print_report_summary(js, report, argv, upload, trace, pid); + + if (!upload) { + free(payload); + return EXIT_SUCCESS; + } + + ant_value_t report_json = js_json_stringify(js, &report, 1); + if (vtype(report_json) != T_STR) { + // TODO: make this dry + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + free(payload); + return EXIT_FAILURE; + } + + size_t report_payload_len = 0; + const char *report_payload = js_getstr(js, report_json, &report_payload_len); + if (!report_payload) { + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + free(payload); + return EXIT_FAILURE; + } + + fflush(stderr); + char url[512] = ""; + crash_report_url(url, sizeof(url)); + + ant_value_t headers = js_mkobj(js); + js_set(js, headers, "content-type", js_mkstr(js, "application/json", 16)); + + ant_value_t init = js_mkobj(js); + js_set(js, init, "headers", headers); + js_set(js, init, "method", js_mkstr(js, "POST", 4)); + js_set(js, init, "body", js_mkstr(js, report_payload, report_payload_len)); + + ant_value_t fetch_args[2] = { js_mkstr(js, url, strlen(url)), init }; + ant_value_t fetch_promise = ant_fetch(js, fetch_args, 2); + + if (is_err(fetch_promise)) { + crash_report_status_printed = true; + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + const char *message = js_str(js, fetch_promise); + if (message && *message) fprintf(stderr, "%s\n", message); + free(payload); + return EXIT_FAILURE; + } + + ant_value_t report_promise = js_promise_then( + js, fetch_promise, + js_mkfun(crash_report_response_text), + js_mkfun(crash_report_noop) + ); + + promise_mark_handled(fetch_promise); + if (is_err(report_promise)) { + crash_report_status_printed = true; + // TODO: make this dry + if (crash_report_status_inline) crfprintf(stderr, "\r\033[2K Crash report upload failed.\n"); + else crfprintf(stderr, "Crash report upload failed.\n"); + const char *message = js_str(js, report_promise); + if (message && *message) fprintf(stderr, "%s\n", message); + free(payload); + return EXIT_FAILURE; + } + + promise_mark_handled(report_promise); + js_run_event_loop(js); + free(payload); + + return EXIT_SUCCESS; +} + +void ant_crash_suppress_reporting(void) { + crash_reporting_suppressed = 1; +} + +#ifndef _WIN32 static int install_altstack(void) { #ifdef SA_ONSTACK static void *stack_mem; @@ -58,16 +929,6 @@ static int install_altstack(void) { return 0; } -static const char *signal_name(int sig) { -switch (sig) { - case SIGSEGV: return "SIGSEGV (invalid memory access)"; - case SIGBUS: return "SIGBUS (bus error)"; - case SIGFPE: return "SIGFPE (arithmetic exception)"; - case SIGILL: return "SIGILL (illegal instruction)"; - case SIGABRT: return "SIGABRT (abort)"; - default: return "fatal signal"; -}} - static void crash_handler(int sig, siginfo_t *info, void *ucontext) { struct sigaction dfl; memset(&dfl, 0, sizeof(dfl)); @@ -76,55 +937,45 @@ static void crash_handler(int sig, siginfo_t *info, void *ucontext) { sigemptyset(&dfl.sa_mask); sigaction(sig, &dfl, NULL); - int fd = STDERR_FILENO; - as_write(fd, "\n=== ant crashed: "); - as_write(fd, signal_name(sig)); - as_write(fd, " (signal "); - as_write_uint(fd, (unsigned long)sig); - as_write(fd, ") ===\n"); - - if (info) { - as_write(fd, " fault address: 0x"); - char hex[2 + sizeof(void *) * 2 + 1]; - int hi = (int)sizeof(hex); - - hex[--hi] = '\0'; - uintptr_t addr = (uintptr_t)info->si_addr; - - if (addr == 0) hex[--hi] = '0'; - else while (addr && hi > 0) { - unsigned d = (unsigned)(addr & 0xF); - hex[--hi] = (char)(d < 10 ? '0' + d : 'a' + d - 10); - addr >>= 4; - } - - as_write(fd, &hex[hi]); - as_write(fd, "\n"); - } - - as_write(fd, " ant version: " ANT_VERSION "\n"); - as_write(fd, " pid: "); - as_write_uint(fd, (unsigned long)getpid()); - as_write(fd, "\n\nNative backtrace:\n"); - + uintptr_t frames[ANT_CRASH_FRAME_MAX] = {0}; + int frame_count = 0; #ifdef ANT_CRASH_HAVE_EXECINFO - void *frames[64]; - int n = backtrace(frames, (int)(sizeof(frames) / sizeof(frames[0]))); - int skip = n > 2 ? 2 : 0; - if (skip) as_write(fd, " (omitted crash_handler and signal trampoline frames)\n"); - backtrace_symbols_fd(frames + skip, n - skip, fd); -#else - as_write(fd, " (no execinfo support on this platform)\n"); + void *raw_frames[64]; + int n = backtrace(raw_frames, (int)(sizeof(raw_frames) / sizeof(raw_frames[0]))); + int skip = n > 1 ? 1 : 0; + for (int i = skip; i < n && frame_count < ANT_CRASH_FRAME_MAX; i++) + frames[frame_count++] = (uintptr_t)raw_frames[i]; #endif - as_write(fd, - "\nPlease report this at https://github.com/themackabu/ant/issues\n" - "Include the backtrace above and a minimal reproducer if possible.\n\n"); + uint64_t fault_addr = info ? (uint64_t)(uintptr_t)info->si_addr : 0; + char payload[ANT_CRASH_PAYLOAD_MAX] = ""; + const char *reason = posix_signal_reason(sig); + const char *code = posix_signal_code(sig); + + size_t payload_len = build_report_payload( + payload, sizeof(payload), "signal", code, + reason, fault_addr, frames, frame_count + ); + spawn_reporter(payload, payload_len); +#ifdef SIGTRAP + struct sigaction trap_dfl; + memset(&trap_dfl, 0, sizeof(trap_dfl)); + trap_dfl.sa_handler = SIG_DFL; + sigemptyset(&trap_dfl.sa_mask); + sigaction(SIGTRAP, &trap_dfl, NULL); + raise(SIGTRAP); +#else raise(sig); +#endif } -void ant_crash_init(void) { +void ant_crash_init(int argc, char **argv) { + crash_start_ms = now_ms(); + init_exe_path(argc, argv); + init_argv_strings(argc, argv); + init_report_controls(); + struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = crash_handler; @@ -133,16 +984,11 @@ void ant_crash_init(void) { if (install_altstack()) sa.sa_flags |= SA_ONSTACK; #endif sigemptyset(&sa.sa_mask); - static const int sigs[] = { SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGABRT }; - for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) { - sigaction(sigs[i], &sa, NULL); - } + for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) sigaction(sigs[i], &sa, NULL); } #else // _WIN32 - -#include #include static LPTOP_LEVEL_EXCEPTION_FILTER previous_filter; @@ -172,11 +1018,21 @@ switch (code) { default: return "fatal exception"; }} +static const char *exception_reason(DWORD code) { +switch (code) { + case EXCEPTION_ACCESS_VIOLATION: return "Segmentation fault"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "Illegal instruction"; + case EXCEPTION_STACK_OVERFLOW: return "Stack overflow"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "Divide by zero"; + default: return "Fatal exception"; +}} + static DWORD64 exception_fault_address(EXCEPTION_RECORD *record) { if (!record) return 0; if (( - record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION || - record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) + record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION || + record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) && record->NumberParameters >= 2 ) return (DWORD64)record->ExceptionInformation[1]; return (DWORD64)(uintptr_t)record->ExceptionAddress; @@ -184,7 +1040,6 @@ static DWORD64 exception_fault_address(EXCEPTION_RECORD *record) { static BOOL init_stack_frame(CONTEXT *ctx, STACKFRAME64 *frame, DWORD *machine) { memset(frame, 0, sizeof(*frame)); - #if defined(_M_X64) || defined(__x86_64__) *machine = IMAGE_FILE_MACHINE_AMD64; frame->AddrPC.Offset = ctx->Rip; @@ -209,6 +1064,31 @@ static BOOL init_stack_frame(CONTEXT *ctx, STACKFRAME64 *frame, DWORD *machine) return TRUE; } +static int collect_windows_frames(EXCEPTION_POINTERS *exc, uintptr_t *frames, int max_frames) { + if (!exc || !exc->ContextRecord) return 0; + HANDLE process = GetCurrentProcess(); + HANDLE thread = GetCurrentThread(); + CONTEXT ctx = *exc->ContextRecord; + STACKFRAME64 frame; + DWORD machine; + if (!init_stack_frame(&ctx, &frame, &machine)) return 0; + + int count = 0; + while (count < max_frames) { + DWORD64 addr = frame.AddrPC.Offset; + if (addr == 0) break; + frames[count++] = (uintptr_t)addr; + DWORD64 prev_pc = frame.AddrPC.Offset; + DWORD64 prev_sp = frame.AddrStack.Offset; + BOOL ok = StackWalk64( + machine, process, thread, &frame, &ctx, NULL, + SymFunctionTableAccess64, SymGetModuleBase64, NULL + ); + if (!ok || (frame.AddrPC.Offset == prev_pc && frame.AddrStack.Offset == prev_sp)) break; + } + return count; +} + static void print_windows_backtrace(EXCEPTION_POINTERS *exc) { if (!exc || !exc->ContextRecord) { fprintf(stderr, " (no exception context available)\n"); @@ -216,22 +1096,15 @@ static void print_windows_backtrace(EXCEPTION_POINTERS *exc) { } HANDLE process = GetCurrentProcess(); - HANDLE thread = GetCurrentThread(); - SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME); if (!SymInitialize(process, NULL, TRUE)) { fprintf(stderr, " (SymInitialize failed: %lu)\n", GetLastError()); return; } - CONTEXT ctx = *exc->ContextRecord; - STACKFRAME64 frame; - DWORD machine; - if (!init_stack_frame(&ctx, &frame, &machine)) { - fprintf(stderr, " (unsupported Windows architecture for StackWalk64)\n"); - return; - } - + uintptr_t frames[ANT_CRASH_FRAME_MAX] = {0}; + int frame_count = collect_windows_frames(exc, frames, ANT_CRASH_FRAME_MAX); + union { SYMBOL_INFO info; char storage[sizeof(SYMBOL_INFO) + MAX_SYM_NAME]; @@ -242,57 +1115,55 @@ static void print_windows_backtrace(EXCEPTION_POINTERS *exc) { symbol->SizeOfStruct = sizeof(SYMBOL_INFO); symbol->MaxNameLen = MAX_SYM_NAME; - for (int i = 0; i < 64; i++) { - DWORD64 addr = frame.AddrPC.Offset; - if (addr == 0) break; + for (int i = 0; i < frame_count; i++) { + DWORD64 addr = (DWORD64)frames[i]; DWORD64 displacement = 0; fprintf(stderr, "%d 0x%016llx", i, (unsigned long long)addr); - if (SymFromAddr(process, addr, &displacement, symbol)) + if (SymFromAddr(process, addr, &displacement, symbol)) fprintf(stderr, " %s + %llu", symbol->Name, (unsigned long long)displacement); - + IMAGEHLP_LINE64 line; DWORD line_disp = 0; memset(&line, 0, sizeof(line)); line.SizeOfStruct = sizeof(line); - if (SymGetLineFromAddr64(process, addr, &line_disp, &line)) + if (SymGetLineFromAddr64(process, addr, &line_disp, &line)) fprintf(stderr, " (%s:%lu)", line.FileName, line.LineNumber); - fputc('\n', stderr); - DWORD64 prev_pc = frame.AddrPC.Offset; - DWORD64 prev_sp = frame.AddrStack.Offset; - BOOL ok = StackWalk64( - machine, process, thread, &frame, &ctx, NULL, - SymFunctionTableAccess64, SymGetModuleBase64, NULL - ); - if (!ok || (frame.AddrPC.Offset == prev_pc && frame.AddrStack.Offset == prev_sp)) break; } } static LONG WINAPI windows_crash_handler(EXCEPTION_POINTERS *exc) { - if (InterlockedExchange(&crash_in_progress, 1) != 0) { + if (InterlockedExchange(&crash_in_progress, 1) != 0) return EXCEPTION_CONTINUE_SEARCH; - } + + HANDLE process = GetCurrentProcess(); + SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME); + SymInitialize(process, NULL, TRUE); EXCEPTION_RECORD *record = exc ? exc->ExceptionRecord : NULL; DWORD code = record ? record->ExceptionCode : 0; + uint64_t fault_addr = (uint64_t)exception_fault_address(record); - fprintf(stderr, "\n=== ant crashed: %s (0x%08lx) ===\n", exception_name(code), (unsigned long)code); - fprintf(stderr, " fault address: 0x%016llx\n", (unsigned long long)exception_fault_address(record)); - fprintf(stderr, " ant version: " ANT_VERSION "\n"); - fprintf(stderr, " pid: %lu\n\n", (unsigned long)_getpid()); - fprintf(stderr, "Native backtrace:\n"); - print_windows_backtrace(exc); + uintptr_t frames[ANT_CRASH_FRAME_MAX] = {0}; + int frame_count = collect_windows_frames(exc, frames, ANT_CRASH_FRAME_MAX); + char payload[ANT_CRASH_PAYLOAD_MAX] = ""; - fprintf(stderr, - "\nPlease report this at https://github.com/themackabu/ant/issues\n" - "Include the backtrace above and a minimal reproducer if possible.\n\n"); + size_t payload_len = build_report_payload( + payload, sizeof(payload), "exception", exception_name(code), + exception_reason(code), fault_addr, frames, frame_count + ); + spawn_reporter(payload, payload_len); if (previous_filter) return previous_filter(exc); return EXCEPTION_EXECUTE_HANDLER; } -void ant_crash_init(void) { +void ant_crash_init(int argc, char **argv) { + crash_start_ms = now_ms(); + init_exe_path(argc, argv); + init_argv_strings(argc, argv); + init_report_controls(); previous_filter = SetUnhandledExceptionFilter(windows_crash_handler); } diff --git a/src/main.c b/src/main.c index 3cc9c08..5c6dcc6 100644 --- a/src/main.c +++ b/src/main.c @@ -379,7 +379,10 @@ static int execute_module(ant_t *js, const char *filename) { } int main(int argc, char *argv[]) { - if (!getenv("ANT_NO_CRASH_HANDLER")) ant_crash_init(); + bool internal_crash_report_mode = ant_crash_is_internal_report(argc, argv); + + if (internal_crash_report_mode) argc = 1; + if (!internal_crash_report_mode && !getenv("ANT_NO_CRASH_HANDLER")) ant_crash_init(argc, argv); setup_console_colors(); parse_ant_debug_flags(); @@ -688,7 +691,11 @@ int main(int argc, char *argv[]) { crfprintf(stderr, msg.snapshot_warn, js_str(js, snapshot_result)); } - if (eval->count > 0) { + if (internal_crash_report_mode) { + js_result = ant_crash_run_internal_report(js); + } + + else if (eval->count > 0) { const char *script = eval->sval[0]; eval_code(js, script, strlen(script), "[eval]", print->count > 0); } diff --git a/src/modules/builtin.c b/src/modules/builtin.c index 898c17b..542098d 100644 --- a/src/modules/builtin.c +++ b/src/modules/builtin.c @@ -16,6 +16,7 @@ #include "ant.h" #include "gc.h" +#include "crash.h" #include "errors.h" #include "runtime.h" #include "internal.h" @@ -163,6 +164,12 @@ static ant_value_t js_usleep(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } +// Ant.suppressReporting() +static ant_value_t js_suppress_reporting(ant_t *js, ant_value_t *args, int nargs) { + ant_crash_suppress_reporting(); + return js_mkundef(); +} + // Ant.stats() static ant_value_t js_stats_fn(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result = js_newobj(js); @@ -457,6 +464,7 @@ void init_builtin_module() { js_set(js, ant_obj, "sleep", js_mkfun(js_sleep)); js_set(js, ant_obj, "msleep", js_mkfun(js_msleep)); js_set(js, ant_obj, "usleep", js_mkfun(js_usleep)); + js_set(js, ant_obj, "suppressReporting", js_mkfun(js_suppress_reporting)); ant_value_t hl_obj = js_newobj(js); ant_value_t hl_fn = js_obj_to_func(hl_obj); diff --git a/src/modules/fetch.c b/src/modules/fetch.c index 4e0f738..4110c5a 100644 --- a/src/modules/fetch.c +++ b/src/modules/fetch.c @@ -847,11 +847,13 @@ static ant_value_t fetch_abort_listener(ant_t *js, ant_value_t *args, int nargs) return js_mkundef(); } -static ant_value_t js_fetch(ant_t *js, ant_value_t *args, int nargs) { +ant_value_t ant_fetch(ant_t *js, ant_value_t *args, int nargs) { ant_value_t input = (nargs >= 1) ? args[0] : js_mkundef(); ant_value_t init = (nargs >= 2) ? args[1] : js_mkundef(); + ant_value_t promise = js_mkpromise(js); ant_value_t request_obj = 0; + request_data_t *request = NULL; fetch_request_t *req = NULL; ant_value_t signal = 0; @@ -903,7 +905,7 @@ static ant_value_t js_fetch(ant_t *js, ant_value_t *args, int nargs) { void init_fetch_module() { utarray_new(pending_requests, &ut_ptr_icd); - js_set(rt->js, rt->js->global, "fetch", js_mkfun_flags(js_fetch, CFUNC_HAS_PROTOTYPE)); + js_set(rt->js, rt->js->global, "fetch", js_mkfun_flags(ant_fetch, CFUNC_HAS_PROTOTYPE)); } int has_pending_fetches(void) { diff --git a/src/types/ant.d.ts b/src/types/ant.d.ts index 80d436f..c0ad2d5 100644 --- a/src/types/ant.d.ts +++ b/src/types/ant.d.ts @@ -163,6 +163,7 @@ interface AntStatic { raw: AntRaw; stats(): AntStatsResult; + suppressReporting(): void; sleep(seconds: number): void; msleep(milliseconds: number): void;