From 4870a145c8790a2711fe55f38646f06666ae2587 Mon Sep 17 00:00:00 2001 From: Barret Rennie Date: Fri, 26 Jun 2020 16:15:01 -0400 Subject: [PATCH] Make protocol less noisy Instead of the recorder constantly sending messages to the runner, it now sends a single `Request` message and receives a series of responses from the runner. Tests will be fixed in a later commit. Received profiles, prefs, and Firefox builds are not presently persisted through restarts. This will be fixed in an upcoming patch. The protocol docs no longer match what happens, so they've been removed for now. --- docs/diagrams/download-build.png | Bin 20701 -> 0 bytes docs/diagrams/handshake-failure.png | Bin 9271 -> 0 bytes docs/diagrams/handshake.png | Bin 14767 -> 0 bytes docs/diagrams/send-prefs.png | Bin 9617 -> 0 bytes docs/diagrams/send-profile-empty.png | Bin 7788 -> 0 bytes docs/diagrams/send-profile.png | Bin 18788 -> 0 bytes docs/diagrams/wait-for-idle.png | Bin 9626 -> 0 bytes docs/protocol.md | 99 ----- fxrecorder/src/bin/main.rs | 14 +- fxrecorder/src/lib/proto.rs | 387 +++++++++--------- fxrunner/src/bin/main.rs | 26 +- fxrunner/src/lib/proto.rs | 563 +++++++++++++-------------- libfxrecord/src/net/message.rs | 122 +++--- 13 files changed, 535 insertions(+), 676 deletions(-) delete mode 100644 docs/diagrams/download-build.png delete mode 100644 docs/diagrams/handshake-failure.png delete mode 100644 docs/diagrams/handshake.png delete mode 100644 docs/diagrams/send-prefs.png delete mode 100644 docs/diagrams/send-profile-empty.png delete mode 100644 docs/diagrams/send-profile.png delete mode 100644 docs/diagrams/wait-for-idle.png delete mode 100644 docs/protocol.md diff --git a/docs/diagrams/download-build.png b/docs/diagrams/download-build.png deleted file mode 100644 index 75f270ee3f51ae5a16862ff64d2c0e6f9dd78cbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20701 zcmeIacT|&E7dILaJBqzhEZFG~dXW-(FM%K(l0YDYmV^WW(NRYc7#oO4u_8TErQD8S+B?{TD(y>xzu+q($=es_i}hakT3%L8RZ31?N>0g6UT(jFjyw{6$jVB~ z%PTuAwD-Vc0~Y%-C((WV{XO=}8z7}+p{qjH~!3Kx;k^&Z*$;nGAO2ahyGYzE$Vi%rzV=2B~*oCKoFrkpe z#{4G=C|EdeUk`k+2N7QKU0fJNB+iHINg?QjSt_{M`q-#?d65@72=E{-c54M)NrCA6 zPL$EXo}OfLd6bo`ht7U^T{(Sy%RnLx1JemJu_c;1VyV9Jb{@(&bbvL=SJp1j&jC#$ znkqZWsoIi=N)9+vc`s9A2MZlNLnEYto`uE#v)DLsO1sQvr+2K8L zfrd_&*kB@=7)-(W8NmR0@ByVqE>qOJ@ z^EWc~Mw9Hx`Z{Jr6=Ra6xvV2vPCk%ks}!W@qiSGap<)@PXAf&8uZuBOq1coCtnnx_ z_{&g*|I?Z*7oy}4h}ZSBMB4i3gpyD=M-wMhn6iyO!N%9$%0Y=@VW?{mNLHbSD0v1M zS$S9?z33==6$5hzya@=Tg2Y)7EEH8O!{jVbia1ZRFbaiWNAySf*xEQK(z)bSA}Y$ zqT=Ii>1Z2jt!Lt=r=aLd^2M422J3l-(M(Nj=@b;zOkd9&CSmJ{F|_uz#F{u@Y;dwR zmZtUrKE|dPd8C3N(Thx%cd{j0=~yF?w*Kf4FB79cM|lHus16ZpOfc0`Ry8pqt4qZ?7?3a&J!Q~8pbQLxZB*bVPM%^l|F~to+8SEW0_-e((0;}?x)e*2IoZzN z7L=7!efA@gZmh8={YLu(2(?iPE82+Yn^Qa;A>nMoJ_46P)B2&Grdn@Sdee!d=hbaQ_LMHJo3(V9#!umvq= zKyuOzF{fKAAd&Kp%6j}ks7Ob1GmM3kfu579A~uBN9~exw!BNaq0=)wbLufXt@}X8n z%2=gPtep?u!OF)Tg;X{1R}J*lF+<|?bv=-VM6#g)#wp0k5^JZggeH0DQ7jcLbWP>y z=DxDJPMBbtr-KRIG>j1Dt88E;3)U~his&8eWJM*~;Y}UGbPYi+IkdHsmzbk9z^ISzzeS!tc2I|3eXMmvNCc&qP$I=$U%6=Fb@+? z9m9}dyrsOo6G}JO+Jj6C!kd#kKo=-@m^diO83Y?peI1QR*1n!Ta&~goXmdkrc?{VP zBNr%VtZ#`nwWB%O`B3##9i6;LLDr^36ss7iMT zu+XO|5{!&JLJUKdg0V;&8S(S=b!nJKvkGS` zWM_+$VXRwv=Sq6EN!C&Y{kPUU3PQqIiRxRXt9 zOPP^#)Ypd94@&s$V)`7X`CmUKWzVb}5q(vQx9J)ugT@@V^9VA zHZU;XpLWH=S3;4;Ifm||E35ap7k-T`7Kg8Pd@t#WukGD%!mt)Mw#!c&zEb!~EEbQh z+Q*mRZ{?0i{#T#72{o~TJdNi4abBzHtWNFzV=Sz);{)mRAWeQ}Y)N?}BDpZJ=3#;4 ze&fr8iy}hH!%IF%dx;1td1ch(9=afLVehI_$4>O+&sKH6*@n7w*SGx3^ee?{hc5gz zIU`yXl{ywCS@$k5x54>|fk60WiOUktof6B4f|Ocf__tlV)}7mU_SRKw8J-=w1QvTo z#S9UPIpDE9FH>1vc!W)lx!nELT$`M+s>1Gs6=xen$@{;<_SP7vz!uMwd zIrNtzbq05=uQ403%`YOjlSf6@AXjuuS3b}lu>MWLI7cJ%waiKZb_7nlQ1Z&|l{s2D zp1IL3H6neJ*}~WW_nsk?_3xw8vAY~kKP-F571qThFvaC&PTMBj;~E}l9v>EVw%*(! zUa1ndKdwJ>?c3={t`p}P_YB(D>AfV3%<3@3#b${PH8c)INu&Dsv5=i0n$9 zecQNe-?n8Gsftz`k$cfqlBPS>owJQ<8HgnpP8A-8vyJm7oQ*)EH*H||sm&e|wfv&7 zt#2~7;4!T%e>Sr1cCA*v_PADmm_}akT?>ok879}j+U~>K+!Yig>21d`w=_=2;O#>j zFFoIu?iRZJY{%I(b&Rg<<%aDO@7U*34DVd}T1~2YI$n9?dVIat;S0A{kE%`Q=eO~@ z-FrobIT>-d!REqkN7J?1Q4dZw5$c30v<9cE$$~abJNG578Y!GLO7c2BQylZO95eHJ z^HUvn1>S9m`>AWmN)^6uS{~jL>p%?*R%6&3F&bN6vYRWLuiwukPwY0g^$T`nsf)$m zlWZ7{awr~*cEJbUuqe2})oLD>fSp)#6Itk;rR^|R5c-;%G>ghlW~HGbx(?4;ElFl3 z)aGqk&b<@8x3K=1`7$2LtH-r5E19oNI_=!W;Y8O5^?$YAjIs{~S&?O#_Y`s0{<=+0 zqHOFO3+Psz1z#b`jnpVf?D|8)kIPDffY-GAHGt#t^6G>O&0ntv-yx z$oSMgUH28`52T_sMa= zx}Hc)5#-Tkk|w86+nhdnyR)vOz9_gVE0m%2HfQqVD(e^Dm)swTlH~R z2Di3CY)cEPi5{3sh92{j^vcI=97=;mWRsGp_b+w|vCA4I3UF zk($tzP~%dM_(Y~S%XUpX-_q7MS$aU{px-wh73{TP3H+8ZdG-Ung z`f)Bn=^ss&VlgH+?%PLLUzV`lr6-)$ZW+o{qiE{AK9QCx%T$ce_Bpk+U}P;9IgW z3$1V7wD*?`!C*ChZqXCw+u?Ff0K&pc#9tZiQONeWF7tZr(KjK@*Vy?rk=-+og;=iP zxi_O`zjc3F{#Fs}G2aNB8_Vwr{XYJwW-byZ?KJZFUEP}Vfj=}E8cE0#=L%kEG6m)0wwH)pu+@Zr>0 zR{LrA*7~0>gA^6?f+Yyan4tB?+{)BuH*_wkmHR2m|Im6VbR)fo!T*pN;EQ6l^5@H{ z+d5q|Un39+PgGW>=R1qetsFtT-TtfK|J8a4bd$Kg6FyFEofqYh;Ln$Dt{KTo^+zBY zJpX<9D(L^!8ag{^3PU0g$71G1*(~t$<(JDS4wg!YlUTWR0z*d~>a_*b5h`aFXZ>%j zp_|(Muww{B`V+n=Vyl<@e0flyBKzPjL~X(1!iz_XB1GyIXZIbVr{U+zf1Vga z*!y}o)AO_bm)8ILj$2M!bkBI@wVjBG=L39JaK({EFwU5wiQFhUK5KV8%dI7ys6aVt0dOn~S?`1dgAgd7`UD>fqnBUOS&{ zm^Y*LdV!l!N4dH4{)}l&WNMEf51gY5w~M9J8{}8j1%8N3k8;+O5JJQ};$I{O=d|Um ziss5#P7-!~WxU=TR)rhS@ z`O=!+=oXw^Ugn{BlajfeNkU<9(PUhpB=GeK_%ihc6ln0zx!{ z-Njb54uaJQBmJ`1l4i)7t|A(-+u&N6sg0>pZ>t57=?zjbj>?f2$-sc4w5-ewvQl zqTjEQaQo2xgeq{b*am$6Q)Vw`YjRM&yzr`}E#ipKoMVRc(C$TZ7X{%3(??v<@gYJ;$`1=m&L z64l<>`lhd`Yq@ca$sM{_{_LUOHFogrLs@@j(sT*7;uETc?Y9rDUUe#6P1il)-t~hi zN-paPGW!ar7#C9n?W+Q4+s`s7+t;1@d%F6sAZN}Wn^7LudWQy?oURE?sq(uSWg1ed zuS8Grj~dmv#Vs#2^v6+ar3Z9#7U_k{##3jVxnf)ZUs?uk*{l_ zYMf~wu62J}hj14rt3BD+xn$x=%`8_&HSBYGSn9zQ>qM^adgJorHZQg~v!k;(>G8~1 zy?DMWd)V0FhjtpticT@O(^~!}PW{s(7n!99X9IWAE&JK8pU9L%$cG$r==*;hu+SOp6jwzGr69+)vHB}}TceHN- z-S?*BV{3Blx^&B@{XzZx@Y3~kiTh{3*c}7y{=_&#N*_MKz8n$=UjsJ$Bv_COm#=T# z3ziz*c4p1Ljez$8T`~+w0_r^78}~P&F2PLiwEE;Wh{f0cO-qH4IRTrLz?oE=wkpje zCJvT?sxiKtLriip00q6u#M9~b+FlZ9TZneg-uf%3@=`6vt?$6nidiazsDjZDr%wb! zG}k=7Q)ar<#9euM{n=ak))KVT(V@cJSLf_e)RY^|dCexW+e z<9+3dXRtkED}U(8E46tM+u0Pmx{s}Xbo_jQH@Z|s-@RbCG>_}()fby2k#E*c@T)yv1ZcleTyAfrL z)CKYgDQ2#k#xRqv??;g3-f;hAn zZG7?B-K)Bm1MPHnt51=gJLJ{cyaps6e%kC4)rd`2-#idv9<90Z6k)|N%h`6(GuWl! zwly%B0QH{Na;4e7J0fRO2koOPG9~V33J=IT96py+9H#|NbFJXPl#=z)Rec)G*|-z1 zTLRvIfC%NNu91{qMR(e|sea7n;E5w^OFXta6+SN)OR?UjN=XTHv4C|=M_L*Yhfix| z6m$1p2^103A^%n@xk}DbjaV+?ZCkUAd&_m;4w>zH1D)3Jy|VlSk=tOZ#MRMG6#M9i znmJ_B2UpbnRBbQZ{&Z0a{q@rVNvd7ESpC?GJ$=}pC+nE{hy;VpJb})1aiA~T^(jA@pP?V|sj)b+Cq)EM?k$R01Z~zlc=+BZJ1k}6t7CJ9@*K^bhP}svIU{Mn_VtdX!~#z7{wl{ zA#zlp0#G)@_1%n8{$R>S8OqCEqj`G;pwZ8_TCxX)}l*|_WAd9xQHp$T0=nFB7`+^!EhWt)!mZr+$IP~qC} zW^(4~v78TE8-8<4abdBB$xUpQUGumzoZCF({JFfu-`kf?1gaF7;ELJ}N3D*?C;8?@ zEw4I%HDFT}KOoL;h>o4Uxl?DhFnALcF@ zW~req-x=ykdeHaXB-UUbo9a@NWH&R8B!;XV!D7%2ZOw#FD?j!GC6^?QPTR(hB0=*# z%5A%&94)GTi)w&mF3owlK1QUX;lbH31?PahjC{`ywWeAz!N+zV8*;eKYeu*gF$Ck- zUXJ}JF01e*D0zZrgCK9p;`dMMuQ#(d2#>EkRd(}!GwdK6FF!56T_|VY8qie4I?PPp zyq>k1H%=q^wmVD`u=eLKl+Yi*+FzD)Px#ZaVw! zO>G|gV!z{MiOd&9p}dHcXcy|;3%9Qy`G&!}v!+64%*tP?N!3C4cuYO;l`j}7{=upe z``Q?Yb6v-9RlBo)_ghj&P8VS| z-ic^A`tobFo0ONedp>FMetqNR?Q2`Q#K*}4B>_{?=v5=-l_;tQvE1fjN284Gt0L*2 z$2R-b)aR*HXZP0Y7QJNnt?YR1lcK6*d8)A>r(DR>)~MZT$Jtw3?6|8X%bgY8avuvJb1gANtZLOr?I)kI#u7h9 z_frjl+l5>%Lfg;qpn02S&8?099OHF-yT7O(5MU_M7fkQG>wt16?GXsC`M5=I*gEDgQ*uu;ABWkg^PZ9vq<9XPRhiNpJQEmP6Z&f zv}dim-#?ZO44RI+;pn{ToOf*|j{_`^x+HY2j!0eBd4%sAES#zZ_XpG~TWI9;<6{;o z?*wTRt%lTYDMY93k0tKd&?!?jVMF_KQ|j&&!eUpKu2}c%w6NBryiZGNQ+ETX(ygXR zA3eZuqrAojzX_^;{`mBYC*vAp8W5eQU-==G`l=C@b4Zh&6|02Qr=>dgef-1UF)|4; zGS!Y+W9Ka$IsM{ukC#-wX_goFAUTbe-;EIdPErG;^lw%&^08j(tA;8q@XL3kN68mi(|YOxM{j zi4GjBOn4I0b?Q~3lT@5ncGdg+G7q}|86*@#%IhA1pUh)IiqF4T@7y{J=ZQ?@L#FG* z?QIy&8JQCvOxMq+wmEXXHAhu5VPbWn!dcmQ1zk)BH)3&fB(KnZ`puvgzPLGRb{swZ=eBD1VwuRX z81(e3#A@$inW%3K_R}vMsy&O}1^N`{M{={#u$0xf;(Q2D?5Ce9S9=umcLhD&QCf{H z=5HH%`X#m6tC+v9_7wv+{Bn3zqY=%7zPZ`STpk>lT7vfXcfz?8PB9D3DQD*ucKIk4 zba7d3q~cuxj~){s9qPr56VoIVCkK>2CumQ##^>3zdg6i;kBjB77>*DP82dh z`ZNq@P5i#v@PxqsAwXT z_C;1OKRxPSm)!%KIA1%?-<7sow#WmkQ z$e^d$ZgYfUjpkflY0XR%AqoK3TnfvDYR~ypFZ;A=*gA>{%s6=i`XxRP>a31ecoefQL|91`tsr^ zc5Zei_-*YH0ris>T`{$Jli)rc`%~8cgt_%*o6S$+SP!MZ2~Qo2ZH}#n>?Pw!tUyZu zD8&{v8Fm=c-r@QAgQaF^{26Gym5*v6g~I?I)G_GsFE0q6(}M)*Wr-vw2Z-YYqXBWS-fSNq z^E(7v9@`;ch*cN?mkljmB<`fO>8uAgX@IMf2t(o25k3aSX8ah@eHvnpkRXxc?~23> zs0yb=&))LJ;Bx!MH2^*VOr^M&HUn{?ZFIzI(ba{*^I+70cC#BlxB&8E@!3(y4L6Gm zz49t%#Sq!=?zZs*&SMozr+pMR0z4Jx1)0Ct2&erF$dUTqFyVV!kzR*Lt1GjYLoO+q zc@7}0()U=jc&I8Sf&AsJOS<{W5q>Cpk^;%Tov~T+m*{B;e3$3~AUSOIz5VK(?jz9= zuHpct#R*hA^Tug5$osuUpbZ!2L|QssX{DC^Uhg#HWl8emPH-WUCJ#7i-Nx?2rA*~; zlV{eQN*^n;7nrWpQoQz;)V(Rwn83jwVwRM4nL5~jO4D$%t=?$n$ z5I3Hg#6-xJx~v!6STBc_L);IY2O3VQrwE+nYaB2l{~5lYj12qw?%apo@X?g%o&QAg z>EagZ97R54ar4BN=L)CQ_scDY`zIq+`Y$T{#kWe9aoV$A%O3Y2=iLH+zb(Qz_xu%; zpn=l5KlK_cL=Cls1*D>Dornsff09y_SJig7V;m%5jC_3x0CvL-TxY!cCl%EbUl~Aa zSgO!B_~-ct|IumGt1GDzmXTkse8?$$>GV}gU^-y|asWZKXF~acifEm1{8Tn5rW*YI znT^iP+G>7O14eb|+&MA25qy(6+y|K{cmiR}F>O8-0Xz?AGoufD1GpjJ;Y@4-Qo+I! zwOIbYw{h*%(&^mN-ty#62)4p1~8ygS*m z0>B`?GoUdwbca2%6X7qkunvszS_`WcE!c&nfQ{tiPzN6@4HQ`X-pSpv&CWvYqr|t# znmMVxSG2uT2Z5yUCDSq@i!K#Et`a{U)-A3b{N9K=ii)kRM!|YJMx31i%XRr6cq_s0 z&TM<(6|Y(Hu6IdoZ;*#lmGOc`qPW&s1g?0ycp}{=9nSB0v^4MucqE6Z;2D)vJftij zZjOEAPUNG$z`C{Up{)pa^93NEF{_@^3s_vmM*!QJemsNyc0n!N+Ih6`#t)<41@8{*|9|!j_>pRy`aMQvGwNYsvj5h z7cukL6GP?;P}?s8r7t+Z2!9y{ex7JTPC8<4d;Yc&pf-S7bYOvq09W)y!OiLNo)gvl zjKpg8L*Tgx)tzAj1pP{}r zGxkrdDjLg`MLT0|UoSIXaT=6=UuTW15LR}(f2DA3z&t<(3(2$RpxpbK_${eZWgXvK zMxM-3TPzp6djwC35pS?>Rb30gq`|QxTnkoS-VQ zZay<}cQ{5`h7W`T#V4&x&asGiqdEo?(Gq#R+u4*jz+G~z&?3VTw7>|_UHDY`YU0ig z{kSuQ{A`2id=4jHAS+KB5)OZ{L;A-Y^$+ad#{Jx}d1I57H|7A~VcZZ1^wX4qTJ(DA zjXa=30>A>9GV5YtqI*t^7DQJaTJ49w=c_IWAm)FXyh>VOxA{z4%KGNL{bpO0oK2fj?#~|Y%VGO$de7_@Dzb=7_REf+&4$9h0D%>Ty=gq3uICs{L zD30EY=D5IEIp4swYquSVKu}IEUaj!s8@-k*raa&JaOn+*p!3(9N~gz4dnMHdq zE0i#xt}fCZC}`penvA}&(^rHfIrwgs)gw6uFX5BIE-D>^hX|&W4#%ZIM+rWhLas6;t(B} zRGarsO-Rr0tTXG5!tmZJh#i7D#WxRK$g(1){z2e*h^-x=+h7y09j(h?BR(KgLbIA# z?5cj1t#H(Y>as@Zd+$s8y}n|>z3Zru&ZgdF=O1l&r6z}-#;1nvJbR1$D(~6I6^rC4 zcb;D9rL&VX5G*Ep9w0+JYCHTt(IsG<#3LO)RZaGm<=^_pJhupEq)={*^wqclz2SY* z*;ajwuK~Hx8q7L|ay?E3KufT(R zz^tlf;>dEi+9LJ4R^8-siSi6~6pOU4TBxeXNwX+M^3!CFR%1o-yDV@AA6@&V2o9>2 z?9y<}M|0pPZ{7g#`}x{lH{41^QhfC`_VL8~j2v|_ded-Em$5rbD~GN2oZU+qkQ$JC zXyTp$dMB?0g4&U(_XQq{1oz+htPVi|FM(V6&8zrVAd7cw5&Da&OI& zdgQX)(q4~O4tKQ+})roG*kW%NTGlmN8@8I>vsax=goP4fi<7AoD zfi`|hqSuYP{=u9W(3?=!xTdvTzI;D9L6MJ}?;I`h0X)28ZnE*U zM?{`jLdteu5Gxr1IOCZY%4(F&aVzdAD0erR`PlD4#bQf7y2=B+m{v%E9DH>Am>Po}1ZOSdUg zSLz>#w!^11v64#d((JYK6vmTRk2Fj?1_EDOks85DMo(_k+xI`w^CapY&QzKWke-%I zJpP!GCOthlnpk&ExB()YeTH!&m?OV$>MXqo3sPv}3Gh%Ahk9AUen-8#tmAQa7RGaJ z`us(=cI;rmG(idK^RyU=Sl=FkOX>MuPv`EOTGuqGG^VmtjBoj}DUI-;m%1#z6ZKe6 zSW^N}14}Lm*g@+%ryGQ#VS)E=+=)Q_b%dX0wBe^2Ntz33#%Dx&Me3p?g>BN6j{TzC|y zx~8uOjBS=ZE0E0I;TW>j(R<@Wk>r86{(BZ*?RNUt$rjf*qJM%F&jNoL%Tv%pWTm?8P=<6G-P+QDduV=exs4Ubqyyb`VXIu zl-7Lp@WPb!=AW)(EwAZDkK0|z22tjn`k>0-XH>{M9TQ*hHvRi70Ay~$U+W^@b6~u) z$f)PPxZ!`{bH*t-g-*6swS;O;sA;NFb`4W8E*U9rRuxxQP3ZTLCyBP;n@mA+YJ(u9 z=rwG}<|kdJzK3ZfK<4LY(a^QyTbEJRvIn-f0PKzI;gEY0UT1cm2MXLJ37tePC<=XyHYZ$>e5LN}v;u>spLeAZ;I=dxE* zqFUU%%)RPsMhacCLo@z#ZkFU$e^jgQ5884TXbxLCP;s1_?M>xDHUPB#t~7e;#vqN% zvX0;cs^sWa_TI85RoeB50f5Xm=eHxrkJ<~r;wBq^9Bfm6dIQd=-zWc}FE=Px8z+k( zg}S)!arPfmtj)QIY=yt})>~+`uO}@pHwRy-(J5mG$z*?8)`J}TR9Ni$nA(nTvta7Y)GJeQ0>;Bg<-%eT{!yc zXr>)76EOC-Z|8~)k830XBiE{wQ=3;fn6-eM?*?4nydgXBuirdWlo@O_AQ4CyO9(+= zpME344FGe|Ovs~|MB;!-nGAW?LVeZ^fpE>wN$+xiInl>` zwL@yjhD(GFmiKbqHh0hAh3Jq$CW`hEJXJuw64a`=FG8#?8p3208k`3S^o zi<9`L=~=tZSL<^N89Psrm7MT#O;>goN@h!+?%dpo6g=zUv&_CqAV!)c^fJ4h5hJ2L zW;dNTn%%tJPn_FleA!I&e+{Fz!Es zF=>AQWRP=SN7&8~6o~wwd0}$@=V9s0JqID*w)0W7B{}L0fTMc8JlhvPa^Doe2%2A> zJ8P)71eJ&`(K7&GC9o<7kimbSKlp+ljgv#yB9zW9s0j{1v{q?EiIJ=`lL45RTE%k2 zET^|d2{WCY~j=bwEfjvPL()0exmbB@4)! zk-5>i8SQWH4{G~r=H<@P#Rh}K2FFUYBHup)!u9yh=!lqMx4+9>V**+(U%!2*q-TqJ zc7kZ%1bMPAs-|*^nV-OiPlLyslt~4Ch?B^9?!;tr!e3T(an;9>jBF@qhHBXRuM@zj}-%q42?aWG9PC-G)xO&?#GkG5Mn9e=4T0ng_yJB6%jyGlN z)K5z9U2uuMXe1uG;H@GOP;s=jBlqY+_kfTLNzBs?)*;x!BZ!lfj}t>xd9_;Gh2HI+~@rY2-9)XjZsKtb6FVVeb;+5%#RhD6=M1X7o@WyUot*>v@DpnxE`Pc$KtI=Yy zbQ+uJasW7&H=z-uJY5U)g2Q}90lYdNW^H5>l4Z7xOq}L%u8cQpyGd7n^(_1Pcg%-P zX1T#op2fmZU+d5npZ{EwOL@EuKx;km zDJ!Pd*T=$6obTh!rClquJ}=3xnwuFd?E-idUmVh`SxOkZ84|~QsWt7NokH%wpiSG_ z38UeC(^Gqha;xSv){WdkCcP?(Q5Opu|i9 ziPmsDhX?g5uL&f2TXGf;VB}T;DXeZ+PJ+aF=1Yxnv+NXbOYOsf1VburvjkwA@7gk5 zC(BtFPH}!jF4J}V2Fs4a%#SQ)x^lA|Fq~`oC+;y_$1XTHaz4mJ(U`8hfqVp8?#gB!C3TfX!+R#_V=JmBVBGJUG{!s+55|;@A&{XbNT6LrZi-IvRxpngPv~0Yk3tn$kbF4`0N{IUgZ~) zY!@=q9$hgtoSn;i-5gm$NCO#jc#weTCAh&Pa(IxTXeGEoIwOY%OHxK~gM3GhlE}JT z9;_Ra;5OaG%HeVIqpApfU!N-H@BklgBlJyml;-dN5bq}ReS1mG;q{D0mJ%R!6w8Fv z%_xQgIi6}}%d6;{$s^Rj@y+FdgpUbPFla6hwAOP1U#n&FAW!gwz-L(HCJdG_XW@KeIHOT> zkP?7=L{<;zNOZ+uYIqK1v})3c13c|>Owf(hSv^s6GyGDnO`S{L&hgXE>Qd*+AhF`z zR~^B}=}kNlp<;`EKISy92Sw*QUWAcywstD_K(JK#&T9OKD;#bO_W=k?Gz*WkW>wY(ZH= z!BTAV+T}}u1qv)y$p+pd{4wf6;vNyG%-Dw71BriPLo(ndfW+@|i~rpybr5j-U1>4> zj<*lppjQH*q5<_cePQA!^ZFOSuagzg0ICC@6<)+@h{X)jAcOIgwPn5x>s5G1Pfg0i zlA+ZL@O1i2v}SV%j1O#3@ZH}g4bqSy&YRCbYQE|FecvNG>mdVzu5HJj@o)sTW>r}# zR3c%<`N6;lxWxsRfj2duuz*A^@X73Cl~`V9`{qliAa`urY-lY}Jqm;_h!2@Hz|>(W zanga;Q8<-8)4~xw>w#3)&K5p-baCN|v>C}{7GxSk>X{*GF+ixX8@h?C%Uhykyb&lI zvH9==4Xm(-e?G#v79{WRmvWFZH7k(Z)I9DjyWI!?hrt`l6+IKQ#MieTOx`+lAu}p@ z)Z?t^Q0f96Riw`F3u~`c5sQB^8No(E+1GkOTb)pAcaoY;2y`G)Z~u&m1?p^}SmeRz z2*sWDg+r->n1+r>WhoL93Xq~e@I6sm1=L$K7Z6h)symC3`C$jQ08FvO%2Q9T9C297 z0LGR`J*1-A{Z90`9d{Rxuiq%B!|cOES3RExc80cKaeEkb*hMU_OSBU;!f;ObU`I1Z zz{YxzK@0O;oS{=Fk@}XOq61dk| z2$%W=9^pye3f@?lbIPHekX&EBbc3qWrkp*!8awQ~H-2imA~&v)Zz0LmXjV(I@!sgn z^?m{Q=PO6JcWuXMwm0qrpXN7@N1yf03f+t^8$mq|q63__2f8Ud>(X;t_Z!+KrLrET zu~rm{J52_3bSiU~1TI@ZT3cq}x`dzk57y$PYYUJtpS6fyNDKUW&lcg@THFG}eyJK! zdted%OkOpT=oVQv(p_rF8|{dsr^EFz=q!!U07XhzML{M3LrU<`P)K2egIy3>Y(`<%B3eWsmycG^1|Nd z<0lN$h}X1oj$F8Xtt1`_w-B=Lh97nC7iLshK@b#>$M#RJr#kK(mSo2nt``KJ6QwFH zMUcX&bC-qEPNm*+_WO->-IZYm?td=M^8Y87{wui@b{c|`)gzEG_E5iY`-mCg5Nm2u zvO&VUt*o%Sup@)q&wcHK1gH&zA14E%(2(@6;Mn=w4{q#HX0?WH^aD!moxOz=%J9Uq zlVx`V;eta_1UjzNy`;&vZ2T8+TQk2 z>{NQU1(njsIkFcWTLW0*+khkIeTPq%HQK-Mz%6ibe>sH!VDIRYFwR0?60pdnelG=L z%ub~yJON|ZzADY+G!Y6|{R{F8@$INMmxIoUn^4wUe3~}dBu}L``x2|3Nr(XaCb~z~ zR2PEaWa$;J+ec9m4-&um^w1%#GAOT|Pdjg3*f&0vo@N>!nsYmH^S#kuufd8gkBmg3 zWJ9nacPL0CU*yX#RPJ>}b^nEdZ33SJ*j+yq!Y}uGQSBBnZ&$>BI&+_Wi?uRyPcj0r zB8&h30(^Y_Fv4ce0xYYA*{8j*k7kb+^+zkEP$1lTv)`I8kf}Mf`$1mIZHE91zymp~ z{2@=*%=?koQfLza1<8PSaUvpY$;^lM=IAZK8 zGOI@h*4|6_IKJ0B@y|EEVdF#^+a|sZD?c9I$sd2g3(7h26BToiYQ+!~!fsG!+LQ2W zDY&nGtKXTwvsysKo&kBBkBYss#W#j;+mjsfjp!bsnueSj*E(0>r}TQ} zRR6d@IDjZXc!bKCY+P$Gl$oD80O;GSSda0Br0S!?u2&Cp_(avkwFl}b12~mOtXt8- zTv)fu5@^{s6nEv*nMNVT7ZcuIUqV&f*;J|42-E1HwZ7U8mEW!z$cdeAxX75{=Rg70 z!bL`v7e4fdymnEcvL|p$YJbb_cbY$*>aCk4*?U?gA0L9$9b&?GM%GOyes%-xg^d zw>+7%q790EuU&7K+3%BZ>iT2awVjkpUI$YNecT9c($JLg4WO|u_m{fU%1Q4wKAnH| z9X&JF^?;4#%Pp#uSD69(+5%NJ&!IqX0UBb*M_WJV z7LL%r__bMFjjZRo=Na1Z{tmuY!6yzZlxM6Pxw(cV?sg8ty0a~5WV<4>uhiTY%G^kq z{1Oh{f;Uic2e2>UyUACvuN!wvUMa>En74D~HxBoHjmr*}OD!#jhmO@c^XoJjZmg z9Ay2aVaoW;6Bnxado4Kc7ybiM@tL5qI`i(^t^IQCj9-c#D8HLkcfaz5tCJZ#c=02j)DFDRrosanv3_$GSwP#gyL{BRG^2qnb3!o-S z09~&JAD+M#W(VYf7RonFa9VhH~;*#}*g%$Gb>35^^$rH)|zER>3HcMzl^@&dVk z$RhYk diff --git a/docs/diagrams/handshake-failure.png b/docs/diagrams/handshake-failure.png deleted file mode 100644 index 9de76cf2c06524049879750065a6b96130bff8cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9271 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 diff --git a/docs/diagrams/send-prefs.png b/docs/diagrams/send-prefs.png deleted file mode 100644 index 8ce42fccb21e5074f630b97e2d39316a5ef5da2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9617 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 Pun56B5gd!{L-+m{F-$nn diff --git a/docs/diagrams/send-profile-empty.png b/docs/diagrams/send-profile-empty.png deleted file mode 100644 index cdd60a9ee38df7be02977f44e0b537362f29b2b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7788 zcmeHseLU0s_x}<_qLQQ{-B2nt8ylr$3^Q{-+vIL$W1HQXZA=nfanaSCicoZi+^Z`S zBBfFnQq)|T+mt1t5|W#~FI>OJ_jCRI`~LHNJihvdk|yv})_=Q(FdZY~aL zD>klxKp<)uM>`J)WJwYD{Z)Axc&3#eH-$iy&PJe#5v)iWJ&X!5fZHzqGJrutIs6C% zxSatEM&@!&g6ZUta59T;!lp(55}0Rkg6T9mHF$9i3<`r9LlMTN2rpAh1GtU31$dYt zOkhZu&*FM=2$lU0Lvs@-U|>j~vx7Mz{;tJ&Y6LlCk(ULE?u?=_2n;AnY;PAXq%Z;~ zi))0`a6X;GUR-7hH-VX$Elxy4ajA>cV5*Q#q0Up`XrRtNa)aaO|43?PP74(T2^lt# zuJ8cQP!CHAMX<<(O=kVW#0>~*Y`Vw<=}mJAir|aTZcws~0SpBV4e{_1x>0|HFoK-<9yln9jpf*hg3Nswp>{%Im>u5T1@6eQ z6gr^=41xj7-UDTa5bxAxZwE?rYJ5;1P?)b2E(1~olObOUKEZumybnzm|}fAcrciYCq0sf z4f2NhV14XF3~m%A5=VBx5-i-s1O}VMMG#nF-mYi@mFIwQ=2(V>M7Yqs?9d!<5-G$9 z4TF0)qU_)}8Vi_^P2oC)Lc@bx`L^7T|GEn2&}1_C7^e8j7w>hr`WTZtg^ynLP+i z;E4YhofbkB82=M|;b_fOUkxD;?KF&?jaTI5;m+Hqi=M4}%JaTc$|6;3tm1B0u&!IZ zkNb4{YMf;4mt}~DA9I7%qn7&DJtl07Kc}fzC6b|$2yt&b9mQxX%Gf3-T(uSaIEdt@#$Q_%jn3+v+eEe#j@inrKP3czI-VYz#)0=%O`O| zSLIPz_a8jqbZvHyjE!C2GWO!v)?Qx2w`)~DB$nY_3?yS>clp{auOiKQe~G1b7r zzOKrmVd6~P9mNJ^;(Gbtb8CpHz1mBL-s#A!6&f{kwg>uMrc65olHw_fMJM@Tzr0n7 z3JPwP3}u?|u>wD=Op+;yJFh})KhSbJmwvOLpm`{n#okDyl9ETKHPGjNBO69#^#oTQ zZX1595vNo%`qBRt42n6Ng4`89YCMHE6Ns?x*CLWs};ho0g4YK5zHnYOgMl^C& zkr~c4t7XjhuRA)kH3hq$-*Nuy&V{NHgGSi%J4KUo*A1)WiG=OAwusUXQQgrsELk^` z(pWU)16CO|-ZV@e5yY4@!c^+N=$GYoM6fH{x0M|ebivCK1Vu)T{--w1v85SSEr4Bq zfsmv?OY&szdS;~jsZ7W!2&v&cJr{7q4xyyI?arlPlBJ5cIJZI3|E+`GH+ zPIbYvG2bISqe7oHvlM;IWVb#>yxa`usZ#G&Pyh(}%JJ+XQf|unSTbQ_ zVS&>l=;!gdPgSYVl?KWRD)nXUCtFwHv*aF{Cjf8NqDn-w@#J=8qd-4qHpK^N)pFi2 zSwHzYyCi@~a@X`|FOSS>S5+>?*sONcX_&uEO5(V5?EZ)-CRMiVLchU-|Yfhb<>0&wvu?0TkQb=wiZYNZCwVVt#X!YyvQ)8p6zMNq?CIG zchx0dzeSKIRJ?O7ryGmro%!+P>_~2M-@3nT%4) zy*Yd`ZF-=?tfKSG7T`dv4Mbo^zFmC>Rl?a__WhBc`~iCn5$*0QZRfLm8b8J5EPtc} zYs2h2H@e>dxWGLwab~D0b&GP9gC@EwGE64v!fot`F_)*Bz5H%Iw)AXn{=82`;c#u= z*_XAu4J_JoD?c1n1Ro5uv96V_7~{V;qdD-^CY=OxZ@IjjmL5BHu1;ghzols-L;Y7X zP(?b!%)K3fY@Q7{#r!q&v{H~2J)0V4VSlVS%(_83Gm46yYC%gCrLTB@924+P59`HI zRsM%Ug0c~oyT~+PJ#ln&^l*jJ+U}kn%k*Ox7sJowmoHmDxH??1eC>R=?CT>B(I752 z)JHOUJyGESEwunuXoe6 z_wU~i>(Z>FzkWCi8@jr4+qan+qipNgfxzKsGFj%4xfv3O(-rI$TxGCggY+RA@oQ>| z1j6%tERBhwDugcuB7Xb%bN87uXWmU8BrA1-xCfq}~DoCimywguE4d0VvsQnrCGv?Wdna++7JzHdD-;K9+0r2!Av zT`M7f|4}G-Sy`t5xt9bU#D|r2;pKZ)La>!S%de;!5q54`61eMMdrO2T7D!k5fFE}) zc$CHS<32D5+K|-n1@0v-2!%S25~+=XDnzHBp&MaR<~zJ%=>ka(XuUhYBTz}M(DmXtn65~Y=QJbNefbk0~2*CUMQs&`jH{TzZO^Q zd#k)4s)`YWyQs*2N)I`fscZx}w(c@er1}x0?81Vo;etDB7DUw`LVC3cLqL)LlpX>> zt|3Aodo0!y|3lRepMtk9h^k)(sWSz-{abnnguSf{0`cblOV!3V&+QjPbqri`i~@A~ zxAaROopymsAVt0ZQZ=>k{HX;|&jNci+*>rme@p-WXQ(%E0DjP`AQlP>GS% zdEH-Qz8w-jfi6qtlqFB0zJ2~Ypk2V31w%aE1OKL36|J41(eQOZ=@xw>K*VoWX}R<& zxlE$lWnE=0J?K(Uusf;$hsjV!^>xF?lQFxJgNAZHZwALljMnXL^tf)Q(K87kA~V|p zAT23%+qRV4d^>l2fW$ft$ibw$tUa}(*i@>A<`c~HxOvVKtK@9}LF9VSe1qD?K zm*i0#J$aN#?dW8DzTt}JcRrV}N{4*z7*!uWq`nTl)m>9SJ~&ZXnFmk{*VZiMsIsg6 zz81YamGlxsK{Fi3$FT_&nso_}@4D&uDcpp`Po)B^H%vyXDq>bJSCOOlZhVPJiFm-- zpw09(topsK<687GZ<*xJ69qE9O&jCDD zYkwm!wpsn%zfB@x$z-Dg5_-$4&%E8k7%B--g`;F2m#_>@QvkBZZz0}iiSOSv9tbZ_ zJ0Pj;OM5py&L6%tRr`| zc+C@$S)$hcFRvl^7Orw#v$F3uSH>UuX0?9JIg8WP4+@9Gu(F7$p9hC`J>eNqNy4ol zWg~bz$zh&&C>KV!5_H}6Y(wHo{ zVC#)i1I#v_j%duC_A*_z|MHEt%7T2=3%BJ0kSYn+Be5v5HUhh})t954(52K^Df}1` zNWaQBG#K)HWl`lU^XO5*2&Up|M$4(>{!9MSxHZJ}u;W{m71j){dW*-uSr;7BY&0>D zWOaYH?y}U0ruZ!%)vQ1LOia^+j zEf#M}pev(Kyj}Sve|TdFpB|XbKP7HT(|n_k0X~aqi-7g!c3fL;J1||sYS2`>Wz|wW zOP~0jH&Ghp9&&V~WVZ9t@T+mQd1>PCY9axas64L!dK0?GV z6DhL$k8{q;>+4`k83kSSTW^;y-GfQ#t|jjKoLloEe}8Xo!j^`)x$VQ+H5UQ}=Ry=k)SoyEvN`aptWVs3nc zj(Clw@yiNuaV-CLt(hiGn?FD=<#n)@3ERwe*0dg8CM^?H-<=GVG|X6CKAgR*?PaCV z7oJE}U-C)k*4)AJ*gS=y?(EDP?{ZJ-R)qu(CY97j)F%b2sp>w0%QJHofbGA=JP_5_ zfn+PwwbFk!hW*P01gVCD$WCeP%D8gd!)huo{I2-JXTpc}wz%NBV_$piFQq?7=xG%) z9#+%*#B2XD#tru~CuTP`Fxlx9bNBtKWB0Ng@OTZyhh)yZA3eY3Dt>S_XmoCEpKE(^ z?&JODR)48^kDhGlaE-d2gK?GKIcoX^D*c1JSF1K=dIx~pa{;llf}z~7r=NDa1nM52 z>v?!XYxkk|(Sb2PzhmaU=UM9_?iH21Q`9zO{=(TW3))z=XrfrYm>OY>Z_--{khUzr&% zjXkhG?*po|asJRH7AxRo2V;Kf$IkTYeHe}XRSn+1h?;ha(x(6q9lj3BkW;gxu{}x` zCvK%KTN77qs-Rlu?i{n~Bq*=qlzI(w=h~D&+6AbuK$*1#fQ0V<4gYuDM&Nl+eYT7h zXOhb$tQ;fkkThFf`C3qeI;m{LTup4JXV@oSztHGq5(l?yd8K-pXwl2**B^&+MI$qLP|e+2C``!17Q1hpZq?PNDW6D^wI zoT|I?TVvcsYB~UFfX)D@jd$SjN%APazCW(tiq^-ps-~SOvUPoF)e=1n3T9CBtsA*+ zsNV?tYBhR&&)=X*lk@~xQ%O58Dc>hrjDtx#$lBduQ56yF%<-UiTA+}duU6-4QBlzi z!wj|f+gBF=?Nh2}`}h-bcs@Ws<@l5J^3DlA;LpDm#^D#r&~-_7j0_s@WE#h66KRg2 zp-1i#0}fx^t7i14^7|;0EP0FtH*s37RZ!5u3QTI^j2Wx4QuH+%cJ|f7Otz6p7P=0Ru-D`X^*@b}BSkQ^-V+1BSBJIxWV>q=ACyTS54P2^ShL%ZM z&2Xci-hjGv^zKfW7SZS4_n40U*0ZnHMvpjE78I29dg0$NAE@uejo*Blg{;7;KKW8` zpPs?Kb(_ACE|U<-+RWxvok(ATSecdouDJfpw$|YugPKZUtJD1_S5&hzwVoIzS0OS^ z`0cfOI+1-;JtDJ2rReqp^C!If_qapy$DJWGSKoH{%EC&-N8zCSRp$%PZ0RqmkHo&X z2r`4J2(vAx`|)BDXd?Nl8=dQ6`+SLcP`&SU_eFo)4oqt_tEj@%NRwh4Pjh{IKUq4i zruw$**_j#+J9HqW4kpjLyJZkl%93tw<8Bgcvu+uCPQC$pCxpK}pU67TL}g8O!yP7{ z1!>=(GMw)y+(bn@JTqoo3 z{<0MPLDuLOMkJB=eX{Q;LnbsUYYM!yr+eSqqk~NT zCVh=luituzpC$N_)pxe{btSAKwt3=@Q&yK{LWLp9=lpluebtO=NQXF)+Ah*udPjBK zJKy=f&Uq5&0#Sn~ZaY!ia4MqiH1*|KGCj|`!~5=sz&Ht}oE_TT`jn#xvztv6 znu&;=nTY&+$t1lVCc9OCkDf76>rZ~6tCqH3c>d%DqIjlxwg_1qI*M&K7nL{<2k-;b z95hpVB@<`<%u7CVPY^i!bEs60{-Qbk`~an7YG9`L0jqRvM}=8=mwck(LLVX%&1T0s zvt;Hy4_{r6ZQS!Yf1-IfkKYhG66sPinbGpvJbTk{l;6)QWlU?NC+l@oKenUvfG?Z* zOIrb3yZlYs-hlbk;X>@(SawC|M&aj<(N7<{#A#Z4a(eP)ve>FJj9;2r5=B zdk-d%Qb8$Is1psErg~~dY6aOxe`1c#O@)9wWRBlBK+Y?fZBdKD~u3+p^@(S6R<&iJfW;cpvcVXWH*atdC{ML8Px)`h3i) zWz34xI2JoC>#35r98fM&i@BRP4$>*n6X_BNd(aIyd9PS3-gXaS@nNXF^xODQdvPX@ zpSaQp&0WGhr#gnGsOu7wN6AaLDj<`qSYcFN@hf5pq&?7)o5E%9plGu(IewH@GJWC#ZB KVpoC+jQ=lyaOxER diff --git a/docs/diagrams/send-profile.png b/docs/diagrams/send-profile.png deleted file mode 100644 index 28626f6e2fce2a22b88ae2c13a4070227e08f311..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18788 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 T3@LQ0xnF{y+T}52}#~ diff --git a/docs/protocol.md b/docs/protocol.md deleted file mode 100644 index 9e94fdc..0000000 --- a/docs/protocol.md +++ /dev/null @@ -1,99 +0,0 @@ -# fxrecord protocol - -The fxrecord protocol is broken up into a number of sections: - -1. Handshake -2. DownloadBuild -3. SendProfile -4. SendPrefs -5. WaitForIdle - -## Message Format - -Messages are encoded as JSON blobs (via Serde). Each message is prefixed with -a 4-byte length. - -Example: - -``` -00 00 00 1F # Length of Message (31) -{"Handshake":{"restart":false}} -``` - -In replies, it is common for the recorder to send a `Result` back. If the -result is `Ok`, then this indicates that the corresponding request was -successful. However, if an `Err` is returned to the recorder, then a fatal -error has occurred and the protocol cannot continue. At this point, the -recorder and runner will disconnect from eachother. - -An example of protocol failure can be seen below in Figure 2. - -## 1. Handshake - -The protocol is initiated by the recorder connecting to the runner over TCP. -The recorder will send a `Handshake` message to the runner, indicating that -it should restart. The runner replies with a `HandshakeReply` with the status -of the restart operation. They then disconnect and the recorder waits for the -runner to restart. - -> ![](/docs/diagrams/handshake.png) -> -> Figure 1: Handshake - -If something goes wrong with the handshake on the runner's end (such as a -failure with the Windows API when attempting to restart), it will instead -reply with an error message inside its `HandshakeReply`: - -> ![](/docs/diagrams/handshake-failure.png) -> -> Figure 2: Handshake Failure - -If the recorder requested a restart, it will then attempt to reconnect to the -runner with exponential backoff and handshake again, this time not requesting -a restart - -## 2. DownloadBuild - -After reconnecting, the next message from the recorder will be for the runner -to download a specific build of Firefox from Taskcluster. - -> ![](/docs/diagrams/download-build.png) -> -> Figure 3: Download Build - -## 3. SendProfile - -After fxrunner has downloaded a build, fxrecorder can optionally send a -zipped profile for it to use when running Firefox. If it does, it will send a -`SendProfile` message with the given profile size. It will then drop the -protocol down to a raw TCP connection and transfer the profile as raw bytes. -The runner will receive these bytes and write them to disk, then extract the -profile. - -> ![](/docs/diagrams/send-profile.png) -> -> Figure 4: Send Profile - -However, if no preset profile is to be used, an empty `SendProfile` message is -sent and fxrunner will have Firefox generate a new profile on start. - -> ![](/docs/diagrams/send-profile-empty.png) -> -> Figure 5: Send Profile (Empty Profile Case) - -## 4. SendPrefs - -Next, the fxrecorder may send a list of prefs that fxrunner should use when -running Firefox. If provided, they will be written to the `prefs.js` in the -profile directory from the `SendProfile` phase. If no profile was transferred -in that phase, a new profile directory will be created containing `prefs.js`. - -> ![](/docs/diagrams/send-prefs.png) -> -> Figure 6: Send Prefs - -## 5. WaitForIdle - -> ![](/docs/diagrams/wait-for-idle.png) -> -> Figure 7: Wait for Idle diff --git a/fxrecorder/src/bin/main.rs b/fxrecorder/src/bin/main.rs index 2570df8..defd84c 100644 --- a/fxrecorder/src/bin/main.rs +++ b/fxrecorder/src/bin/main.rs @@ -72,9 +72,13 @@ async fn fxrecorder(log: Logger, options: Options, config: Config) -> Result<(), let mut proto = RecorderProto::new(log.clone(), stream); - proto.handshake(true).await?; + proto + .send_new_request(&task_id, profile_path.as_ref().map(PathBuf::as_path), prefs) + .await?; } + info!(log, "Disconnected from runner. Waiting to reconnect..."); + { let reconnect = || { info!(log, "Attempting re-connection to runner..."); @@ -97,13 +101,7 @@ async fn fxrecorder(log: Logger, options: Options, config: Config) -> Result<(), let mut proto = RecorderProto::new(log, stream); - proto.handshake(false).await?; - proto.download_build(&task_id).await?; - proto - .send_profile(profile_path.as_ref().map(PathBuf::as_path)) - .await?; - proto.send_prefs(prefs).await?; - proto.wait_for_idle().await?; + proto.send_resume_request().await?; } Ok(()) diff --git a/fxrecorder/src/lib/proto.rs b/fxrecorder/src/lib/proto.rs index d799fb3..92e4313 100644 --- a/fxrecorder/src/lib/proto.rs +++ b/fxrecorder/src/lib/proto.rs @@ -21,6 +21,7 @@ pub struct RecorderProto { } impl RecorderProto { + /// Create a new RecorderProto. pub fn new(log: Logger, stream: TcpStream) -> RecorderProto { Self { inner: Some(Proto::new(stream)), @@ -28,11 +29,174 @@ impl RecorderProto { } } - /// Consume the RecorderProto and return the underlying `Proto`. - pub fn into_inner( - self, - ) -> Proto { - self.inner.unwrap() + /// Send a new request to the runner. + pub async fn send_new_request( + &mut self, + task_id: &str, + profile_path: Option<&Path>, + prefs: Vec<(String, PrefValue)>, + ) -> Result<(), RecorderProtoError> { + info!(self.log, "Sending request"); + + let profile_size = match profile_path { + None => None, + Some(profile_path) => Some(tokio::fs::metadata(profile_path).await?.len()), + }; + + self.send::( + NewRequest { + build_task_id: task_id.into(), + profile_size, + prefs, + } + .into(), + ) + .await?; + + loop { + let DownloadBuild { result } = self.recv().await?; + + match result { + Ok(DownloadStatus::Downloading) => { + info!(self.log, "Downloading build ..."); + } + + Ok(DownloadStatus::Downloaded) => { + info!(self.log, "Build download complete; extracting build ..."); + } + + Ok(DownloadStatus::Extracted) => { + info!(self.log, "Build extracted"); + break; + } + + Err(e) => { + error!(self.log, "Build download failed"; "task_id" => task_id, "error" => ?e); + return Err(e.into()); + } + } + } + + if let Some(profile_path) = profile_path { + self.send_profile(profile_path, profile_size.unwrap()) + .await? + } else { + info!(self.log, "No profile to send"); + } + + if let WritePrefs { result: Err(e) } = self.recv().await? { + error!(self.log, "Runner could not write prefs"; "error" => ?e); + return Err(e.into()); + } + + if let Restarting { result: Err(e) } = self.recv().await? { + error!(self.log, "Runner could not restart"; "error" => ?e); + return Err(e.into()); + } + + info!(self.log, "Runner is restarting..."); + + Ok(()) + } + + /// Send a resume request to the runner. + pub async fn send_resume_request(&mut self) -> Result<(), RecorderProtoError> { + info!(self.log, "Resuming request"); + self.send::(ResumeRequest {}.into()).await?; + + if let ResumeResponse { result: Err(e) } = self.recv().await? { + error!(self.log, "Could not resume request with runner"; "error" => ?e); + return Err(e.into()); + } + + info!(self.log, "Waiting for runner to become idle..."); + + if let WaitForIdle { result: Err(e) } = self.recv().await? { + error!(self.log, "Runner could not become idle"; "error" => ?e); + return Err(e.into()); + } + + info!(self.log, "Runner became idle"); + + Ok(()) + } + + /// Send the profile at the given path to the runner. + async fn send_profile( + &mut self, + profile_path: &Path, + profile_size: u64, + ) -> Result<(), RecorderProtoError> { + let RecvProfile { result } = self.recv().await?; + + match result? { + DownloadStatus::Downloading => { + info!(self.log, "Sending profile"; "profile_size" => profile_size); + } + + unexpected => { + return Err(RecorderProtoError::RecvProfileMismatch { + received: unexpected, + expected: DownloadStatus::Downloading, + } + .into()) + } + } + + let mut stream = self.inner.take().unwrap().into_inner(); + let result = RecorderProto::send_profile_impl(&mut stream, profile_path).await; + self.inner = Some(Proto::new(stream)); + + result?; + + let mut state = DownloadStatus::Downloading; + loop { + let next_state = self.recv::().await?.result?; + + assert_ne!(state, DownloadStatus::Extracted); + let expected = state.next().unwrap(); + + if expected != next_state { + return Err(RecorderProtoError::RecvProfileMismatch { + received: next_state, + expected: expected, + } + .into()); + } + + state = next_state; + + match state { + // This would be caught above because this is never an expected state. + DownloadStatus::Downloading => unreachable!(), + + DownloadStatus::Downloaded => { + info!(self.log, "Profile sent; extracting..."); + } + + DownloadStatus::Extracted => { + info!(self.log, "Profile extracted"); + break; + } + } + } + + assert!(state == DownloadStatus::Extracted); + + Ok(()) + } + + /// Write the raw bytes from the profile to the runner. + async fn send_profile_impl( + stream: &mut TcpStream, + profile_path: &Path, + ) -> Result<(), RecorderProtoError> { + let mut f = File::open(profile_path).await?; + + tokio::io::copy(&mut f, stream) + .await + .map_err(Into::into) + .map(drop) } /// Send the given message to the recorder. @@ -54,209 +218,6 @@ impl RecorderProto { { self.inner.as_mut().unwrap().recv::().await } - - /// Handshake with FxRunner. - pub async fn handshake(&mut self, restart: bool) -> Result<(), RecorderProtoError> { - info!(self.log, "Handshaking ..."); - self.send(Handshake { restart }).await?; - let HandshakeReply { result } = self.recv().await?; - - match result { - Ok(..) => { - info!(self.log, "Handshake complete"); - Ok(()) - } - Err(e) => { - info!(self.log, "Handshake failed: runner could not restart"; "error" => ?e); - Err(e.into()) - } - } - } - - pub async fn download_build(&mut self, task_id: &str) -> Result<(), RecorderProtoError> { - info!(self.log, "Requesting download of build from task"; "task_id" => task_id); - self.send(DownloadBuild { - task_id: task_id.into(), - }) - .await?; - - loop { - let DownloadBuildReply { result } = self.recv().await?; - - match result { - Ok(DownloadStatus::Downloading) => { - info!(self.log, "Downloading build ..."); - } - - Ok(DownloadStatus::Downloaded) => { - info!(self.log, "Build download complete; extracting build ..."); - } - - Ok(DownloadStatus::Extracted) => { - info!(self.log, "Build extracted"); - return Ok(()); - } - - Err(e) => { - error!(self.log, "Build download failed"; "task_id" => task_id, "error" => ?e); - return Err(e.into()); - } - } - } - } - - /// Send the profile at the given path to the runner. - /// - /// If the profile path is specified, the profile must exist, or this function will panic. - pub async fn send_profile( - &mut self, - profile_path: Option<&Path>, - ) -> Result<(), RecorderProtoError> { - let profile_path = match profile_path { - Some(profile_path) => profile_path, - - None => { - info!(self.log, "No profile to send"); - - self.send(SendProfile { profile_size: None }).await?; - let SendProfileReply { result } = self.recv().await?; - - return match result? { - Some(unexpected) => Err(RecorderProtoError::SendProfileMismatch { - expected: None, - received: Some(unexpected), - } - .into()), - - None => Ok(()), - }; - } - }; - - assert!(profile_path.exists()); - let profile_size = tokio::fs::metadata(profile_path).await?.len(); - - self.send(SendProfile { - profile_size: Some(profile_size), - }) - .await?; - - let SendProfileReply { result } = self.recv().await?; - - match result? { - Some(DownloadStatus::Downloading) => { - info!(self.log, "Sending profile"; "profile_size" => profile_size); - } - - unexpected => { - return Err(RecorderProtoError::SendProfileMismatch { - received: unexpected, - expected: Some(DownloadStatus::Downloading), - } - .into()) - } - } - - let mut stream = self.inner.take().unwrap().into_inner(); - let result = RecorderProto::send_profile_impl(&mut stream, profile_path).await; - self.inner = Some(Proto::new(stream)); - - result?; - - let mut state = DownloadStatus::Downloading; - loop { - let SendProfileReply { result } = self.recv().await?; - - match result? { - Some(next_state) => { - assert_ne!(state, DownloadStatus::Extracted); - let expected = state.next().unwrap(); - - if expected != next_state { - return Err(RecorderProtoError::SendProfileMismatch { - received: Some(next_state), - expected: Some(expected), - } - .into()); - } - - state = next_state; - - match state { - // This would be caught above because this is never an expected state. - DownloadStatus::Downloading => unreachable!(), - - DownloadStatus::Downloaded => { - info!(self.log, "Profile sent; extracting..."); - } - - DownloadStatus::Extracted => { - info!(self.log, "Profile extracted"); - break; - } - } - } - - None => { - return Err(RecorderProtoError::SendProfileMismatch { - received: None, - expected: state.next(), - } - .into()) - } - } - } - - assert!(state == DownloadStatus::Extracted); - - Ok(()) - } - - async fn send_profile_impl( - stream: &mut TcpStream, - profile_path: &Path, - ) -> Result<(), RecorderProtoError> { - let mut f = File::open(profile_path).await?; - - tokio::io::copy(&mut f, stream) - .await - .map_err(Into::into) - .map(drop) - } - - /// Send the preferences that the runner should use. - pub async fn send_prefs( - &mut self, - prefs: Vec<(String, PrefValue)>, - ) -> Result<(), RecorderProtoError> { - info!(self.log, "Sending prefs ..."); - self.send(SendPrefs { prefs }).await?; - let SendPrefsReply { result } = self.recv().await?; - - if let Err(e) = result { - error!(self.log, "Could not send prefs"; "error" => ?e); - return Err(e.into()); - } - - info!(self.log, "Prefs sent"); - - Ok(()) - } - - pub async fn wait_for_idle(&mut self) -> Result<(), RecorderProtoError> { - info!(self.log, "Waiting for runner to become idle..."); - self.send(WaitForIdle).await?; - - let WaitForIdleReply { result } = self.recv().await?; - - if let Err(e) = result { - error!(self.log, "Runner did not go idle"; "error" => %e); - Err(e.into()) - } else { - info!(self.log, "Runner is now idle"); - Ok(()) - } - } } #[derive(Debug, Display)] @@ -264,13 +225,13 @@ pub enum RecorderProtoError { Proto(ProtoError), #[display( - fmt = "Expected a download status of `{:?}', but received `{:?}' instead", + fmt = "Expected a download status of `{}', but received `{}' instead", expected, received )] - SendProfileMismatch { - expected: Option, - received: Option, + RecvProfileMismatch { + expected: DownloadStatus, + received: DownloadStatus, }, } @@ -278,7 +239,7 @@ impl Error for RecorderProtoError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { RecorderProtoError::Proto(ref e) => Some(e), - RecorderProtoError::SendProfileMismatch { .. } => None, + RecorderProtoError::RecvProfileMismatch { .. } => None, } } } diff --git a/fxrunner/src/bin/main.rs b/fxrunner/src/bin/main.rs index 9f59619..42e2ec1 100644 --- a/fxrunner/src/bin/main.rs +++ b/fxrunner/src/bin/main.rs @@ -13,8 +13,6 @@ use libfxrunner::proto::RunnerProto; use libfxrunner::taskcluster::Taskcluster; use slog::{info, Logger}; use structopt::StructOpt; -use tempfile::TempDir; -use tokio::fs::create_dir_all; use tokio::net::TcpListener; use tokio::time::delay_for; @@ -79,31 +77,9 @@ async fn fxrunner(log: Logger, options: Options, config: Config) -> Result<(), B WindowsPerfProvider::default(), ); - if proto.handshake_reply().await? { + if proto.handle_request().await? { break; } - // We download everything into a temporary directory that will be - // cleaned up after the connection closes. - let download_dir = TempDir::new()?; - let firefox_bin = proto.download_build_reply(download_dir.path()).await?; - assert!(firefox_bin.is_file()); - - let profile_path = match proto.send_profile_reply(download_dir.path()).await? { - Some(profile_path) => profile_path, - None => { - let profile_path = download_dir.path().join("profile"); - info!(log, "Creating new empty profile"); - create_dir_all(&profile_path).await?; - profile_path - } - }; - assert!(profile_path.is_dir()); - - proto - .send_prefs_reply(&profile_path.join("user.js")) - .await?; - - proto.wait_for_idle_reply().await?; info!(log, "Client disconnected"); } diff --git a/fxrunner/src/lib/proto.rs b/fxrunner/src/lib/proto.rs index 735b24b..47ad8cb 100644 --- a/fxrunner/src/lib/proto.rs +++ b/fxrunner/src/lib/proto.rs @@ -11,7 +11,8 @@ use libfxrecord::error::ErrorExt; use libfxrecord::net::*; use libfxrecord::prefs::write_prefs; use slog::{error, info, Logger}; -use tokio::fs::{File, OpenOptions}; +use tempfile::TempDir; +use tokio::fs::{create_dir_all, File, OpenOptions}; use tokio::net::TcpStream; use tokio::prelude::*; use tokio::task::spawn_blocking; @@ -50,11 +51,282 @@ where } } - /// Consume the RunnerProto and return the underlying `Proto`. - pub fn into_inner( - self, - ) -> Proto { - self.inner.unwrap() + /// Handle a request from the recorder. + pub async fn handle_request(&mut self) -> Result> { + match self.recv::().await?.request { + RecorderRequest::NewRequest(req) => { + self.handle_new_request(req).await?; + Ok(true) + } + + RecorderRequest::ResumeRequest(req) => { + self.handle_resume_request(req).await?; + Ok(false) + } + } + } + + /// Handle a new request from the recorder. + async fn handle_new_request( + &mut self, + request: NewRequest, + ) -> Result<(), RunnerProtoError> { + let download_dir = TempDir::new()?; + + let firefox_bin = self + .download_build(&request.build_task_id, download_dir.path()) + .await?; + assert!(firefox_bin.is_file()); + + let profile_path = match request.profile_size { + Some(profile_size) => self.recv_profile(profile_size, download_dir.path()).await?, + None => { + let profile_path = download_dir.path().join("profile"); + info!(self.log, "Creating new empty profile"); + create_dir_all(&profile_path).await?; + profile_path + } + }; + assert!(profile_path.is_dir()); + + if request.prefs.len() > 0 { + let prefs_path = profile_path.join("user.js"); + let mut f = match OpenOptions::new() + .append(true) + .create(true) + .open(&prefs_path) + .await + { + Ok(f) => f, + Err(e) => { + self.send(WritePrefs { + result: Err(e.into_error_message()), + }) + .await?; + + return Err(e.into()); + } + }; + + if let Err(e) = write_prefs(&mut f, request.prefs.into_iter()).await { + self.send(WritePrefs { + result: Err(e.into_error_message()), + }) + .await?; + return Err(e.into()); + } + } + + self.send(WritePrefs { result: Ok(()) }).await?; + + // TODO: Persist the profile and Firefox instance for a restart + + if let Err(e) = self + .shutdown_handler + .initiate_restart("fxrunner: restarting for cold Firefox start") + { + // TODO: Once we persist firefox and profile, we need + error!(self.log, "Could not restart"; "error" => ?e); + self.send(Restarting { + result: Err(e.into_error_message()), + }) + .await?; + + return Err(RunnerProtoError::Shutdown(e)); + } + + self.send(Restarting { result: Ok(()) }).await?; + + Ok(()) + } + + /// Handle a resume request from the runner. + async fn handle_resume_request( + &mut self, + _request: ResumeRequest, + ) -> Result<(), RunnerProtoError> { + info!(self.log, "Received resumption request"); + + self.send(ResumeResponse { result: Ok(()) }).await?; + + info!(self.log, "Waiting to become idle"); + + if let Err(e) = cpu_and_disk_idle(&self.perf_provider).await { + error!(self.log, "CPU and disk did not become idle"; "error" => %e); + self.send(WaitForIdle { + result: Err(e.into_error_message()), + }) + .await?; + + return Err(RunnerProtoError::WaitForIdle(e)); + } + info!(self.log, "Became idle"); + + self.send(WaitForIdle { result: Ok(()) }).await?; + + Ok(()) + } + + /// Download a build from taskcluster. + async fn download_build( + &mut self, + task_id: &str, + download_dir: &Path, + ) -> Result> { + info!(self.log, "Download build from Taskcluster"; "task_id" => &task_id); + self.send(DownloadBuild { + result: Ok(DownloadStatus::Downloading), + }) + .await?; + + let download_path = match self.tc.download_build_artifact(task_id, download_dir).await { + Ok(download_path) => download_path, + Err(e) => { + error!(self.log, "Could not download build"; "error" => ?e); + self.send(DownloadBuild { + result: Err(e.into_error_message()), + }) + .await?; + return Err(e.into()); + } + }; + + self.send(DownloadBuild { + result: Ok(DownloadStatus::Downloaded), + }) + .await?; + info!(self.log, "Extracting downloaded artifact..."); + + let unzip_result = spawn_blocking({ + let download_dir = PathBuf::from(download_dir); + move || unzip(&download_path, &download_dir) + }) + .await + .expect("unzip task was cancelled or panicked"); + + if let Err(e) = unzip_result { + self.send(DownloadBuild { + result: Err(e.into_error_message()), + }) + .await?; + return Err(e.into()); + } + + let firefox_path = download_dir.join("firefox").join("firefox.exe"); + if !firefox_path.exists() { + let err = RunnerProtoError::MissingFirefox; + + self.send(DownloadBuild { + result: Err(err.into_error_message()), + }) + .await?; + + return Err(err); + } + + info!(self.log, "Extracted build"); + self.send(DownloadBuild { + result: Ok(DownloadStatus::Extracted), + }) + .await?; + Ok(firefox_path) + } + + /// Receive a profile from the recorder. + async fn recv_profile( + &mut self, + profile_size: u64, + download_dir: &Path, + ) -> Result> { + info!(self.log, "Receiving profile..."); + self.send(RecvProfile { + result: Ok(DownloadStatus::Downloading), + }) + .await?; + + let mut stream = self.inner.take().unwrap().into_inner(); + let result = Self::recv_profile_raw(&mut stream, download_dir, profile_size).await; + self.inner = Some(Proto::new(stream)); + + let zip_path = match result { + Ok(zip_path) => zip_path, + Err(e) => { + self.send(DownloadBuild { + result: Err(e.into_error_message()), + }) + .await?; + return Err(e.into()); + } + }; + + info!(self.log, "Profile received; extracting..."); + self.send(RecvProfile { + result: Ok(DownloadStatus::Downloaded), + }) + .await?; + + let unzip_path = download_dir.join("profile"); + + let unzip_result = spawn_blocking({ + let zip_path = zip_path.clone(); + let unzip_path = unzip_path.clone(); + move || unzip(&zip_path, &unzip_path) + }) + .await + .expect("unzip profile task was cancelled or panicked"); + + let stats = match unzip_result { + Ok(stats) => stats, + Err(e) => { + error!(self.log, "Could not extract profile"; "error" => ?e); + + self.send(RecvProfile { + result: Err(e.into_error_message()), + }) + .await?; + + return Err(e.into()); + } + }; + + if stats.extracted == 0 { + error!(self.log, "Profile was empty"); + let e = RunnerProtoError::EmptyProfile; + self.send(RecvProfile { + result: Err(e.into_error_message()), + }) + .await?; + + return Err(e); + } + + info!(self.log, "Profile extracted"); + + let profile_dir = match stats.top_level_dir { + Some(top_level_dir) => unzip_path.join(top_level_dir), + None => unzip_path, + }; + + self.send(RecvProfile { + result: { Ok(DownloadStatus::Extracted) }, + }) + .await?; + + Ok(profile_dir) + } + + /// Receive the raw bytes of a profile from the recorder. + async fn recv_profile_raw( + stream: &mut TcpStream, + download_dir: &Path, + profile_size: u64, + ) -> Result> { + let zip_path = download_dir.join("profile.zip"); + let mut f = File::create(&zip_path).await?; + + tokio::io::copy(&mut stream.take(profile_size), &mut f).await?; + + Ok(zip_path) } /// Send the given message to the runner. @@ -76,285 +348,6 @@ where { self.inner.as_mut().unwrap().recv::().await } - - /// Handshake with FxRecorder. - pub async fn handshake_reply(&mut self) -> Result> { - info!(self.log, "Handshaking ..."); - let Handshake { restart } = self.recv().await?; - - if restart { - if let Err(e) = self - .shutdown_handler - .initiate_restart("fxrecord: recorder requested restart") - { - error!(self.log, "an error occurred while handshaking"; "error" => ?e); - self.send(HandshakeReply { - result: Err(e.into_error_message()), - }) - .await?; - - return Err(RunnerProtoError::Shutdown(e)); - } - info!(self.log, "Restart requested; restarting ..."); - } - - self.send(HandshakeReply { result: Ok(()) }).await?; - info!(self.log, "Handshake complete"); - - Ok(restart) - } - - pub async fn download_build_reply( - &mut self, - download_dir: &Path, - ) -> Result> { - let DownloadBuild { task_id } = self.recv().await?; - - info!(self.log, "Received build download request"; "task_id" => &task_id); - - self.send(DownloadBuildReply { - result: Ok(DownloadStatus::Downloading), - }) - .await?; - - match self - .tc - .download_build_artifact(&task_id, download_dir) - .await - { - Ok(download_path) => { - self.send(DownloadBuildReply { - result: Ok(DownloadStatus::Downloaded), - }) - .await?; - - let unzip_result = spawn_blocking({ - let download_dir = PathBuf::from(download_dir); - move || unzip(&download_path, &download_dir) - }) - .await - .expect("unzip task was cancelled or panicked"); - - if let Err(e) = unzip_result { - self.send(DownloadBuildReply { - result: Err(e.into_error_message()), - }) - .await?; - - Err(e.into()) - } else { - let firefox_path = download_dir.join("firefox").join("firefox.exe"); - - if !firefox_path.exists() { - let err = RunnerProtoError::MissingFirefox; - self.send(DownloadBuildReply { - result: Err(err.into_error_message()), - }) - .await?; - - Err(err) - } else { - self.send(DownloadBuildReply { - result: Ok(DownloadStatus::Extracted), - }) - .await?; - - Ok(firefox_path) - } - } - } - - Err(e) => { - error!(self.log, "could not download build"; "error" => ?e); - self.send(DownloadBuildReply { - result: Err(e.into_error_message()), - }) - .await?; - Err(e.into()) - } - } - } - - pub async fn send_profile_reply( - &mut self, - download_dir: &Path, - ) -> Result, RunnerProtoError> { - info!(self.log, "Waiting for profile..."); - - let SendProfile { profile_size } = self.recv().await?; - - let profile_size = match profile_size { - Some(profile_size) => profile_size, - None => { - info!(self.log, "No profile provided"); - self.send(SendProfileReply { result: Ok(None) }).await?; - - return Ok(None); - } - }; - - info!(self.log, "Receiving profile..."); - self.send(SendProfileReply { - result: Ok(Some(DownloadStatus::Downloading)), - }) - .await?; - - let mut stream = self.inner.take().unwrap().into_inner(); - let result = Self::send_profile_reply_impl( - &mut stream, - download_dir, - profile_size, - ) - .await; - self.inner = Some(Proto::new(stream)); - - info!(self.log, "Profile received; extracting..."); - - let zip_path = match result { - Ok(zip_path) => { - self.send(SendProfileReply { - result: { Ok(Some(DownloadStatus::Downloaded)) }, - }) - .await?; - zip_path - } - - Err(e) => { - self.send(SendProfileReply { - result: { Err(e.into_error_message()) }, - }) - .await?; - return Err(e); - } - }; - - let unzip_path = download_dir.join("profile"); - - let unzip_result = spawn_blocking({ - let zip_path = zip_path.clone(); - let unzip_path = unzip_path.clone(); - move || unzip(&zip_path, &unzip_path) - }) - .await - .expect("unzip profile task was cancelled or panicked"); - - let stats = match unzip_result { - Ok(stats) => stats, - Err(e) => { - error!(self.log, "Could not extract profile"; "error" => ?e); - - self.send(SendProfileReply { - result: Err(e.into_error_message()), - }) - .await?; - - return Err(e.into()); - } - }; - - if stats.extracted == 0 { - error!(self.log, "Profile was empty!"); - let e = RunnerProtoError::EmptyProfile; - self.send(SendProfileReply { - result: Err(e.into_error_message()), - }) - .await?; - - return Err(e); - } - - error!(self.log, "Profile extracted"); - - let profile_dir = match stats.top_level_dir { - Some(top_level_dir) => unzip_path.join(top_level_dir), - None => unzip_path, - }; - - self.send(SendProfileReply { - result: { Ok(Some(DownloadStatus::Extracted)) }, - }) - .await?; - - Ok(Some(profile_dir)) - } - - async fn send_profile_reply_impl( - stream: &mut TcpStream, - download_dir: &Path, - profile_size: u64, - ) -> Result> { - let zip_path = download_dir.join("profile.zip"); - let mut f = File::create(&zip_path).await?; - - tokio::io::copy(&mut stream.take(profile_size), &mut f).await?; - - Ok(zip_path) - } - - pub async fn send_prefs_reply( - &mut self, - prefs_path: &Path, - ) -> Result<(), RunnerProtoError> { - let SendPrefs { prefs } = self.recv().await?; - - if prefs.is_empty() { - return self - .send(SendPrefsReply { result: Ok(()) }) - .await - .map_err(Into::into); - } - - let mut f = match OpenOptions::new() - .append(true) - .create(true) - .open(&prefs_path) - .await - { - Ok(f) => f, - Err(e) => { - self.send(SendPrefsReply { - result: Err(e.into_error_message()), - }) - .await?; - return Err(e.into()); - } - }; - - match write_prefs(&mut f, prefs.into_iter()).await { - Ok(()) => { - self.send(SendPrefsReply { result: Ok(()) }).await?; - Ok(()) - } - Err(e) => { - self.send(SendPrefsReply { - result: Err(e.into_error_message()), - }) - .await?; - Err(e.into()) - } - } - } - - pub async fn wait_for_idle_reply(&mut self) -> Result<(), RunnerProtoError> { - self.recv::().await?; - - info!(self.log, "Waiting for CPU and disk to become idle..."); - - if let Err(e) = cpu_and_disk_idle(&self.perf_provider).await { - error!(self.log, "CPU and disk did not become idle"; "error" => %e); - self.send(WaitForIdleReply { - result: Err(e.into_error_message()), - }) - .await?; - - return Err(RunnerProtoError::WaitForIdle(e)); - } else { - self.send(WaitForIdleReply { result: Ok(()) }).await?; - } - - info!(self.log, "Did become idle"); - Ok(()) - } } #[derive(Debug, Display)] diff --git a/libfxrecord/src/net/message.rs b/libfxrecord/src/net/message.rs index 5b356de..57e299a 100644 --- a/libfxrecord/src/net/message.rs +++ b/libfxrecord/src/net/message.rs @@ -110,7 +110,7 @@ where fn kind() -> K; } -/// An error that occurs when attempting to extract a message variant.. +/// An error that occurs when attempting to extract a message variant. #[derive(Debug, Display)] #[display( fmt = "could not convert message of kind `{}' to kind `{}'", @@ -308,6 +308,7 @@ macro_rules! impl_message { type Error = KindMismatch<$kind_ty>; fn try_from(msg: $msg_ty) -> Result { + #[allow(irrefutable_let_patterns)] if let $msg_ty::$inner_ty(msg) = msg { Ok(msg) } else { @@ -328,6 +329,54 @@ macro_rules! impl_message { }; } +/// A request from the recorder to the runner. +#[derive(Debug, Deserialize, Serialize)] +pub enum RecorderRequest { + /// A new request. + /// + /// If successful, the runner will restart and the recorder should send a + /// [`ResumeRequest`](enum.RecorderRequest.html#variant.ResumeRequest) + /// upon reconnection. + NewRequest(NewRequest), + + /// A request to resume a [previous + /// request](enum.RecorderRequest.html#variant.NewRequest). + ResumeRequest(ResumeRequest), +} + +impl From for Request { + fn from(req: NewRequest) -> Request { + Request { + request: RecorderRequest::NewRequest(req), + } + } +} + +impl From for Request { + fn from(req: ResumeRequest) -> Request { + Request { + request: RecorderRequest::ResumeRequest(req), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NewRequest { + /// The task ID of the Taskcluster build task. + /// + /// The build artifact from this task will be downloaded by the runner. + pub build_task_id: String, + + /// The size of the profile that will be sent, if any. + pub profile_size: Option, + + /// Prefs to override in the profile. + pub prefs: Vec<(String, PrefValue)>, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ResumeRequest {} + impl_message! { /// A message from FxRecorder to FxRunner. RecorderMessage, @@ -335,35 +384,13 @@ impl_message! { /// The kind of a [`RecorderMessage`](struct.RecorderMessage.html). RecorderMessageKind; - /// A handshake from FxRecorder to FxRunner. - Handshake { - /// Whether or not the runner should restart. - restart: bool, + /// A request from the recorder to the runner. + Request { + request: RecorderRequest, }; - - /// A request to download a specific build of Firefox. - DownloadBuild { - /// The build task ID. - task_id: String, - }; - - /// A request to send a profile of the given size. - /// - /// A size of zero indicates that there is no profile. - SendProfile { - profile_size: Option, - }; - - /// A request for the runner to use the provided prefs. - SendPrefs { - prefs: Vec<(String, PrefValue)>, - }; - - /// A request for the runner to wait for its CPU and disk to become idle. - WaitForIdle; } -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Display, Eq, PartialEq, Serialize, Deserialize)] pub enum DownloadStatus { Downloading, Downloaded, @@ -381,6 +408,8 @@ impl DownloadStatus { } } +pub type ForeignResult = Result>; + impl_message! { /// A message from FxRunner to FxRecorder. RunnerMessage, @@ -388,32 +417,33 @@ impl_message! { /// The kind of a [`RunnerMessage`](struct.RunnerMessage.html). RunnerMessageKind; - /// A reply to a [`Handshake`](struct.Handshake.html) from FxRecorder. - HandshakeReply { - result: Result<(), ErrorMessage>, + /// The status of the DownloadBuild phase. + DownloadBuild { + result: ForeignResult, }; - /// A reply to a [`DownloadBuild`](struct.DownloadBuild.html) message from - /// FxRecorder. - DownloadBuildReply { - result: Result>, + /// The status of the RecvProfile phase. + RecvProfile { + result: ForeignResult, }; - /// A reply to a [`SendProfile`](struct.SendProfile.html) message from - /// FxRecorder. - SendProfileReply { - result: Result, ErrorMessage>, + /// The status of the WritePrefs phase. + WritePrefs { + result: ForeignResult<()>, }; - /// A reply to a [`SendPrefs`](struct.SendPrefs.html) message from - /// FxRecorder. - SendPrefsReply { - result: Result<(), ErrorMessage>, + /// The status of the Restarting phase. + Restarting { + result: ForeignResult<()>, }; - /// A reply to a [`WaitForIdle`](struct.WaitForIdle.html) message from - /// FxRecorder. - WaitForIdleReply { - result: Result<(), ErrorMessage>, + /// The status of the ResumeResponse phase. + ResumeResponse { + result: ForeignResult<()>, + }; + + /// The status of the WaitForIdle phase. + WaitForIdle { + result: ForeignResult<()>, }; }