| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971 |
- #!/usr/bin/env perl
- # Copyright (C) 2007,2008,2009,2010,2011,2012,2013,2014 Ole Tange and
- # Free Software Foundation, Inc.
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful, but
- # WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- # General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, see <http://www.gnu.org/licenses/>
- # or write to the Free Software Foundation, Inc., 51 Franklin St,
- # Fifth Floor, Boston, MA 02110-1301 USA
- # open3 used in Job::start
- use IPC::Open3;
- # &WNOHANG used in reaper
- use POSIX qw(:sys_wait_h setsid ceil :errno_h);
- # gensym used in Job::start
- use Symbol qw(gensym);
- # tempfile used in Job::start
- use File::Temp qw(tempfile tempdir);
- # mkpath used in openresultsfile
- use File::Path;
- # GetOptions used in get_options_from_array
- use Getopt::Long;
- # Used to ensure code quality
- use strict;
- use File::Basename;
- if(not $ENV{HOME}) {
- # $ENV{HOME} is sometimes not set if called from PHP
- ::warning("\$HOME not set. Using /tmp\n");
- $ENV{HOME} = "/tmp";
- }
- save_stdin_stdout_stderr();
- save_original_signal_handler();
- parse_options();
- ::debug("init", "Open file descriptors: ", join(" ",keys %Global::fd), "\n");
- my $number_of_args;
- if($Global::max_number_of_args) {
- $number_of_args=$Global::max_number_of_args;
- } elsif ($opt::X or $opt::m or $opt::xargs) {
- $number_of_args = undef;
- } else {
- $number_of_args = 1;
- }
- my @command;
- @command = @ARGV;
- my @fhlist;
- if($opt::pipepart) {
- @fhlist = map { open_or_exit($_) } "/dev/null";
- } else {
- @fhlist = map { open_or_exit($_) } @opt::a;
- if(not @fhlist and not $opt::pipe) {
- @fhlist = (*STDIN);
- }
- }
- if($opt::skip_first_line) {
- # Skip the first line for the first file handle
- my $fh = $fhlist[0];
- <$fh>;
- }
- if($opt::header and not $opt::pipe) {
- my $fh = $fhlist[0];
- # split with colsep or \t
- # $header force $colsep = \t if undef?
- my $delimiter = $opt::colsep;
- $delimiter ||= "\$";
- my $id = 1;
- for my $fh (@fhlist) {
- my $line = <$fh>;
- chomp($line);
- ::debug("init", "Delimiter: '$delimiter'");
- for my $s (split /$delimiter/o, $line) {
- ::debug("init", "Colname: '$s'");
- # Replace {colname} with {2}
- # TODO accept configurable short hands
- # TODO how to deal with headers in {=...=}
- for(@command) {
- s:\{$s(|/|//|\.|/\.)\}:\{$id$1\}:g;
- }
- $Global::input_source_header{$id} = $s;
- $id++;
- }
- }
- } else {
- my $id = 1;
- for my $fh (@fhlist) {
- $Global::input_source_header{$id} = $id;
- $id++;
- }
- }
- if($opt::filter_hosts and (@opt::sshlogin or @opt::sshloginfile)) {
- # Parallel check all hosts are up. Remove hosts that are down
- filter_hosts();
- }
- if($opt::nonall or $opt::onall) {
- onall(@command);
- wait_and_exit(min(undef_as_zero($Global::exitstatus),254));
- }
- # TODO --transfer foo/./bar --cleanup
- # multiple --transfer and --basefile with different /./
- $Global::JobQueue = JobQueue->new(
- \@command,\@fhlist,$Global::ContextReplace,$number_of_args,\@Global::ret_files);
- if($opt::eta or $opt::bar) {
- # Count the number of jobs before starting any
- $Global::JobQueue->total_jobs();
- }
- if($opt::pipepart) {
- @Global::cat_partials = map { pipe_part_files($_) } @opt::a;
- # Unget the command as many times as there are parts
- $Global::JobQueue->{'commandlinequeue'}->unget(
- map { $Global::JobQueue->{'commandlinequeue'}->get() } @Global::cat_partials
- );
- }
- for my $sshlogin (values %Global::host) {
- $sshlogin->max_jobs_running();
- }
- init_run_jobs();
- my $sem;
- if($Global::semaphore) {
- $sem = acquire_semaphore();
- }
- $SIG{TERM} = \&start_no_new_jobs;
- start_more_jobs();
- if(not $opt::pipepart) {
- if($opt::pipe) {
- spreadstdin();
- }
- }
- ::debug("init", "Start draining\n");
- drain_job_queue();
- ::debug("init", "Done draining\n");
- reaper();
- ::debug("init", "Done reaping\n");
- if($opt::pipe and @opt::a) {
- for my $job (@Global::tee_jobs) {
- unlink $job->fh(2,"name");
- $job->set_fh(2,"name","");
- $job->print();
- unlink $job->fh(1,"name");
- }
- }
- ::debug("init", "Cleaning\n");
- cleanup();
- if($Global::semaphore) {
- $sem->release();
- }
- for(keys %Global::sshmaster) {
- kill "TERM", $_;
- }
- ::debug("init", "Halt\n");
- if($opt::halt_on_error) {
- wait_and_exit($Global::halt_on_error_exitstatus);
- } else {
- wait_and_exit(min(undef_as_zero($Global::exitstatus),254));
- }
- sub __PIPE_MODE__ {}
- sub pipe_part_files {
- # Input:
- # $file = the file to read
- # Returns:
- # @commands that will cat_partial each part
- my ($file) = @_;
- my $buf = "";
- my $header = find_header(\$buf,open_or_exit($file));
- # find positions
- my @pos = find_split_positions($file,$opt::blocksize,length $header);
- # Make @cat_partials
- my @cat_partials = ();
- for(my $i=0; $i<$#pos; $i++) {
- push @cat_partials, cat_partial($file, 0, length($header), $pos[$i], $pos[$i+1]);
- }
- # Remote exec should look like:
- # ssh -oLogLevel=quiet lo 'eval `echo $SHELL | grep "/t\{0,1\}csh" > /dev/null && echo setenv PARALLEL_SEQ '$PARALLEL_SEQ'\; setenv PARALLEL_PID '$PARALLEL_PID' || echo PARALLEL_SEQ='$PARALLEL_SEQ'\;export PARALLEL_SEQ\; PARALLEL_PID='$PARALLEL_PID'\;export PARALLEL_PID` ;' tty\ \>/dev/null\ \&\&\ stty\ isig\ -onlcr\ -echo\;echo\ \$SHELL\ \|\ grep\ \"/t\\\{0,1\\\}csh\"\ \>\ /dev/null\ \&\&\ setenv\ FOO\ /tmp/foo\ \|\|\ export\ FOO=/tmp/foo\; \(wc\ -\ \$FOO\)
- # ssh -tt not allowed. Remote will die due to broken pipe anyway.
- # TODO test remote with --fifo / --cat
- return @cat_partials;
- }
- sub find_header {
- # Input:
- # $buf_ref = reference to read-in buffer
- # $fh = filehandle to read from
- # Uses:
- # $opt::header
- # $opt::blocksize
- # Returns:
- # $header string
- my ($buf_ref, $fh) = @_;
- my $header = "";
- if($opt::header) {
- if($opt::header eq ":") { $opt::header = "(.*\n)"; }
- # Number = number of lines
- $opt::header =~ s/^(\d+)$/"(.*\n)"x$1/e;
- while(read($fh,substr($$buf_ref,length $$buf_ref,0),$opt::blocksize)) {
- if($$buf_ref=~s/^($opt::header)//) {
- $header = $1;
- last;
- }
- }
- }
- return $header;
- }
- sub find_split_positions {
- # Input:
- # $file = the file to read
- # $block = (minimal) --block-size of each chunk
- # $headerlen = length of header to be skipped
- # Uses:
- # $opt::recstart
- # $opt::recend
- # Returns:
- # @positions of block start/end
- my($file, $block, $headerlen) = @_;
- my $size = -s $file;
- $block = int $block;
- # The optimal dd blocksize for mint, redhat, solaris, openbsd = 2^17..2^20
- # The optimal dd blocksize for freebsd = 2^15..2^17
- my $dd_block_size = 131072; # 2^17
- my @pos;
- my ($recstart,$recend) = recstartrecend();
- my $recendrecstart = $recend.$recstart;
- my $fh = ::open_or_exit($file);
- push(@pos,$headerlen);
- for(my $pos = $block+$headerlen; $pos < $size; $pos += $block) {
- my $buf;
- seek($fh, $pos, 0) || die;
- while(read($fh,substr($buf,length $buf,0),$dd_block_size)) {
- if($opt::regexp) {
- # If match /$recend$recstart/ => Record position
- if($buf =~ /(.*$recend)$recstart/os) {
- my $i = length($1);
- push(@pos,$pos+$i);
- # Start looking for next record _after_ this match
- $pos += $i;
- last;
- }
- } else {
- # If match $recend$recstart => Record position
- my $i = index($buf,$recendrecstart);
- if($i != -1) {
- push(@pos,$pos+$i);
- # Start looking for next record _after_ this match
- $pos += $i;
- last;
- }
- }
- }
- }
- push(@pos,$size);
- close $fh;
- return @pos;
- }
- sub cat_partial {
- # Input:
- # $file = the file to read
- # ($start, $end, [$start2, $end2, ...]) = start byte, end byte
- # Returns:
- # Efficient perl command to copy $start..$end, $start2..$end2, ... to stdout
- my($file, @start_end) = @_;
- my($start, $i);
- # Convert start_end to start_len
- my @start_len = map { if(++$i % 2) { $start = $_; } else { $_-$start } } @start_end;
- return "<". shell_quote_scalar($file) .
- q{ perl -e 'while(@ARGV) { sysseek(STDIN,shift,0) || die; $left = shift; while($read = sysread(STDIN,$buf, ($left > 32768 ? 32768 : $left))){ $left -= $read; syswrite(STDOUT,$buf); } }' } .
- " @start_len";
- }
- sub spreadstdin {
- # read a record
- # Spawn a job and print the record to it.
- # Uses:
- # $opt::blocksize
- # STDIN
- # $opr::r
- # $Global::max_lines
- # $Global::max_number_of_args
- # $opt::regexp
- # $Global::start_no_new_jobs
- # $opt::roundrobin
- # %Global::running
- my $buf = "";
- my ($recstart,$recend) = recstartrecend();
- my $recendrecstart = $recend.$recstart;
- my $chunk_number = 1;
- my $one_time_through;
- my $blocksize = $opt::blocksize;
- my $in = *STDIN;
- my $header = find_header(\$buf,$in);
- while(1) {
- my $anything_written = 0;
- if(not read($in,substr($buf,length $buf,0),$blocksize)) {
- # End-of-file
- $chunk_number != 1 and last;
- # Force the while-loop once if everything was read by header reading
- $one_time_through++ and last;
- }
- if($opt::r) {
- # Remove empty lines
- $buf =~ s/^\s*\n//gm;
- if(length $buf == 0) {
- next;
- }
- }
- if($Global::max_lines and not $Global::max_number_of_args) {
- # Read n-line records
- my $n_lines = $buf =~ tr/\n/\n/;
- my $last_newline_pos = rindex($buf,"\n");
- while($n_lines % $Global::max_lines) {
- $n_lines--;
- $last_newline_pos = rindex($buf,"\n",$last_newline_pos-1);
- }
- # Chop at $last_newline_pos as that is where n-line record ends
- $anything_written +=
- write_record_to_pipe($chunk_number++,\$header,\$buf,
- $recstart,$recend,$last_newline_pos+1);
- substr($buf,0,$last_newline_pos+1) = "";
- } elsif($opt::regexp) {
- if($Global::max_number_of_args) {
- # -N => (start..*?end){n}
- # -L -N => (start..*?end){n*l}
- my $read_n_lines = $Global::max_number_of_args * ($Global::max_lines || 1);
- while($buf =~ s/((?:$recstart.*?$recend){$read_n_lines})($recstart.*)$/$2/os) {
- # Copy to modifiable variable
- my $b = $1;
- $anything_written +=
- write_record_to_pipe($chunk_number++,\$header,\$b,
- $recstart,$recend,length $1);
- }
- } else {
- # Find the last recend-recstart in $buf
- if($buf =~ s/(.*$recend)($recstart.*?)$/$2/os) {
- # Copy to modifiable variable
- my $b = $1;
- $anything_written +=
- write_record_to_pipe($chunk_number++,\$header,\$b,
- $recstart,$recend,length $1);
- }
- }
- } else {
- if($Global::max_number_of_args) {
- # -N => (start..*?end){n}
- my $i = 0;
- my $read_n_lines = $Global::max_number_of_args * ($Global::max_lines || 1);
- while(($i = nindex(\$buf,$recendrecstart,$read_n_lines)) != -1) {
- $i += length $recend; # find the actual splitting location
- $anything_written +=
- write_record_to_pipe($chunk_number++,\$header,\$buf,
- $recstart,$recend,$i);
- substr($buf,0,$i) = "";
- }
- } else {
- # Find the last recend-recstart in $buf
- my $i = rindex($buf,$recendrecstart);
- if($i != -1) {
- $i += length $recend; # find the actual splitting location
- $anything_written +=
- write_record_to_pipe($chunk_number++,\$header,\$buf,
- $recstart,$recend,$i);
- substr($buf,0,$i) = "";
- }
- }
- }
- if(not $anything_written and not eof($in)) {
- # Nothing was written - maybe the block size < record size?
- # Increase blocksize exponentially
- my $old_blocksize = $blocksize;
- $blocksize = ceil($blocksize * 1.3 + 1);
- ::warning("A record was longer than $old_blocksize. " .
- "Increasing to --blocksize $blocksize\n");
- }
- }
- ::debug("init", "Done reading input\n");
- # If there is anything left in the buffer write it
- substr($buf,0,0) = "";
- write_record_to_pipe($chunk_number++,\$header,\$buf,$recstart,$recend,length $buf);
- $Global::start_no_new_jobs ||= 1;
- if($opt::roundrobin) {
- for my $job (values %Global::running) {
- close $job->fh(0,"w");
- }
- my %incomplete_jobs = %Global::running;
- my $sleep = 1;
- while(keys %incomplete_jobs) {
- my $something_written = 0;
- for my $pid (keys %incomplete_jobs) {
- my $job = $incomplete_jobs{$pid};
- if($job->stdin_buffer_length()) {
- $something_written += $job->non_block_write();
- } else {
- delete $incomplete_jobs{$pid}
- }
- }
- if($something_written) {
- $sleep = $sleep/2+0.001;
- }
- $sleep = ::reap_usleep($sleep);
- }
- }
- }
- sub recstartrecend {
- # Uses:
- # $opt::recstart
- # $opt::recend
- # Returns:
- # $recstart,$recend with default values and regexp conversion
- my($recstart,$recend);
- if(defined($opt::recstart) and defined($opt::recend)) {
- # If both --recstart and --recend is given then both must match
- $recstart = $opt::recstart;
- $recend = $opt::recend;
- } elsif(defined($opt::recstart)) {
- # If --recstart is given it must match start of record
- $recstart = $opt::recstart;
- $recend = "";
- } elsif(defined($opt::recend)) {
- # If --recend is given then it must match end of record
- $recstart = "";
- $recend = $opt::recend;
- }
- if($opt::regexp) {
- # If $recstart/$recend contains '|' this should only apply to the regexp
- $recstart = "(?:".$recstart.")";
- $recend = "(?:".$recend.")";
- } else {
- # $recstart/$recend = printf strings (\n)
- $recstart =~ s/\\([0rnt\'\"\\])/"qq|\\$1|"/gee;
- $recend =~ s/\\([0rnt\'\"\\])/"qq|\\$1|"/gee;
- }
- return ($recstart,$recend);
- }
- sub nindex {
- # See if string is in buffer N times
- # Returns:
- # the position where the Nth copy is found
- my ($buf_ref, $str, $n) = @_;
- my $i = 0;
- for(1..$n) {
- $i = index($$buf_ref,$str,$i+1);
- if($i == -1) { last }
- }
- return $i;
- }
- {
- my @robin_queue;
- sub round_robin_write {
- # Input:
- # $header_ref = ref to $header string
- # $block_ref = ref to $block to be written
- # $recstart = record start string
- # $recend = record end string
- # $endpos = end position of $block
- # Uses:
- # %Global::running
- my ($header_ref,$block_ref,$recstart,$recend,$endpos) = @_;
- my $something_written = 0;
- my $block_passed = 0;
- my $sleep = 1;
- while(not $block_passed) {
- # Continue flushing existing buffers
- # until one is empty and a new block is passed
- # Make a queue to spread the blocks evenly
- if(not @robin_queue) {
- push @robin_queue, values %Global::running;
- }
- while(my $job = shift @robin_queue) {
- if($job->stdin_buffer_length() > 0) {
- $something_written += $job->non_block_write();
- } else {
- $job->set_stdin_buffer($header_ref,$block_ref,$endpos,$recstart,$recend);
- $block_passed = 1;
- $job->set_virgin(0);
- $something_written += $job->non_block_write();
- last;
- }
- }
- $sleep = ::reap_usleep($sleep);
- }
- return $something_written;
- }
- }
- sub write_record_to_pipe {
- # Fork then
- # Write record from pos 0 .. $endpos to pipe
- # Input:
- # $chunk_number = sequence number - to see if already run
- # $header_ref = reference to header string to prepend
- # $record_ref = reference to record to write
- # $recstart = start string of record
- # $recend = end string of record
- # $endpos = position in $record_ref where record ends
- # Uses:
- # $Global::job_already_run
- # $opt::roundrobin
- # @Global::virgin_jobs
- # Returns:
- # Number of chunks written (0 or 1)
- my ($chunk_number,$header_ref,$record_ref,$recstart,$recend,$endpos) = @_;
- if($endpos == 0) { return 0; }
- if(vec($Global::job_already_run,$chunk_number,1)) { return 1; }
- if($opt::roundrobin) {
- return round_robin_write($header_ref,$record_ref,$recstart,$recend,$endpos);
- }
- # If no virgin found, backoff
- my $sleep = 0.0001; # 0.01 ms - better performance on highend
- while(not @Global::virgin_jobs) {
- ::debug("pipe", "No virgin jobs");
- $sleep = ::reap_usleep($sleep);
- # Jobs may not be started because of loadavg
- # or too little time between each ssh login.
- start_more_jobs();
- }
- my $job = shift @Global::virgin_jobs;
- # Job is no longer virgin
- $job->set_virgin(0);
- if(fork()) {
- # Skip
- } else {
- # Chop of at $endpos as we do not know how many rec_sep will
- # be removed.
- substr($$record_ref,$endpos,length $$record_ref) = "";
- # Remove rec_sep
- if($opt::remove_rec_sep) {
- Job::remove_rec_sep($record_ref,$recstart,$recend);
- }
- $job->write($header_ref);
- $job->write($record_ref);
- close $job->fh(0,"w");
- exit(0);
- }
- close $job->fh(0,"w");
- return 1;
- }
- sub __SEM_MODE__ {}
- sub acquire_semaphore {
- # Acquires semaphore. If needed: spawns to the background
- # Uses:
- # @Global::host
- # Returns:
- # The semaphore to be released when jobs is complete
- $Global::host{':'} = SSHLogin->new(":");
- my $sem = Semaphore->new($Semaphore::name,$Global::host{':'}->max_jobs_running());
- $sem->acquire();
- if($Semaphore::fg) {
- # skip
- } else {
- # If run in the background, the PID will change
- # therefore release and re-acquire the semaphore
- $sem->release();
- if(fork()) {
- exit(0);
- } else {
- # child
- # Get a semaphore for this pid
- ::die_bug("Can't start a new session: $!") if setsid() == -1;
- $sem = Semaphore->new($Semaphore::name,$Global::host{':'}->max_jobs_running());
- $sem->acquire();
- }
- }
- return $sem;
- }
- sub __PARSE_OPTIONS__ {}
- sub options_hash {
- # Returns:
- # %hash = the GetOptions config
- return
- ("debug|D=s" => \$opt::D,
- "xargs" => \$opt::xargs,
- "m" => \$opt::m,
- "X" => \$opt::X,
- "v" => \@opt::v,
- "joblog=s" => \$opt::joblog,
- "results|result|res=s" => \$opt::results,
- "resume" => \$opt::resume,
- "resume-failed|resumefailed" => \$opt::resume_failed,
- "silent" => \$opt::silent,
- #"silent-error|silenterror" => \$opt::silent_error,
- "keep-order|keeporder|k" => \$opt::keeporder,
- "group" => \$opt::group,
- "g" => \$opt::retired,
- "ungroup|u" => \$opt::ungroup,
- "linebuffer|linebuffered|line-buffer|line-buffered" => \$opt::linebuffer,
- "tmux" => \$opt::tmux,
- "null|0" => \$opt::0,
- "quote|q" => \$opt::q,
- # Replacement strings
- "parens=s" => \$opt::parens,
- "rpl=s" => \@opt::rpl,
- "plus" => \$opt::plus,
- "I=s" => \$opt::I,
- "extensionreplace|er=s" => \$opt::U,
- "U=s" => \$opt::retired,
- "basenamereplace|bnr=s" => \$opt::basenamereplace,
- "dirnamereplace|dnr=s" => \$opt::dirnamereplace,
- "basenameextensionreplace|bner=s" => \$opt::basenameextensionreplace,
- "seqreplace=s" => \$opt::seqreplace,
- "slotreplace=s" => \$opt::slotreplace,
- "jobs|j=s" => \$opt::jobs,
- "delay=f" => \$opt::delay,
- "sshdelay=f" => \$opt::sshdelay,
- "load=s" => \$opt::load,
- "noswap" => \$opt::noswap,
- "max-line-length-allowed" => \$opt::max_line_length_allowed,
- "number-of-cpus" => \$opt::number_of_cpus,
- "number-of-cores" => \$opt::number_of_cores,
- "use-cpus-instead-of-cores" => \$opt::use_cpus_instead_of_cores,
- "shellquote|shell_quote|shell-quote" => \$opt::shellquote,
- "nice=i" => \$opt::nice,
- "timeout=s" => \$opt::timeout,
- "tag" => \$opt::tag,
- "tagstring|tag-string=s" => \$opt::tagstring,
- "onall" => \$opt::onall,
- "nonall" => \$opt::nonall,
- "filter-hosts|filterhosts|filter-host" => \$opt::filter_hosts,
- "sshlogin|S=s" => \@opt::sshlogin,
- "sshloginfile|slf=s" => \@opt::sshloginfile,
- "controlmaster|M" => \$opt::controlmaster,
- "return=s" => \@opt::return,
- "trc=s" => \@opt::trc,
- "transfer" => \$opt::transfer,
- "cleanup" => \$opt::cleanup,
- "basefile|bf=s" => \@opt::basefile,
- "B=s" => \$opt::retired,
- "ctrlc|ctrl-c" => \$opt::ctrlc,
- "noctrlc|no-ctrlc|no-ctrl-c" => \$opt::noctrlc,
- "workdir|work-dir|wd=s" => \$opt::workdir,
- "W=s" => \$opt::retired,
- "tmpdir=s" => \$opt::tmpdir,
- "tempdir=s" => \$opt::tmpdir,
- "use-compress-program|compress-program=s" => \$opt::compress_program,
- "use-decompress-program|decompress-program=s" => \$opt::decompress_program,
- "compress" => \$opt::compress,
- "tty" => \$opt::tty,
- "T" => \$opt::retired,
- "halt-on-error|halt=s" => \$opt::halt_on_error,
- "H=i" => \$opt::retired,
- "retries=i" => \$opt::retries,
- "dry-run|dryrun" => \$opt::dryrun,
- "progress" => \$opt::progress,
- "eta" => \$opt::eta,
- "bar" => \$opt::bar,
- "arg-sep|argsep=s" => \$opt::arg_sep,
- "arg-file-sep|argfilesep=s" => \$opt::arg_file_sep,
- "trim=s" => \$opt::trim,
- "env=s" => \@opt::env,
- "recordenv|record-env" => \$opt::record_env,
- "plain" => \$opt::plain,
- "profile|J=s" => \@opt::profile,
- "pipe|spreadstdin" => \$opt::pipe,
- "robin|round-robin|roundrobin" => \$opt::roundrobin,
- "recstart=s" => \$opt::recstart,
- "recend=s" => \$opt::recend,
- "regexp|regex" => \$opt::regexp,
- "remove-rec-sep|removerecsep|rrs" => \$opt::remove_rec_sep,
- "files|output-as-files|outputasfiles" => \$opt::files,
- "block|block-size|blocksize=s" => \$opt::blocksize,
- "tollef" => \$opt::retired,
- "gnu" => \$opt::gnu,
- "xapply" => \$opt::xapply,
- "bibtex" => \$opt::bibtex,
- "nn|nonotice|no-notice" => \$opt::no_notice,
- # xargs-compatibility - implemented, man, testsuite
- "max-procs|P=s" => \$opt::jobs,
- "delimiter|d=s" => \$opt::d,
- "max-chars|s=i" => \$opt::max_chars,
- "arg-file|a=s" => \@opt::a,
- "no-run-if-empty|r" => \$opt::r,
- "replace|i:s" => \$opt::i,
- "E=s" => \$opt::eof,
- "eof|e:s" => \$opt::eof,
- "max-args|n=i" => \$opt::max_args,
- "max-replace-args|N=i" => \$opt::max_replace_args,
- "colsep|col-sep|C=s" => \$opt::colsep,
- "help|h" => \$opt::help,
- "L=f" => \$opt::L,
- "max-lines|l:f" => \$opt::max_lines,
- "interactive|p" => \$opt::p,
- "verbose|t" => \$opt::verbose,
- "version|V" => \$opt::version,
- "minversion|min-version=i" => \$opt::minversion,
- "show-limits|showlimits" => \$opt::show_limits,
- "exit|x" => \$opt::x,
- # Semaphore
- "semaphore" => \$opt::semaphore,
- "semaphoretimeout=i" => \$opt::semaphoretimeout,
- "semaphorename|id=s" => \$opt::semaphorename,
- "fg" => \$opt::fg,
- "bg" => \$opt::bg,
- "wait" => \$opt::wait,
- # Shebang #!/usr/bin/parallel --shebang
- "shebang|hashbang" => \$opt::shebang,
- "internal-pipe-means-argfiles" => \$opt::internal_pipe_means_argfiles,
- "Y" => \$opt::retired,
- "skip-first-line" => \$opt::skip_first_line,
- "header=s" => \$opt::header,
- "cat" => \$opt::cat,
- "fifo" => \$opt::fifo,
- "pipepart|pipe-part" => \$opt::pipepart,
- "hgrp|hostgroup|hostgroups" => \$opt::hostgroups,
- );
- }
- sub get_options_from_array {
- # Run GetOptions on @array
- # Input:
- # $array_ref = ref to @ARGV to parse
- # @keep_only = Keep only these options
- # Uses:
- # @ARGV
- # Returns:
- # true if parsing worked
- # false if parsing failed
- # @$array_ref is changed
- my ($array_ref, @keep_only) = @_;
- if(not @$array_ref) {
- # Empty array: No need to look more at that
- return 1;
- }
- # A bit of shuffling of @ARGV needed as GetOptionsFromArray is not
- # supported everywhere
- my @save_argv;
- my $this_is_ARGV = (\@::ARGV == $array_ref);
- if(not $this_is_ARGV) {
- @save_argv = @::ARGV;
- @::ARGV = @{$array_ref};
- }
- # If @keep_only set: Ignore all values except @keep_only
- my %options = options_hash();
- if(@keep_only) {
- my (%keep,@dummy);
- @keep{@keep_only} = @keep_only;
- for my $k (grep { not $keep{$_} } keys %options) {
- # Store the value of the option in @dummy
- $options{$k} = \@dummy;
- }
- }
- my $retval = GetOptions(%options);
- if(not $this_is_ARGV) {
- @{$array_ref} = @::ARGV;
- @::ARGV = @save_argv;
- }
- return $retval;
- }
- sub parse_options {
- # Returns: N/A
- # Defaults:
- $Global::version = 20141122;
- $Global::progname = 'parallel';
- $Global::infinity = 2**31;
- $Global::debug = 0;
- $Global::verbose = 0;
- $Global::quoting = 0;
- # Read only table with default --rpl values
- %Global::replace =
- (
- '{}' => '',
- '{#}' => '1 $_=$job->seq()',
- '{%}' => '1 $_=$job->slot()',
- '{/}' => 's:.*/::',
- '{//}' => '$Global::use{"File::Basename"} ||= eval "use File::Basename; 1;"; $_ = dirname($_);',
- '{/.}' => 's:.*/::; s:\.[^/.]+$::;',
- '{.}' => 's:\.[^/.]+$::',
- );
- %Global::plus =
- (
- # {} = {+/}/{/}
- # = {.}.{+.} = {+/}/{/.}.{+.}
- # = {..}.{+..} = {+/}/{/..}.{+..}
- # = {...}.{+...} = {+/}/{/...}.{+...}
- '{+/}' => 's:/[^/]*$::',
- '{+.}' => 's:.*\.::',
- '{+..}' => 's:.*\.([^.]*\.):$1:',
- '{+...}' => 's:.*\.([^.]*\.[^.]*\.):$1:',
- '{..}' => 's:\.[^/.]+$::; s:\.[^/.]+$::',
- '{...}' => 's:\.[^/.]+$::; s:\.[^/.]+$::; s:\.[^/.]+$::',
- '{/..}' => 's:.*/::; s:\.[^/.]+$::; s:\.[^/.]+$::',
- '{/...}' => 's:.*/::; s:\.[^/.]+$::; s:\.[^/.]+$::; s:\.[^/.]+$::',
- );
- # Modifiable copy of %Global::replace
- %Global::rpl = %Global::replace;
- $Global::parens = "{==}";
- $/="\n";
- $Global::ignore_empty = 0;
- $Global::interactive = 0;
- $Global::stderr_verbose = 0;
- $Global::default_simultaneous_sshlogins = 9;
- $Global::exitstatus = 0;
- $Global::halt_on_error_exitstatus = 0;
- $Global::arg_sep = ":::";
- $Global::arg_file_sep = "::::";
- $Global::trim = 'n';
- $Global::max_jobs_running = 0;
- $Global::job_already_run = '';
- $ENV{'TMPDIR'} ||= "/tmp";
- @ARGV=read_options();
- if(@opt::v) { $Global::verbose = $#opt::v+1; } # Convert -v -v to v=2
- $Global::debug = $opt::D;
- $Global::shell = $ENV{'PARALLEL_SHELL'} || parent_shell($$) || $ENV{'SHELL'} || "/bin/sh";
- if(defined $opt::X) { $Global::ContextReplace = 1; }
- if(defined $opt::silent) { $Global::verbose = 0; }
- if(defined $opt::0) { $/ = "\0"; }
- if(defined $opt::d) { my $e="sprintf \"$opt::d\""; $/ = eval $e; }
- if(defined $opt::p) { $Global::interactive = $opt::p; }
- if(defined $opt::q) { $Global::quoting = 1; }
- if(defined $opt::r) { $Global::ignore_empty = 1; }
- if(defined $opt::verbose) { $Global::stderr_verbose = 1; }
- # Deal with --rpl
- sub rpl {
- # Modify %Global::rpl
- # Replace $old with $new
- my ($old,$new) = @_;
- if($old ne $new) {
- $Global::rpl{$new} = $Global::rpl{$old};
- delete $Global::rpl{$old};
- }
- }
- if(defined $opt::parens) { $Global::parens = $opt::parens; }
- my $parenslen = 0.5*length $Global::parens;
- $Global::parensleft = substr($Global::parens,0,$parenslen);
- $Global::parensright = substr($Global::parens,$parenslen);
- if(defined $opt::plus) { %Global::rpl = (%Global::plus,%Global::rpl); }
- if(defined $opt::I) { rpl('{}',$opt::I); }
- if(defined $opt::U) { rpl('{.}',$opt::U); }
- if(defined $opt::i and $opt::i) { rpl('{}',$opt::i); }
- if(defined $opt::basenamereplace) { rpl('{/}',$opt::basenamereplace); }
- if(defined $opt::dirnamereplace) { rpl('{//}',$opt::dirnamereplace); }
- if(defined $opt::seqreplace) { rpl('{#}',$opt::seqreplace); }
- if(defined $opt::slotreplace) { rpl('{%}',$opt::slotreplace); }
- if(defined $opt::basenameextensionreplace) {
- rpl('{/.}',$opt::basenameextensionreplace);
- }
- for(@opt::rpl) {
- # Create $Global::rpl entries for --rpl options
- # E.g: "{..} s:\.[^.]+$:;s:\.[^.]+$:;"
- my ($shorthand,$long) = split/ /,$_,2;
- $Global::rpl{$shorthand} = $long;
- }
- if(defined $opt::eof) { $Global::end_of_file_string = $opt::eof; }
- if(defined $opt::max_args) { $Global::max_number_of_args = $opt::max_args; }
- if(defined $opt::timeout) { $Global::timeoutq = TimeoutQueue->new($opt::timeout); }
- if(defined $opt::tmpdir) { $ENV{'TMPDIR'} = $opt::tmpdir; }
- if(defined $opt::help) { die_usage(); }
- if(defined $opt::colsep) { $Global::trim = 'lr'; }
- if(defined $opt::header) { $opt::colsep = defined $opt::colsep ? $opt::colsep : "\t"; }
- if(defined $opt::trim) { $Global::trim = $opt::trim; }
- if(defined $opt::arg_sep) { $Global::arg_sep = $opt::arg_sep; }
- if(defined $opt::arg_file_sep) { $Global::arg_file_sep = $opt::arg_file_sep; }
- if(defined $opt::number_of_cpus) { print SSHLogin::no_of_cpus(),"\n"; wait_and_exit(0); }
- if(defined $opt::number_of_cores) {
- print SSHLogin::no_of_cores(),"\n"; wait_and_exit(0);
- }
- if(defined $opt::max_line_length_allowed) {
- print Limits::Command::real_max_length(),"\n"; wait_and_exit(0);
- }
- if(defined $opt::version) { version(); wait_and_exit(0); }
- if(defined $opt::bibtex) { bibtex(); wait_and_exit(0); }
- if(defined $opt::record_env) { record_env(); wait_and_exit(0); }
- if(defined $opt::show_limits) { show_limits(); }
- if(@opt::sshlogin) { @Global::sshlogin = @opt::sshlogin; }
- if(@opt::sshloginfile) { read_sshloginfiles(@opt::sshloginfile); }
- if(@opt::return) { push @Global::ret_files, @opt::return; }
- if(not defined $opt::recstart and
- not defined $opt::recend) { $opt::recend = "\n"; }
- if(not defined $opt::blocksize) { $opt::blocksize = "1M"; }
- $opt::blocksize = multiply_binary_prefix($opt::blocksize);
- if(defined $opt::controlmaster) { $opt::noctrlc = 1; }
- if(defined $opt::semaphore) { $Global::semaphore = 1; }
- if(defined $opt::semaphoretimeout) { $Global::semaphore = 1; }
- if(defined $opt::semaphorename) { $Global::semaphore = 1; }
- if(defined $opt::fg) { $Global::semaphore = 1; }
- if(defined $opt::bg) { $Global::semaphore = 1; }
- if(defined $opt::wait) { $Global::semaphore = 1; }
- if(defined $opt::halt_on_error and
- $opt::halt_on_error=~/%/) { $opt::halt_on_error /= 100; }
- if(defined $opt::timeout and $opt::timeout !~ /^\d+(\.\d+)?%?$/) {
- ::error("--timeout must be seconds or percentage\n");
- wait_and_exit(255);
- }
- if(defined $opt::minversion) {
- print $Global::version,"\n";
- if($Global::version < $opt::minversion) {
- wait_and_exit(255);
- } else {
- wait_and_exit(0);
- }
- }
- if(not defined $opt::delay) {
- # Set --delay to --sshdelay if not set
- $opt::delay = $opt::sshdelay;
- }
- if($opt::compress_program) {
- $opt::compress = 1;
- $opt::decompress_program ||= $opt::compress_program." -dc";
- }
- if($opt::compress) {
- my ($compress, $decompress) = find_compression_program();
- $opt::compress_program ||= $compress;
- $opt::decompress_program ||= $decompress;
- }
- if(defined $opt::nonall) {
- # Append a dummy empty argument
- push @ARGV, $Global::arg_sep, "";
- }
- if(defined $opt::tty) {
- # Defaults for --tty: -j1 -u
- # Can be overridden with -jXXX -g
- if(not defined $opt::jobs) {
- $opt::jobs = 1;
- }
- if(not defined $opt::group) {
- $opt::ungroup = 0;
- }
- }
- if(@opt::trc) {
- push @Global::ret_files, @opt::trc;
- $opt::transfer = 1;
- $opt::cleanup = 1;
- }
- if(defined $opt::max_lines) {
- if($opt::max_lines eq "-0") {
- # -l -0 (swallowed -0)
- $opt::max_lines = 1;
- $opt::0 = 1;
- $/ = "\0";
- } elsif ($opt::max_lines == 0) {
- # If not given (or if 0 is given) => 1
- $opt::max_lines = 1;
- }
- $Global::max_lines = $opt::max_lines;
- if(not $opt::pipe) {
- # --pipe -L means length of record - not max_number_of_args
- $Global::max_number_of_args ||= $Global::max_lines;
- }
- }
- # Read more than one arg at a time (-L, -N)
- if(defined $opt::L) {
- $Global::max_lines = $opt::L;
- if(not $opt::pipe) {
- # --pipe -L means length of record - not max_number_of_args
- $Global::max_number_of_args ||= $Global::max_lines;
- }
- }
- if(defined $opt::max_replace_args) {
- $Global::max_number_of_args = $opt::max_replace_args;
- $Global::ContextReplace = 1;
- }
- if((defined $opt::L or defined $opt::max_replace_args)
- and
- not ($opt::xargs or $opt::m)) {
- $Global::ContextReplace = 1;
- }
- if(defined $opt::tag and not defined $opt::tagstring) {
- $opt::tagstring = "\257<\257>"; # Default = {}
- }
- if(defined $opt::pipepart and
- (defined $opt::L or defined $opt::max_lines
- or defined $opt::max_replace_args)) {
- ::error("--pipepart is incompatible with --max-replace-args, ",
- "--max-lines, and -L.\n");
- wait_and_exit(255);
- }
- if(grep /^$Global::arg_sep$|^$Global::arg_file_sep$/o, @ARGV) {
- # Deal with ::: and ::::
- @ARGV=read_args_from_command_line();
- }
- # Semaphore defaults
- # Must be done before computing number of processes and max_line_length
- # because when running as a semaphore GNU Parallel does not read args
- $Global::semaphore ||= ($0 =~ m:(^|/)sem$:); # called as 'sem'
- if($Global::semaphore) {
- # A semaphore does not take input from neither stdin nor file
- @opt::a = ("/dev/null");
- push(@Global::unget_argv, [Arg->new("")]);
- $Semaphore::timeout = $opt::semaphoretimeout || 0;
- if(defined $opt::semaphorename) {
- $Semaphore::name = $opt::semaphorename;
- } else {
- $Semaphore::name = `tty`;
- chomp $Semaphore::name;
- }
- $Semaphore::fg = $opt::fg;
- $Semaphore::wait = $opt::wait;
- $Global::default_simultaneous_sshlogins = 1;
- if(not defined $opt::jobs) {
- $opt::jobs = 1;
- }
- if($Global::interactive and $opt::bg) {
- ::error("Jobs running in the ".
- "background cannot be interactive.\n");
- ::wait_and_exit(255);
- }
- }
- if(defined $opt::eta) {
- $opt::progress = $opt::eta;
- }
- if(defined $opt::bar) {
- $opt::progress = $opt::bar;
- }
- if(defined $opt::retired) {
- ::error("-g has been retired. Use --group.\n");
- ::error("-B has been retired. Use --bf.\n");
- ::error("-T has been retired. Use --tty.\n");
- ::error("-U has been retired. Use --er.\n");
- ::error("-W has been retired. Use --wd.\n");
- ::error("-Y has been retired. Use --shebang.\n");
- ::error("-H has been retired. Use --halt.\n");
- ::error("--tollef has been retired. Use -u -q --arg-sep -- and --load for -l.\n");
- ::wait_and_exit(255);
- }
- citation_notice();
- parse_sshlogin();
- parse_env_var();
- if(remote_hosts() and ($opt::X or $opt::m or $opt::xargs)) {
- # As we do not know the max line length on the remote machine
- # long commands generated by xargs may fail
- # If opt_N is set, it is probably safe
- ::warning("Using -X or -m with --sshlogin may fail.\n");
- }
- if(not defined $opt::jobs) {
- $opt::jobs = "100%";
- }
- open_joblog();
- }
- sub env_quote {
- # Input:
- # $v = value to quote
- # Returns:
- # $v = value quoted as environment variable
- my $v = $_[0];
- $v =~ s/([\\])/\\$1/g;
- $v =~ s/([\[\] \#\'\&\<\>\(\)\;\{\}\t\"\$\`\*\174\!\?\~])/\\$1/g;
- $v =~ s/\n/"\n"/g;
- return $v;
- }
- sub record_env {
- # Record current %ENV-keys in ~/.parallel/ignored_vars
- # Returns: N/A
- my $ignore_filename = $ENV{'HOME'} . "/.parallel/ignored_vars";
- if(open(my $vars_fh, ">", $ignore_filename)) {
- print $vars_fh map { $_,"\n" } keys %ENV;
- } else {
- ::error("Cannot write to $ignore_filename\n");
- ::wait_and_exit(255);
- }
- }
- sub parse_env_var {
- # Parse --env and set $Global::envvar, $Global::envwarn and $Global::envvarlen
- #
- # Bash functions must be parsed to export them remotely
- # Pre-shellshock style bash function:
- # myfunc=() {...
- # Post-shellshock style bash function:
- # BASH_FUNC_myfunc()=() {...
- #
- # Uses:
- # $Global::envvar = eval string that will set variables in both bash and csh
- # $Global::envwarn = If functions are used: Give warning in csh
- # $Global::envvarlen = length of $Global::envvar
- # @opt::env
- # $Global::shell
- # %ENV
- # Returns: N/A
- $Global::envvar = "";
- $Global::envwarn = "";
- my @vars = ('parallel_bash_environment');
- for my $varstring (@opt::env) {
- # Split up --env VAR1,VAR2
- push @vars, split /,/, $varstring;
- }
- if(grep { /^_$/ } @vars) {
- # --env _
- # Include all vars that are not in a clean environment
- if(open(my $vars_fh, "<", $ENV{'HOME'} . "/.parallel/ignored_vars")) {
- my @ignore = <$vars_fh>;
- chomp @ignore;
- my %ignore;
- @ignore{@ignore} = @ignore;
- close $vars_fh;
- push @vars, grep { not defined $ignore{$_} } keys %ENV;
- @vars = grep { not /^_$/ } @vars;
- } else {
- ::error("Run '$Global::progname --record-env' in a clean environment first.\n");
- ::wait_and_exit(255);
- }
- }
- # Duplicate vars as BASH functions to include post-shellshock functions.
- # So --env myfunc should also look for BASH_FUNC_myfunc()
- @vars = map { $_, "BASH_FUNC_$_()" } @vars;
- # Keep only defined variables
- @vars = grep { defined($ENV{$_}) } @vars;
- # Pre-shellshock style bash function:
- # myfunc=() { echo myfunc
- # }
- # Post-shellshock style bash function:
- # BASH_FUNC_myfunc()=() { echo myfunc
- # }
- my @bash_functions = grep { substr($ENV{$_},0,4) eq "() {" } @vars;
- my @non_functions = grep { substr($ENV{$_},0,4) ne "() {" } @vars;
- if(@bash_functions) {
- # Functions are not supported for all shells
- if($Global::shell !~ m:/(bash|rbash|zsh|rzsh|dash|ksh):) {
- ::warning("Shell functions may not be supported in $Global::shell\n");
- }
- }
- # Pre-shellschock names are without ()
- my @bash_pre_shellshock = grep { not /\(\)/ } @bash_functions;
- # Post-shellschock names are with ()
- my @bash_post_shellshock = grep { /\(\)/ } @bash_functions;
- my @qcsh = (map { my $a=$_; "setenv $a " . env_quote($ENV{$a}) }
- grep { not /^parallel_bash_environment$/ } @non_functions);
- my @qbash = (map { my $a=$_; "export $a=" . env_quote($ENV{$a}) }
- @non_functions, @bash_pre_shellshock);
- push @qbash, map { my $a=$_; "eval $a\"\$$a\"" } @bash_pre_shellshock;
- push @qbash, map { /BASH_FUNC_(.*)\(\)/; "$1 $ENV{$_}" } @bash_post_shellshock;
- #ssh -tt -oLogLevel=quiet lo 'eval `echo PARALLEL_SEQ='$PARALLEL_SEQ'\;export PARALLEL_SEQ\; PARALLEL_PID='$PARALLEL_PID'\;export PARALLEL_PID` ;' tty\ \>/dev/null\ \&\&\ stty\ isig\ -onlcr\ -echo\;echo\ \$SHELL\ \|\ grep\ \"/t\\\{0,1\\\}csh\"\ \>\ /dev/null\ \&\&\ setenv\ BASH_FUNC_myfunc\ \\\(\\\)\\\ \\\{\\\ \\\ echo\\\ a\"'
- #'\"\\\}\ \|\|\ myfunc\(\)\ \{\ \ echo\ a'
- #'\}\ \;myfunc\ 1;
- # Check if any variables contain \n
- if(my @v = map { s/BASH_FUNC_(.*)\(\)/$1/; $_ } grep { $ENV{$_}=~/\n/ } @vars) {
- # \n is bad for csh and will cause it to fail.
- $Global::envwarn = ::shell_quote_scalar(q{echo $SHELL | grep -E "/t?csh" > /dev/null && echo CSH/TCSH DO NOT SUPPORT newlines IN VARIABLES/FUNCTIONS. Unset }."@v".q{ && exec false;}."\n\n") . $Global::envwarn;
- }
- if(not @qcsh) { push @qcsh, "true"; }
- if(not @qbash) { push @qbash, "true"; }
- # Create lines like:
- # echo $SHELL | grep "/t\\{0,1\\}csh" >/dev/null && setenv V1 val1 && setenv V2 val2 || export V1=val1 && export V2=val2 ; echo "$V1$V2"
- if(@vars) {
- $Global::envvar .=
- join"",
- (q{echo $SHELL | grep "/t\\{0,1\\}csh" > /dev/null && }
- . join(" && ", @qcsh)
- . q{ || }
- . join(" && ", @qbash)
- .q{;});
- if($ENV{'parallel_bash_environment'}) {
- $Global::envvar .= 'eval "$parallel_bash_environment";'."\n";
- }
- }
- $Global::envvarlen = length $Global::envvar;
- }
- sub open_joblog {
- # Open joblog as specified by --joblog
- # Uses:
- # $opt::resume
- # $opt::resume_failed
- # $opt::joblog
- # $opt::results
- # $Global::job_already_run
- # %Global::fd
- my $append = 0;
- if(($opt::resume or $opt::resume_failed)
- and
- not ($opt::joblog or $opt::results)) {
- ::error("--resume and --resume-failed require --joblog or --results.\n");
- ::wait_and_exit(255);
- }
- if($opt::joblog) {
- if($opt::resume || $opt::resume_failed) {
- if(open(my $joblog_fh, "<", $opt::joblog)) {
- # Read the joblog
- $append = <$joblog_fh>; # If there is a header: Open as append later
- my $joblog_regexp;
- if($opt::resume_failed) {
- # Make a regexp that only matches commands with exit+signal=0
- # 4 host 1360490623.067 3.445 1023 1222 0 0 command
- $joblog_regexp='^(\d+)(?:\t[^\t]+){5}\t0\t0\t';
- } else {
- # Just match the job number
- $joblog_regexp='^(\d+)';
- }
- while(<$joblog_fh>) {
- if(/$joblog_regexp/o) {
- # This is 30% faster than set_job_already_run($1);
- vec($Global::job_already_run,($1||0),1) = 1;
- } elsif(not /\d+\s+[^\s]+\s+([0-9.]+\s+){6}/) {
- ::error("Format of '$opt::joblog' is wrong: $_");
- ::wait_and_exit(255);
- }
- }
- close $joblog_fh;
- }
- }
- if($append) {
- # Append to joblog
- if(not open($Global::joblog, ">>", $opt::joblog)) {
- ::error("Cannot append to --joblog $opt::joblog.\n");
- ::wait_and_exit(255);
- }
- } else {
- if($opt::joblog eq "-") {
- # Use STDOUT as joblog
- $Global::joblog = $Global::fd{1};
- } elsif(not open($Global::joblog, ">", $opt::joblog)) {
- # Overwrite the joblog
- ::error("Cannot write to --joblog $opt::joblog.\n");
- ::wait_and_exit(255);
- }
- print $Global::joblog
- join("\t", "Seq", "Host", "Starttime", "JobRuntime",
- "Send", "Receive", "Exitval", "Signal", "Command"
- ). "\n";
- }
- }
- }
- sub find_compression_program {
- # Find a fast compression program
- # Returns:
- # $compress_program = compress program with options
- # $decompress_program = decompress program with options
- # Search for these. Sorted by speed
- my @prg = qw(lzop pigz pxz gzip plzip pbzip2 lzma xz lzip bzip2);
- for my $p (@prg) {
- if(which($p)) {
- return ("$p -c -1","$p -dc");
- }
- }
- # Fall back to cat
- return ("cat","cat");
- }
- sub read_options {
- # Read options from command line, profile and $PARALLEL
- # Uses:
- # $opt::shebang_wrap
- # $opt::shebang
- # @ARGV
- # $opt::plain
- # @opt::profile
- # $ENV{'HOME'}
- # $ENV{'PARALLEL'}
- # Returns:
- # @ARGV_no_opt = @ARGV without --options
- # This must be done first as this may exec myself
- if(defined $ARGV[0] and ($ARGV[0] =~ /^--shebang/ or
- $ARGV[0] =~ /^--shebang-?wrap/ or
- $ARGV[0] =~ /^--hashbang/)) {
- # Program is called from #! line in script
- # remove --shebang-wrap if it is set
- $opt::shebang_wrap = ($ARGV[0] =~ s/^--shebang-?wrap *//);
- # remove --shebang if it is set
- $opt::shebang = ($ARGV[0] =~ s/^--shebang *//);
- # remove --hashbang if it is set
- $opt::shebang .= ($ARGV[0] =~ s/^--hashbang *//);
- if($opt::shebang) {
- my $argfile = shell_quote_scalar(pop @ARGV);
- # exec myself to split $ARGV[0] into separate fields
- exec "$0 --skip-first-line -a $argfile @ARGV";
- }
- if($opt::shebang_wrap) {
- my @options;
- my @parser;
- if ($^O eq 'freebsd') {
- # FreeBSD's #! puts different values in @ARGV than Linux' does.
- my @nooptions = @ARGV;
- get_options_from_array(\@nooptions);
- while($#ARGV > $#nooptions) {
- push @options, shift @ARGV;
- }
- while(@ARGV and $ARGV[0] ne ":::") {
- push @parser, shift @ARGV;
- }
- if(@ARGV and $ARGV[0] eq ":::") {
- shift @ARGV;
- }
- } else {
- @options = shift @ARGV;
- }
- my $script = shell_quote_scalar(shift @ARGV);
- # exec myself to split $ARGV[0] into separate fields
- exec "$0 --internal-pipe-means-argfiles @options @parser $script ::: @ARGV";
- }
- }
- Getopt::Long::Configure("bundling","require_order");
- my @ARGV_copy = @ARGV;
- # Check if there is a --profile to set @opt::profile
- get_options_from_array(\@ARGV_copy,"profile|J=s","plain") || die_usage();
- my @ARGV_profile = ();
- my @ARGV_env = ();
- if(not $opt::plain) {
- # Add options from .parallel/config and other profiles
- my @config_profiles = (
- "/etc/parallel/config",
- $ENV{'HOME'}."/.parallel/config",
- $ENV{'HOME'}."/.parallelrc");
- my @profiles = @config_profiles;
- if(@opt::profile) {
- # --profile overrides default profiles
- @profiles = ();
- for my $profile (@opt::profile) {
- if(-r $profile) {
- push @profiles, $profile;
- } else {
- push @profiles, $ENV{'HOME'}."/.parallel/".$profile;
- }
- }
- }
- for my $profile (@profiles) {
- if(-r $profile) {
- open (my $in_fh, "<", $profile) || ::die_bug("read-profile: $profile");
- while(<$in_fh>) {
- /^\s*\#/ and next;
- chomp;
- push @ARGV_profile, shellwords($_);
- }
- close $in_fh;
- } else {
- if(grep /^$profile$/, @config_profiles) {
- # config file is not required to exist
- } else {
- ::error("$profile not readable.\n");
- wait_and_exit(255);
- }
- }
- }
- # Add options from shell variable $PARALLEL
- if($ENV{'PARALLEL'}) {
- @ARGV_env = shellwords($ENV{'PARALLEL'});
- }
- }
- Getopt::Long::Configure("bundling","require_order");
- get_options_from_array(\@ARGV_profile) || die_usage();
- get_options_from_array(\@ARGV_env) || die_usage();
- get_options_from_array(\@ARGV) || die_usage();
- # Prepend non-options to @ARGV (such as commands like 'nice')
- unshift @ARGV, @ARGV_profile, @ARGV_env;
- return @ARGV;
- }
- sub read_args_from_command_line {
- # Arguments given on the command line after:
- # ::: ($Global::arg_sep)
- # :::: ($Global::arg_file_sep)
- # Removes the arguments from @ARGV and:
- # - puts filenames into -a
- # - puts arguments into files and add the files to -a
- # Input:
- # @::ARGV = command option ::: arg arg arg :::: argfiles
- # Uses:
- # $Global::arg_sep
- # $Global::arg_file_sep
- # $opt::internal_pipe_means_argfiles
- # $opt::pipe
- # @opt::a
- # Returns:
- # @argv_no_argsep = @::ARGV without ::: and :::: and following args
- my @new_argv = ();
- for(my $arg = shift @ARGV; @ARGV; $arg = shift @ARGV) {
- if($arg eq $Global::arg_sep
- or
- $arg eq $Global::arg_file_sep) {
- my $group = $arg; # This group of arguments is args or argfiles
- my @group;
- while(defined ($arg = shift @ARGV)) {
- if($arg eq $Global::arg_sep
- or
- $arg eq $Global::arg_file_sep) {
- # exit while loop if finding new separator
- last;
- } else {
- # If not hitting ::: or ::::
- # Append it to the group
- push @group, $arg;
- }
- }
- if($group eq $Global::arg_file_sep
- or ($opt::internal_pipe_means_argfiles and $opt::pipe)
- ) {
- # Group of file names on the command line.
- # Append args into -a
- push @opt::a, @group;
- } elsif($group eq $Global::arg_sep) {
- # Group of arguments on the command line.
- # Put them into a file.
- # Create argfile
- my ($outfh,$name) = ::tmpfile(SUFFIX => ".arg");
- unlink($name);
- # Put args into argfile
- print $outfh map { $_,$/ } @group;
- seek $outfh, 0, 0;
- # Append filehandle to -a
- push @opt::a, $outfh;
- } else {
- ::die_bug("Unknown command line group: $group");
- }
- if(defined($arg)) {
- # $arg is ::: or ::::
- redo;
- } else {
- # $arg is undef -> @ARGV empty
- last;
- }
- }
- push @new_argv, $arg;
- }
- # Output: @ARGV = command to run with options
- return @new_argv;
- }
- sub cleanup {
- # Returns: N/A
- if(@opt::basefile) { cleanup_basefile(); }
- }
- sub __QUOTING_ARGUMENTS_FOR_SHELL__ {}
- sub shell_quote {
- # Input:
- # @strings = strings to be quoted
- # Output:
- # @shell_quoted_strings = string quoted with \ as needed by the shell
- my @strings = (@_);
- for my $a (@strings) {
- $a =~ s/([\002-\011\013-\032\\\#\?\`\(\)\{\}\[\]\*\>\<\~\|\; \"\!\$\&\'\202-\377])/\\$1/g;
- $a =~ s/[\n]/'\n'/g; # filenames with '\n' is quoted using \'
- }
- return wantarray ? @strings : "@strings";
- }
- sub shell_quote_empty {
- # Inputs:
- # @strings = strings to be quoted
- # Returns:
- # @quoted_strings = empty strings quoted as ''.
- my @strings = shell_quote(@_);
- for my $a (@strings) {
- if($a eq "") {
- $a = "''";
- }
- }
- return wantarray ? @strings : "@strings";
- }
- sub shell_quote_scalar {
- # Quote the string so shell will not expand any special chars
- # Inputs:
- # $string = string to be quoted
- # Returns:
- # $shell_quoted = string quoted with \ as needed by the shell
- my $a = $_[0];
- if(defined $a) {
- # $a =~ s/([\002-\011\013-\032\\\#\?\`\(\)\{\}\[\]\*\>\<\~\|\; \"\!\$\&\'\202-\377])/\\$1/g;
- # This is 1% faster than the above
- $a =~ s/[\002-\011\013-\032\\\#\?\`\(\)\{\}\[\]\*\>\<\~\|\; \"\!\$\&\'\202-\377]/\\$&/go;
- $a =~ s/[\n]/'\n'/go; # filenames with '\n' is quoted using \'
- }
- return $a;
- }
- sub shell_quote_file {
- # Quote the string so shell will not expand any special chars and prepend ./ if needed
- # Input:
- # $filename = filename to be shell quoted
- # Returns:
- # $quoted_filename = filename quoted with \ as needed by the shell and ./ if needed
- my $a = shell_quote_scalar(shift);
- if(defined $a) {
- if($a =~ m:^/: or $a =~ m:^\./:) {
- # /abs/path or ./rel/path => skip
- } else {
- # rel/path => ./rel/path
- $a = "./".$a;
- }
- }
- return $a;
- }
- sub shellwords {
- # Input:
- # $string = shell line
- # Returns:
- # @shell_words = $string split into words as shell would do
- $Global::use{"Text::ParseWords"} ||= eval "use Text::ParseWords; 1;";
- return Text::ParseWords::shellwords(@_);
- }
- sub __FILEHANDLES__ {}
- sub save_stdin_stdout_stderr {
- # Remember the original STDIN, STDOUT and STDERR
- # and file descriptors opened by the shell (e.g. 3>/tmp/foo)
- # Uses:
- # %Global::fd
- # $Global::original_stderr
- # $Global::original_stdin
- # Returns: N/A
- # Find file descriptors that are already opened (by the shell)
- for my $fdno (1..61) {
- # /dev/fd/62 and above are used by bash for <(cmd)
- my $fh;
- # 2-argument-open is used to be compatible with old perl 5.8.0
- # bug #43570: Perl 5.8.0 creates 61 files
- if(open($fh,">&=$fdno")) {
- $Global::fd{$fdno}=$fh;
- }
- }
- open $Global::original_stderr, ">&", "STDERR" or
- ::die_bug("Can't dup STDERR: $!");
- open $Global::original_stdin, "<&", "STDIN" or
- ::die_bug("Can't dup STDIN: $!");
- $Global::is_terminal = (-t $Global::original_stderr) && !$ENV{'CIRCLECI'}&& !$ENV{'GITHUB_ACTIONS'} && !$ENV{'TRAVIS'};
- }
- sub enough_file_handles {
- # Check that we have enough filehandles available for starting
- # another job
- # Uses:
- # $opt::ungroup
- # %Global::fd
- # Returns:
- # 1 if ungrouped (thus not needing extra filehandles)
- # 0 if too few filehandles
- # 1 if enough filehandles
- if(not $opt::ungroup) {
- my %fh;
- my $enough_filehandles = 1;
- # perl uses 7 filehandles for something?
- # open3 uses 2 extra filehandles temporarily
- # We need a filehandle for each redirected file descriptor
- # (normally just STDOUT and STDERR)
- for my $i (1..(7+2+keys %Global::fd)) {
- $enough_filehandles &&= open($fh{$i}, "<", "/dev/null");
- }
- for (values %fh) { close $_; }
- return $enough_filehandles;
- } else {
- # Ungrouped does not need extra file handles
- return 1;
- }
- }
- sub open_or_exit {
- # Open a file name or exit if the file cannot be opened
- # Inputs:
- # $file = filehandle or filename to open
- # Uses:
- # $Global::stdin_in_opt_a
- # $Global::original_stdin
- # Returns:
- # $fh = file handle to read-opened file
- my $file = shift;
- if($file eq "-") {
- $Global::stdin_in_opt_a = 1;
- return ($Global::original_stdin || *STDIN);
- }
- if(ref $file eq "GLOB") {
- # This is an open filehandle
- return $file;
- }
- my $fh = gensym;
- if(not open($fh, "<", $file)) {
- ::error("Cannot open input file `$file': No such file or directory.\n");
- wait_and_exit(255);
- }
- return $fh;
- }
- sub __RUNNING_THE_JOBS_AND_PRINTING_PROGRESS__ {}
- # Variable structure:
- #
- # $Global::running{$pid} = Pointer to Job-object
- # @Global::virgin_jobs = Pointer to Job-object that have received no input
- # $Global::host{$sshlogin} = Pointer to SSHLogin-object
- # $Global::total_running = total number of running jobs
- # $Global::total_started = total jobs started
- sub init_run_jobs {
- $Global::total_running = 0;
- $Global::total_started = 0;
- $Global::tty_taken = 0;
- $SIG{USR1} = \&list_running_jobs;
- $SIG{USR2} = \&toggle_progress;
- if(@opt::basefile) { setup_basefile(); }
- }
- {
- my $last_time;
- my %last_mtime;
- sub start_more_jobs {
- # Run start_another_job() but only if:
- # * not $Global::start_no_new_jobs set
- # * not JobQueue is empty
- # * not load on server is too high
- # * not server swapping
- # * not too short time since last remote login
- # Uses:
- # $Global::max_procs_file
- # $Global::max_procs_file_last_mod
- # %Global::host
- # @opt::sshloginfile
- # $Global::start_no_new_jobs
- # $opt::filter_hosts
- # $Global::JobQueue
- # $opt::pipe
- # $opt::load
- # $opt::noswap
- # $opt::delay
- # $Global::newest_starttime
- # Returns:
- # $jobs_started = number of jobs started
- my $jobs_started = 0;
- my $jobs_started_this_round = 0;
- if($Global::start_no_new_jobs) {
- return $jobs_started;
- }
- if(time - ($last_time||0) > 1) {
- # At most do this every second
- $last_time = time;
- if($Global::max_procs_file) {
- # --jobs filename
- my $mtime = (stat($Global::max_procs_file))[9];
- if($mtime > $Global::max_procs_file_last_mod) {
- # file changed: Force re-computing max_jobs_running
- $Global::max_procs_file_last_mod = $mtime;
- for my $sshlogin (values %Global::host) {
- $sshlogin->set_max_jobs_running(undef);
- }
- }
- }
- if(@opt::sshloginfile) {
- # Is --sshloginfile changed?
- for my $slf (@opt::sshloginfile) {
- my $actual_file = expand_slf_shorthand($slf);
- my $mtime = (stat($actual_file))[9];
- $last_mtime{$actual_file} ||= $mtime;
- if($mtime - $last_mtime{$actual_file} > 1) {
- ::debug("run","--sshloginfile $actual_file changed. reload\n");
- $last_mtime{$actual_file} = $mtime;
- # Reload $slf
- # Empty sshlogins
- @Global::sshlogin = ();
- for (values %Global::host) {
- # Don't start new jobs on any host
- # except the ones added back later
- $_->set_max_jobs_running(0);
- }
- # This will set max_jobs_running on the SSHlogins
- read_sshloginfile($actual_file);
- parse_sshlogin();
- $opt::filter_hosts and filter_hosts();
- setup_basefile();
- }
- }
- }
- }
- do {
- $jobs_started_this_round = 0;
- # This will start 1 job on each --sshlogin (if possible)
- # thus distribute the jobs on the --sshlogins round robin
- for my $sshlogin (values %Global::host) {
- if($Global::JobQueue->empty() and not $opt::pipe) {
- # No more jobs in the queue
- last;
- }
- debug("run", "Running jobs before on ", $sshlogin->string(), ": ",
- $sshlogin->jobs_running(), "\n");
- if ($sshlogin->jobs_running() < $sshlogin->max_jobs_running()) {
- if($opt::load and $sshlogin->loadavg_too_high()) {
- # The load is too high or unknown
- next;
- }
- if($opt::noswap and $sshlogin->swapping()) {
- # The server is swapping
- next;
- }
- if($sshlogin->too_fast_remote_login()) {
- # It has been too short since
- next;
- }
- if($opt::delay and $opt::delay > ::now() - $Global::newest_starttime) {
- # It has been too short since last start
- next;
- }
- debug("run", $sshlogin->string(), " has ", $sshlogin->jobs_running(),
- " out of ", $sshlogin->max_jobs_running(),
- " jobs running. Start another.\n");
- if(start_another_job($sshlogin) == 0) {
- # No more jobs to start on this $sshlogin
- debug("run","No jobs started on ", $sshlogin->string(), "\n");
- next;
- }
- $sshlogin->inc_jobs_running();
- $sshlogin->set_last_login_at(::now());
- $jobs_started++;
- $jobs_started_this_round++;
- }
- debug("run","Running jobs after on ", $sshlogin->string(), ": ",
- $sshlogin->jobs_running(), " of ",
- $sshlogin->max_jobs_running(), "\n");
- }
- } while($jobs_started_this_round);
- return $jobs_started;
- }
- }
- {
- my $no_more_file_handles_warned;
- sub start_another_job {
- # If there are enough filehandles
- # and JobQueue not empty
- # and not $job is in joblog
- # Then grab a job from Global::JobQueue,
- # start it at sshlogin
- # mark it as virgin_job
- # Inputs:
- # $sshlogin = the SSHLogin to start the job on
- # Uses:
- # $Global::JobQueue
- # $opt::pipe
- # $opt::results
- # $opt::resume
- # @Global::virgin_jobs
- # Returns:
- # 1 if another jobs was started
- # 0 otherwise
- my $sshlogin = shift;
- # Do we have enough file handles to start another job?
- if(enough_file_handles()) {
- if($Global::JobQueue->empty() and not $opt::pipe) {
- # No more commands to run
- debug("start", "Not starting: JobQueue empty\n");
- return 0;
- } else {
- my $job;
- # Skip jobs already in job log
- # Skip jobs already in results
- do {
- $job = get_job_with_sshlogin($sshlogin);
- if(not defined $job) {
- # No command available for that sshlogin
- debug("start", "Not starting: no jobs available for ",
- $sshlogin->string(), "\n");
- return 0;
- }
- } while ($job->is_already_in_joblog()
- or
- ($opt::results and $opt::resume and $job->is_already_in_results()));
- debug("start", "Command to run on '", $job->sshlogin()->string(), "': '",
- $job->replaced(),"'\n");
- if($job->start()) {
- if($opt::pipe) {
- push(@Global::virgin_jobs,$job);
- }
- debug("start", "Started as seq ", $job->seq(),
- " pid:", $job->pid(), "\n");
- return 1;
- } else {
- # Not enough processes to run the job.
- # Put it back on the queue.
- $Global::JobQueue->unget($job);
- # Count down the number of jobs to run for this SSHLogin.
- my $max = $sshlogin->max_jobs_running();
- if($max > 1) { $max--; } else {
- ::error("No more processes: cannot run a single job. Something is wrong.\n");
- ::wait_and_exit(255);
- }
- $sshlogin->set_max_jobs_running($max);
- # Sleep up to 300 ms to give other processes time to die
- ::usleep(rand()*300);
- ::warning("No more processes: ",
- "Decreasing number of running jobs to $max. ",
- "Raising ulimit -u or /etc/security/limits.conf may help.\n");
- return 0;
- }
- }
- } else {
- # No more file handles
- $no_more_file_handles_warned++ or
- ::warning("No more file handles. ",
- "Raising ulimit -n or /etc/security/limits.conf may help.\n");
- return 0;
- }
- }
- }
- $opt::min_progress_interval = 0;
- sub init_progress {
- # Uses:
- # $opt::bar
- # Returns:
- # list of computers for progress output
- $|=1;
- if (not $Global::is_terminal) {
- $opt::min_progress_interval = 30;
- }
- if($opt::bar) {
- return("","");
- }
- my %progress = progress();
- return ("\nComputers / CPU cores / Max jobs to run\n",
- $progress{'workerlist'});
- }
- sub drain_job_queue {
- # Uses:
- # $opt::progress
- # $Global::original_stderr
- # $Global::total_running
- # $Global::max_jobs_running
- # %Global::running
- # $Global::JobQueue
- # %Global::host
- # $Global::start_no_new_jobs
- # Returns: N/A
- if($opt::progress) {
- print $Global::original_stderr init_progress();
- }
- my $last_header="";
- my $sleep = 0.2;
- my $last_left = 1000000000;
- my $last_progress_time = 0;
- my $ps_reported = 0;
- do {
- while($Global::total_running > 0) {
- debug($Global::total_running, "==", scalar
- keys %Global::running," slots: ", $Global::max_jobs_running);
- if($opt::pipe) {
- # When using --pipe sometimes file handles are not closed properly
- for my $job (values %Global::running) {
- close $job->fh(0,"w");
- }
- }
- # When not connected to terminal, assume CI (e.g. CircleCI). In
- # that case we want occasional progress output to prevent abort
- # due to timeout with no output, but we also need to stop sending
- # progress output if there has been no actual progress, so that
- # the job can time out appropriately (CirecleCI: 10m) in case of
- # a hung test. But without special output, it is extremely
- # annoying to diagnose which test is hung, so we add that using
- # `ps` below.
- if($opt::progress and
- ($Global::is_terminal or (time() - $last_progress_time) >= 30)) {
- my %progress = progress();
- if($last_header ne $progress{'header'}) {
- print $Global::original_stderr "\n", $progress{'header'}, "\n";
- $last_header = $progress{'header'};
- }
- if ($Global::is_terminal) {
- print $Global::original_stderr "\r",$progress{'status'};
- }
- if ($last_left > $Global::left) {
- if (not $Global::is_terminal) {
- print $Global::original_stderr $progress{'status'},"\n";
- }
- $last_progress_time = time();
- $ps_reported = 0;
- } elsif (not $ps_reported and (time() - $last_progress_time) >= 60) {
- # No progress in at least 60 seconds: run ps
- print $Global::original_stderr "\n";
- my $script_dir = ::dirname($0);
- system("$script_dir/ps_with_stack || ps -wwf");
- $ps_reported = 1;
- }
- $last_left = $Global::left;
- flush $Global::original_stderr;
- }
- if($Global::total_running < $Global::max_jobs_running
- and not $Global::JobQueue->empty()) {
- # These jobs may not be started because of loadavg
- # or too little time between each ssh login.
- if(start_more_jobs() > 0) {
- # Exponential back-on if jobs were started
- $sleep = $sleep/2+0.001;
- }
- }
- # Sometimes SIGCHLD is not registered, so force reaper
- $sleep = ::reap_usleep($sleep);
- }
- if(not $Global::JobQueue->empty()) {
- # These jobs may not be started:
- # * because there the --filter-hosts has removed all
- if(not %Global::host) {
- ::error("There are no hosts left to run on.\n");
- ::wait_and_exit(255);
- }
- # * because of loadavg
- # * because of too little time between each ssh login.
- start_more_jobs();
- $sleep = ::reap_usleep($sleep);
- if($Global::max_jobs_running == 0) {
- ::warning("There are no job slots available. Increase --jobs.\n");
- }
- }
- } while ($Global::total_running > 0
- or
- not $Global::start_no_new_jobs and not $Global::JobQueue->empty());
- if($opt::progress) {
- my %progress = progress();
- print $Global::original_stderr $opt::progress_sep, $progress{'status'}, "\n";
- flush $Global::original_stderr;
- }
- }
- sub toggle_progress {
- # Turn on/off progress view
- # Uses:
- # $opt::progress
- # $Global::original_stderr
- # Returns: N/A
- $opt::progress = not $opt::progress;
- if($opt::progress) {
- print $Global::original_stderr init_progress();
- }
- }
- sub progress {
- # Uses:
- # $opt::bar
- # $opt::eta
- # %Global::host
- # $Global::total_started
- # Returns:
- # $workerlist = list of workers
- # $header = that will fit on the screen
- # $status = message that will fit on the screen
- if($opt::bar) {
- return ("workerlist" => "", "header" => "", "status" => bar());
- }
- my $eta = "";
- my ($status,$header)=("","");
- if($opt::eta) {
- my($total, $completed, $left, $pctcomplete, $avgtime, $this_eta) =
- compute_eta();
- $eta = sprintf("ETA: %ds Left: %d AVG: %.2fs ",
- $this_eta, $left, $avgtime);
- $Global::left = $left;
- }
- my $termcols = terminal_columns();
- my @workers = sort keys %Global::host;
- my %sshlogin = map { $_ eq ":" ? ($_=>"local") : ($_=>$_) } @workers;
- my $workerno = 1;
- my %workerno = map { ($_=>$workerno++) } @workers;
- my $workerlist = "";
- for my $w (@workers) {
- $workerlist .=
- $workerno{$w}.":".$sshlogin{$w} ." / ".
- ($Global::host{$w}->ncpus() || "-")." / ".
- $Global::host{$w}->max_jobs_running()."\n";
- }
- $status = "x"x($termcols+1);
- if(length $status > $termcols) {
- # sshlogin1:XX/XX/XX%/XX.Xs sshlogin2:XX/XX/XX%/XX.Xs sshlogin3:XX/XX/XX%/XX.Xs
- $header = "Computer:jobs running/jobs completed/%of started jobs/Average seconds to complete";
- $status = $eta .
- join(" ",map
- {
- if($Global::total_started) {
- my $completed = ($Global::host{$_}->jobs_completed()||0);
- my $running = $Global::host{$_}->jobs_running();
- my $time = $completed ? (time-$^T)/($completed) : "0";
- sprintf("%s:%d/%d/%d%%/%.1fs ",
- $sshlogin{$_}, $running, $completed,
- ($running+$completed)*100
- / $Global::total_started, $time);
- }
- } @workers);
- }
- if(length $status > $termcols) {
- # 1:XX/XX/XX%/XX.Xs 2:XX/XX/XX%/XX.Xs 3:XX/XX/XX%/XX.Xs 4:XX/XX/XX%/XX.Xs
- $header = "Computer:jobs running/jobs completed/%of started jobs";
- $status = $eta .
- join(" ",map
- {
- my $completed = ($Global::host{$_}->jobs_completed()||0);
- my $running = $Global::host{$_}->jobs_running();
- my $time = $completed ? (time-$^T)/($completed) : "0";
- sprintf("%s:%d/%d/%d%%/%.1fs ",
- $workerno{$_}, $running, $completed,
- ($running+$completed)*100
- / $Global::total_started, $time);
- } @workers);
- }
- if(length $status > $termcols) {
- # sshlogin1:XX/XX/XX% sshlogin2:XX/XX/XX% sshlogin3:XX/XX/XX%
- $header = "Computer:jobs running/jobs completed/%of started jobs";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d/%d/%d%%",
- $sshlogin{$_},
- $Global::host{$_}->jobs_running(),
- ($Global::host{$_}->jobs_completed()||0),
- ($Global::host{$_}->jobs_running()+
- ($Global::host{$_}->jobs_completed()||0))*100
- / $Global::total_started) }
- @workers);
- }
- if(length $status > $termcols) {
- # 1:XX/XX/XX% 2:XX/XX/XX% 3:XX/XX/XX% 4:XX/XX/XX% 5:XX/XX/XX% 6:XX/XX/XX%
- $header = "Computer:jobs running/jobs completed/%of started jobs";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d/%d/%d%%",
- $workerno{$_},
- $Global::host{$_}->jobs_running(),
- ($Global::host{$_}->jobs_completed()||0),
- ($Global::host{$_}->jobs_running()+
- ($Global::host{$_}->jobs_completed()||0))*100
- / $Global::total_started) }
- @workers);
- }
- if(length $status > $termcols) {
- # sshlogin1:XX/XX/XX% sshlogin2:XX/XX/XX% sshlogin3:XX/XX sshlogin4:XX/XX
- $header = "Computer:jobs running/jobs completed";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d/%d",
- $sshlogin{$_}, $Global::host{$_}->jobs_running(),
- ($Global::host{$_}->jobs_completed()||0)) }
- @workers);
- }
- if(length $status > $termcols) {
- # sshlogin1:XX/XX sshlogin2:XX/XX sshlogin3:XX/XX sshlogin4:XX/XX
- $header = "Computer:jobs running/jobs completed";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d/%d",
- $sshlogin{$_}, $Global::host{$_}->jobs_running(),
- ($Global::host{$_}->jobs_completed()||0)) }
- @workers);
- }
- if(length $status > $termcols) {
- # 1:XX/XX 2:XX/XX 3:XX/XX 4:XX/XX 5:XX/XX 6:XX/XX
- $header = "Computer:jobs running/jobs completed";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d/%d",
- $workerno{$_}, $Global::host{$_}->jobs_running(),
- ($Global::host{$_}->jobs_completed()||0)) }
- @workers);
- }
- if(length $status > $termcols) {
- # sshlogin1:XX sshlogin2:XX sshlogin3:XX sshlogin4:XX sshlogin5:XX
- $header = "Computer:jobs completed";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d",
- $sshlogin{$_},
- ($Global::host{$_}->jobs_completed()||0)) }
- @workers);
- }
- if(length $status > $termcols) {
- # 1:XX 2:XX 3:XX 4:XX 5:XX 6:XX
- $header = "Computer:jobs completed";
- $status = $eta .
- join(" ",map
- { sprintf("%s:%d",
- $workerno{$_},
- ($Global::host{$_}->jobs_completed()||0)) }
- @workers);
- }
- return ("workerlist" => $workerlist, "header" => $header, "status" => $status);
- }
- {
- my ($total, $first_completed, $smoothed_avg_time);
- sub compute_eta {
- # Calculate important numbers for ETA
- # Returns:
- # $total = number of jobs in total
- # $completed = number of jobs completed
- # $left = number of jobs left
- # $pctcomplete = percent of jobs completed
- # $avgtime = averaged time
- # $eta = smoothed eta
- $total ||= $Global::JobQueue->total_jobs();
- my $completed = 0;
- for(values %Global::host) { $completed += $_->jobs_completed() }
- my $left = $total - $completed;
- if(not $completed) {
- return($total, $completed, $left, 0, 0, 0);
- }
- my $pctcomplete = $completed / $total;
- $first_completed ||= time;
- my $timepassed = (time - $first_completed);
- my $avgtime = $timepassed / $completed;
- $smoothed_avg_time ||= $avgtime;
- # Smooth the eta so it does not jump wildly
- $smoothed_avg_time = (1 - $pctcomplete) * $smoothed_avg_time +
- $pctcomplete * $avgtime;
- my $eta = int($left * $smoothed_avg_time);
- return($total, $completed, $left, $pctcomplete, $avgtime, $eta);
- }
- }
- {
- my ($rev,$reset);
- sub bar {
- # Return:
- # $status = bar with eta, completed jobs, arg and pct
- $rev ||= "\033[7m";
- $reset ||= "\033[0m";
- my($total, $completed, $left, $pctcomplete, $avgtime, $eta) =
- compute_eta();
- my $arg = $Global::newest_job ?
- $Global::newest_job->{'commandline'}->replace_placeholders(["\257<\257>"],0,0) : "";
- # These chars mess up display in the terminal
- $arg =~ tr/[\011-\016\033\302-\365]//d;
- my $bar_text =
- sprintf("%d%% %d:%d=%ds %s",
- $pctcomplete*100, $completed, $left, $eta, $arg);
- my $terminal_width = terminal_columns();
- my $s = sprintf("%-${terminal_width}s",
- substr($bar_text." "x$terminal_width,
- 0,$terminal_width));
- my $width = int($terminal_width * $pctcomplete);
- substr($s,$width,0) = $reset;
- my $zenity = sprintf("%-${terminal_width}s",
- substr("# $eta sec $arg",
- 0,$terminal_width));
- $s = "\r" . $zenity . "\r" . $pctcomplete*100 . # Prefix with zenity header
- "\r" . $rev . $s . $reset;
- return $s;
- }
- }
- {
- my ($columns,$last_column_time);
- sub terminal_columns {
- # Get the number of columns of the display
- # Returns:
- # number of columns of the screen
- if(not $columns or $last_column_time < time) {
- $last_column_time = time;
- $columns = $ENV{'COLUMNS'};
- if(not $columns) {
- my $resize = qx{ resize 2>/dev/null };
- $resize =~ /COLUMNS=(\d+);/ and do { $columns = $1; };
- }
- $columns ||= 80;
- }
- return $columns;
- }
- }
- sub get_job_with_sshlogin {
- # Returns:
- # next job object for $sshlogin if any available
- my $sshlogin = shift;
- my $job = undef;
- if ($opt::hostgroups) {
- my @other_hostgroup_jobs = ();
- while($job = $Global::JobQueue->get()) {
- if($sshlogin->in_hostgroups($job->hostgroups())) {
- # Found a job for this hostgroup
- last;
- } else {
- # This job was not in the hostgroups of $sshlogin
- push @other_hostgroup_jobs, $job;
- }
- }
- $Global::JobQueue->unget(@other_hostgroup_jobs);
- if(not defined $job) {
- # No more jobs
- return undef;
- }
- } else {
- $job = $Global::JobQueue->get();
- if(not defined $job) {
- # No more jobs
- ::debug("start", "No more jobs: JobQueue empty\n");
- return undef;
- }
- }
- my $clean_command = $job->replaced();
- if($clean_command =~ /^\s*$/) {
- # Do not run empty lines
- if(not $Global::JobQueue->empty()) {
- return get_job_with_sshlogin($sshlogin);
- } else {
- return undef;
- }
- }
- $job->set_sshlogin($sshlogin);
- if($opt::retries and $clean_command and
- $job->failed_here()) {
- # This command with these args failed for this sshlogin
- my ($no_of_failed_sshlogins,$min_failures) = $job->min_failed();
- # Only look at the Global::host that have > 0 jobslots
- if($no_of_failed_sshlogins == grep { $_->max_jobs_running() > 0 } values %Global::host
- and $job->failed_here() == $min_failures) {
- # It failed the same or more times on another host:
- # run it on this host
- } else {
- # If it failed fewer times on another host:
- # Find another job to run
- my $nextjob;
- if(not $Global::JobQueue->empty()) {
- # This can potentially recurse for all args
- no warnings 'recursion';
- $nextjob = get_job_with_sshlogin($sshlogin);
- }
- # Push the command back on the queue
- $Global::JobQueue->unget($job);
- return $nextjob;
- }
- }
- return $job;
- }
- sub __REMOTE_SSH__ {}
- sub read_sshloginfiles {
- # Returns: N/A
- for my $s (@_) {
- read_sshloginfile(expand_slf_shorthand($s));
- }
- }
- sub expand_slf_shorthand {
- my $file = shift;
- if($file eq "-") {
- # skip: It is stdin
- } elsif($file eq "..") {
- $file = $ENV{'HOME'}."/.parallel/sshloginfile";
- } elsif($file eq ".") {
- $file = "/etc/parallel/sshloginfile";
- } elsif(not -r $file) {
- if(not -r $ENV{'HOME'}."/.parallel/".$file) {
- # Try prepending ~/.parallel
- ::error("Cannot open $file.\n");
- ::wait_and_exit(255);
- } else {
- $file = $ENV{'HOME'}."/.parallel/".$file;
- }
- }
- return $file;
- }
- sub read_sshloginfile {
- # Returns: N/A
- my $file = shift;
- my $close = 1;
- my $in_fh;
- ::debug("init","--slf ",$file);
- if($file eq "-") {
- $in_fh = *STDIN;
- $close = 0;
- } else {
- if(not open($in_fh, "<", $file)) {
- # Try the filename
- ::error("Cannot open $file.\n");
- ::wait_and_exit(255);
- }
- }
- while(<$in_fh>) {
- chomp;
- /^\s*#/ and next;
- /^\s*$/ and next;
- push @Global::sshlogin, $_;
- }
- if($close) {
- close $in_fh;
- }
- }
- sub parse_sshlogin {
- # Returns: N/A
- my @login;
- if(not @Global::sshlogin) { @Global::sshlogin = (":"); }
- for my $sshlogin (@Global::sshlogin) {
- # Split up -S sshlogin,sshlogin
- for my $s (split /,/, $sshlogin) {
- if ($s eq ".." or $s eq "-") {
- # This may add to @Global::sshlogin - possibly bug
- read_sshloginfile(expand_slf_shorthand($s));
- } else {
- push (@login, $s);
- }
- }
- }
- $Global::minimal_command_line_length = 8_000_000;
- my @allowed_hostgroups;
- for my $ncpu_sshlogin_string (::uniq(@login)) {
- my $sshlogin = SSHLogin->new($ncpu_sshlogin_string);
- my $sshlogin_string = $sshlogin->string();
- if($sshlogin_string eq "") {
- # This is an ssh group: -S @webservers
- push @allowed_hostgroups, $sshlogin->hostgroups();
- next;
- }
- if($Global::host{$sshlogin_string}) {
- # This sshlogin has already been added:
- # It is probably a host that has come back
- # Set the max_jobs_running back to the original
- debug("run","Already seen $sshlogin_string\n");
- if($sshlogin->{'ncpus'}) {
- # If ncpus set by '#/' of the sshlogin, overwrite it:
- $Global::host{$sshlogin_string}->set_ncpus($sshlogin->ncpus());
- }
- $Global::host{$sshlogin_string}->set_max_jobs_running(undef);
- next;
- }
- if($sshlogin_string eq ":") {
- $sshlogin->set_maxlength(Limits::Command::max_length());
- } else {
- # If all chars needs to be quoted, every other character will be \
- $sshlogin->set_maxlength(int(Limits::Command::max_length()/2));
- }
- $Global::minimal_command_line_length =
- ::min($Global::minimal_command_line_length, $sshlogin->maxlength());
- $Global::host{$sshlogin_string} = $sshlogin;
- }
- if(@allowed_hostgroups) {
- # Remove hosts that are not in these groups
- while (my ($string, $sshlogin) = each %Global::host) {
- if(not $sshlogin->in_hostgroups(@allowed_hostgroups)) {
- delete $Global::host{$string};
- }
- }
- }
- # debug("start", "sshlogin: ", my_dump(%Global::host),"\n");
- if($opt::transfer or @opt::return or $opt::cleanup or @opt::basefile) {
- if(not remote_hosts()) {
- # There are no remote hosts
- if(@opt::trc) {
- ::warning("--trc ignored as there are no remote --sshlogin.\n");
- } elsif (defined $opt::transfer) {
- ::warning("--transfer ignored as there are no remote --sshlogin.\n");
- } elsif (@opt::return) {
- ::warning("--return ignored as there are no remote --sshlogin.\n");
- } elsif (defined $opt::cleanup) {
- ::warning("--cleanup ignored as there are no remote --sshlogin.\n");
- } elsif (@opt::basefile) {
- ::warning("--basefile ignored as there are no remote --sshlogin.\n");
- }
- }
- }
- }
- sub remote_hosts {
- # Return sshlogins that are not ':'
- # Returns:
- # list of sshlogins with ':' removed
- return grep !/^:$/, keys %Global::host;
- }
- sub setup_basefile {
- # Transfer basefiles to each $sshlogin
- # This needs to be done before first jobs on $sshlogin is run
- # Returns: N/A
- my $cmd = "";
- my $rsync_destdir;
- my $workdir;
- for my $sshlogin (values %Global::host) {
- if($sshlogin->string() eq ":") { next }
- for my $file (@opt::basefile) {
- if($file !~ m:^/: and $opt::workdir eq "...") {
- ::error("Work dir '...' will not work with relative basefiles\n");
- ::wait_and_exit(255);
- }
- $workdir ||= Job->new("")->workdir();
- $cmd .= $sshlogin->rsync_transfer_cmd($file,$workdir) . "&";
- }
- }
- $cmd .= "wait;";
- debug("init", "basesetup: $cmd\n");
- print `$cmd`;
- }
- sub cleanup_basefile {
- # Remove the basefiles transferred
- # Returns: N/A
- my $cmd="";
- my $workdir = Job->new("")->workdir();
- for my $sshlogin (values %Global::host) {
- if($sshlogin->string() eq ":") { next }
- for my $file (@opt::basefile) {
- $cmd .= $sshlogin->cleanup_cmd($file,$workdir)."&";
- }
- }
- $cmd .= "wait;";
- debug("init", "basecleanup: $cmd\n");
- print `$cmd`;
- }
- sub filter_hosts {
- my(@cores, @cpus, @maxline, @echo);
- my $envvar = ::shell_quote_scalar($Global::envvar);
- while (my ($host, $sshlogin) = each %Global::host) {
- if($host eq ":") { next }
- # The 'true' is used to get the $host out later
- my $sshcmd = "true $host;" . $sshlogin->sshcommand()." ".$sshlogin->serverlogin();
- push(@cores, $host."\t".$sshcmd." ".$envvar." parallel --number-of-cores\n\0");
- push(@cpus, $host."\t".$sshcmd." ".$envvar." parallel --number-of-cpus\n\0");
- push(@maxline, $host."\t".$sshcmd." ".$envvar." parallel --max-line-length-allowed\n\0");
- # 'echo' is used to get the best possible value for an ssh login time
- push(@echo, $host."\t".$sshcmd." echo\n\0");
- }
- my ($fh, $tmpfile) = ::tmpfile(SUFFIX => ".ssh");
- print $fh @cores, @cpus, @maxline, @echo;
- close $fh;
- # --timeout 5: Setting up an SSH connection and running a simple
- # command should never take > 5 sec.
- # --delay 0.1: If multiple sshlogins use the same proxy the delay
- # will make it less likely to overload the ssh daemon.
- # --retries 3: If the ssh daemon it overloaded, try 3 times
- # -s 16000: Half of the max line on UnixWare
- my $cmd = "cat $tmpfile | $0 -j0 --timeout 5 -s 16000 --joblog - --plain --delay 0.1 --retries 3 --tag --tagstring {1} -0 --colsep '\t' -k eval {2} 2>/dev/null";
- ::debug("init", $cmd, "\n");
- open(my $host_fh, "-|", $cmd) || ::die_bug("parallel host check: $cmd");
- my (%ncores, %ncpus, %time_to_login, %maxlen, %echo, @down_hosts);
- my $prepend = "";
- while(<$host_fh>) {
- if(/\'$/) {
- # if last char = ' then append next line
- # This may be due to quoting of $Global::envvar
- $prepend .= $_;
- next;
- }
- $_ = $prepend . $_;
- $prepend = "";
- chomp;
- my @col = split /\t/, $_;
- if(defined $col[6]) {
- # This is a line from --joblog
- # seq host time spent sent received exit signal command
- # 2 : 1372607672.654 0.675 0 0 0 0 eval true\ m\;ssh\ m\ parallel\ --number-of-cores
- if($col[0] eq "Seq" and $col[1] eq "Host" and
- $col[2] eq "Starttime") {
- # Header => skip
- next;
- }
- # Get server from: eval true server\;
- $col[8] =~ /eval true..([^;]+).;/ or ::die_bug("col8 does not contain host: $col[8]");
- my $host = $1;
- $host =~ tr/\\//d;
- $Global::host{$host} or next;
- if($col[6] eq "255" or $col[7] eq "15") {
- # exit == 255 or signal == 15: ssh failed
- # Remove sshlogin
- ::debug("init", "--filtered $host\n");
- push(@down_hosts, $host);
- @down_hosts = uniq(@down_hosts);
- } elsif($col[6] eq "127") {
- # signal == 127: parallel not installed remote
- # Set ncpus and ncores = 1
- ::warning("Could not figure out ",
- "number of cpus on $host. Using 1.\n");
- $ncores{$host} = 1;
- $ncpus{$host} = 1;
- $maxlen{$host} = Limits::Command::max_length();
- } elsif($col[0] =~ /^\d+$/ and $Global::host{$host}) {
- # Remember how log it took to log in
- # 2 : 1372607672.654 0.675 0 0 0 0 eval true\ m\;ssh\ m\ echo
- $time_to_login{$host} = ::min($time_to_login{$host},$col[3]);
- } else {
- ::die_bug("host check unmatched long jobline: $_");
- }
- } elsif($Global::host{$col[0]}) {
- # This output from --number-of-cores, --number-of-cpus,
- # --max-line-length-allowed
- # ncores: server 8
- # ncpus: server 2
- # maxlen: server 131071
- if(not $ncores{$col[0]}) {
- $ncores{$col[0]} = $col[1];
- } elsif(not $ncpus{$col[0]}) {
- $ncpus{$col[0]} = $col[1];
- } elsif(not $maxlen{$col[0]}) {
- $maxlen{$col[0]} = $col[1];
- } elsif(not $echo{$col[0]}) {
- $echo{$col[0]} = $col[1];
- } elsif(m/perl: warning:|LANGUAGE =|LC_ALL =|LANG =|are supported and installed/) {
- # Skip these:
- # perl: warning: Setting locale failed.
- # perl: warning: Please check that your locale settings:
- # LANGUAGE = (unset),
- # LC_ALL = (unset),
- # LANG = "en_US.UTF-8"
- # are supported and installed on your system.
- # perl: warning: Falling back to the standard locale ("C").
- } else {
- ::die_bug("host check too many col0: $_");
- }
- } else {
- ::die_bug("host check unmatched short jobline ($col[0]): $_");
- }
- }
- close $host_fh;
- $Global::debug or unlink $tmpfile;
- delete @Global::host{@down_hosts};
- @down_hosts and ::warning("Removed @down_hosts\n");
- $Global::minimal_command_line_length = 8_000_000;
- while (my ($sshlogin, $obj) = each %Global::host) {
- if($sshlogin eq ":") { next }
- $ncpus{$sshlogin} or ::die_bug("ncpus missing: ".$obj->serverlogin());
- $ncores{$sshlogin} or ::die_bug("ncores missing: ".$obj->serverlogin());
- $time_to_login{$sshlogin} or ::die_bug("time_to_login missing: ".$obj->serverlogin());
- $maxlen{$sshlogin} or ::die_bug("maxlen missing: ".$obj->serverlogin());
- if($opt::use_cpus_instead_of_cores) {
- $obj->set_ncpus($ncpus{$sshlogin});
- } else {
- $obj->set_ncpus($ncores{$sshlogin});
- }
- $obj->set_time_to_login($time_to_login{$sshlogin});
- $obj->set_maxlength($maxlen{$sshlogin});
- $Global::minimal_command_line_length =
- ::min($Global::minimal_command_line_length,
- int($maxlen{$sshlogin}/2));
- ::debug("init", "Timing from -S:$sshlogin ncpus:",$ncpus{$sshlogin},
- " ncores:", $ncores{$sshlogin},
- " time_to_login:", $time_to_login{$sshlogin},
- " maxlen:", $maxlen{$sshlogin},
- " min_max_len:", $Global::minimal_command_line_length,"\n");
- }
- }
- sub onall {
- sub tmp_joblog {
- my $joblog = shift;
- if(not defined $joblog) {
- return undef;
- }
- my ($fh, $tmpfile) = ::tmpfile(SUFFIX => ".log");
- close $fh;
- return $tmpfile;
- }
- my @command = @_;
- if($Global::quoting) {
- @command = shell_quote_empty(@command);
- }
- # Copy all @fhlist into tempfiles
- my @argfiles = ();
- for my $fh (@fhlist) {
- my ($outfh, $name) = ::tmpfile(SUFFIX => ".all", UNLINK => 1);
- print $outfh (<$fh>);
- close $outfh;
- push @argfiles, $name;
- }
- if(@opt::basefile) { setup_basefile(); }
- # for each sshlogin do:
- # parallel -S $sshlogin $command :::: @argfiles
- #
- # Pass some of the options to the sub-parallels, not all of them as
- # -P should only go to the first, and -S should not be copied at all.
- my $options =
- join(" ",
- ((defined $opt::jobs) ? "-P $opt::jobs" : ""),
- ((defined $opt::linebuffer) ? "--linebuffer" : ""),
- ((defined $opt::ungroup) ? "-u" : ""),
- ((defined $opt::group) ? "-g" : ""),
- ((defined $opt::keeporder) ? "--keeporder" : ""),
- ((defined $opt::D) ? "-D $opt::D" : ""),
- ((defined $opt::plain) ? "--plain" : ""),
- ((defined $opt::max_chars) ? "--max-chars ".$opt::max_chars : ""),
- );
- my $suboptions =
- join(" ",
- ((defined $opt::ungroup) ? "-u" : ""),
- ((defined $opt::linebuffer) ? "--linebuffer" : ""),
- ((defined $opt::group) ? "-g" : ""),
- ((defined $opt::files) ? "--files" : ""),
- ((defined $opt::keeporder) ? "--keeporder" : ""),
- ((defined $opt::colsep) ? "--colsep ".shell_quote($opt::colsep) : ""),
- ((@opt::v) ? "-vv" : ""),
- ((defined $opt::D) ? "-D $opt::D" : ""),
- ((defined $opt::timeout) ? "--timeout ".$opt::timeout : ""),
- ((defined $opt::plain) ? "--plain" : ""),
- ((defined $opt::retries) ? "--retries ".$opt::retries : ""),
- ((defined $opt::max_chars) ? "--max-chars ".$opt::max_chars : ""),
- ((defined $opt::arg_sep) ? "--arg-sep ".$opt::arg_sep : ""),
- ((defined $opt::arg_file_sep) ? "--arg-file-sep ".$opt::arg_file_sep : ""),
- (@opt::env ? map { "--env ".::shell_quote_scalar($_) } @opt::env : ""),
- );
- ::debug("init", "| $0 $options\n");
- open(my $parallel_fh, "|-", "$0 --no-notice -j0 $options") ||
- ::die_bug("This does not run GNU Parallel: $0 $options");
- my @joblogs;
- for my $host (sort keys %Global::host) {
- my $sshlogin = $Global::host{$host};
- my $joblog = tmp_joblog($opt::joblog);
- if($joblog) {
- push @joblogs, $joblog;
- $joblog = "--joblog $joblog";
- }
- my $quad = $opt::arg_file_sep || "::::";
- ::debug("init", "$0 $suboptions -j1 $joblog ",
- ((defined $opt::tag) ?
- "--tagstring ".shell_quote_scalar($sshlogin->string()) : ""),
- " -S ", shell_quote_scalar($sshlogin->string())," ",
- join(" ",shell_quote(@command))," $quad @argfiles\n");
- print $parallel_fh "$0 $suboptions -j1 $joblog ",
- ((defined $opt::tag) ?
- "--tagstring ".shell_quote_scalar($sshlogin->string()) : ""),
- " -S ", shell_quote_scalar($sshlogin->string())," ",
- join(" ",shell_quote(@command))," $quad @argfiles\n";
- }
- close $parallel_fh;
- $Global::exitstatus = $? >> 8;
- debug("init", "--onall exitvalue ", $?);
- if(@opt::basefile) { cleanup_basefile(); }
- $Global::debug or unlink(@argfiles);
- my %seen;
- for my $joblog (@joblogs) {
- # Append to $joblog
- open(my $fh, "<", $joblog) || ::die_bug("Cannot open tmp joblog $joblog");
- # Skip first line (header);
- <$fh>;
- print $Global::joblog (<$fh>);
- close $fh;
- unlink($joblog);
- }
- }
- sub __SIGNAL_HANDLING__ {}
- sub save_original_signal_handler {
- # Remember the original signal handler
- # Returns: N/A
- $SIG{TERM} ||= sub { exit 0; }; # $SIG{TERM} is not set on Mac OS X
- $SIG{INT} = sub { if($opt::tmux) { qx { tmux kill-session -t p$$ }; }
- unlink keys %Global::unlink; exit -1 };
- $SIG{TERM} = sub { if($opt::tmux) { qx { tmux kill-session -t p$$ }; }
- unlink keys %Global::unlink; exit -1 };
- %Global::original_sig = %SIG;
- $SIG{TERM} = sub {}; # Dummy until jobs really start
- }
- sub list_running_jobs {
- # Returns: N/A
- for my $v (values %Global::running) {
- print $Global::original_stderr "$Global::progname: ",$v->replaced(),"\n";
- }
- }
- sub start_no_new_jobs {
- # Returns: N/A
- $SIG{TERM} = $Global::original_sig{TERM};
- print $Global::original_stderr
- ("$Global::progname: SIGTERM received. No new jobs will be started.\n",
- "$Global::progname: Waiting for these ", scalar(keys %Global::running),
- " jobs to finish. Send SIGTERM again to stop now.\n");
- list_running_jobs();
- $Global::start_no_new_jobs ||= 1;
- }
- sub reaper {
- # A job finished.
- # Print the output.
- # Start another job
- # Returns: N/A
- my $stiff;
- my $children_reaped = 0;
- debug("run", "Reaper ");
- while (($stiff = waitpid(-1, &WNOHANG)) > 0) {
- $children_reaped++;
- if($Global::sshmaster{$stiff}) {
- # This is one of the ssh -M: ignore
- next;
- }
- my $job = $Global::running{$stiff};
- # '-a <(seq 10)' will give us a pid not in %Global::running
- $job or next;
- $job->set_exitstatus($? >> 8);
- $job->set_exitsignal($? & 127);
- debug("run", "died (", $job->exitstatus(), "): ", $job->seq());
- $job->set_endtime(::now());
- if($stiff == $Global::tty_taken) {
- # The process that died had the tty => release it
- $Global::tty_taken = 0;
- }
- if(not $job->should_be_retried()) {
- # The job is done
- # Free the jobslot
- push @Global::slots, $job->slot();
- if($opt::timeout) {
- # Update average runtime for timeout
- $Global::timeoutq->update_delta_time($job->runtime());
- }
- # Force printing now if the job failed and we are going to exit
- my $print_now = ($opt::halt_on_error and $opt::halt_on_error == 2
- and $job->exitstatus());
- if($opt::keeporder and not $print_now) {
- print_earlier_jobs($job);
- } else {
- $job->print();
- }
- if($job->exitstatus()) {
- process_failed_job($job);
- }
- }
- my $sshlogin = $job->sshlogin();
- $sshlogin->dec_jobs_running();
- $sshlogin->inc_jobs_completed();
- $Global::total_running--;
- delete $Global::running{$stiff};
- start_more_jobs();
- }
- debug("run", "done ");
- return $children_reaped;
- }
- sub process_failed_job {
- # The jobs had a exit status <> 0, so error
- # Returns: N/A
- my $job = shift;
- $Global::exitstatus++;
- $Global::total_failed++;
- if($opt::halt_on_error) {
- if($opt::halt_on_error == 1
- or
- ($opt::halt_on_error < 1 and $Global::total_failed > 3
- and
- $Global::total_failed / $Global::total_started > $opt::halt_on_error)) {
- # If halt on error == 1 or --halt 10%
- # we should gracefully exit
- print $Global::original_stderr
- ("$Global::progname: Starting no more jobs. ",
- "Waiting for ", scalar(keys %Global::running),
- " jobs to finish. This job failed:\n",
- $job->replaced(),"\n");
- $Global::start_no_new_jobs ||= 1;
- $Global::halt_on_error_exitstatus = $job->exitstatus();
- } elsif($opt::halt_on_error == 2) {
- # If halt on error == 2 we should exit immediately
- print $Global::original_stderr
- ("$Global::progname: This job failed:\n",
- $job->replaced(),"\n");
- exit ($job->exitstatus());
- }
- }
- }
- {
- my (%print_later,$job_end_sequence);
- sub print_earlier_jobs {
- # Print jobs completed earlier
- # Returns: N/A
- my $job = shift;
- $print_later{$job->seq()} = $job;
- $job_end_sequence ||= 1;
- debug("run", "Looking for: $job_end_sequence ",
- "Current: ", $job->seq(), "\n");
- for(my $j = $print_later{$job_end_sequence};
- $j or vec($Global::job_already_run,$job_end_sequence,1);
- $job_end_sequence++,
- $j = $print_later{$job_end_sequence}) {
- debug("run", "Found job end $job_end_sequence");
- if($j) {
- $j->print();
- delete $print_later{$job_end_sequence};
- }
- }
- }
- }
- sub __USAGE__ {}
- sub wait_and_exit {
- # If we do not wait, we sometimes get segfault
- # Returns: N/A
- my $error = shift;
- if($error) {
- # Kill all without printing
- for my $job (values %Global::running) {
- $job->kill("TERM");
- $job->kill("TERM");
- }
- }
- for (keys %Global::unkilled_children) {
- kill 9, $_;
- waitpid($_,0);
- delete $Global::unkilled_children{$_};
- }
- wait();
- exit($error);
- }
- sub die_usage {
- # Returns: N/A
- usage();
- wait_and_exit(255);
- }
- sub usage {
- # Returns: N/A
- print join
- ("\n",
- "Usage:",
- "",
- "$Global::progname [options] [command [arguments]] < list_of_arguments",
- "$Global::progname [options] [command [arguments]] (::: arguments|:::: argfile(s))...",
- "cat ... | $Global::progname --pipe [options] [command [arguments]]",
- "",
- "-j n Run n jobs in parallel",
- "-k Keep same order",
- "-X Multiple arguments with context replace",
- "--colsep regexp Split input on regexp for positional replacements",
- "{} {.} {/} {/.} {#} {%} {= perl code =} Replacement strings",
- "{3} {3.} {3/} {3/.} {=3 perl code =} Positional replacement strings",
- "With --plus: {} = {+/}/{/} = {.}.{+.} = {+/}/{/.}.{+.} = {..}.{+..} =",
- " {+/}/{/..}.{+..} = {...}.{+...} = {+/}/{/...}.{+...}",
- "",
- "-S sshlogin Example: foo\@server.example.com",
- "--slf .. Use ~/.parallel/sshloginfile as the list of sshlogins",
- "--trc {}.bar Shorthand for --transfer --return {}.bar --cleanup",
- "--onall Run the given command with argument on all sshlogins",
- "--nonall Run the given command with no arguments on all sshlogins",
- "",
- "--pipe Split stdin (standard input) to multiple jobs.",
- "--recend str Record end separator for --pipe.",
- "--recstart str Record start separator for --pipe.",
- "",
- "See 'man $Global::progname' for details",
- "",
- "When using programs that use GNU Parallel to process data for publication please cite:",
- "",
- "O. Tange (2011): GNU Parallel - The Command-Line Power Tool,",
- ";login: The USENIX Magazine, February 2011:42-47.",
- "",
- "Or you can get GNU Parallel without this requirement by paying 10000 EUR.",
- "");
- }
- sub citation_notice {
- # if --no-notice or --plain: do nothing
- # if stderr redirected: do nothing
- # if ~/.parallel/will-cite: do nothing
- # else: print citation notice to stderr
- if($opt::no_notice
- or
- $opt::plain
- or
- not -t $Global::original_stderr
- or
- -e $ENV{'HOME'}."/.parallel/will-cite") {
- # skip
- } else {
- print $Global::original_stderr
- ("When using programs that use GNU Parallel to process data for publication please cite:\n",
- "\n",
- " O. Tange (2011): GNU Parallel - The Command-Line Power Tool,\n",
- " ;login: The USENIX Magazine, February 2011:42-47.\n",
- "\n",
- "This helps funding further development; and it won't cost you a cent.\n",
- "Or you can get GNU Parallel without this requirement by paying 10000 EUR.\n",
- "\n",
- "To silence this citation notice run 'parallel --bibtex' once or use '--no-notice'.\n\n",
- );
- flush $Global::original_stderr;
- }
- }
- sub warning {
- my @w = @_;
- my $fh = $Global::original_stderr || *STDERR;
- my $prog = $Global::progname || "parallel";
- print $fh $prog, ": Warning: ", @w;
- }
- sub error {
- my @w = @_;
- my $fh = $Global::original_stderr || *STDERR;
- my $prog = $Global::progname || "parallel";
- print $fh $prog, ": Error: ", @w;
- }
- sub die_bug {
- my $bugid = shift;
- print STDERR
- ("$Global::progname: This should not happen. You have found a bug.\n",
- "Please contact <parallel\@gnu.org> and include:\n",
- "* The version number: $Global::version\n",
- "* The bugid: $bugid\n",
- "* The command line being run\n",
- "* The files being read (put the files on a webserver if they are big)\n",
- "\n",
- "If you get the error on smaller/fewer files, please include those instead.\n");
- ::wait_and_exit(255);
- }
- sub version {
- # Returns: N/A
- if($opt::tollef and not $opt::gnu) {
- print "WARNING: YOU ARE USING --tollef. IF THINGS ARE ACTING WEIRD USE --gnu.\n";
- }
- print join("\n",
- "GNU $Global::progname $Global::version",
- "Copyright (C) 2007,2008,2009,2010,2011,2012,2013,2014 Ole Tange and Free Software Foundation, Inc.",
- "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>",
- "This is free software: you are free to change and redistribute it.",
- "GNU $Global::progname comes with no warranty.",
- "",
- "Web site: http://www.gnu.org/software/${Global::progname}\n",
- "When using programs that use GNU Parallel to process data for publication please cite:\n",
- "O. Tange (2011): GNU Parallel - The Command-Line Power Tool, ",
- ";login: The USENIX Magazine, February 2011:42-47.\n",
- "Or you can get GNU Parallel without this requirement by paying 10000 EUR.\n",
- );
- }
- sub bibtex {
- # Returns: N/A
- if($opt::tollef and not $opt::gnu) {
- print "WARNING: YOU ARE USING --tollef. IF THINGS ARE ACTING WEIRD USE --gnu.\n";
- }
- print join("\n",
- "When using programs that use GNU Parallel to process data for publication please cite:",
- "",
- "\@article{Tange2011a,",
- " title = {GNU Parallel - The Command-Line Power Tool},",
- " author = {O. Tange},",
- " address = {Frederiksberg, Denmark},",
- " journal = {;login: The USENIX Magazine},",
- " month = {Feb},",
- " number = {1},",
- " volume = {36},",
- " url = {http://www.gnu.org/s/parallel},",
- " year = {2011},",
- " pages = {42-47}",
- "}",
- "",
- "(Feel free to use \\nocite{Tange2011a})",
- "",
- "This helps funding further development.",
- "",
- "Or you can get GNU Parallel without this requirement by paying 10000 EUR.",
- ""
- );
- while(not -e $ENV{'HOME'}."/.parallel/will-cite") {
- print "\nType: 'will cite' and press enter.\n> ";
- my $input = <STDIN>;
- if($input =~ /will cite/i) {
- mkdir $ENV{'HOME'}."/.parallel";
- open (my $fh, ">", $ENV{'HOME'}."/.parallel/will-cite")
- || ::die_bug("Cannot write: ".$ENV{'HOME'}."/.parallel/will-cite");
- close $fh;
- print "\nThank you for your support. It is much appreciated. The citation\n",
- "notice is now silenced.\n";
- }
- }
- }
- sub show_limits {
- # Returns: N/A
- print("Maximal size of command: ",Limits::Command::real_max_length(),"\n",
- "Maximal used size of command: ",Limits::Command::max_length(),"\n",
- "\n",
- "Execution of will continue now, and it will try to read its input\n",
- "and run commands; if this is not what you wanted to happen, please\n",
- "press CTRL-D or CTRL-C\n");
- }
- sub __GENERIC_COMMON_FUNCTION__ {}
- sub uniq {
- # Remove duplicates and return unique values
- return keys %{{ map { $_ => 1 } @_ }};
- }
- sub min {
- # Returns:
- # Minimum value of array
- my $min;
- for (@_) {
- # Skip undefs
- defined $_ or next;
- defined $min or do { $min = $_; next; }; # Set $_ to the first non-undef
- $min = ($min < $_) ? $min : $_;
- }
- return $min;
- }
- sub max {
- # Returns:
- # Maximum value of array
- my $max;
- for (@_) {
- # Skip undefs
- defined $_ or next;
- defined $max or do { $max = $_; next; }; # Set $_ to the first non-undef
- $max = ($max > $_) ? $max : $_;
- }
- return $max;
- }
- sub sum {
- # Returns:
- # Sum of values of array
- my @args = @_;
- my $sum = 0;
- for (@args) {
- # Skip undefs
- $_ and do { $sum += $_; }
- }
- return $sum;
- }
- sub undef_as_zero {
- my $a = shift;
- return $a ? $a : 0;
- }
- sub undef_as_empty {
- my $a = shift;
- return $a ? $a : "";
- }
- {
- my $hostname;
- sub hostname {
- if(not $hostname) {
- $hostname = `hostname`;
- chomp($hostname);
- $hostname ||= "nohostname";
- }
- return $hostname;
- }
- }
- sub which {
- # Input:
- # @programs = programs to find the path to
- # Returns:
- # @full_path = full paths to @programs. Nothing if not found
- my @which;
- for my $prg (@_) {
- push @which, map { $_."/".$prg } grep { -x $_."/".$prg } split(":",$ENV{'PATH'});
- }
- return @which;
- }
- {
- my ($regexp,%fakename);
- sub parent_shell {
- # Input:
- # $pid = pid to see if (grand)*parent is a shell
- # Returns:
- # $shellpath = path to shell - undef if no shell found
- my $pid = shift;
- if(not $regexp) {
- # All shells known to mankind
- #
- # ash bash csh dash fdsh fish fizsh ksh ksh93 mksh pdksh
- # posh rbash rush rzsh sash sh static-sh tcsh yash zsh
- my @shells = qw(ash bash csh dash fdsh fish fizsh ksh
- ksh93 mksh pdksh posh rbash rush rzsh
- sash sh static-sh tcsh yash zsh -sh -csh);
- # Can be formatted as:
- # [sh] -sh sh busybox sh
- # /bin/sh /sbin/sh /opt/csw/sh
- # NOT: foo.sh sshd crash flush pdflush scosh fsflush ssh
- my $shell = "(?:".join("|",@shells).")";
- $regexp = '^((\[)('. $shell. ')(\])|(|\S+/|busybox )('. $shell. '))($| )';
- %fakename = (
- # csh and tcsh disguise themselves as -sh/-csh
- "-sh" => ["csh", "tcsh"],
- "-csh" => ["tcsh", "csh"],
- );
- }
- my ($children_of_ref, $parent_of_ref, $name_of_ref) = pid_table();
- my $shellpath;
- my $testpid = $pid;
- while($testpid) {
- ::debug("init", "shell? ". $name_of_ref->{$testpid}."\n");
- if($name_of_ref->{$testpid} =~ /$regexp/o) {
- ::debug("init", "which ".($3||$6)." => ");
- $shellpath = (which($3 || $6,@{$fakename{$3 || $6}}))[0];
- ::debug("init", "shell path $shellpath\n");
- $shellpath and last;
- }
- $testpid = $parent_of_ref->{$testpid};
- }
- return $shellpath;
- }
- }
- {
- my %pid_parentpid_cmd;
- sub pid_table {
- # Returns:
- # %children_of = { pid -> children of pid }
- # %parent_of = { pid -> pid of parent }
- # %name_of = { pid -> commandname }
- if(not %pid_parentpid_cmd) {
- # Filter for SysV-style `ps`
- my $sysv = q( ps -ef | perl -ane '1..1 and /^(.*)CO?MM?A?N?D/ and $s=length $1;).
- q(s/^.{$s}//; print "@F[1,2] $_"' );
- # BSD-style `ps`
- my $bsd = q(ps -o pid,ppid,command -ax);
- %pid_parentpid_cmd =
- (
- 'aix' => $sysv,
- 'cygwin' => $sysv,
- 'msys' => $sysv,
- 'dec_osf' => $sysv,
- 'darwin' => $bsd,
- 'dragonfly' => $bsd,
- 'freebsd' => $bsd,
- 'gnu' => $sysv,
- 'hpux' => $sysv,
- 'linux' => $sysv,
- 'mirbsd' => $bsd,
- 'netbsd' => $bsd,
- 'nto' => $sysv,
- 'openbsd' => $bsd,
- 'solaris' => $sysv,
- 'svr5' => $sysv,
- );
- }
- $pid_parentpid_cmd{$^O} or ::die_bug("pid_parentpid_cmd for $^O missing");
- my (@pidtable,%parent_of,%children_of,%name_of);
- # Table with pid -> children of pid
- @pidtable = `$pid_parentpid_cmd{$^O}`;
- my $p=$$;
- for (@pidtable) {
- # must match: 24436 21224 busybox ash
- /(\S+)\s+(\S+)\s+(\S+.*)/ or ::die_bug("pidtable format: $_");
- $parent_of{$1} = $2;
- push @{$children_of{$2}}, $1;
- $name_of{$1} = $3;
- }
- return(\%children_of, \%parent_of, \%name_of);
- }
- }
- sub reap_usleep {
- # Reap dead children.
- # If no dead children: Sleep specified amount with exponential backoff
- # Input:
- # $ms = milliseconds to sleep
- # Returns:
- # $ms/2+0.001 if children reaped
- # $ms*1.1 if no children reaped
- my $ms = shift;
- if(reaper()) {
- # Sleep exponentially shorter (1/2^n) if a job finished
- return $ms/2+0.001;
- } else {
- if($opt::timeout) {
- $Global::timeoutq->process_timeouts();
- }
- usleep($ms);
- Job::exit_if_disk_full();
- if($opt::linebuffer) {
- for my $job (values %Global::running) {
- $job->print();
- }
- }
- # Sleep exponentially longer (1.1^n) if a job did not finish
- # though at most 1000 ms.
- return (($ms < 1000) ? ($ms * 1.1) : ($ms));
- }
- }
- sub usleep {
- # Sleep this many milliseconds.
- # Input:
- # $ms = milliseconds to sleep
- my $ms = shift;
- ::debug(int($ms),"ms ");
- select(undef, undef, undef, $ms/1000);
- }
- sub now {
- # Returns time since epoch as in seconds with 3 decimals
- # Uses:
- # @Global::use
- # Returns:
- # $time = time now with millisecond accuracy
- if(not $Global::use{"Time::HiRes"}) {
- if(eval "use Time::HiRes qw ( time );") {
- eval "sub TimeHiRestime { return Time::HiRes::time };";
- } else {
- eval "sub TimeHiRestime { return time() };";
- }
- $Global::use{"Time::HiRes"} = 1;
- }
- return (int(TimeHiRestime()*1000))/1000;
- }
- sub multiply_binary_prefix {
- # Evalualte numbers with binary prefix
- # Ki=2^10, Mi=2^20, Gi=2^30, Ti=2^40, Pi=2^50, Ei=2^70, Zi=2^80, Yi=2^80
- # ki=2^10, mi=2^20, gi=2^30, ti=2^40, pi=2^50, ei=2^70, zi=2^80, yi=2^80
- # K =2^10, M =2^20, G =2^30, T =2^40, P =2^50, E =2^70, Z =2^80, Y =2^80
- # k =10^3, m =10^6, g =10^9, t=10^12, p=10^15, e=10^18, z=10^21, y=10^24
- # 13G = 13*1024*1024*1024 = 13958643712
- # Input:
- # $s = string with prefixes
- # Returns:
- # $value = int with prefixes multiplied
- my $s = shift;
- $s =~ s/ki/*1024/gi;
- $s =~ s/mi/*1024*1024/gi;
- $s =~ s/gi/*1024*1024*1024/gi;
- $s =~ s/ti/*1024*1024*1024*1024/gi;
- $s =~ s/pi/*1024*1024*1024*1024*1024/gi;
- $s =~ s/ei/*1024*1024*1024*1024*1024*1024/gi;
- $s =~ s/zi/*1024*1024*1024*1024*1024*1024*1024/gi;
- $s =~ s/yi/*1024*1024*1024*1024*1024*1024*1024*1024/gi;
- $s =~ s/xi/*1024*1024*1024*1024*1024*1024*1024*1024*1024/gi;
- $s =~ s/K/*1024/g;
- $s =~ s/M/*1024*1024/g;
- $s =~ s/G/*1024*1024*1024/g;
- $s =~ s/T/*1024*1024*1024*1024/g;
- $s =~ s/P/*1024*1024*1024*1024*1024/g;
- $s =~ s/E/*1024*1024*1024*1024*1024*1024/g;
- $s =~ s/Z/*1024*1024*1024*1024*1024*1024*1024/g;
- $s =~ s/Y/*1024*1024*1024*1024*1024*1024*1024*1024/g;
- $s =~ s/X/*1024*1024*1024*1024*1024*1024*1024*1024*1024/g;
- $s =~ s/k/*1000/g;
- $s =~ s/m/*1000*1000/g;
- $s =~ s/g/*1000*1000*1000/g;
- $s =~ s/t/*1000*1000*1000*1000/g;
- $s =~ s/p/*1000*1000*1000*1000*1000/g;
- $s =~ s/e/*1000*1000*1000*1000*1000*1000/g;
- $s =~ s/z/*1000*1000*1000*1000*1000*1000*1000/g;
- $s =~ s/y/*1000*1000*1000*1000*1000*1000*1000*1000/g;
- $s =~ s/x/*1000*1000*1000*1000*1000*1000*1000*1000*1000/g;
- $s = eval $s;
- ::debug($s);
- return $s;
- }
- sub tmpfile {
- # Create tempfile as $TMPDIR/parXXXXX
- # Returns:
- # $filename = file name created
- return ::tempfile(DIR=>$ENV{'TMPDIR'}, TEMPLATE => 'parXXXXX', @_);
- }
- sub __DEBUGGING__ {}
- sub debug {
- # Uses:
- # $Global::debug
- # %Global::fd
- # Returns: N/A
- $Global::debug or return;
- @_ = grep { defined $_ ? $_ : "" } @_;
- if($Global::debug eq "all" or $Global::debug eq $_[0]) {
- if($Global::fd{1}) {
- # Original stdout was saved
- my $stdout = $Global::fd{1};
- print $stdout @_[1..$#_];
- } else {
- print @_[1..$#_];
- }
- }
- }
- sub my_memory_usage {
- # Returns:
- # memory usage if found
- # 0 otherwise
- use strict;
- use FileHandle;
- my $pid = $$;
- if(-e "/proc/$pid/stat") {
- my $fh = FileHandle->new("</proc/$pid/stat");
- my $data = <$fh>;
- chomp $data;
- $fh->close;
- my @procinfo = split(/\s+/,$data);
- return undef_as_zero($procinfo[22]);
- } else {
- return 0;
- }
- }
- sub my_size {
- # Returns:
- # $size = size of object if Devel::Size is installed
- # -1 otherwise
- my @size_this = (@_);
- eval "use Devel::Size qw(size total_size)";
- if ($@) {
- return -1;
- } else {
- return total_size(@_);
- }
- }
- sub my_dump {
- # Returns:
- # ascii expression of object if Data::Dump(er) is installed
- # error code otherwise
- my @dump_this = (@_);
- eval "use Data::Dump qw(dump);";
- if ($@) {
- # Data::Dump not installed
- eval "use Data::Dumper;";
- if ($@) {
- my $err = "Neither Data::Dump nor Data::Dumper is installed\n".
- "Not dumping output\n";
- print $Global::original_stderr $err;
- return $err;
- } else {
- return Dumper(@dump_this);
- }
- } else {
- # Create a dummy Data::Dump:dump as Hans Schou sometimes has
- # it undefined
- eval "sub Data::Dump:dump {}";
- eval "use Data::Dump qw(dump);";
- return (Data::Dump::dump(@dump_this));
- }
- }
- sub my_croak {
- eval "use Carp; 1";
- $Carp::Verbose = 1;
- croak(@_);
- }
- sub my_carp {
- eval "use Carp; 1";
- $Carp::Verbose = 1;
- carp(@_);
- }
- sub __OBJECT_ORIENTED_PARTS__ {}
- package SSHLogin;
- sub new {
- my $class = shift;
- my $sshlogin_string = shift;
- my $ncpus;
- my %hostgroups;
- # SSHLogins can have these formats:
- # @grp+grp/ncpu//usr/bin/ssh user@server
- # ncpu//usr/bin/ssh user@server
- # /usr/bin/ssh user@server
- # user@server
- # ncpu/user@server
- # @grp+grp/user@server
- if($sshlogin_string =~ s:^\@([^/]+)/?::) {
- # Look for SSHLogin hostgroups
- %hostgroups = map { $_ => 1 } split(/\+/, $1);
- }
- if ($sshlogin_string =~ s:^(\d+)/::) {
- # Override default autodetected ncpus unless missing
- $ncpus = $1;
- }
- my $string = $sshlogin_string;
- # An SSHLogin is always in the hostgroup of its $string-name
- $hostgroups{$string} = 1;
- @Global::hostgroups{keys %hostgroups} = values %hostgroups;
- my @unget = ();
- my $no_slash_string = $string;
- $no_slash_string =~ s/[^-a-z0-9:]/_/gi;
- return bless {
- 'string' => $string,
- 'jobs_running' => 0,
- 'jobs_completed' => 0,
- 'maxlength' => undef,
- 'max_jobs_running' => undef,
- 'orig_max_jobs_running' => undef,
- 'ncpus' => $ncpus,
- 'hostgroups' => \%hostgroups,
- 'sshcommand' => undef,
- 'serverlogin' => undef,
- 'control_path_dir' => undef,
- 'control_path' => undef,
- 'time_to_login' => undef,
- 'last_login_at' => undef,
- 'loadavg_file' => $ENV{'HOME'} . "/.parallel/tmp/loadavg-" .
- $no_slash_string,
- 'loadavg' => undef,
- 'last_loadavg_update' => 0,
- 'swap_activity_file' => $ENV{'HOME'} . "/.parallel/tmp/swap_activity-" .
- $no_slash_string,
- 'swap_activity' => undef,
- }, ref($class) || $class;
- }
- sub DESTROY {
- my $self = shift;
- # Remove temporary files if they are created.
- unlink $self->{'loadavg_file'};
- unlink $self->{'swap_activity_file'};
- }
- sub string {
- my $self = shift;
- return $self->{'string'};
- }
- sub jobs_running {
- my $self = shift;
- return ($self->{'jobs_running'} || "0");
- }
- sub inc_jobs_running {
- my $self = shift;
- $self->{'jobs_running'}++;
- }
- sub dec_jobs_running {
- my $self = shift;
- $self->{'jobs_running'}--;
- }
- sub set_maxlength {
- my $self = shift;
- $self->{'maxlength'} = shift;
- }
- sub maxlength {
- my $self = shift;
- return $self->{'maxlength'};
- }
- sub jobs_completed {
- my $self = shift;
- return $self->{'jobs_completed'};
- }
- sub in_hostgroups {
- # Input:
- # @hostgroups = the hostgroups to look for
- # Returns:
- # true if intersection of @hostgroups and the hostgroups of this
- # SSHLogin is non-empty
- my $self = shift;
- return grep { defined $self->{'hostgroups'}{$_} } @_;
- }
- sub hostgroups {
- my $self = shift;
- return keys %{$self->{'hostgroups'}};
- }
- sub inc_jobs_completed {
- my $self = shift;
- $self->{'jobs_completed'}++;
- }
- sub set_max_jobs_running {
- my $self = shift;
- if(defined $self->{'max_jobs_running'}) {
- $Global::max_jobs_running -= $self->{'max_jobs_running'};
- }
- $self->{'max_jobs_running'} = shift;
- if(defined $self->{'max_jobs_running'}) {
- # max_jobs_running could be resat if -j is a changed file
- $Global::max_jobs_running += $self->{'max_jobs_running'};
- }
- # Initialize orig to the first non-zero value that comes around
- $self->{'orig_max_jobs_running'} ||= $self->{'max_jobs_running'};
- }
- sub swapping {
- my $self = shift;
- my $swapping = $self->swap_activity();
- return (not defined $swapping or $swapping)
- }
- sub swap_activity {
- # If the currently known swap activity is too old:
- # Recompute a new one in the background
- # Returns:
- # last swap activity computed
- my $self = shift;
- # Should we update the swap_activity file?
- my $update_swap_activity_file = 0;
- if(-r $self->{'swap_activity_file'}) {
- open(my $swap_fh, "<", $self->{'swap_activity_file'}) || ::die_bug("swap_activity_file-r");
- my $swap_out = <$swap_fh>;
- close $swap_fh;
- if($swap_out =~ /^(\d+)$/) {
- $self->{'swap_activity'} = $1;
- ::debug("swap", "New swap_activity: ", $self->{'swap_activity'});
- }
- ::debug("swap", "Last update: ", $self->{'last_swap_activity_update'});
- if(time - $self->{'last_swap_activity_update'} > 10) {
- # last swap activity update was started 10 seconds ago
- ::debug("swap", "Older than 10 sec: ", $self->{'swap_activity_file'});
- $update_swap_activity_file = 1;
- }
- } else {
- ::debug("swap", "No swap_activity file: ", $self->{'swap_activity_file'});
- $self->{'swap_activity'} = undef;
- $update_swap_activity_file = 1;
- }
- if($update_swap_activity_file) {
- ::debug("swap", "Updating swap_activity file ", $self->{'swap_activity_file'});
- $self->{'last_swap_activity_update'} = time;
- -e $ENV{'HOME'}."/.parallel" or mkdir $ENV{'HOME'}."/.parallel";
- -e $ENV{'HOME'}."/.parallel/tmp" or mkdir $ENV{'HOME'}."/.parallel/tmp";
- my $swap_activity;
- $swap_activity = swapactivityscript();
- if($self->{'string'} ne ":") {
- $swap_activity = $self->sshcommand() . " " . $self->serverlogin() . " " .
- ::shell_quote_scalar($swap_activity);
- }
- # Run swap_activity measuring.
- # As the command can take long to run if run remote
- # save it to a tmp file before moving it to the correct file
- my $file = $self->{'swap_activity_file'};
- my ($dummy_fh, $tmpfile) = ::tmpfile(SUFFIX => ".swp");
- ::debug("swap", "\n", $swap_activity, "\n");
- qx{ ($swap_activity > $tmpfile && mv $tmpfile $file || rm $tmpfile) & };
- }
- return $self->{'swap_activity'};
- }
- {
- my $script;
- sub swapactivityscript {
- # Returns:
- # shellscript for detecting swap activity
- #
- # arguments for vmstat are OS dependant
- # swap_in and swap_out are in different columns depending on OS
- #
- if(not $script) {
- my %vmstat = (
- # linux: $7*$8
- # $ vmstat 1 2
- # procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
- # r b swpd free buff cache si so bi bo in cs us sy id wa
- # 5 0 51208 1701096 198012 18857888 0 0 37 153 28 19 56 11 33 1
- # 3 0 51208 1701288 198012 18857972 0 0 0 0 3638 10412 15 3 82 0
- 'linux' => ['vmstat 1 2 | tail -n1', '$7*$8'],
- # solaris: $6*$7
- # $ vmstat -S 1 2
- # kthr memory page disk faults cpu
- # r b w swap free si so pi po fr de sr s3 s4 -- -- in sy cs us sy id
- # 0 0 0 4628952 3208408 0 0 3 1 1 0 0 -0 2 0 0 263 613 246 1 2 97
- # 0 0 0 4552504 3166360 0 0 0 0 0 0 0 0 0 0 0 246 213 240 1 1 98
- 'solaris' => ['vmstat -S 1 2 | tail -1', '$6*$7'],
- # darwin (macosx): $21*$22
- # $ vm_stat -c 2 1
- # Mach Virtual Memory Statistics: (page size of 4096 bytes)
- # free active specul inactive throttle wired prgable faults copy 0fill reactive purged file-backed anonymous cmprssed cmprssor dcomprs comprs pageins pageout swapins swapouts
- # 346306 829050 74871 606027 0 240231 90367 544858K 62343596 270837K 14178 415070 570102 939846 356 370 116 922 4019813 4 0 0
- # 345740 830383 74875 606031 0 239234 90369 2696 359 553 0 0 570110 941179 356 370 0 0 0 0 0 0
- 'darwin' => ['vm_stat -c 2 1 | tail -n1', '$21*$22'],
- # ultrix: $12*$13
- # $ vmstat -S 1 2
- # procs faults cpu memory page disk
- # r b w in sy cs us sy id avm fre si so pi po fr de sr s0
- # 1 0 0 4 23 2 3 0 97 7743 217k 0 0 0 0 0 0 0 0
- # 1 0 0 6 40 8 0 1 99 7743 217k 0 0 3 0 0 0 0 0
- 'ultrix' => ['vmstat -S 1 2 | tail -1', '$12*$13'],
- # aix: $6*$7
- # $ vmstat 1 2
- # System configuration: lcpu=1 mem=2048MB
- #
- # kthr memory page faults cpu
- # ----- ----------- ------------------------ ------------ -----------
- # r b avm fre re pi po fr sr cy in sy cs us sy id wa
- # 0 0 333933 241803 0 0 0 0 0 0 10 143 90 0 0 99 0
- # 0 0 334125 241569 0 0 0 0 0 0 37 5368 184 0 9 86 5
- 'aix' => ['vmstat 1 2 | tail -n1', '$6*$7'],
- # freebsd: $8*$9
- # $ vmstat -H 1 2
- # procs memory page disks faults cpu
- # r b w avm fre flt re pi po fr sr ad0 ad1 in sy cs us sy id
- # 1 0 0 596716 19560 32 0 0 0 33 8 0 0 11 220 277 0 0 99
- # 0 0 0 596716 19560 2 0 0 0 0 0 0 0 11 144 263 0 1 99
- 'freebsd' => ['vmstat -H 1 2 | tail -n1', '$8*$9'],
- # mirbsd: $8*$9
- # $ vmstat 1 2
- # procs memory page disks traps cpu
- # r b w avm fre flt re pi po fr sr wd0 cd0 int sys cs us sy id
- # 0 0 0 25776 164968 34 0 0 0 0 0 0 0 230 259 38 4 0 96
- # 0 0 0 25776 164968 24 0 0 0 0 0 0 0 237 275 37 0 0 100
- 'mirbsd' => ['vmstat 1 2 | tail -n1', '$8*$9'],
- # netbsd: $7*$8
- # $ vmstat 1 2
- # procs memory page disks faults cpu
- # r b avm fre flt re pi po fr sr w0 w1 in sy cs us sy id
- # 0 0 138452 6012 54 0 0 0 1 2 3 0 4 100 23 0 0 100
- # 0 0 138456 6008 1 0 0 0 0 0 0 0 7 26 19 0 0 100
- 'netbsd' => ['vmstat 1 2 | tail -n1', '$7*$8'],
- # openbsd: $8*$9
- # $ vmstat 1 2
- # procs memory page disks traps cpu
- # r b w avm fre flt re pi po fr sr wd0 wd1 int sys cs us sy id
- # 0 0 0 76596 109944 73 0 0 0 0 0 0 1 5 259 22 0 1 99
- # 0 0 0 76604 109936 24 0 0 0 0 0 0 0 7 114 20 0 1 99
- 'openbsd' => ['vmstat 1 2 | tail -n1', '$8*$9'],
- # hpux: $8*$9
- # $ vmstat 1 2
- # procs memory page faults cpu
- # r b w avm free re at pi po fr de sr in sy cs us sy id
- # 1 0 0 247211 216476 4 1 0 0 0 0 0 102 73005 54 6 11 83
- # 1 0 0 247211 216421 43 9 0 0 0 0 0 144 1675 96 25269512791222387000 25269512791222387000 105
- 'hpux' => ['vmstat 1 2 | tail -n1', '$8*$9'],
- # dec_osf (tru64): $11*$12
- # $ vmstat 1 2
- # Virtual Memory Statistics: (pagesize = 8192)
- # procs memory pages intr cpu
- # r w u act free wire fault cow zero react pin pout in sy cs us sy id
- # 3 181 36 51K 1895 8696 348M 59M 122M 259 79M 0 5 218 302 4 1 94
- # 3 181 36 51K 1893 8696 3 15 21 0 28 0 4 81 321 1 1 98
- 'dec_osf' => ['vmstat 1 2 | tail -n1', '$11*$12'],
- # gnu (hurd): $7*$8
- # $ vmstat -k 1 2
- # (pagesize: 4, size: 512288, swap size: 894972)
- # free actv inact wired zeroed react pgins pgouts pfaults cowpfs hrat caobj cache swfree
- # 371940 30844 89228 20276 298348 0 48192 19016 756105 99808 98% 876 20628 894972
- # 371940 30844 89228 20276 +0 +0 +0 +0 +42 +2 98% 876 20628 894972
- 'gnu' => ['vmstat -k 1 2 | tail -n1', '$7*$8'],
- # -nto (qnx has no swap)
- #-irix
- #-svr5 (scosysv)
- );
- my $perlscript = "";
- for my $os (keys %vmstat) {
- #q[ { vmstat 1 2 2> /dev/null || vmstat -c 1 2; } | ].
- # q[ awk 'NR!=4{next} NF==17||NF==16{print $7*$8} NF==22{print $21*$22} {exit}' ];
- $vmstat{$os}[1] =~ s/\$/\\\\\\\$/g; # $ => \\\$
- $perlscript .= 'if($^O eq "'.$os.'") { print `'.$vmstat{$os}[0].' | awk "{print ' .
- $vmstat{$os}[1] . '}"` }';
- }
- $perlscript = "perl -e " . ::shell_quote_scalar($perlscript);
- $script = $Global::envvar. " " .$perlscript;
- }
- return $script;
- }
- }
- sub too_fast_remote_login {
- my $self = shift;
- if($self->{'last_login_at'} and $self->{'time_to_login'}) {
- # sshd normally allows 10 simultaneous logins
- # A login takes time_to_login
- # So time_to_login/5 should be safe
- # If now <= last_login + time_to_login/5: Then it is too soon.
- my $too_fast = (::now() <= $self->{'last_login_at'}
- + $self->{'time_to_login'}/5);
- ::debug("run", "Too fast? $too_fast ");
- return $too_fast;
- } else {
- # No logins so far (or time_to_login not computed): it is not too fast
- return 0;
- }
- }
- sub last_login_at {
- my $self = shift;
- return $self->{'last_login_at'};
- }
- sub set_last_login_at {
- my $self = shift;
- $self->{'last_login_at'} = shift;
- }
- sub loadavg_too_high {
- my $self = shift;
- my $loadavg = $self->loadavg();
- return (not defined $loadavg or
- $loadavg > $self->max_loadavg());
- }
- sub loadavg {
- # If the currently know loadavg is too old:
- # Recompute a new one in the background
- # The load average is computed as the number of processes waiting for disk
- # or CPU right now. So it is the server load this instant and not averaged over
- # several minutes. This is needed so GNU Parallel will at most start one job
- # that will push the load over the limit.
- #
- # Returns:
- # $last_loadavg = last load average computed (undef if none)
- my $self = shift;
- # Should we update the loadavg file?
- my $update_loadavg_file = 0;
- if(open(my $load_fh, "<", $self->{'loadavg_file'})) {
- local $/ = undef;
- my $load_out = <$load_fh>;
- close $load_fh;
- my $load =()= ($load_out=~/(^[DR]....[^\[])/gm);
- if($load > 0) {
- # load is overestimated by 1
- $self->{'loadavg'} = $load - 1;
- ::debug("load", "New loadavg: ", $self->{'loadavg'});
- } else {
- ::die_bug("loadavg_invalid_content: $load_out");
- }
- ::debug("load", "Last update: ", $self->{'last_loadavg_update'});
- if(time - $self->{'last_loadavg_update'} > 10) {
- # last loadavg was started 10 seconds ago
- ::debug("load", time - $self->{'last_loadavg_update'}, " secs old: ",
- $self->{'loadavg_file'});
- $update_loadavg_file = 1;
- }
- } else {
- ::debug("load", "No loadavg file: ", $self->{'loadavg_file'});
- $self->{'loadavg'} = undef;
- $update_loadavg_file = 1;
- }
- if($update_loadavg_file) {
- ::debug("load", "Updating loadavg file", $self->{'loadavg_file'}, "\n");
- $self->{'last_loadavg_update'} = time;
- -e $ENV{'HOME'}."/.parallel" or mkdir $ENV{'HOME'}."/.parallel";
- -e $ENV{'HOME'}."/.parallel/tmp" or mkdir $ENV{'HOME'}."/.parallel/tmp";
- my $cmd = "";
- if($self->{'string'} ne ":") {
- $cmd = $self->sshcommand() . " " . $self->serverlogin() . " ";
- }
- # TODO Is is called 'ps ax -o state,command' on other platforms?
- $cmd .= "ps ax -o state,command";
- # As the command can take long to run if run remote
- # save it to a tmp file before moving it to the correct file
- my $file = $self->{'loadavg_file'};
- my ($dummy_fh, $tmpfile) = ::tmpfile(SUFFIX => ".loa");
- qx{ ($cmd > $tmpfile && mv $tmpfile $file || rm $tmpfile) & };
- }
- return $self->{'loadavg'};
- }
- sub max_loadavg {
- my $self = shift;
- # If --load is a file it might be changed
- if($Global::max_load_file) {
- my $mtime = (stat($Global::max_load_file))[9];
- if($mtime > $Global::max_load_file_last_mod) {
- $Global::max_load_file_last_mod = $mtime;
- for my $sshlogin (values %Global::host) {
- $sshlogin->set_max_loadavg(undef);
- }
- }
- }
- if(not defined $self->{'max_loadavg'}) {
- $self->{'max_loadavg'} =
- $self->compute_max_loadavg($opt::load);
- }
- ::debug("load", "max_loadavg: ", $self->string(), " ", $self->{'max_loadavg'});
- return $self->{'max_loadavg'};
- }
- sub set_max_loadavg {
- my $self = shift;
- $self->{'max_loadavg'} = shift;
- }
- sub compute_max_loadavg {
- # Parse the max loadaverage that the user asked for using --load
- # Returns:
- # max loadaverage
- my $self = shift;
- my $loadspec = shift;
- my $load;
- if(defined $loadspec) {
- if($loadspec =~ /^\+(\d+)$/) {
- # E.g. --load +2
- my $j = $1;
- $load =
- $self->ncpus() + $j;
- } elsif ($loadspec =~ /^-(\d+)$/) {
- # E.g. --load -2
- my $j = $1;
- $load =
- $self->ncpus() - $j;
- } elsif ($loadspec =~ /^(\d+)\%$/) {
- my $j = $1;
- $load =
- $self->ncpus() * $j / 100;
- } elsif ($loadspec =~ /^(\d+(\.\d+)?)$/) {
- $load = $1;
- } elsif (-f $loadspec) {
- $Global::max_load_file = $loadspec;
- $Global::max_load_file_last_mod = (stat($Global::max_load_file))[9];
- if(open(my $in_fh, "<", $Global::max_load_file)) {
- my $opt_load_file = join("",<$in_fh>);
- close $in_fh;
- $load = $self->compute_max_loadavg($opt_load_file);
- } else {
- print $Global::original_stderr "Cannot open $loadspec\n";
- ::wait_and_exit(255);
- }
- } else {
- print $Global::original_stderr "Parsing of --load failed\n";
- ::die_usage();
- }
- if($load < 0.01) {
- $load = 0.01;
- }
- }
- return $load;
- }
- sub time_to_login {
- my $self = shift;
- return $self->{'time_to_login'};
- }
- sub set_time_to_login {
- my $self = shift;
- $self->{'time_to_login'} = shift;
- }
- sub max_jobs_running {
- my $self = shift;
- if(not defined $self->{'max_jobs_running'}) {
- my $nproc = $self->compute_number_of_processes($opt::jobs);
- $self->set_max_jobs_running($nproc);
- }
- return $self->{'max_jobs_running'};
- }
- sub orig_max_jobs_running {
- my $self = shift;
- return $self->{'orig_max_jobs_running'};
- }
- sub compute_number_of_processes {
- # Number of processes wanted and limited by system resources
- # Returns:
- # Number of processes
- my $self = shift;
- my $opt_P = shift;
- my $wanted_processes = $self->user_requested_processes($opt_P);
- if(not defined $wanted_processes) {
- $wanted_processes = $Global::default_simultaneous_sshlogins;
- }
- ::debug("load", "Wanted procs: $wanted_processes\n");
- my $system_limit =
- $self->processes_available_by_system_limit($wanted_processes);
- ::debug("load", "Limited to procs: $system_limit\n");
- return $system_limit;
- }
- sub processes_available_by_system_limit {
- # If the wanted number of processes is bigger than the system limits:
- # Limit them to the system limits
- # Limits are: File handles, number of input lines, processes,
- # and taking > 1 second to spawn 10 extra processes
- # Returns:
- # Number of processes
- my $self = shift;
- my $wanted_processes = shift;
- my $system_limit = 0;
- my @jobs = ();
- my $job;
- my @args = ();
- my $arg;
- my $more_filehandles = 1;
- my $max_system_proc_reached = 0;
- my $slow_spawining_warning_printed = 0;
- my $time = time;
- my %fh;
- my @children;
- # Reserve filehandles
- # perl uses 7 filehandles for something?
- # parallel uses 1 for memory_usage
- # parallel uses 4 for ?
- for my $i (1..12) {
- open($fh{"init-$i"}, "<", "/dev/null");
- }
- for(1..2) {
- # System process limit
- my $child;
- if($child = fork()) {
- push (@children,$child);
- $Global::unkilled_children{$child} = 1;
- } elsif(defined $child) {
- # The child takes one process slot
- # It will be killed later
- $SIG{TERM} = $Global::original_sig{TERM};
- sleep 10000000;
- exit(0);
- } else {
- $max_system_proc_reached = 1;
- }
- }
- my $count_jobs_already_read = $Global::JobQueue->next_seq();
- my $wait_time_for_getting_args = 0;
- my $start_time = time;
- while(1) {
- $system_limit >= $wanted_processes and last;
- not $more_filehandles and last;
- $max_system_proc_reached and last;
- my $before_getting_arg = time;
- if($Global::semaphore or $opt::pipe) {
- # Skip: No need to get args
- } elsif(defined $opt::retries and $count_jobs_already_read) {
- # For retries we may need to run all jobs on this sshlogin
- # so include the already read jobs for this sshlogin
- $count_jobs_already_read--;
- } else {
- if($opt::X or $opt::m) {
- # The arguments may have to be re-spread over several jobslots
- # So pessimistically only read one arg per jobslot
- # instead of a full commandline
- if($Global::JobQueue->{'commandlinequeue'}->{'arg_queue'}->empty()) {
- if($Global::JobQueue->empty()) {
- last;
- } else {
- ($job) = $Global::JobQueue->get();
- push(@jobs, $job);
- }
- } else {
- ($arg) = $Global::JobQueue->{'commandlinequeue'}->{'arg_queue'}->get();
- push(@args, $arg);
- }
- } else {
- # If there are no more command lines, then we have a process
- # per command line, so no need to go further
- $Global::JobQueue->empty() and last;
- ($job) = $Global::JobQueue->get();
- push(@jobs, $job);
- }
- }
- $wait_time_for_getting_args += time - $before_getting_arg;
- $system_limit++;
- # Every simultaneous process uses 2 filehandles when grouping
- # Every simultaneous process uses 2 filehandles when compressing
- $more_filehandles = open($fh{$system_limit*10}, "<", "/dev/null")
- && open($fh{$system_limit*10+2}, "<", "/dev/null")
- && open($fh{$system_limit*10+3}, "<", "/dev/null")
- && open($fh{$system_limit*10+4}, "<", "/dev/null");
- # System process limit
- my $child;
- if($child = fork()) {
- push (@children,$child);
- $Global::unkilled_children{$child} = 1;
- } elsif(defined $child) {
- # The child takes one process slot
- # It will be killed later
- $SIG{TERM} = $Global::original_sig{TERM};
- sleep 10000000;
- exit(0);
- } else {
- $max_system_proc_reached = 1;
- }
- my $forktime = time - $time - $wait_time_for_getting_args;
- ::debug("run", "Time to fork $system_limit procs: $wait_time_for_getting_args ",
- $forktime,
- " (processes so far: ", $system_limit,")\n");
- if($system_limit > 10 and
- $forktime > 1 and
- $forktime > $system_limit * 0.01
- and not $slow_spawining_warning_printed) {
- # It took more than 0.01 second to fork a processes on avg.
- # Give the user a warning. He can press Ctrl-C if this
- # sucks.
- print $Global::original_stderr
- ("parallel: Warning: Starting $system_limit processes took > $forktime sec.\n",
- "Consider adjusting -j. Press CTRL-C to stop.\n");
- $slow_spawining_warning_printed = 1;
- }
- }
- # Cleanup: Close the files
- for (values %fh) { close $_ }
- # Cleanup: Kill the children
- for my $pid (@children) {
- kill 9, $pid;
- waitpid($pid,0);
- delete $Global::unkilled_children{$pid};
- }
- # Cleanup: Unget the command_lines or the @args
- $Global::JobQueue->{'commandlinequeue'}->{'arg_queue'}->unget(@args);
- $Global::JobQueue->unget(@jobs);
- if($system_limit < $wanted_processes) {
- # The system_limit is less than the wanted_processes
- if($system_limit < 1 and not $Global::JobQueue->empty()) {
- ::warning("Cannot spawn any jobs. Raising ulimit -u or /etc/security/limits.conf\n",
- "or /proc/sys/kernel/pid_max may help.\n");
- ::wait_and_exit(255);
- }
- if(not $more_filehandles) {
- ::warning("Only enough file handles to run ", $system_limit, " jobs in parallel.\n",
- "Running 'parallel -j0 -N", $system_limit, " --pipe parallel -j0' or ",
- "raising ulimit -n or /etc/security/limits.conf may help.\n");
- }
- if($max_system_proc_reached) {
- ::warning("Only enough available processes to run ", $system_limit,
- " jobs in parallel. Raising ulimit -u or /etc/security/limits.conf\n",
- "or /proc/sys/kernel/pid_max may help.\n");
- }
- }
- if($] == 5.008008 and $system_limit > 1000) {
- # https://savannah.gnu.org/bugs/?36942
- $system_limit = 1000;
- }
- if($Global::JobQueue->empty()) {
- $system_limit ||= 1;
- }
- if($self->string() ne ":" and
- $system_limit > $Global::default_simultaneous_sshlogins) {
- $system_limit =
- $self->simultaneous_sshlogin_limit($system_limit);
- }
- return $system_limit;
- }
- sub simultaneous_sshlogin_limit {
- # Test by logging in wanted number of times simultaneously
- # Returns:
- # min($wanted_processes,$working_simultaneous_ssh_logins-1)
- my $self = shift;
- my $wanted_processes = shift;
- if($self->{'time_to_login'}) {
- return $wanted_processes;
- }
- # Try twice because it guesses wrong sometimes
- # Choose the minimal
- my $ssh_limit =
- ::min($self->simultaneous_sshlogin($wanted_processes),
- $self->simultaneous_sshlogin($wanted_processes));
- if($ssh_limit < $wanted_processes) {
- my $serverlogin = $self->serverlogin();
- ::warning("ssh to $serverlogin only allows ",
- "for $ssh_limit simultaneous logins.\n",
- "You may raise this by changing ",
- "/etc/ssh/sshd_config:MaxStartups and MaxSessions on $serverlogin.\n",
- "Using only ",$ssh_limit-1," connections ",
- "to avoid race conditions.\n");
- }
- # Race condition can cause problem if using all sshs.
- if($ssh_limit > 1) { $ssh_limit -= 1; }
- return $ssh_limit;
- }
- sub simultaneous_sshlogin {
- # Using $sshlogin try to see if we can do $wanted_processes
- # simultaneous logins
- # (ssh host echo simultaneouslogin & ssh host echo simultaneouslogin & ...)|grep simul|wc -l
- # Returns:
- # Number of succesful logins
- my $self = shift;
- my $wanted_processes = shift;
- my $sshcmd = $self->sshcommand();
- my $serverlogin = $self->serverlogin();
- my $sshdelay = $opt::sshdelay ? "sleep $opt::sshdelay;" : "";
- my $cmd = "$sshdelay$sshcmd $serverlogin echo simultaneouslogin </dev/null 2>&1 &"x$wanted_processes;
- ::debug("init", "Trying $wanted_processes logins at $serverlogin\n");
- open (my $simul_fh, "-|", "($cmd)|grep simultaneouslogin | wc -l") or
- ::die_bug("simultaneouslogin");
- my $ssh_limit = <$simul_fh>;
- close $simul_fh;
- chomp $ssh_limit;
- return $ssh_limit;
- }
- sub set_ncpus {
- my $self = shift;
- $self->{'ncpus'} = shift;
- }
- sub user_requested_processes {
- # Parse the number of processes that the user asked for using -j
- # Returns:
- # the number of processes to run on this sshlogin
- my $self = shift;
- my $opt_P = shift;
- my $processes;
- if(defined $opt_P) {
- if($opt_P =~ /^\+(\d+)$/) {
- # E.g. -P +2
- my $j = $1;
- $processes =
- $self->ncpus() + $j;
- } elsif ($opt_P =~ /^-(\d+)$/) {
- # E.g. -P -2
- my $j = $1;
- $processes =
- $self->ncpus() - $j;
- } elsif ($opt_P =~ /^(\d+(\.\d+)?)\%$/) {
- # E.g. -P 10.5%
- my $j = $1;
- $processes =
- $self->ncpus() * $j / 100;
- } elsif ($opt_P =~ /^(\d+)$/) {
- $processes = $1;
- if($processes == 0) {
- # -P 0 = infinity (or at least close)
- $processes = $Global::infinity;
- }
- } elsif (-f $opt_P) {
- $Global::max_procs_file = $opt_P;
- $Global::max_procs_file_last_mod = (stat($Global::max_procs_file))[9];
- if(open(my $in_fh, "<", $Global::max_procs_file)) {
- my $opt_P_file = join("",<$in_fh>);
- close $in_fh;
- $processes = $self->user_requested_processes($opt_P_file);
- } else {
- ::error("Cannot open $opt_P.\n");
- ::wait_and_exit(255);
- }
- } else {
- ::error("Parsing of --jobs/-j/--max-procs/-P failed.\n");
- ::die_usage();
- }
- $processes = ::ceil($processes);
- }
- return $processes;
- }
- sub ncpus {
- my $self = shift;
- if(not defined $self->{'ncpus'}) {
- my $sshcmd = $self->sshcommand();
- my $serverlogin = $self->serverlogin();
- if($serverlogin eq ":") {
- if($opt::use_cpus_instead_of_cores) {
- $self->{'ncpus'} = no_of_cpus();
- } else {
- $self->{'ncpus'} = no_of_cores();
- }
- } else {
- my $ncpu;
- my $sqe = ::shell_quote_scalar($Global::envvar);
- if($opt::use_cpus_instead_of_cores) {
- $ncpu = qx(echo|$sshcmd $serverlogin $sqe parallel --number-of-cpus);
- } else {
- ::debug("init",qq(echo|$sshcmd $serverlogin $sqe parallel --number-of-cores\n));
- $ncpu = qx(echo|$sshcmd $serverlogin $sqe parallel --number-of-cores);
- }
- chomp $ncpu;
- if($ncpu =~ /^\s*[0-9]+\s*$/s) {
- $self->{'ncpus'} = $ncpu;
- } else {
- ::warning("Could not figure out ",
- "number of cpus on $serverlogin ($ncpu). Using 1.\n");
- $self->{'ncpus'} = 1;
- }
- }
- }
- return $self->{'ncpus'};
- }
- sub no_of_cpus {
- # Returns:
- # Number of physical CPUs
- local $/="\n"; # If delimiter is set, then $/ will be wrong
- my $no_of_cpus;
- if ($^O eq 'linux') {
- $no_of_cpus = no_of_cpus_gnu_linux() || no_of_cores_gnu_linux();
- } elsif ($^O eq 'freebsd') {
- $no_of_cpus = no_of_cpus_freebsd();
- } elsif ($^O eq 'netbsd') {
- $no_of_cpus = no_of_cpus_netbsd();
- } elsif ($^O eq 'openbsd') {
- $no_of_cpus = no_of_cpus_openbsd();
- } elsif ($^O eq 'gnu') {
- $no_of_cpus = no_of_cpus_hurd();
- } elsif ($^O eq 'darwin') {
- $no_of_cpus = no_of_cpus_darwin();
- } elsif ($^O eq 'solaris') {
- $no_of_cpus = no_of_cpus_solaris();
- } elsif ($^O eq 'aix') {
- $no_of_cpus = no_of_cpus_aix();
- } elsif ($^O eq 'hpux') {
- $no_of_cpus = no_of_cpus_hpux();
- } elsif ($^O eq 'nto') {
- $no_of_cpus = no_of_cpus_qnx();
- } elsif ($^O eq 'svr5') {
- $no_of_cpus = no_of_cpus_openserver();
- } elsif ($^O eq 'irix') {
- $no_of_cpus = no_of_cpus_irix();
- } elsif ($^O eq 'dec_osf') {
- $no_of_cpus = no_of_cpus_tru64();
- } else {
- $no_of_cpus = (no_of_cpus_gnu_linux()
- || no_of_cpus_freebsd()
- || no_of_cpus_netbsd()
- || no_of_cpus_openbsd()
- || no_of_cpus_hurd()
- || no_of_cpus_darwin()
- || no_of_cpus_solaris()
- || no_of_cpus_aix()
- || no_of_cpus_hpux()
- || no_of_cpus_qnx()
- || no_of_cpus_openserver()
- || no_of_cpus_irix()
- || no_of_cpus_tru64()
- # Number of cores is better than no guess for #CPUs
- || nproc()
- );
- }
- if($no_of_cpus) {
- chomp $no_of_cpus;
- return $no_of_cpus;
- } else {
- ::warning("Cannot figure out number of cpus. Using 1.\n");
- return 1;
- }
- }
- sub no_of_cores {
- # Returns:
- # Number of CPU cores
- local $/="\n"; # If delimiter is set, then $/ will be wrong
- my $no_of_cores;
- if ($^O eq 'linux') {
- $no_of_cores = no_of_cores_gnu_linux();
- } elsif ($^O eq 'freebsd') {
- $no_of_cores = no_of_cores_freebsd();
- } elsif ($^O eq 'netbsd') {
- $no_of_cores = no_of_cores_netbsd();
- } elsif ($^O eq 'openbsd') {
- $no_of_cores = no_of_cores_openbsd();
- } elsif ($^O eq 'gnu') {
- $no_of_cores = no_of_cores_hurd();
- } elsif ($^O eq 'darwin') {
- $no_of_cores = no_of_cores_darwin();
- } elsif ($^O eq 'solaris') {
- $no_of_cores = no_of_cores_solaris();
- } elsif ($^O eq 'aix') {
- $no_of_cores = no_of_cores_aix();
- } elsif ($^O eq 'hpux') {
- $no_of_cores = no_of_cores_hpux();
- } elsif ($^O eq 'nto') {
- $no_of_cores = no_of_cores_qnx();
- } elsif ($^O eq 'svr5') {
- $no_of_cores = no_of_cores_openserver();
- } elsif ($^O eq 'irix') {
- $no_of_cores = no_of_cores_irix();
- } elsif ($^O eq 'dec_osf') {
- $no_of_cores = no_of_cores_tru64();
- } else {
- $no_of_cores = (no_of_cores_gnu_linux()
- || no_of_cores_freebsd()
- || no_of_cores_netbsd()
- || no_of_cores_openbsd()
- || no_of_cores_hurd()
- || no_of_cores_darwin()
- || no_of_cores_solaris()
- || no_of_cores_aix()
- || no_of_cores_hpux()
- || no_of_cores_qnx()
- || no_of_cores_openserver()
- || no_of_cores_irix()
- || no_of_cores_tru64()
- || nproc()
- );
- }
- if($no_of_cores) {
- chomp $no_of_cores;
- return $no_of_cores;
- } else {
- ::warning("Cannot figure out number of CPU cores. Using 1.\n");
- return 1;
- }
- }
- sub nproc {
- # Returns:
- # Number of cores using `nproc`
- my $no_of_cores = `nproc 2>/dev/null`;
- return $no_of_cores;
- }
- sub no_of_cpus_gnu_linux {
- # Returns:
- # Number of physical CPUs on GNU/Linux
- # undef if not GNU/Linux
- my $no_of_cpus;
- my $no_of_cores;
- if(-e "/proc/cpuinfo") {
- $no_of_cpus = 0;
- $no_of_cores = 0;
- my %seen;
- open(my $in_fh, "<", "/proc/cpuinfo") || return undef;
- while(<$in_fh>) {
- if(/^physical id.*[:](.*)/ and not $seen{$1}++) {
- $no_of_cpus++;
- }
- /^processor.*[:]/i and $no_of_cores++;
- }
- close $in_fh;
- }
- return ($no_of_cpus||$no_of_cores);
- }
- sub no_of_cores_gnu_linux {
- # Returns:
- # Number of CPU cores on GNU/Linux
- # undef if not GNU/Linux
- my $no_of_cores;
- if(-e "/proc/cpuinfo") {
- $no_of_cores = 0;
- open(my $in_fh, "<", "/proc/cpuinfo") || return undef;
- while(<$in_fh>) {
- /^processor.*[:]/i and $no_of_cores++;
- }
- close $in_fh;
- }
- return $no_of_cores;
- }
- sub no_of_cpus_freebsd {
- # Returns:
- # Number of physical CPUs on FreeBSD
- # undef if not FreeBSD
- my $no_of_cpus =
- (`sysctl -a dev.cpu 2>/dev/null | grep \%parent | awk '{ print \$2 }' | uniq | wc -l | awk '{ print \$1 }'`
- or
- `sysctl hw.ncpu 2>/dev/null | awk '{ print \$2 }'`);
- chomp $no_of_cpus;
- return $no_of_cpus;
- }
- sub no_of_cores_freebsd {
- # Returns:
- # Number of CPU cores on FreeBSD
- # undef if not FreeBSD
- my $no_of_cores =
- (`sysctl hw.ncpu 2>/dev/null | awk '{ print \$2 }'`
- or
- `sysctl -a hw 2>/dev/null | grep [^a-z]logicalcpu[^a-z] | awk '{ print \$2 }'`);
- chomp $no_of_cores;
- return $no_of_cores;
- }
- sub no_of_cpus_netbsd {
- # Returns:
- # Number of physical CPUs on NetBSD
- # undef if not NetBSD
- my $no_of_cpus = `sysctl -n hw.ncpu 2>/dev/null`;
- chomp $no_of_cpus;
- return $no_of_cpus;
- }
- sub no_of_cores_netbsd {
- # Returns:
- # Number of CPU cores on NetBSD
- # undef if not NetBSD
- my $no_of_cores = `sysctl -n hw.ncpu 2>/dev/null`;
- chomp $no_of_cores;
- return $no_of_cores;
- }
- sub no_of_cpus_openbsd {
- # Returns:
- # Number of physical CPUs on OpenBSD
- # undef if not OpenBSD
- my $no_of_cpus = `sysctl -n hw.ncpu 2>/dev/null`;
- chomp $no_of_cpus;
- return $no_of_cpus;
- }
- sub no_of_cores_openbsd {
- # Returns:
- # Number of CPU cores on OpenBSD
- # undef if not OpenBSD
- my $no_of_cores = `sysctl -n hw.ncpu 2>/dev/null`;
- chomp $no_of_cores;
- return $no_of_cores;
- }
- sub no_of_cpus_hurd {
- # Returns:
- # Number of physical CPUs on HURD
- # undef if not HURD
- my $no_of_cpus = `nproc`;
- chomp $no_of_cpus;
- return $no_of_cpus;
- }
- sub no_of_cores_hurd {
- # Returns:
- # Number of physical CPUs on HURD
- # undef if not HURD
- my $no_of_cores = `nproc`;
- chomp $no_of_cores;
- return $no_of_cores;
- }
- sub no_of_cpus_darwin {
- # Returns:
- # Number of physical CPUs on Mac Darwin
- # undef if not Mac Darwin
- my $no_of_cpus =
- (`sysctl -n hw.physicalcpu 2>/dev/null`
- or
- `sysctl -a hw 2>/dev/null | grep [^a-z]physicalcpu[^a-z] | awk '{ print \$2 }'`);
- return $no_of_cpus;
- }
- sub no_of_cores_darwin {
- # Returns:
- # Number of CPU cores on Mac Darwin
- # undef if not Mac Darwin
- my $no_of_cores =
- (`sysctl -n hw.logicalcpu 2>/dev/null`
- or
- `sysctl -a hw 2>/dev/null | grep [^a-z]logicalcpu[^a-z] | awk '{ print \$2 }'`);
- return $no_of_cores;
- }
- sub no_of_cpus_solaris {
- # Returns:
- # Number of physical CPUs on Solaris
- # undef if not Solaris
- if(-x "/usr/sbin/psrinfo") {
- my @psrinfo = `/usr/sbin/psrinfo`;
- if($#psrinfo >= 0) {
- return $#psrinfo +1;
- }
- }
- if(-x "/usr/sbin/prtconf") {
- my @prtconf = `/usr/sbin/prtconf | grep cpu..instance`;
- if($#prtconf >= 0) {
- return $#prtconf +1;
- }
- }
- return undef;
- }
- sub no_of_cores_solaris {
- # Returns:
- # Number of CPU cores on Solaris
- # undef if not Solaris
- if(-x "/usr/sbin/psrinfo") {
- my @psrinfo = `/usr/sbin/psrinfo`;
- if($#psrinfo >= 0) {
- return $#psrinfo +1;
- }
- }
- if(-x "/usr/sbin/prtconf") {
- my @prtconf = `/usr/sbin/prtconf | grep cpu..instance`;
- if($#prtconf >= 0) {
- return $#prtconf +1;
- }
- }
- return undef;
- }
- sub no_of_cpus_aix {
- # Returns:
- # Number of physical CPUs on AIX
- # undef if not AIX
- my $no_of_cpus = 0;
- if(-x "/usr/sbin/lscfg") {
- open(my $in_fh, "-|", "/usr/sbin/lscfg -vs |grep proc | wc -l|tr -d ' '")
- || return undef;
- $no_of_cpus = <$in_fh>;
- chomp ($no_of_cpus);
- close $in_fh;
- }
- return $no_of_cpus;
- }
- sub no_of_cores_aix {
- # Returns:
- # Number of CPU cores on AIX
- # undef if not AIX
- my $no_of_cores;
- if(-x "/usr/bin/vmstat") {
- open(my $in_fh, "-|", "/usr/bin/vmstat 1 1") || return undef;
- while(<$in_fh>) {
- /lcpu=([0-9]*) / and $no_of_cores = $1;
- }
- close $in_fh;
- }
- return $no_of_cores;
- }
- sub no_of_cpus_hpux {
- # Returns:
- # Number of physical CPUs on HP-UX
- # undef if not HP-UX
- my $no_of_cpus =
- (`/usr/bin/mpsched -s 2>&1 | grep 'Locality Domain Count' | awk '{ print \$4 }'`);
- return $no_of_cpus;
- }
- sub no_of_cores_hpux {
- # Returns:
- # Number of CPU cores on HP-UX
- # undef if not HP-UX
- my $no_of_cores =
- (`/usr/bin/mpsched -s 2>&1 | grep 'Processor Count' | awk '{ print \$3 }'`);
- return $no_of_cores;
- }
- sub no_of_cpus_qnx {
- # Returns:
- # Number of physical CPUs on QNX
- # undef if not QNX
- # BUG: It is now known how to calculate this.
- my $no_of_cpus = 0;
- return $no_of_cpus;
- }
- sub no_of_cores_qnx {
- # Returns:
- # Number of CPU cores on QNX
- # undef if not QNX
- # BUG: It is now known how to calculate this.
- my $no_of_cores = 0;
- return $no_of_cores;
- }
- sub no_of_cpus_openserver {
- # Returns:
- # Number of physical CPUs on SCO OpenServer
- # undef if not SCO OpenServer
- my $no_of_cpus = 0;
- if(-x "/usr/sbin/psrinfo") {
- my @psrinfo = `/usr/sbin/psrinfo`;
- if($#psrinfo >= 0) {
- return $#psrinfo +1;
- }
- }
- return $no_of_cpus;
- }
- sub no_of_cores_openserver {
- # Returns:
- # Number of CPU cores on SCO OpenServer
- # undef if not SCO OpenServer
- my $no_of_cores = 0;
- if(-x "/usr/sbin/psrinfo") {
- my @psrinfo = `/usr/sbin/psrinfo`;
- if($#psrinfo >= 0) {
- return $#psrinfo +1;
- }
- }
- return $no_of_cores;
- }
- sub no_of_cpus_irix {
- # Returns:
- # Number of physical CPUs on IRIX
- # undef if not IRIX
- my $no_of_cpus = `hinv | grep HZ | grep Processor | awk '{print \$1}'`;
- return $no_of_cpus;
- }
- sub no_of_cores_irix {
- # Returns:
- # Number of CPU cores on IRIX
- # undef if not IRIX
- my $no_of_cores = `hinv | grep HZ | grep Processor | awk '{print \$1}'`;
- return $no_of_cores;
- }
- sub no_of_cpus_tru64 {
- # Returns:
- # Number of physical CPUs on Tru64
- # undef if not Tru64
- my $no_of_cpus = `sizer -pr`;
- return $no_of_cpus;
- }
- sub no_of_cores_tru64 {
- # Returns:
- # Number of CPU cores on Tru64
- # undef if not Tru64
- my $no_of_cores = `sizer -pr`;
- return $no_of_cores;
- }
- sub sshcommand {
- my $self = shift;
- if (not defined $self->{'sshcommand'}) {
- $self->sshcommand_of_sshlogin();
- }
- return $self->{'sshcommand'};
- }
- sub serverlogin {
- my $self = shift;
- if (not defined $self->{'serverlogin'}) {
- $self->sshcommand_of_sshlogin();
- }
- return $self->{'serverlogin'};
- }
- sub sshcommand_of_sshlogin {
- # 'server' -> ('ssh -S /tmp/parallel-ssh-RANDOM/host-','server')
- # 'user@server' -> ('ssh','user@server')
- # 'myssh user@server' -> ('myssh','user@server')
- # 'myssh -l user server' -> ('myssh -l user','server')
- # '/usr/bin/myssh -l user server' -> ('/usr/bin/myssh -l user','server')
- # Returns:
- # sshcommand - defaults to 'ssh'
- # login@host
- my $self = shift;
- my ($sshcmd, $serverlogin);
- if($self->{'string'} =~ /(.+) (\S+)$/) {
- # Own ssh command
- $sshcmd = $1; $serverlogin = $2;
- } else {
- # Normal ssh
- if($opt::controlmaster) {
- # Use control_path to make ssh faster
- my $control_path = $self->control_path_dir()."/ssh-%r@%h:%p";
- $sshcmd = "ssh -S ".$control_path;
- $serverlogin = $self->{'string'};
- if(not $self->{'control_path'}{$control_path}++) {
- # Master is not running for this control_path
- # Start it
- my $pid = fork();
- if($pid) {
- $Global::sshmaster{$pid} ||= 1;
- } else {
- $SIG{'TERM'} = undef;
- # Ignore the 'foo' being printed
- open(STDOUT,">","/dev/null");
- # OpenSSH_3.6.1p2 gives 'tcgetattr: Invalid argument' with -tt
- # STDERR >/dev/null to ignore "process_mux_new_session: tcgetattr: Invalid argument"
- open(STDERR,">","/dev/null");
- open(STDIN,"<","/dev/null");
- # Run a sleep that outputs data, so it will discover if the ssh connection closes.
- my $sleep = ::shell_quote_scalar('$|=1;while(1){sleep 1;print "foo\n"}');
- my @master = ("ssh", "-tt", "-MTS", $control_path, $serverlogin, "perl", "-e", $sleep);
- exec(@master);
- }
- }
- } else {
- $sshcmd = "ssh"; $serverlogin = $self->{'string'};
- }
- }
- $self->{'sshcommand'} = $sshcmd;
- $self->{'serverlogin'} = $serverlogin;
- }
- sub control_path_dir {
- # Returns:
- # path to directory
- my $self = shift;
- if(not defined $self->{'control_path_dir'}) {
- -e $ENV{'HOME'}."/.parallel" or mkdir $ENV{'HOME'}."/.parallel";
- -e $ENV{'HOME'}."/.parallel/tmp" or mkdir $ENV{'HOME'}."/.parallel/tmp";
- $self->{'control_path_dir'} =
- File::Temp::tempdir($ENV{'HOME'}
- . "/.parallel/tmp/control_path_dir-XXXX",
- CLEANUP => 1);
- }
- return $self->{'control_path_dir'};
- }
- sub rsync_transfer_cmd {
- # Command to run to transfer a file
- # Input:
- # $file = filename of file to transfer
- # $workdir = destination dir
- # Returns:
- # $cmd = rsync command to run to transfer $file ("" if unreadable)
- my $self = shift;
- my $file = shift;
- my $workdir = shift;
- if(not -r $file) {
- ::warning($file, " is not readable and will not be transferred.\n");
- return "true";
- }
- my $rsync_destdir;
- if($file =~ m:^/:) {
- # rsync /foo/bar /
- $rsync_destdir = "/";
- } else {
- $rsync_destdir = ::shell_quote_file($workdir);
- }
- $file = ::shell_quote_file($file);
- my $sshcmd = $self->sshcommand();
- my $rsync_opt = "-rlDzR -e" . ::shell_quote_scalar($sshcmd);
- my $serverlogin = $self->serverlogin();
- # Make dir if it does not exist
- return "( $sshcmd $serverlogin mkdir -p $rsync_destdir;" .
- rsync()." $rsync_opt $file $serverlogin:$rsync_destdir )";
- }
- sub cleanup_cmd {
- # Command to run to remove the remote file
- # Input:
- # $file = filename to remove
- # $workdir = destination dir
- # Returns:
- # $cmd = ssh command to run to remove $file and empty parent dirs
- my $self = shift;
- my $file = shift;
- my $workdir = shift;
- my $f = $file;
- if($f =~ m:/\./:) {
- # foo/bar/./baz/quux => workdir/baz/quux
- # /foo/bar/./baz/quux => workdir/baz/quux
- $f =~ s:.*/\./:$workdir/:;
- } elsif($f =~ m:^[^/]:) {
- # foo/bar => workdir/foo/bar
- $f = $workdir."/".$f;
- }
- my @subdirs = split m:/:, ::dirname($f);
- my @rmdir;
- my $dir = "";
- for(@subdirs) {
- $dir .= $_."/";
- unshift @rmdir, ::shell_quote_file($dir);
- }
- my $rmdir = @rmdir ? "rmdir @rmdir 2>/dev/null;" : "";
- if(defined $opt::workdir and $opt::workdir eq "...") {
- $rmdir .= "rm -rf " . ::shell_quote_file($workdir).';';
- }
- $f = ::shell_quote_file($f);
- my $sshcmd = $self->sshcommand();
- my $serverlogin = $self->serverlogin();
- return "$sshcmd $serverlogin ".::shell_quote_scalar("(rm -f $f; $rmdir)");
- }
- {
- my $rsync;
- sub rsync {
- # rsync 3.1.x uses protocol 31 which is unsupported by 2.5.7.
- # If the version >= 3.1.0: downgrade to protocol 30
- if(not $rsync) {
- my @out = `rsync --version`;
- for (@out) {
- if(/version (\d+.\d+)(.\d+)?/) {
- if($1 >= 3.1) {
- # Version 3.1.0 or later: Downgrade to protocol 30
- $rsync = "rsync --protocol 30";
- } else {
- $rsync = "rsync";
- }
- }
- }
- $rsync or ::die_bug("Cannot figure out version of rsync: @out");
- }
- return $rsync;
- }
- }
- package JobQueue;
- sub new {
- my $class = shift;
- my $commandref = shift;
- my $read_from = shift;
- my $context_replace = shift;
- my $max_number_of_args = shift;
- my $return_files = shift;
- my $commandlinequeue = CommandLineQueue->new
- ($commandref, $read_from, $context_replace, $max_number_of_args,
- $return_files);
- my @unget = ();
- return bless {
- 'unget' => \@unget,
- 'commandlinequeue' => $commandlinequeue,
- 'total_jobs' => undef,
- }, ref($class) || $class;
- }
- sub get {
- my $self = shift;
- if(@{$self->{'unget'}}) {
- my $job = shift @{$self->{'unget'}};
- return ($job);
- } else {
- my $commandline = $self->{'commandlinequeue'}->get();
- if(defined $commandline) {
- my $job = Job->new($commandline);
- return $job;
- } else {
- return undef;
- }
- }
- }
- sub unget {
- my $self = shift;
- unshift @{$self->{'unget'}}, @_;
- }
- sub empty {
- my $self = shift;
- my $empty = (not @{$self->{'unget'}})
- && $self->{'commandlinequeue'}->empty();
- ::debug("run", "JobQueue->empty $empty ");
- return $empty;
- }
- sub total_jobs {
- my $self = shift;
- if(not defined $self->{'total_jobs'}) {
- my $job;
- my @queue;
- my $start = time;
- while($job = $self->get()) {
- if(time - $start > 10) {
- ::warning("Reading all arguments takes longer than 10 seconds.\n");
- $opt::eta && ::warning("Consider removing --eta.\n");
- $opt::bar && ::warning("Consider removing --bar.\n");
- last;
- }
- push @queue, $job;
- }
- while($job = $self->get()) {
- push @queue, $job;
- }
- $self->unget(@queue);
- $self->{'total_jobs'} = $#queue+1;
- }
- return $self->{'total_jobs'};
- }
- sub next_seq {
- my $self = shift;
- return $self->{'commandlinequeue'}->seq();
- }
- sub quote_args {
- my $self = shift;
- return $self->{'commandlinequeue'}->quote_args();
- }
- package Job;
- sub new {
- my $class = shift;
- my $commandlineref = shift;
- return bless {
- 'commandline' => $commandlineref, # CommandLine object
- 'workdir' => undef, # --workdir
- 'stdin' => undef, # filehandle for stdin (used for --pipe)
- # filename for writing stdout to (used for --files)
- 'remaining' => "", # remaining data not sent to stdin (used for --pipe)
- 'datawritten' => 0, # amount of data sent via stdin (used for --pipe)
- 'transfersize' => 0, # size of files using --transfer
- 'returnsize' => 0, # size of files using --return
- 'pid' => undef,
- # hash of { SSHLogins => number of times the command failed there }
- 'failed' => undef,
- 'sshlogin' => undef,
- # The commandline wrapped with rsync and ssh
- 'sshlogin_wrap' => undef,
- 'exitstatus' => undef,
- 'exitsignal' => undef,
- # Timestamp for timeout if any
- 'timeout' => undef,
- 'virgin' => 1,
- }, ref($class) || $class;
- }
- sub replaced {
- my $self = shift;
- $self->{'commandline'} or ::die_bug("commandline empty");
- return $self->{'commandline'}->replaced();
- }
- sub seq {
- my $self = shift;
- return $self->{'commandline'}->seq();
- }
- sub slot {
- my $self = shift;
- return $self->{'commandline'}->slot();
- }
- {
- my($cattail);
- sub cattail {
- # Returns:
- # $cattail = perl program for: cattail "decompress program" writerpid [file_to_decompress or stdin] [file_to_unlink]
- if(not $cattail) {
- $cattail = q{
- # cat followed by tail.
- # If $writerpid dead: finish after this round
- use Fcntl;
- $|=1;
- my ($cmd, $writerpid, $read_file, $unlink_file) = @ARGV;
- if($read_file) {
- open(IN,"<",$read_file) || die("cattail: Cannot open $read_file");
- } else {
- *IN = *STDIN;
- }
- my $flags;
- fcntl(IN, F_GETFL, $flags) || die $!; # Get the current flags on the filehandle
- $flags |= O_NONBLOCK; # Add non-blocking to the flags
- fcntl(IN, F_SETFL, $flags) || die $!; # Set the flags on the filehandle
- open(OUT,"|-",$cmd) || die("cattail: Cannot run $cmd");
- while(1) {
- # clear EOF
- seek(IN,0,1);
- my $writer_running = kill 0, $writerpid;
- $read = sysread(IN,$buf,32768);
- if($read) {
- # We can unlink the file now: The writer has written something
- -e $unlink_file and unlink $unlink_file;
- # Blocking print
- while($buf) {
- my $bytes_written = syswrite(OUT,$buf);
- # syswrite may be interrupted by SIGHUP
- substr($buf,0,$bytes_written) = "";
- }
- # Something printed: Wait less next time
- $sleep /= 2;
- } else {
- if(eof(IN) and not $writer_running) {
- # Writer dead: There will never be more to read => exit
- exit;
- }
- # TODO This could probably be done more efficiently using select(2)
- # Nothing read: Wait longer before next read
- # Up to 30 milliseconds
- $sleep = ($sleep < 30) ? ($sleep * 1.001 + 0.01) : ($sleep);
- usleep($sleep);
- }
- }
- sub usleep {
- # Sleep this many milliseconds.
- my $secs = shift;
- select(undef, undef, undef, $secs/1000);
- }
- };
- $cattail =~ s/#.*//mg;
- $cattail =~ s/\s+/ /g;
- }
- return $cattail;
- }
- }
- sub openoutputfiles {
- # Open files for STDOUT and STDERR
- # Set file handles in $self->fh
- my $self = shift;
- my ($outfhw, $errfhw, $outname, $errname);
- if($opt::results) {
- my $args_as_dirname = $self->{'commandline'}->args_as_dirname();
- # Output in: prefix/name1/val1/name2/val2/stdout
- my $dir = $opt::results."/".$args_as_dirname;
- if(eval{ File::Path::mkpath($dir); }) {
- # OK
- } else {
- # mkpath failed: Argument probably too long.
- # Set $Global::max_file_length, which will keep the individual
- # dir names shorter than the max length
- max_file_name_length($opt::results);
- $args_as_dirname = $self->{'commandline'}->args_as_dirname();
- # prefix/name1/val1/name2/val2/
- $dir = $opt::results."/".$args_as_dirname;
- File::Path::mkpath($dir);
- }
- # prefix/name1/val1/name2/val2/stdout
- $outname = "$dir/stdout";
- if(not open($outfhw, "+>", $outname)) {
- ::error("Cannot write to `$outname'.\n");
- ::wait_and_exit(255);
- }
- # prefix/name1/val1/name2/val2/stderr
- $errname = "$dir/stderr";
- if(not open($errfhw, "+>", $errname)) {
- ::error("Cannot write to `$errname'.\n");
- ::wait_and_exit(255);
- }
- $self->set_fh(1,"unlink","");
- $self->set_fh(2,"unlink","");
- } elsif(not $opt::ungroup) {
- # To group we create temporary files for STDOUT and STDERR
- # To avoid the cleanup unlink the files immediately (but keep them open)
- if(@Global::tee_jobs) {
- # files must be removed when the tee is done
- } elsif($opt::files) {
- ($outfhw, $outname) = ::tmpfile(SUFFIX => ".par");
- ($errfhw, $errname) = ::tmpfile(SUFFIX => ".par");
- # --files => only remove stderr
- $self->set_fh(1,"unlink","");
- $self->set_fh(2,"unlink",$errname);
- } else {
- ($outfhw, $outname) = ::tmpfile(SUFFIX => ".par");
- ($errfhw, $errname) = ::tmpfile(SUFFIX => ".par");
- $self->set_fh(1,"unlink",$outname);
- $self->set_fh(2,"unlink",$errname);
- }
- } else {
- # --ungroup
- open($outfhw,">&",$Global::fd{1}) || die;
- open($errfhw,">&",$Global::fd{2}) || die;
- # File name must be empty as it will otherwise be printed
- $outname = "";
- $errname = "";
- $self->set_fh(1,"unlink",$outname);
- $self->set_fh(2,"unlink",$errname);
- }
- # Set writing FD
- $self->set_fh(1,'w',$outfhw);
- $self->set_fh(2,'w',$errfhw);
- $self->set_fh(1,'name',$outname);
- $self->set_fh(2,'name',$errname);
- if($opt::compress) {
- # Send stdout to stdin for $opt::compress_program(1)
- # Send stderr to stdin for $opt::compress_program(2)
- # cattail get pid: $pid = $self->fh($fdno,'rpid');
- my $cattail = cattail();
- for my $fdno (1,2) {
- my $wpid = open(my $fdw,"|-","$opt::compress_program >>".
- $self->fh($fdno,'name')) || die $?;
- $self->set_fh($fdno,'w',$fdw);
- $self->set_fh($fdno,'wpid',$wpid);
- my $rpid = open(my $fdr, "-|", "perl", "-e", $cattail,
- $opt::decompress_program, $wpid,
- $self->fh($fdno,'name'),$self->fh($fdno,'unlink')) || die $?;
- $self->set_fh($fdno,'r',$fdr);
- $self->set_fh($fdno,'rpid',$rpid);
- }
- } elsif(not $opt::ungroup) {
- # Set reading FD if using --group (--ungroup does not need)
- for my $fdno (1,2) {
- # Re-open the file for reading
- # so fdw can be closed separately
- # and fdr can be seeked separately (for --line-buffer)
- open(my $fdr,"<", $self->fh($fdno,'name')) ||
- ::die_bug("fdr: Cannot open ".$self->fh($fdno,'name'));
- $self->set_fh($fdno,'r',$fdr);
- # Unlink if required
- $Global::debug or unlink $self->fh($fdno,"unlink");
- }
- }
- if($opt::linebuffer) {
- # Set non-blocking when using --linebuffer
- $Global::use{"Fcntl"} ||= eval "use Fcntl qw(:DEFAULT :flock); 1;";
- for my $fdno (1,2) {
- my $fdr = $self->fh($fdno,'r');
- my $flags;
- fcntl($fdr, &F_GETFL, $flags) || die $!; # Get the current flags on the filehandle
- $flags |= &O_NONBLOCK; # Add non-blocking to the flags
- fcntl($fdr, &F_SETFL, $flags) || die $!; # Set the flags on the filehandle
- }
- }
- }
- sub max_file_name_length {
- # Figure out the max length of a subdir
- # TODO and the max total length
- # Ext4 = 255,130816
- my $testdir = shift;
- my $upper = 8_000_000;
- my $len = 8;
- my $dir="x"x$len;
- do {
- rmdir($testdir."/".$dir);
- $len *= 16;
- $dir="x"x$len;
- } while (mkdir $testdir."/".$dir);
- # Then search for the actual max length between $len/16 and $len
- my $min = $len/16;
- my $max = $len;
- while($max-$min > 5) {
- # If we are within 5 chars of the exact value:
- # it is not worth the extra time to find the exact value
- my $test = int(($min+$max)/2);
- $dir="x"x$test;
- if(mkdir $testdir."/".$dir) {
- rmdir($testdir."/".$dir);
- $min = $test;
- } else {
- $max = $test;
- }
- }
- $Global::max_file_length = $min;
- return $min;
- }
- sub set_fh {
- # Set file handle
- my ($self, $fd_no, $key, $fh) = @_;
- $self->{'fd'}{$fd_no,$key} = $fh;
- }
- sub fh {
- # Get file handle
- my ($self, $fd_no, $key) = @_;
- return $self->{'fd'}{$fd_no,$key};
- }
- sub write {
- my $self = shift;
- my $remaining_ref = shift;
- my $stdin_fh = $self->fh(0,"w");
- syswrite($stdin_fh,$$remaining_ref);
- }
- sub set_stdin_buffer {
- # Copy stdin buffer from $block_ref up to $endpos
- # Prepend with $header_ref
- # Remove $recstart and $recend if needed
- # Input:
- # $header_ref = ref to $header to prepend
- # $block_ref = ref to $block to pass on
- # $endpos = length of $block to pass on
- # $recstart = --recstart regexp
- # $recend = --recend regexp
- # Returns:
- # N/A
- my $self = shift;
- my ($header_ref,$block_ref,$endpos,$recstart,$recend) = @_;
- $self->{'stdin_buffer'} = ($self->virgin() ? $$header_ref : "").substr($$block_ref,0,$endpos);
- if($opt::remove_rec_sep) {
- remove_rec_sep(\$self->{'stdin_buffer'},$recstart,$recend);
- }
- $self->{'stdin_buffer_length'} = length $self->{'stdin_buffer'};
- $self->{'stdin_buffer_pos'} = 0;
- }
- sub stdin_buffer_length {
- my $self = shift;
- return $self->{'stdin_buffer_length'};
- }
- sub remove_rec_sep {
- my ($block_ref,$recstart,$recend) = @_;
- # Remove record separator
- $$block_ref =~ s/$recend$recstart//gos;
- $$block_ref =~ s/^$recstart//os;
- $$block_ref =~ s/$recend$//os;
- }
- sub non_block_write {
- my $self = shift;
- my $something_written = 0;
- use POSIX qw(:errno_h);
- # use Fcntl;
- # my $flags = '';
- for my $buf (substr($self->{'stdin_buffer'},$self->{'stdin_buffer_pos'})) {
- my $in = $self->fh(0,"w");
- # fcntl($in, F_GETFL, $flags)
- # or die "Couldn't get flags for HANDLE : $!\n";
- # $flags |= O_NONBLOCK;
- # fcntl($in, F_SETFL, $flags)
- # or die "Couldn't set flags for HANDLE: $!\n";
- my $rv = syswrite($in, $buf);
- if (!defined($rv) && $! == EAGAIN) {
- # would block
- $something_written = 0;
- } elsif ($self->{'stdin_buffer_pos'}+$rv != $self->{'stdin_buffer_length'}) {
- # incomplete write
- # Remove the written part
- $self->{'stdin_buffer_pos'} += $rv;
- $something_written = $rv;
- } else {
- # successfully wrote everything
- my $a="";
- $self->set_stdin_buffer(\$a,\$a,"","");
- $something_written = $rv;
- }
- }
- ::debug("pipe", "Non-block: ", $something_written);
- return $something_written;
- }
- sub virgin {
- my $self = shift;
- return $self->{'virgin'};
- }
- sub set_virgin {
- my $self = shift;
- $self->{'virgin'} = shift;
- }
- sub pid {
- my $self = shift;
- return $self->{'pid'};
- }
- sub set_pid {
- my $self = shift;
- $self->{'pid'} = shift;
- }
- sub starttime {
- # Returns:
- # UNIX-timestamp this job started
- my $self = shift;
- return sprintf("%.3f",$self->{'starttime'});
- }
- sub set_starttime {
- my $self = shift;
- my $starttime = shift || ::now();
- $self->{'starttime'} = $starttime;
- }
- sub runtime {
- # Returns:
- # Run time in seconds
- my $self = shift;
- return sprintf("%.3f",int(($self->endtime() - $self->starttime())*1000)/1000);
- }
- sub endtime {
- # Returns:
- # UNIX-timestamp this job ended
- # 0 if not ended yet
- my $self = shift;
- return ($self->{'endtime'} || 0);
- }
- sub set_endtime {
- my $self = shift;
- my $endtime = shift;
- $self->{'endtime'} = $endtime;
- }
- sub timedout {
- # Is the job timedout?
- # Input:
- # $delta_time = time that the job may run
- # Returns:
- # True or false
- my $self = shift;
- my $delta_time = shift;
- return time > $self->{'starttime'} + $delta_time;
- }
- sub kill {
- # Kill the job.
- # Send the signals to (grand)*children and pid.
- # If no signals: TERM TERM KILL
- # Wait 200 ms after each TERM.
- # Input:
- # @signals = signals to send
- my $self = shift;
- my @signals = @_;
- my @family_pids = $self->family_pids();
- # Record this jobs as failed
- $self->set_exitstatus(-1);
- # Send two TERMs to give time to clean up
- ::debug("run", "Kill seq ", $self->seq(), "\n");
- my @send_signals = @signals || ("TERM", "TERM", "KILL");
- for my $signal (@send_signals) {
- my $alive = 0;
- for my $pid (@family_pids) {
- if(kill 0, $pid) {
- # The job still running
- kill $signal, $pid;
- $alive = 1;
- }
- }
- # If a signal was given as input, do not do the sleep below
- @signals and next;
- if($signal eq "TERM" and $alive) {
- # Wait up to 200 ms between TERMs - but only if any pids are alive
- my $sleep = 1;
- for (my $sleepsum = 0; kill 0, $family_pids[0] and $sleepsum < 200;
- $sleepsum += $sleep) {
- $sleep = ::reap_usleep($sleep);
- }
- }
- }
- }
- sub family_pids {
- # Find the pids with this->pid as (grand)*parent
- # Returns:
- # @pids = pids of (grand)*children
- my $self = shift;
- my $pid = $self->pid();
- my @pids;
- my ($children_of_ref, $parent_of_ref, $name_of_ref) = ::pid_table();
- my @more = ($pid);
- # While more (grand)*children
- while(@more) {
- my @m;
- push @pids, @more;
- for my $parent (@more) {
- if($children_of_ref->{$parent}) {
- # add the children of this parent
- push @m, @{$children_of_ref->{$parent}};
- }
- }
- @more = @m;
- }
- return (@pids);
- }
- sub failed {
- # return number of times failed for this $sshlogin
- # Input:
- # $sshlogin
- # Returns:
- # Number of times failed for $sshlogin
- my $self = shift;
- my $sshlogin = shift;
- return $self->{'failed'}{$sshlogin};
- }
- sub failed_here {
- # return number of times failed for the current $sshlogin
- # Returns:
- # Number of times failed for this sshlogin
- my $self = shift;
- return $self->{'failed'}{$self->sshlogin()};
- }
- sub add_failed {
- # increase the number of times failed for this $sshlogin
- my $self = shift;
- my $sshlogin = shift;
- $self->{'failed'}{$sshlogin}++;
- }
- sub add_failed_here {
- # increase the number of times failed for the current $sshlogin
- my $self = shift;
- $self->{'failed'}{$self->sshlogin()}++;
- }
- sub reset_failed {
- # increase the number of times failed for this $sshlogin
- my $self = shift;
- my $sshlogin = shift;
- delete $self->{'failed'}{$sshlogin};
- }
- sub reset_failed_here {
- # increase the number of times failed for this $sshlogin
- my $self = shift;
- delete $self->{'failed'}{$self->sshlogin()};
- }
- sub min_failed {
- # Returns:
- # the number of sshlogins this command has failed on
- # the minimal number of times this command has failed
- my $self = shift;
- my $min_failures =
- ::min(map { $self->{'failed'}{$_} } keys %{$self->{'failed'}});
- my $number_of_sshlogins_failed_on = scalar keys %{$self->{'failed'}};
- return ($number_of_sshlogins_failed_on,$min_failures);
- }
- sub total_failed {
- # Returns:
- # $total_failures = the number of times this command has failed
- my $self = shift;
- my $total_failures = 0;
- for (values %{$self->{'failed'}}) {
- $total_failures += $_;
- }
- return $total_failures;
- }
- sub wrapped {
- # Wrap command with:
- # * --shellquote
- # * --nice
- # * --cat
- # * --fifo
- # * --sshlogin
- # * --pipepart (@Global::cat_partials)
- # * --pipe
- # * --tmux
- # The ordering of the wrapping is important:
- # * --nice/--cat/--fifo should be done on the remote machine
- # * --pipepart/--pipe should be done on the local machine inside --tmux
- # Uses:
- # $Global::envvar
- # $opt::shellquote
- # $opt::nice
- # $Global::shell
- # $opt::cat
- # $opt::fifo
- # @Global::cat_partials
- # $opt::pipe
- # $opt::tmux
- # Returns:
- # $self->{'wrapped'} = the command wrapped with the above
- my $self = shift;
- if(not defined $self->{'wrapped'}) {
- my $command = $Global::envvar.$self->replaced();
- if($opt::shellquote) {
- # Prepend echo
- # and quote twice
- $command = "echo " .
- ::shell_quote_scalar(::shell_quote_scalar($command));
- }
- if($opt::nice) {
- # Prepend \nice -n19 $SHELL -c
- # and quote.
- # The '\' before nice is needed to avoid tcsh's built-in
- $command = '\nice'. " -n". $opt::nice. " ".
- $Global::shell. " -c ".
- ::shell_quote_scalar($command);
- }
- if($opt::cat) {
- # Prepend 'cat > {};'
- # Append '_EXIT=$?;(rm {};exit $_EXIT)'
- $command =
- $self->{'commandline'}->replace_placeholders(["cat > \257<\257>; "], 0, 0).
- $command.
- $self->{'commandline'}->replace_placeholders(
- ["; _EXIT=\$?; rm \257<\257>; exit \$_EXIT"], 0, 0);
- } elsif($opt::fifo) {
- # Prepend 'mkfifo {}; ('
- # Append ') & _PID=$!; cat > {}; wait $_PID; _EXIT=$?;(rm {};exit $_EXIT)'
- $command =
- $self->{'commandline'}->replace_placeholders(["mkfifo \257<\257>; ("], 0, 0).
- $command.
- $self->{'commandline'}->replace_placeholders([") & _PID=\$!; cat > \257<\257>; ",
- "wait \$_PID; _EXIT=\$?; ",
- "rm \257<\257>; exit \$_EXIT"],
- 0,0);
- }
- # Wrap with ssh + tranferring of files
- $command = $self->sshlogin_wrap($command);
- if(@Global::cat_partials) {
- # Prepend:
- # < /tmp/foo perl -e 'while(@ARGV) { sysseek(STDIN,shift,0) || die; $left = shift; while($read = sysread(STDIN,$buf, ($left > 32768 ? 32768 : $left))){ $left -= $read; syswrite(STDOUT,$buf); } }' 0 0 0 11 |
- $command = (shift @Global::cat_partials). "|". "(". $command. ")";
- } elsif($opt::pipe) {
- # Prepend EOF-detector to avoid starting $command if EOF.
- # The $tmpfile might exist if run on a remote system - we accept that risk
- my ($dummy_fh, $tmpfile) = ::tmpfile(SUFFIX => ".chr");
- # Unlink to avoid leaving files if --dry-run or --sshlogin
- unlink $tmpfile;
- $command =
- # Exit value:
- # empty input = true
- # some input = exit val from command
- qq{ sh -c 'dd bs=1 count=1 of=$tmpfile 2>/dev/null'; }.
- qq{ test \! -s "$tmpfile" && rm -f "$tmpfile" && exec true; }.
- qq{ (cat $tmpfile; rm $tmpfile; cat - ) | }.
- "($command);";
- }
- if($opt::tmux) {
- # Wrap command with 'tmux'
- $command = $self->tmux_wrap($command);
- }
- $self->{'wrapped'} = $command;
- }
- return $self->{'wrapped'};
- }
- sub set_sshlogin {
- my $self = shift;
- my $sshlogin = shift;
- $self->{'sshlogin'} = $sshlogin;
- delete $self->{'sshlogin_wrap'}; # If sshlogin is changed the wrap is wrong
- delete $self->{'wrapped'};
- }
- sub sshlogin {
- my $self = shift;
- return $self->{'sshlogin'};
- }
- sub sshlogin_wrap {
- # Wrap the command with the commands needed to run remotely
- # Returns:
- # $self->{'sshlogin_wrap'} = command wrapped with ssh+transfer commands
- my $self = shift;
- my $command = shift;
- if(not defined $self->{'sshlogin_wrap'}) {
- my $sshlogin = $self->sshlogin();
- my $sshcmd = $sshlogin->sshcommand();
- my $serverlogin = $sshlogin->serverlogin();
- my ($pre,$post,$cleanup)=("","","");
- if($serverlogin eq ":") {
- # No transfer neeeded
- $self->{'sshlogin_wrap'} = $command;
- } else {
- # --transfer
- $pre .= $self->sshtransfer();
- # --return
- $post .= $self->sshreturn();
- # --cleanup
- $post .= $self->sshcleanup();
- if($post) {
- # We need to save the exit status of the job
- $post = '_EXIT_status=$?; ' . $post . ' exit $_EXIT_status;';
- }
- # If the remote login shell is (t)csh then use 'setenv'
- # otherwise use 'export'
- # We cannot use parse_env_var(), as PARALLEL_SEQ changes
- # for each command
- my $parallel_env =
- ($Global::envwarn
- . q{ 'eval `echo $SHELL | grep "/t\\{0,1\\}csh" > /dev/null }
- . q{ && echo setenv PARALLEL_SEQ '$PARALLEL_SEQ'\; }
- . q{ setenv PARALLEL_PID '$PARALLEL_PID' }
- . q{ || echo PARALLEL_SEQ='$PARALLEL_SEQ'\;export PARALLEL_SEQ\; }
- . q{ PARALLEL_PID='$PARALLEL_PID'\;export PARALLEL_PID` ;' });
- my $remote_pre = "";
- my $ssh_options = "";
- if(($opt::pipe or $opt::pipepart) and $opt::ctrlc
- or
- not ($opt::pipe or $opt::pipepart) and not $opt::noctrlc) {
- # TODO Determine if this is needed
- # Propagating CTRL-C to kill remote jobs requires
- # remote jobs to be run with a terminal.
- $ssh_options = "-tt -oLogLevel=quiet";
- # $ssh_options = "";
- # tty - check if we have a tty.
- # stty:
- # -onlcr - make output 8-bit clean
- # isig - pass CTRL-C as signal
- # -echo - do not echo input
- $remote_pre .= ::shell_quote_scalar('tty >/dev/null && stty isig -onlcr -echo;');
- }
- if($opt::workdir) {
- my $wd = ::shell_quote_file($self->workdir());
- $remote_pre .= ::shell_quote_scalar("mkdir -p ") . $wd .
- ::shell_quote_scalar("; cd ") . $wd .
- # exit 255 (instead of exec false) would be the correct thing,
- # but that fails on tcsh
- ::shell_quote_scalar(qq{ || exec false;});
- }
- # This script is to solve the problem of
- # * not mixing STDERR and STDOUT
- # * terminating with ctrl-c
- # It works on Linux but not Solaris
- # Finishes on Solaris, but wrong exit code:
- # $SIG{CHLD} = sub {exit ($?&127 ? 128+($?&127) : 1+$?>>8)};
- # Hangs on Solaris, but correct exit code on Linux:
- # $SIG{CHLD} = sub { $done = 1 };
- # $p->poll;
- my $signal_script = "perl -e '".
- q{
- use IO::Poll;
- $SIG{CHLD} = sub { $done = 1 };
- $p = IO::Poll->new;
- $p->mask(STDOUT, POLLHUP);
- $pid=fork; unless($pid) {setpgrp; exec $ENV{SHELL}, "-c", @ARGV; die "exec: $!\n"}
- $p->poll;
- kill SIGHUP, -${pid} unless $done;
- wait; exit ($?&127 ? 128+($?&127) : 1+$?>>8)
- } . "' ";
- $signal_script =~ s/\s+/ /g;
- $self->{'sshlogin_wrap'} =
- ($pre
- . "$sshcmd $ssh_options $serverlogin $parallel_env "
- . $remote_pre
- # . ::shell_quote_scalar($signal_script . ::shell_quote_scalar($command))
- . ::shell_quote_scalar($command)
- . ";"
- . $post);
- }
- }
- return $self->{'sshlogin_wrap'};
- }
- sub transfer {
- # Files to transfer
- # Returns:
- # @transfer - File names of files to transfer
- my $self = shift;
- my @transfer = ();
- $self->{'transfersize'} = 0;
- if($opt::transfer) {
- for my $record (@{$self->{'commandline'}{'arg_list'}}) {
- # Merge arguments from records into args
- for my $arg (@$record) {
- CORE::push @transfer, $arg->orig();
- # filesize
- if(-e $arg->orig()) {
- $self->{'transfersize'} += (stat($arg->orig()))[7];
- }
- }
- }
- }
- return @transfer;
- }
- sub transfersize {
- my $self = shift;
- return $self->{'transfersize'};
- }
- sub sshtransfer {
- # Returns for each transfer file:
- # rsync $file remote:$workdir
- my $self = shift;
- my @pre;
- my $sshlogin = $self->sshlogin();
- my $workdir = $self->workdir();
- for my $file ($self->transfer()) {
- push @pre, $sshlogin->rsync_transfer_cmd($file,$workdir).";";
- }
- return join("",@pre);
- }
- sub return {
- # Files to return
- # Non-quoted and with {...} substituted
- # Returns:
- # @non_quoted_filenames
- my $self = shift;
- return $self->{'commandline'}->
- replace_placeholders($self->{'commandline'}{'return_files'},0,0);
- }
- sub returnsize {
- # This is called after the job has finished
- # Returns:
- # $number_of_bytes transferred in return
- my $self = shift;
- for my $file ($self->return()) {
- if(-e $file) {
- $self->{'returnsize'} += (stat($file))[7];
- }
- }
- return $self->{'returnsize'};
- }
- sub sshreturn {
- # Returns for each return-file:
- # rsync remote:$workdir/$file .
- my $self = shift;
- my $sshlogin = $self->sshlogin();
- my $sshcmd = $sshlogin->sshcommand();
- my $serverlogin = $sshlogin->serverlogin();
- my $rsync_opt = "-rlDzR -e".::shell_quote_scalar($sshcmd);
- my $pre = "";
- for my $file ($self->return()) {
- $file =~ s:^\./::g; # Remove ./ if any
- my $relpath = ($file !~ m:^/:); # Is the path relative?
- my $cd = "";
- my $wd = "";
- if($relpath) {
- # rsync -avR /foo/./bar/baz.c remote:/tmp/
- # == (on old systems)
- # rsync -avR --rsync-path="cd /foo; rsync" remote:bar/baz.c /tmp/
- $wd = ::shell_quote_file($self->workdir()."/");
- }
- # Only load File::Basename if actually needed
- $Global::use{"File::Basename"} ||= eval "use File::Basename; 1;";
- # dir/./file means relative to dir, so remove dir on remote
- $file =~ m:(.*)/\./:;
- my $basedir = $1 ? ::shell_quote_file($1."/") : "";
- my $nobasedir = $file;
- $nobasedir =~ s:.*/\./::;
- $cd = ::shell_quote_file(::dirname($nobasedir));
- my $rsync_cd = '--rsync-path='.::shell_quote_scalar("cd $wd$cd; rsync");
- my $basename = ::shell_quote_scalar(::shell_quote_file(basename($file)));
- # --return
- # mkdir -p /home/tange/dir/subdir/;
- # rsync (--protocol 30) -rlDzR --rsync-path="cd /home/tange/dir/subdir/; rsync"
- # server:file.gz /home/tange/dir/subdir/
- $pre .= "mkdir -p $basedir$cd; ".$sshlogin->rsync()." $rsync_cd $rsync_opt $serverlogin:".
- $basename . " ".$basedir.$cd.";";
- }
- return $pre;
- }
- sub sshcleanup {
- # Return the sshcommand needed to remove the file
- # Returns:
- # ssh command needed to remove files from sshlogin
- my $self = shift;
- my $sshlogin = $self->sshlogin();
- my $sshcmd = $sshlogin->sshcommand();
- my $serverlogin = $sshlogin->serverlogin();
- my $workdir = $self->workdir();
- my $cleancmd = "";
- for my $file ($self->cleanup()) {
- my @subworkdirs = parentdirs_of($file);
- $cleancmd .= $sshlogin->cleanup_cmd($file,$workdir).";";
- }
- if(defined $opt::workdir and $opt::workdir eq "...") {
- $cleancmd .= "$sshcmd $serverlogin rm -rf " . ::shell_quote_scalar($workdir).';';
- }
- return $cleancmd;
- }
- sub cleanup {
- # Returns:
- # Files to remove at cleanup
- my $self = shift;
- if($opt::cleanup) {
- my @transfer = $self->transfer();
- my @return = $self->return();
- return (@transfer,@return);
- } else {
- return ();
- }
- }
- sub workdir {
- # Returns:
- # the workdir on a remote machine
- my $self = shift;
- if(not defined $self->{'workdir'}) {
- my $workdir;
- if(defined $opt::workdir) {
- if($opt::workdir eq ".") {
- # . means current dir
- my $home = $ENV{'HOME'};
- eval 'use Cwd';
- my $cwd = cwd();
- $workdir = $cwd;
- if($home) {
- # If homedir exists: remove the homedir from
- # workdir if cwd starts with homedir
- # E.g. /home/foo/my/dir => my/dir
- # E.g. /tmp/my/dir => /tmp/my/dir
- my ($home_dev, $home_ino) = (stat($home))[0,1];
- my $parent = "";
- my @dir_parts = split(m:/:,$cwd);
- my $part;
- while(defined ($part = shift @dir_parts)) {
- $part eq "" and next;
- $parent .= "/".$part;
- my ($parent_dev, $parent_ino) = (stat($parent))[0,1];
- if($parent_dev == $home_dev and $parent_ino == $home_ino) {
- # dev and ino is the same: We found the homedir.
- $workdir = join("/",@dir_parts);
- last;
- }
- }
- }
- if($workdir eq "") {
- $workdir = ".";
- }
- } elsif($opt::workdir eq "...") {
- $workdir = ".parallel/tmp/" . ::hostname() . "-" . $$
- . "-" . $self->seq();
- } else {
- $workdir = $opt::workdir;
- # Rsync treats /./ special. We don't want that
- $workdir =~ s:/\./:/:g; # Remove /./
- $workdir =~ s:/+$::; # Remove ending / if any
- $workdir =~ s:^\./::g; # Remove starting ./ if any
- }
- } else {
- $workdir = ".";
- }
- $self->{'workdir'} = ::shell_quote_scalar($workdir);
- }
- return $self->{'workdir'};
- }
- sub parentdirs_of {
- # Return:
- # all parentdirs except . of this dir or file - sorted desc by length
- my $d = shift;
- my @parents = ();
- while($d =~ s:/[^/]+$::) {
- if($d ne ".") {
- push @parents, $d;
- }
- }
- return @parents;
- }
- sub start {
- # Setup STDOUT and STDERR for a job and start it.
- # Returns:
- # job-object or undef if job not to run
- my $job = shift;
- # Get the shell command to be executed (possibly with ssh infront).
- my $command = $job->wrapped();
- if($Global::interactive or $Global::stderr_verbose) {
- if($Global::interactive) {
- print $Global::original_stderr "$command ?...";
- open(my $tty_fh, "<", "/dev/tty") || ::die_bug("interactive-tty");
- my $answer = <$tty_fh>;
- close $tty_fh;
- my $run_yes = ($answer =~ /^\s*y/i);
- if (not $run_yes) {
- $command = "true"; # Run the command 'true'
- }
- } else {
- print $Global::original_stderr "$command\n";
- }
- }
- my $pid;
- $job->openoutputfiles();
- my($stdout_fh,$stderr_fh) = ($job->fh(1,"w"),$job->fh(2,"w"));
- local (*IN,*OUT,*ERR);
- open OUT, '>&', $stdout_fh or ::die_bug("Can't redirect STDOUT: $!");
- open ERR, '>&', $stderr_fh or ::die_bug("Can't dup STDOUT: $!");
- if(($opt::dryrun or $Global::verbose) and $opt::ungroup) {
- if($Global::verbose <= 1) {
- print $stdout_fh $job->replaced(),"\n";
- } else {
- # Verbose level > 1: Print the rsync and stuff
- print $stdout_fh $command,"\n";
- }
- }
- if($opt::dryrun) {
- $command = "true";
- }
- $ENV{'PARALLEL_SEQ'} = $job->seq();
- $ENV{'PARALLEL_PID'} = $$;
- ::debug("run", $Global::total_running, " processes . Starting (",
- $job->seq(), "): $command\n");
- if($opt::pipe) {
- my ($stdin_fh);
- # The eval is needed to catch exception from open3
- eval {
- $pid = ::open3($stdin_fh, ">&OUT", ">&ERR", $Global::shell, "-c", $command) ||
- ::die_bug("open3-pipe");
- 1;
- };
- $job->set_fh(0,"w",$stdin_fh);
- } elsif(@opt::a and not $Global::stdin_in_opt_a and $job->seq() == 1
- and $job->sshlogin()->string() eq ":") {
- # Give STDIN to the first job if using -a (but only if running
- # locally - otherwise CTRL-C does not work for other jobs Bug#36585)
- *IN = *STDIN;
- # The eval is needed to catch exception from open3
- eval {
- $pid = ::open3("<&IN", ">&OUT", ">&ERR", $Global::shell, "-c", $command) ||
- ::die_bug("open3-a");
- 1;
- };
- # Re-open to avoid complaining
- open(STDIN, "<&", $Global::original_stdin)
- or ::die_bug("dup-\$Global::original_stdin: $!");
- } elsif ($opt::tty and not $Global::tty_taken and -c "/dev/tty" and
- open(my $devtty_fh, "<", "/dev/tty")) {
- # Give /dev/tty to the command if no one else is using it
- *IN = $devtty_fh;
- # The eval is needed to catch exception from open3
- eval {
- $pid = ::open3("<&IN", ">&OUT", ">&ERR", $Global::shell, "-c", $command) ||
- ::die_bug("open3-/dev/tty");
- $Global::tty_taken = $pid;
- close $devtty_fh;
- 1;
- };
- } else {
- # The eval is needed to catch exception from open3
- eval {
- $pid = ::open3(::gensym, ">&OUT", ">&ERR", $Global::shell, "-c", $command) ||
- ::die_bug("open3-gensym");
- 1;
- };
- }
- if($pid) {
- # A job was started
- $Global::total_running++;
- $Global::total_started++;
- $job->set_pid($pid);
- $job->set_starttime();
- $Global::running{$job->pid()} = $job;
- if($opt::timeout) {
- $Global::timeoutq->insert($job);
- }
- $Global::newest_job = $job;
- $Global::newest_starttime = ::now();
- return $job;
- } else {
- # No more processes
- ::debug("run", "Cannot spawn more jobs.\n");
- return undef;
- }
- }
- sub tmux_wrap {
- # Wrap command with tmux for session pPID
- # Input:
- # $actual_command = the actual command being run (incl ssh wrap)
- my $self = shift;
- my $actual_command = shift;
- # Temporary file name. Used for fifo to communicate exit val
- my ($fh, $tmpfile) = ::tmpfile(SUFFIX => ".tmx");
- $Global::unlink{$tmpfile}=1;
- close $fh;
- unlink $tmpfile;
- my $visual_command = $self->replaced();
- my $title = $visual_command;
- # ; causes problems
- # ascii 194-245 annoys tmux
- $title =~ tr/[\011-\016;\302-\365]//d;
- my $tmux;
- if($Global::total_running == 0) {
- $tmux = "tmux new-session -s p$$ -d -n ".
- ::shell_quote_scalar($title);
- print $Global::original_stderr "See output with: tmux attach -t p$$\n";
- } else {
- $tmux = "tmux new-window -t p$$ -n ".::shell_quote_scalar($title);
- }
- return "mkfifo $tmpfile; $tmux ".
- # Run in tmux
- ::shell_quote_scalar(
- "(".$actual_command.');(echo $?$status;echo 255) >'.$tmpfile."&".
- "echo ".::shell_quote_scalar($visual_command).";".
- "echo \007Job finished at: `date`;sleep 10").
- # Run outside tmux
- # Read the first line from the fifo and use that as status code
- "; exit `perl -ne 'unlink \$ARGV; 1..1 and print' $tmpfile` ";
- }
- sub is_already_in_results {
- # Do we already have results for this job?
- # Returns:
- # $job_already_run = bool whether there is output for this or not
- my $job = $_[0];
- my $args_as_dirname = $job->{'commandline'}->args_as_dirname();
- # prefix/name1/val1/name2/val2/
- my $dir = $opt::results."/".$args_as_dirname;
- ::debug("run", "Test $dir/stdout", -e "$dir/stdout", "\n");
- return -e "$dir/stdout";
- }
- sub is_already_in_joblog {
- my $job = shift;
- return vec($Global::job_already_run,$job->seq(),1);
- }
- sub set_job_in_joblog {
- my $job = shift;
- vec($Global::job_already_run,$job->seq(),1) = 1;
- }
- sub should_be_retried {
- # Should this job be retried?
- # Returns
- # 0 - do not retry
- # 1 - job queued for retry
- my $self = shift;
- if (not $opt::retries) {
- return 0;
- }
- if(not $self->exitstatus()) {
- # Completed with success. If there is a recorded failure: forget it
- $self->reset_failed_here();
- return 0
- } else {
- # The job failed. Should it be retried?
- $self->add_failed_here();
- if($self->total_failed() == $opt::retries) {
- # This has been retried enough
- return 0;
- } else {
- # This command should be retried
- $self->set_endtime(undef);
- $Global::JobQueue->unget($self);
- ::debug("run", "Retry ", $self->seq(), "\n");
- return 1;
- }
- }
- }
- sub print {
- # Print the output of the jobs
- # Returns: N/A
- my $self = shift;
- ::debug("print", ">>joboutput ", $self->replaced(), "\n");
- if($opt::dryrun) {
- # Nothing was printed to this job:
- # cleanup tmp files if --files was set
- unlink $self->fh(1,"name");
- }
- if($opt::pipe and $self->virgin()) {
- # Skip --joblog, --dryrun, --verbose
- } else {
- if($Global::joblog and defined $self->{'exitstatus'}) {
- # Add to joblog when finished
- $self->print_joblog();
- }
- # Printing is only relevant for grouped/--line-buffer output.
- $opt::ungroup and return;
- # Check for disk full
- exit_if_disk_full();
- if(($opt::dryrun or $Global::verbose)
- and
- not $self->{'verbose_printed'}) {
- $self->{'verbose_printed'}++;
- if($Global::verbose <= 1) {
- print STDOUT $self->replaced(),"\n";
- } else {
- # Verbose level > 1: Print the rsync and stuff
- print STDOUT $self->wrapped(),"\n";
- }
- # If STDOUT and STDERR are merged,
- # we want the command to be printed first
- # so flush to avoid STDOUT being buffered
- flush STDOUT;
- }
- }
- for my $fdno (sort { $a <=> $b } keys %Global::fd) {
- # Sort by file descriptor numerically: 1,2,3,..,9,10,11
- $fdno == 0 and next;
- my $out_fd = $Global::fd{$fdno};
- my $in_fh = $self->fh($fdno,"r");
- if(not $in_fh) {
- if(not $Job::file_descriptor_warning_printed{$fdno}++) {
- # ::warning("File descriptor $fdno not defined\n");
- }
- next;
- }
- ::debug("print", "File descriptor $fdno (", $self->fh($fdno,"name"), "):");
- if($opt::files) {
- # If --compress: $in_fh must be closed first.
- close $self->fh($fdno,"w");
- close $in_fh;
- if($opt::pipe and $self->virgin()) {
- # Nothing was printed to this job:
- # cleanup unused tmp files if --files was set
- for my $fdno (1,2) {
- unlink $self->fh($fdno,"name");
- unlink $self->fh($fdno,"unlink");
- }
- } elsif($fdno == 1 and $self->fh($fdno,"name")) {
- print $out_fd $self->fh($fdno,"name"),"\n";
- }
- } elsif($opt::linebuffer) {
- # Line buffered print out
- $self->linebuffer_print($fdno,$in_fh,$out_fd);
- } else {
- my $buf;
- close $self->fh($fdno,"w");
- seek $in_fh, 0, 0;
- # $in_fh is now ready for reading at position 0
- if($opt::tag or defined $opt::tagstring) {
- my $tag = $self->tag();
- if($fdno == 2) {
- # OpenSSH_3.6.1p2 gives 'tcgetattr: Invalid argument' with -tt
- # This is a crappy way of ignoring it.
- while(<$in_fh>) {
- if(/^(client_process_control: )?tcgetattr: Invalid argument\n/) {
- # Skip
- } else {
- print $out_fd $tag,$_;
- }
- # At most run the loop once
- last;
- }
- }
- while(<$in_fh>) {
- print $out_fd $tag,$_;
- }
- } else {
- my $buf;
- if($fdno == 2) {
- # OpenSSH_3.6.1p2 gives 'tcgetattr: Invalid argument' with -tt
- # This is a crappy way of ignoring it.
- sysread($in_fh,$buf,1_000);
- $buf =~ s/^(client_process_control: )?tcgetattr: Invalid argument\n//;
- print $out_fd $buf;
- }
- while(sysread($in_fh,$buf,32768)) {
- print $out_fd $buf;
- }
- }
- close $in_fh;
- }
- flush $out_fd;
- }
- ::debug("print", "<<joboutput @command\n");
- }
- sub linebuffer_print {
- my $self = shift;
- my ($fdno,$in_fh,$out_fd) = @_;
- my $partial = \$self->{'partial_line',$fdno};
- if(defined $self->{'exitstatus'}) {
- # If the job is dead: close printing fh. Needed for --compress
- close $self->fh($fdno,"w");
- if($opt::compress) {
- # Blocked reading in final round
- $Global::use{"Fcntl"} ||= eval "use Fcntl qw(:DEFAULT :flock); 1;";
- for my $fdno (1,2) {
- my $fdr = $self->fh($fdno,'r');
- my $flags;
- fcntl($fdr, &F_GETFL, $flags) || die $!; # Get the current flags on the filehandle
- $flags &= ~&O_NONBLOCK; # Remove non-blocking to the flags
- fcntl($fdr, &F_SETFL, $flags) || die $!; # Set the flags on the filehandle
- }
- }
- }
- # This seek will clear EOF
- seek $in_fh, tell($in_fh), 0;
- # The read is non-blocking: The $in_fh is set to non-blocking.
- # 32768 --tag = 5.1s
- # 327680 --tag = 4.4s
- # 1024000 --tag = 4.4s
- # 3276800 --tag = 4.3s
- # 32768000 --tag = 4.7s
- # 10240000 --tag = 4.3s
- while(read($in_fh,substr($$partial,length $$partial),3276800)) {
- # Append to $$partial
- # Find the last \n
- my $i = rindex($$partial,"\n");
- if($i != -1) {
- # One or more complete lines were found
- if($fdno == 2 and not $self->{'printed_first_line',$fdno}++) {
- # OpenSSH_3.6.1p2 gives 'tcgetattr: Invalid argument' with -tt
- # This is a crappy way of ignoring it.
- $$partial =~ s/^(client_process_control: )?tcgetattr: Invalid argument\n//;
- # Length of partial line has changed: Find the last \n again
- $i = rindex($$partial,"\n");
- }
- if($opt::tag or defined $opt::tagstring) {
- # Replace ^ with $tag within the full line
- my $tag = $self->tag();
- substr($$partial,0,$i+1) =~ s/^/$tag/gm;
- # Length of partial line has changed: Find the last \n again
- $i = rindex($$partial,"\n");
- }
- # Print up to and including the last \n
- print $out_fd substr($$partial,0,$i+1);
- # Remove the printed part
- substr($$partial,0,$i+1)="";
- }
- }
- if(defined $self->{'exitstatus'}) {
- # If the job is dead: print the remaining partial line
- # read remaining
- if($$partial and ($opt::tag or defined $opt::tagstring)) {
- my $tag = $self->tag();
- $$partial =~ s/^/$tag/gm;
- }
- print $out_fd $$partial;
- # Release the memory
- $$partial = undef;
- if($self->fh($fdno,"rpid") and CORE::kill 0, $self->fh($fdno,"rpid")) {
- # decompress still running
- } else {
- # decompress done: close fh
- close $in_fh;
- }
- }
- }
- sub print_joblog {
- my $self = shift;
- my $cmd;
- if($Global::verbose <= 1) {
- $cmd = $self->replaced();
- } else {
- # Verbose level > 1: Print the rsync and stuff
- $cmd = "@command";
- }
- print $Global::joblog
- join("\t", $self->seq(), $self->sshlogin()->string(),
- $self->starttime(), sprintf("%10.3f",$self->runtime()),
- $self->transfersize(), $self->returnsize(),
- $self->exitstatus(), $self->exitsignal(), $cmd
- ). "\n";
- flush $Global::joblog;
- $self->set_job_in_joblog();
- }
- sub tag {
- my $self = shift;
- if(not defined $self->{'tag'}) {
- $self->{'tag'} = $self->{'commandline'}->
- replace_placeholders([$opt::tagstring],0,0)."\t";
- }
- return $self->{'tag'};
- }
- sub hostgroups {
- my $self = shift;
- if(not defined $self->{'hostgroups'}) {
- $self->{'hostgroups'} = $self->{'commandline'}->{'arg_list'}[0][0]->{'hostgroups'};
- }
- return @{$self->{'hostgroups'}};
- }
- sub exitstatus {
- my $self = shift;
- return $self->{'exitstatus'};
- }
- sub set_exitstatus {
- my $self = shift;
- my $exitstatus = shift;
- if($exitstatus) {
- # Overwrite status if non-zero
- $self->{'exitstatus'} = $exitstatus;
- } else {
- # Set status but do not overwrite
- # Status may have been set by --timeout
- $self->{'exitstatus'} ||= $exitstatus;
- }
- }
- sub exitsignal {
- my $self = shift;
- return $self->{'exitsignal'};
- }
- sub set_exitsignal {
- my $self = shift;
- my $exitsignal = shift;
- $self->{'exitsignal'} = $exitsignal;
- }
- {
- my ($disk_full_fh, $b8193, $name);
- sub exit_if_disk_full {
- # Checks if $TMPDIR is full by writing 8kb to a tmpfile
- # If the disk is full: Exit immediately.
- # Returns:
- # N/A
- if(not $disk_full_fh) {
- ($disk_full_fh, $name) = ::tmpfile(SUFFIX => ".df");
- unlink $name;
- $b8193 = "x"x8193;
- }
- # Linux does not discover if a disk is full if writing <= 8192
- # Tested on:
- # bfs btrfs cramfs ext2 ext3 ext4 ext4dev jffs2 jfs minix msdos
- # ntfs reiserfs tmpfs ubifs vfat xfs
- # TODO this should be tested on different OS similar to this:
- #
- # doit() {
- # sudo mount /dev/ram0 /mnt/loop; sudo chmod 1777 /mnt/loop
- # seq 100000 | parallel --tmpdir /mnt/loop/ true &
- # seq 6900000 > /mnt/loop/i && echo seq OK
- # seq 6980868 > /mnt/loop/i
- # seq 10000 > /mnt/loop/ii
- # sleep 3
- # sudo umount /mnt/loop/ || sudo umount -l /mnt/loop/
- # echo >&2
- # }
- print $disk_full_fh $b8193;
- if(not $disk_full_fh
- or
- tell $disk_full_fh == 0) {
- ::error("Output is incomplete. Cannot append to buffer file in $ENV{'TMPDIR'}. Is the disk full?\n");
- ::error("Change \$TMPDIR with --tmpdir or use --compress.\n");
- ::wait_and_exit(255);
- }
- truncate $disk_full_fh, 0;
- seek($disk_full_fh, 0, 0) || die;
- }
- }
- package CommandLine;
- sub new {
- my $class = shift;
- my $seq = shift;
- my $commandref = shift;
- $commandref || die;
- my $arg_queue = shift;
- my $context_replace = shift;
- my $max_number_of_args = shift; # for -N and normal (-n1)
- my $return_files = shift;
- my $replacecount_ref = shift;
- my $len_ref = shift;
- my %replacecount = %$replacecount_ref;
- my %len = %$len_ref;
- for (keys %$replacecount_ref) {
- # Total length of this replacement string {} replaced with all args
- $len{$_} = 0;
- }
- return bless {
- 'command' => $commandref,
- 'seq' => $seq,
- 'len' => \%len,
- 'arg_list' => [],
- 'arg_queue' => $arg_queue,
- 'max_number_of_args' => $max_number_of_args,
- 'replacecount' => \%replacecount,
- 'context_replace' => $context_replace,
- 'return_files' => $return_files,
- 'replaced' => undef,
- }, ref($class) || $class;
- }
- sub seq {
- my $self = shift;
- return $self->{'seq'};
- }
- {
- my $max_slot_number;
- sub slot {
- # Find the number of a free job slot and return it
- # Uses:
- # @Global::slots
- # Returns:
- # $jobslot = number of jobslot
- my $self = shift;
- if(not $self->{'slot'}) {
- if(not @Global::slots) {
- # $Global::max_slot_number will typically be $Global::max_jobs_running
- push @Global::slots, ++$max_slot_number;
- }
- $self->{'slot'} = shift @Global::slots;
- }
- return $self->{'slot'};
- }
- }
- sub populate {
- # Add arguments from arg_queue until the number of arguments or
- # max line length is reached
- # Uses:
- # $Global::minimal_command_line_length
- # $opt::cat
- # $opt::fifo
- # $Global::JobQueue
- # $opt::m
- # $opt::X
- # $CommandLine::already_spread
- # $Global::max_jobs_running
- # Returns: N/A
- my $self = shift;
- my $next_arg;
- my $max_len = $Global::minimal_command_line_length || Limits::Command::max_length();
- if($opt::cat or $opt::fifo) {
- # Generate a tempfile name that will be used as {}
- my($outfh,$name) = ::tmpfile(SUFFIX => ".pip");
- close $outfh;
- # Unlink is needed if: ssh otheruser@localhost
- unlink $name;
- $Global::JobQueue->{'commandlinequeue'}->{'arg_queue'}->unget([Arg->new($name)]);
- }
- while (not $self->{'arg_queue'}->empty()) {
- $next_arg = $self->{'arg_queue'}->get();
- if(not defined $next_arg) {
- next;
- }
- $self->push($next_arg);
- if($self->len() >= $max_len) {
- # Command length is now > max_length
- # If there are arguments: remove the last
- # If there are no arguments: Error
- # TODO stuff about -x opt_x
- if($self->number_of_args() > 1) {
- # There is something to work on
- $self->{'arg_queue'}->unget($self->pop());
- last;
- } else {
- my $args = join(" ", map { $_->orig() } @$next_arg);
- ::error("Command line too long (",
- $self->len(), " >= ",
- $max_len,
- ") at number ",
- $self->{'arg_queue'}->arg_number(),
- ": ".
- (substr($args,0,50))."...\n");
- $self->{'arg_queue'}->unget($self->pop());
- ::wait_and_exit(255);
- }
- }
- if(defined $self->{'max_number_of_args'}) {
- if($self->number_of_args() >= $self->{'max_number_of_args'}) {
- last;
- }
- }
- }
- if(($opt::m or $opt::X) and not $CommandLine::already_spread
- and $self->{'arg_queue'}->empty() and $Global::max_jobs_running) {
- # -m or -X and EOF => Spread the arguments over all jobslots
- # (unless they are already spread)
- $CommandLine::already_spread ||= 1;
- if($self->number_of_args() > 1) {
- $self->{'max_number_of_args'} =
- ::ceil($self->number_of_args()/$Global::max_jobs_running);
- $Global::JobQueue->{'commandlinequeue'}->{'max_number_of_args'} =
- $self->{'max_number_of_args'};
- $self->{'arg_queue'}->unget($self->pop_all());
- while($self->number_of_args() < $self->{'max_number_of_args'}) {
- $self->push($self->{'arg_queue'}->get());
- }
- }
- }
- }
- sub push {
- # Add one or more records as arguments
- # Returns: N/A
- my $self = shift;
- my $record = shift;
- push @{$self->{'arg_list'}}, $record;
- my $quote_arg = $Global::noquote ? 0 : not $Global::quoting;
- my $rep;
- for my $arg (@$record) {
- if(defined $arg) {
- for my $perlexpr (keys %{$self->{'replacecount'}}) {
- # 50% faster than below
- $self->{'len'}{$perlexpr} += length $arg->replace($perlexpr,$quote_arg,$self);
- # $rep = $arg->replace($perlexpr,$quote_arg,$self);
- # $self->{'len'}{$perlexpr} += length $rep;
- # ::debug("length", "Length: ", length $rep,
- # "(", $perlexpr, "=>", $rep, ")\n");
- }
- }
- }
- }
- sub pop {
- # Remove last argument
- # Returns:
- # the last record
- my $self = shift;
- my $record = pop @{$self->{'arg_list'}};
- my $quote_arg = $Global::noquote ? 0 : not $Global::quoting;
- for my $arg (@$record) {
- if(defined $arg) {
- for my $perlexpr (keys %{$self->{'replacecount'}}) {
- $self->{'len'}{$perlexpr} -=
- length $arg->replace($perlexpr,$quote_arg,$self);
- }
- }
- }
- return $record;
- }
- sub pop_all {
- # Remove all arguments and zeros the length of replacement strings
- # Returns:
- # all records
- my $self = shift;
- my @popped = @{$self->{'arg_list'}};
- for my $replacement_string (keys %{$self->{'replacecount'}}) {
- $self->{'len'}{$replacement_string} = 0;
- }
- $self->{'arg_list'} = [];
- return @popped;
- }
- sub number_of_args {
- # The number of records
- # Returns:
- # number of records
- my $self = shift;
- # Ftq rudef oaawuq ime dqxqmeqp az 2011-01-24 mzp ime iaz nk MQhmd
- # Mdzrvadp Nvmdymeaz az 2011-04-10. Ftue oaawuq dqxqmeqp az
- # 2013-08-18 ue m nuf tmdpqd me kag tmhq fa geq daf14. Bxqmeq
- # qymux oaawuq@fmzsq.pw itqz kag dqmp ftue.
- #
- # U my ftq ymuzfmuzqd ar m buqoq ar rdqq earfimdq omxxqp SZG
- # Bmdmxxqx. Rdqq earfimdq sgmdmzfqqe kag mooqee fa ftq eagdoq
- # oapq, ngf U tmhq nqqz iazpqduzs tai ymzk mofgmxxk _dqmp_ ftq
- # eagdoq oapq.
- #
- # Fa fqef ftue U bgf uz m oayyqzf fqxxuzs bqabxq fa qymux yq itqz
- # ftqk dqmp ftue. Ftq oayyqzf ime bgf uz m eqofuaz ar ftq oapq
- # ftmf za azq iagxp xaaw fa ruj ad uybdahq ftq earfimdq - ea ftq
- # eagdoq oapq qcguhmxqzf fa m pgefk oadzqd. Fa ymwq egdq ftq
- # oayyqzf iagxp zaf etai gb ur eayq azq vgef sdqbbqp ftdagst ftq
- # eagdoq oapq U daf13'qp ftq eagdoq oapq
- # tffb://qz.iuwubqpum.ads/iuwu/DAF13
- #
- # 2.5 yazfte xmfqd U dqoquhqp mz qymux rday eayqazq ita zaf azxk
- # ymzmsqp fa ruzp ftq oayyqzf, ngf mxea ymzmsqp fa sgqee ftq oapq
- # tmp fa nq daf13'qp.
- #
- # Ftue nduzse yq fa ftq oazoxgeuaz ftmf ftqdq _mdq_ bqabxq, ita
- # mdq zaf mrruxumfqp iuft ftq bdavqof, ftmf iuxx dqmp ftq eagdoq
- # oapq - ftagst uf ymk zaf tmbbqz hqdk arfqz.
- #
- # This is really the number of records
- return $#{$self->{'arg_list'}}+1;
- }
- sub number_of_recargs {
- # The number of args in records
- # Returns:
- # number of args records
- my $self = shift;
- my $sum = 0;
- my $nrec = scalar @{$self->{'arg_list'}};
- if($nrec) {
- $sum = $nrec * (scalar @{$self->{'arg_list'}[0]});
- }
- return $sum;
- }
- sub args_as_string {
- # Returns:
- # all unmodified arguments joined with ' ' (similar to {})
- my $self = shift;
- return (join " ", map { $_->orig() }
- map { @$_ } @{$self->{'arg_list'}});
- }
- sub args_as_dirname {
- # Returns:
- # all unmodified arguments joined with '/' (similar to {})
- # \t \0 \\ and / are quoted as: \t \0 \\ \_
- # If $Global::max_file_length: Keep subdirs < $Global::max_file_length
- my $self = shift;
- my @res = ();
- for my $rec_ref (@{$self->{'arg_list'}}) {
- # If headers are used, sort by them.
- # Otherwise keep the order from the command line.
- my @header_indexes_sorted = header_indexes_sorted($#$rec_ref+1);
- for my $n (@header_indexes_sorted) {
- CORE::push(@res,
- $Global::input_source_header{$n},
- map { my $s = $_;
- # \t \0 \\ and / are quoted as: \t \0 \\ \_
- $s =~ s/\\/\\\\/g;
- $s =~ s/\t/\\t/g;
- $s =~ s/\0/\\0/g;
- $s =~ s:/:\\_:g;
- if($Global::max_file_length) {
- # Keep each subdir shorter than the longest
- # allowed file name
- $s = substr($s,0,$Global::max_file_length);
- }
- $s; }
- $rec_ref->[$n-1]->orig());
- }
- }
- return join "/", @res;
- }
- sub header_indexes_sorted {
- # Sort headers first by number then by name.
- # E.g.: 1a 1b 11a 11b
- # Returns:
- # Indexes of %Global::input_source_header sorted
- my $max_col = shift;
- no warnings 'numeric';
- for my $col (1 .. $max_col) {
- # Make sure the header is defined. If it is not: use column number
- if(not defined $Global::input_source_header{$col}) {
- $Global::input_source_header{$col} = $col;
- }
- }
- my @header_indexes_sorted = sort {
- # Sort headers numerically then asciibetically
- $Global::input_source_header{$a} <=> $Global::input_source_header{$b}
- or
- $Global::input_source_header{$a} cmp $Global::input_source_header{$b}
- } 1 .. $max_col;
- return @header_indexes_sorted;
- }
- sub len {
- # Uses:
- # $opt::shellquote
- # The length of the command line with args substituted
- my $self = shift;
- my $len = 0;
- # Add length of the original command with no args
- # Length of command w/ all replacement args removed
- $len += $self->{'len'}{'noncontext'} + @{$self->{'command'}} -1;
- ::debug("length", "noncontext + command: $len\n");
- my $recargs = $self->number_of_recargs();
- if($self->{'context_replace'}) {
- # Context is duplicated for each arg
- $len += $recargs * $self->{'len'}{'context'};
- for my $replstring (keys %{$self->{'replacecount'}}) {
- # If the replacements string is more than once: mulitply its length
- $len += $self->{'len'}{$replstring} *
- $self->{'replacecount'}{$replstring};
- ::debug("length", $replstring, " ", $self->{'len'}{$replstring}, "*",
- $self->{'replacecount'}{$replstring}, "\n");
- }
- # echo 11 22 33 44 55 66 77 88 99 1010
- # echo 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
- # 5 + ctxgrp*arg
- ::debug("length", "Ctxgrp: ", $self->{'len'}{'contextgroups'},
- " Groups: ", $self->{'len'}{'noncontextgroups'}, "\n");
- # Add space between context groups
- $len += ($recargs-1) * ($self->{'len'}{'contextgroups'});
- } else {
- # Each replacement string may occur several times
- # Add the length for each time
- $len += 1*$self->{'len'}{'context'};
- ::debug("length", "context+noncontext + command: $len\n");
- for my $replstring (keys %{$self->{'replacecount'}}) {
- # (space between regargs + length of replacement)
- # * number this replacement is used
- $len += ($recargs -1 + $self->{'len'}{$replstring}) *
- $self->{'replacecount'}{$replstring};
- }
- }
- if($opt::nice) {
- # Pessimistic length if --nice is set
- # Worse than worst case: every char needs to be quoted with \
- $len *= 2;
- }
- if($Global::quoting) {
- # Pessimistic length if -q is set
- # Worse than worst case: every char needs to be quoted with \
- $len *= 2;
- }
- if($opt::shellquote) {
- # Pessimistic length if --shellquote is set
- # Worse than worst case: every char needs to be quoted with \ twice
- $len *= 4;
- }
- # If we are using --env, add the prefix for that, too.
- $len += $Global::envvarlen;
- return $len;
- }
- sub replaced {
- # Uses:
- # $Global::noquote
- # $Global::quoting
- # Returns:
- # $replaced = command with place holders replaced and prepended
- my $self = shift;
- if(not defined $self->{'replaced'}) {
- # Don't quote arguments if the input is the full command line
- my $quote_arg = $Global::noquote ? 0 : not $Global::quoting;
- $self->{'replaced'} = $self->replace_placeholders($self->{'command'},$Global::quoting,$quote_arg);
- my $len = length $self->{'replaced'};
- if ($len != $self->len()) {
- ::debug("length", $len, " != ", $self->len(), " ", $self->{'replaced'}, "\n");
- } else {
- ::debug("length", $len, " == ", $self->len(), " ", $self->{'replaced'}, "\n");
- }
- }
- return $self->{'replaced'};
- }
- sub replace_placeholders {
- # Replace foo{}bar with fooargbar
- # Input:
- # $targetref = command as shell words
- # $quote = should everything be quoted?
- # $quote_arg = should replaced arguments be quoted?
- # Returns:
- # @target with placeholders replaced
- my $self = shift;
- my $targetref = shift;
- my $quote = shift;
- my $quote_arg = shift;
- my $context_replace = $self->{'context_replace'};
- my @target = @$targetref;
- ::debug("replace", "Replace @target\n");
- # -X = context replace
- # maybe multiple input sources
- # maybe --xapply
- if(not @target) {
- # @target is empty: Return empty array
- return @target;
- }
- # Fish out the words that have replacement strings in them
- my %word;
- for (@target) {
- my $tt = $_;
- ::debug("replace", "Target: $tt");
- # a{1}b{}c{}d
- # a{=1 $_=$_ =}b{= $_=$_ =}c{= $_=$_ =}d
- # a\257<1 $_=$_ \257>b\257< $_=$_ \257>c\257< $_=$_ \257>d
- # A B C => aAbA B CcA B Cd
- # -X A B C => aAbAcAd aAbBcBd aAbCcCd
- if($context_replace) {
- while($tt =~ s/([^\s\257]* # before {=
- (?:
- \257< # {=
- [^\257]*? # The perl expression
- \257> # =}
- [^\s\257]* # after =}
- )+)/ /x) {
- # $1 = pre \257 perlexpr \257 post
- $word{"$1"} ||= 1;
- }
- } else {
- while($tt =~ s/( (?: \257<([^\257]*?)\257>) )//x) {
- # $f = \257 perlexpr \257
- $word{$1} ||= 1;
- }
- }
- }
- my @word = keys %word;
- my %replace;
- my @arg;
- for my $record (@{$self->{'arg_list'}}) {
- # $self->{'arg_list'} = [ [Arg11, Arg12], [Arg21, Arg22], [Arg31, Arg32] ]
- # Merge arg-objects from records into @arg for easy access
- CORE::push @arg, @$record;
- }
- # Add one arg if empty to allow {#} and {%} to be computed only once
- if(not @arg) { @arg = (Arg->new("")); }
- # Number of arguments - used for positional arguments
- my $n = $#_+1;
- # This is actually a CommandLine-object,
- # but it looks nice to be able to say {= $job->slot() =}
- my $job = $self;
- for my $word (@word) {
- # word = AB \257< perlexpr \257> CD \257< perlexpr \257> EF
- my $w = $word;
- ::debug("replace", "Replacing in $w\n");
- # Replace positional arguments
- $w =~ s< ([^\s\257]*) # before {=
- \257< # {=
- (-?\d+) # Position (eg. -2 or 3)
- ([^\257]*?) # The perl expression
- \257> # =}
- ([^\s\257]*) # after =}
- >
- { $1. # Context (pre)
- (
- $arg[$2 > 0 ? $2-1 : $n+$2] ? # If defined: replace
- $arg[$2 > 0 ? $2-1 : $n+$2]->replace($3,$quote_arg,$self)
- : "")
- .$4 }egx;# Context (post)
- ::debug("replace", "Positional replaced $word with: $w\n");
- if($w !~ /\257/) {
- # No more replacement strings in $w: No need to do more
- if($quote) {
- CORE::push(@{$replace{::shell_quote($word)}}, $w);
- } else {
- CORE::push(@{$replace{$word}}, $w);
- }
- next;
- }
- # for each arg:
- # compute replacement for each string
- # replace replacement strings with replacement in the word value
- # push to replace word value
- ::debug("replace", "Positional done: $w\n");
- for my $arg (@arg) {
- my $val = $w;
- my $number_of_replacements = 0;
- for my $perlexpr (keys %{$self->{'replacecount'}}) {
- # Replace {= perl expr =} with value for each arg
- $number_of_replacements +=
- $val =~ s{\257<\Q$perlexpr\E\257>}
- {$arg ? $arg->replace($perlexpr,$quote_arg,$self) : ""}eg;
- }
- my $ww = $word;
- if($quote) {
- $ww = ::shell_quote_scalar($word);
- $val = ::shell_quote_scalar($val);
- }
- if($number_of_replacements) {
- CORE::push(@{$replace{$ww}}, $val);
- }
- }
- }
- if($quote) {
- @target = ::shell_quote(@target);
- }
- # ::debug("replace", "%replace=",::my_dump(%replace),"\n");
- if(%replace) {
- # Substitute the replace strings with the replacement values
- # Must be sorted by length if a short word is a substring of a long word
- my $regexp = join('|', map { my $s = $_; $s =~ s/(\W)/\\$1/g; $s }
- sort { length $b <=> length $a } keys %replace);
- for(@target) {
- s/($regexp)/join(" ",@{$replace{$1}})/ge;
- }
- }
- ::debug("replace", "Return @target\n");
- return wantarray ? @target : "@target";
- }
- package CommandLineQueue;
- sub new {
- my $class = shift;
- my $commandref = shift;
- my $read_from = shift;
- my $context_replace = shift;
- my $max_number_of_args = shift;
- my $return_files = shift;
- my @unget = ();
- my ($count,%replacecount,$posrpl,$perlexpr,%len);
- my @command = @$commandref;
- # If the first command start with '-' it is probably an option
- if($command[0] =~ /^\s*(-\S+)/) {
- # Is this really a command in $PATH starting with '-'?
- my $cmd = $1;
- if(not ::which($cmd)) {
- ::error("Command ($cmd) starts with '-'. Is this a wrong option?\n");
- ::wait_and_exit(255);
- }
- }
- # Replace replacement strings with {= perl expr =}
- # Protect matching inside {= perl expr =}
- # by replacing {= and =} with \257< and \257>
- for(@command) {
- if(/\257/) {
- ::error("Command cannot contain the character \257. Use a function for that.\n");
- ::wait_and_exit(255);
- }
- s/\Q$Global::parensleft\E(.*?)\Q$Global::parensright\E/\257<$1\257>/gx;
- }
- for my $rpl (keys %Global::rpl) {
- # Replace the short hand string with the {= perl expr =} in $command and $opt::tagstring
- # Avoid replacing inside existing {= perl expr =}
- for(@command,@Global::ret_files) {
- while(s/((^|\257>)[^\257]*?) # Don't replace after \257 unless \257>
- \Q$rpl\E/$1\257<$Global::rpl{$rpl}\257>/xg) {
- }
- }
- if(defined $opt::tagstring) {
- for($opt::tagstring) {
- while(s/((^|\257>)[^\257]*?) # Don't replace after \257 unless \257>
- \Q$rpl\E/$1\257<$Global::rpl{$rpl}\257>/x) {}
- }
- }
- # Do the same for the positional replacement strings
- # A bit harder as we have to put in the position number
- $posrpl = $rpl;
- if($posrpl =~ s/^\{//) {
- # Only do this if the shorthand start with {
- for(@command,@Global::ret_files) {
- s/\{(-?\d+)\Q$posrpl\E/\257<$1 $Global::rpl{$rpl}\257>/g;
- }
- if(defined $opt::tagstring) {
- $opt::tagstring =~ s/\{(-?\d+)\Q$posrpl\E/\257<$1 $perlexpr\257>/g;
- }
- }
- }
- my $sum = 0;
- while($sum == 0) {
- # Count how many times each replacement string is used
- my @cmd = @command;
- my $contextlen = 0;
- my $noncontextlen = 0;
- my $contextgroups = 0;
- for my $c (@cmd) {
- while($c =~ s/ \257<([^\257]*?)\257> /\000/x) {
- # %replacecount = { "perlexpr" => number of times seen }
- # e.g { "$_++" => 2 }
- $replacecount{$1} ++;
- $sum++;
- }
- # Measure the length of the context around the {= perl expr =}
- # Use that {=...=} has been replaced with \000 above
- # So there is no need to deal with \257<
- while($c =~ s/ (\S*\000\S*) //x) {
- my $w = $1;
- $w =~ tr/\000//d; # Remove all \000's
- $contextlen += length($w);
- $contextgroups++;
- }
- # All {= perl expr =} have been removed: The rest is non-context
- $noncontextlen += length $c;
- }
- if($opt::tagstring) {
- my $t = $opt::tagstring;
- while($t =~ s/ \257<([^\257]*)\257> //x) {
- # %replacecount = { "perlexpr" => number of times seen }
- # e.g { "$_++" => 2 }
- # But for tagstring we just need to mark it as seen
- $replacecount{$1}||=1;
- }
- }
- $len{'context'} = 0+$contextlen;
- $len{'noncontext'} = $noncontextlen;
- $len{'contextgroups'} = $contextgroups;
- $len{'noncontextgroups'} = @cmd-$contextgroups;
- ::debug("length", "@command Context: ", $len{'context'},
- " Non: ", $len{'noncontext'}, " Ctxgrp: ", $len{'contextgroups'},
- " NonCtxGrp: ", $len{'noncontextgroups'}, "\n");
- if($sum == 0) {
- # Default command = {}
- # If not replacement string: append {}
- if(not @command) {
- @command = ("\257<\257>");
- $Global::noquote = 1;
- } elsif(($opt::pipe or $opt::pipepart)
- and not $opt::fifo and not $opt::cat) {
- # With --pipe / --pipe-part you can have no replacement
- last;
- } else {
- # Append {} to the command if there are no {...}'s and no {=...=}
- push @command, ("\257<\257>");
- }
- }
- }
- return bless {
- 'unget' => \@unget,
- 'command' => \@command,
- 'replacecount' => \%replacecount,
- 'arg_queue' => RecordQueue->new($read_from,$opt::colsep),
- 'context_replace' => $context_replace,
- 'len' => \%len,
- 'max_number_of_args' => $max_number_of_args,
- 'size' => undef,
- 'return_files' => $return_files,
- 'seq' => 1,
- }, ref($class) || $class;
- }
- sub get {
- my $self = shift;
- if(@{$self->{'unget'}}) {
- my $cmd_line = shift @{$self->{'unget'}};
- return ($cmd_line);
- } else {
- my $cmd_line;
- $cmd_line = CommandLine->new($self->seq(),
- $self->{'command'},
- $self->{'arg_queue'},
- $self->{'context_replace'},
- $self->{'max_number_of_args'},
- $self->{'return_files'},
- $self->{'replacecount'},
- $self->{'len'},
- );
- $cmd_line->populate();
- ::debug("init","cmd_line->number_of_args ",
- $cmd_line->number_of_args(), "\n");
- if($opt::pipe or $opt::pipepart) {
- if($cmd_line->replaced() eq "") {
- # Empty command - pipe requires a command
- ::error("--pipe must have a command to pipe into (e.g. 'cat').\n");
- ::wait_and_exit(255);
- }
- } else {
- if($cmd_line->number_of_args() == 0) {
- # We did not get more args - maybe at EOF string?
- return undef;
- } elsif($cmd_line->replaced() eq "") {
- # Empty command - get the next instead
- return $self->get();
- }
- }
- $self->set_seq($self->seq()+1);
- return $cmd_line;
- }
- }
- sub unget {
- my $self = shift;
- unshift @{$self->{'unget'}}, @_;
- }
- sub empty {
- my $self = shift;
- my $empty = (not @{$self->{'unget'}}) && $self->{'arg_queue'}->empty();
- ::debug("run", "CommandLineQueue->empty $empty");
- return $empty;
- }
- sub seq {
- my $self = shift;
- return $self->{'seq'};
- }
- sub set_seq {
- my $self = shift;
- $self->{'seq'} = shift;
- }
- sub quote_args {
- my $self = shift;
- # If there is not command emulate |bash
- return $self->{'command'};
- }
- sub size {
- my $self = shift;
- if(not $self->{'size'}) {
- my @all_lines = ();
- while(not $self->{'arg_queue'}->empty()) {
- push @all_lines, CommandLine->new($self->{'command'},
- $self->{'arg_queue'},
- $self->{'context_replace'},
- $self->{'max_number_of_args'});
- }
- $self->{'size'} = @all_lines;
- $self->unget(@all_lines);
- }
- return $self->{'size'};
- }
- package Limits::Command;
- # Maximal command line length (for -m and -X)
- sub max_length {
- # Find the max_length of a command line and cache it
- # Returns:
- # number of chars on the longest command line allowed
- if(not $Limits::Command::line_max_len) {
- # Disk cache of max command line length
- my $len_cache = $ENV{'HOME'} . "/.parallel/tmp/linelen-" . ::hostname();
- my $cached_limit;
- if(-e $len_cache) {
- open(my $fh, "<", $len_cache) || ::die_bug("Cannot read $len_cache");
- $cached_limit = <$fh>;
- close $fh;
- } else {
- $cached_limit = real_max_length();
- # If $HOME is write protected: Do not fail
- mkdir($ENV{'HOME'} . "/.parallel");
- mkdir($ENV{'HOME'} . "/.parallel/tmp");
- open(my $fh, ">", $len_cache);
- print $fh $cached_limit;
- close $fh;
- }
- $Limits::Command::line_max_len = $cached_limit;
- if($opt::max_chars) {
- if($opt::max_chars <= $cached_limit) {
- $Limits::Command::line_max_len = $opt::max_chars;
- } else {
- ::warning("Value for -s option ",
- "should be < $cached_limit.\n");
- }
- }
- }
- return $Limits::Command::line_max_len;
- }
- sub real_max_length {
- # Find the max_length of a command line
- # Returns:
- # The maximal command line length
- # Use an upper bound of 8 MB if the shell allows for for infinite long lengths
- my $upper = 8_000_000;
- my $len = 8;
- do {
- if($len > $upper) { return $len };
- $len *= 16;
- } while (is_acceptable_command_line_length($len));
- # Then search for the actual max length between 0 and upper bound
- return binary_find_max_length(int($len/16),$len);
- }
- sub binary_find_max_length {
- # Given a lower and upper bound find the max_length of a command line
- # Returns:
- # number of chars on the longest command line allowed
- my ($lower, $upper) = (@_);
- if($lower == $upper or $lower == $upper-1) { return $lower; }
- my $middle = int (($upper-$lower)/2 + $lower);
- ::debug("init", "Maxlen: $lower,$upper,$middle : ");
- if (is_acceptable_command_line_length($middle)) {
- return binary_find_max_length($middle,$upper);
- } else {
- return binary_find_max_length($lower,$middle);
- }
- }
- sub is_acceptable_command_line_length {
- # Test if a command line of this length can run
- # Returns:
- # 0 if the command line length is too long
- # 1 otherwise
- my $len = shift;
- local *STDERR;
- open (STDERR, ">", "/dev/null");
- system "true "."x"x$len;
- close STDERR;
- ::debug("init", "$len=$? ");
- return not $?;
- }
- package RecordQueue;
- sub new {
- my $class = shift;
- my $fhs = shift;
- my $colsep = shift;
- my @unget = ();
- my $arg_sub_queue;
- if($colsep) {
- # Open one file with colsep
- $arg_sub_queue = RecordColQueue->new($fhs);
- } else {
- # Open one or more files if multiple -a
- $arg_sub_queue = MultifileQueue->new($fhs);
- }
- return bless {
- 'unget' => \@unget,
- 'arg_number' => 0,
- 'arg_sub_queue' => $arg_sub_queue,
- }, ref($class) || $class;
- }
- sub get {
- # Returns:
- # reference to array of Arg-objects
- my $self = shift;
- if(@{$self->{'unget'}}) {
- $self->{'arg_number'}++;
- return shift @{$self->{'unget'}};
- }
- my $ret = $self->{'arg_sub_queue'}->get();
- if(defined $Global::max_number_of_args
- and $Global::max_number_of_args == 0) {
- ::debug("run", "Read 1 but return 0 args\n");
- return [Arg->new("")];
- } else {
- return $ret;
- }
- }
- sub unget {
- my $self = shift;
- ::debug("run", "RecordQueue-unget '@_'\n");
- $self->{'arg_number'} -= @_;
- unshift @{$self->{'unget'}}, @_;
- }
- sub empty {
- my $self = shift;
- my $empty = not @{$self->{'unget'}};
- $empty &&= $self->{'arg_sub_queue'}->empty();
- ::debug("run", "RecordQueue->empty $empty");
- return $empty;
- }
- sub arg_number {
- my $self = shift;
- return $self->{'arg_number'};
- }
- package RecordColQueue;
- sub new {
- my $class = shift;
- my $fhs = shift;
- my @unget = ();
- my $arg_sub_queue = MultifileQueue->new($fhs);
- return bless {
- 'unget' => \@unget,
- 'arg_sub_queue' => $arg_sub_queue,
- }, ref($class) || $class;
- }
- sub get {
- # Returns:
- # reference to array of Arg-objects
- my $self = shift;
- if(@{$self->{'unget'}}) {
- return shift @{$self->{'unget'}};
- }
- my $unget_ref=$self->{'unget'};
- if($self->{'arg_sub_queue'}->empty()) {
- return undef;
- }
- my $in_record = $self->{'arg_sub_queue'}->get();
- if(defined $in_record) {
- my @out_record = ();
- for my $arg (@$in_record) {
- ::debug("run", "RecordColQueue::arg $arg\n");
- my $line = $arg->orig();
- ::debug("run", "line='$line'\n");
- if($line ne "") {
- for my $s (split /$opt::colsep/o, $line, -1) {
- push @out_record, Arg->new($s);
- }
- } else {
- push @out_record, Arg->new("");
- }
- }
- return \@out_record;
- } else {
- return undef;
- }
- }
- sub unget {
- my $self = shift;
- ::debug("run", "RecordColQueue-unget '@_'\n");
- unshift @{$self->{'unget'}}, @_;
- }
- sub empty {
- my $self = shift;
- my $empty = (not @{$self->{'unget'}} and $self->{'arg_sub_queue'}->empty());
- ::debug("run", "RecordColQueue->empty $empty");
- return $empty;
- }
- package MultifileQueue;
- @Global::unget_argv=();
- sub new {
- my $class = shift;
- my $fhs = shift;
- for my $fh (@$fhs) {
- if(-t $fh) {
- ::warning("Input is read from the terminal. ".
- "Only experts do this on purpose. ".
- "Press CTRL-D to exit.\n");
- }
- }
- return bless {
- 'unget' => \@Global::unget_argv,
- 'fhs' => $fhs,
- 'arg_matrix' => undef,
- }, ref($class) || $class;
- }
- sub get {
- my $self = shift;
- if($opt::xapply) {
- return $self->xapply_get();
- } else {
- return $self->nest_get();
- }
- }
- sub unget {
- my $self = shift;
- ::debug("run", "MultifileQueue-unget '@_'\n");
- unshift @{$self->{'unget'}}, @_;
- }
- sub empty {
- my $self = shift;
- my $empty = (not @Global::unget_argv
- and not @{$self->{'unget'}});
- for my $fh (@{$self->{'fhs'}}) {
- $empty &&= eof($fh);
- }
- ::debug("run", "MultifileQueue->empty $empty ");
- return $empty;
- }
- sub xapply_get {
- my $self = shift;
- if(@{$self->{'unget'}}) {
- return shift @{$self->{'unget'}};
- }
- my @record = ();
- my $prepend = undef;
- my $empty = 1;
- for my $fh (@{$self->{'fhs'}}) {
- my $arg = read_arg_from_fh($fh);
- if(defined $arg) {
- # Record $arg for recycling at end of file
- push @{$self->{'arg_matrix'}{$fh}}, $arg;
- push @record, $arg;
- $empty = 0;
- } else {
- ::debug("run", "EOA ");
- # End of file: Recycle arguments
- push @{$self->{'arg_matrix'}{$fh}}, shift @{$self->{'arg_matrix'}{$fh}};
- # return last @{$args->{'args'}{$fh}};
- push @record, @{$self->{'arg_matrix'}{$fh}}[-1];
- }
- }
- if($empty) {
- return undef;
- } else {
- return \@record;
- }
- }
- sub nest_get {
- my $self = shift;
- if(@{$self->{'unget'}}) {
- return shift @{$self->{'unget'}};
- }
- my @record = ();
- my $prepend = undef;
- my $empty = 1;
- my $no_of_inputsources = $#{$self->{'fhs'}} + 1;
- if(not $self->{'arg_matrix'}) {
- # Initialize @arg_matrix with one arg from each file
- # read one line from each file
- my @first_arg_set;
- my $all_empty = 1;
- for (my $fhno = 0; $fhno < $no_of_inputsources ; $fhno++) {
- my $arg = read_arg_from_fh($self->{'fhs'}[$fhno]);
- if(defined $arg) {
- $all_empty = 0;
- }
- $self->{'arg_matrix'}[$fhno][0] = $arg || Arg->new("");
- push @first_arg_set, $self->{'arg_matrix'}[$fhno][0];
- }
- if($all_empty) {
- # All filehandles were at eof or eof-string
- return undef;
- }
- return [@first_arg_set];
- }
- # Treat the case with one input source special. For multiple
- # input sources we need to remember all previously read values to
- # generate all combinations. But for one input source we can
- # forget the value after first use.
- if($no_of_inputsources == 1) {
- my $arg = read_arg_from_fh($self->{'fhs'}[0]);
- if(defined($arg)) {
- return [$arg];
- }
- return undef;
- }
- for (my $fhno = $no_of_inputsources - 1; $fhno >= 0; $fhno--) {
- if(eof($self->{'fhs'}[$fhno])) {
- next;
- } else {
- # read one
- my $arg = read_arg_from_fh($self->{'fhs'}[$fhno]);
- defined($arg) || next; # If we just read an EOF string: Treat this as EOF
- my $len = $#{$self->{'arg_matrix'}[$fhno]} + 1;
- $self->{'arg_matrix'}[$fhno][$len] = $arg;
- # make all new combinations
- my @combarg = ();
- for (my $fhn = 0; $fhn < $no_of_inputsources; $fhn++) {
- push @combarg, [0, $#{$self->{'arg_matrix'}[$fhn]}];
- }
- $combarg[$fhno] = [$len,$len]; # Find only combinations with this new entry
- # map combinations
- # [ 1, 3, 7 ], [ 2, 4, 1 ]
- # =>
- # [ m[0][1], m[1][3], m[3][7] ], [ m[0][2], m[1][4], m[2][1] ]
- my @mapped;
- for my $c (expand_combinations(@combarg)) {
- my @a;
- for my $n (0 .. $no_of_inputsources - 1 ) {
- push @a, $self->{'arg_matrix'}[$n][$$c[$n]];
- }
- push @mapped, \@a;
- }
- # append the mapped to the ungotten arguments
- push @{$self->{'unget'}}, @mapped;
- # get the first
- return shift @{$self->{'unget'}};
- }
- }
- # all are eof or at EOF string; return from the unget queue
- return shift @{$self->{'unget'}};
- }
- sub read_arg_from_fh {
- # Read one Arg from filehandle
- # Returns:
- # Arg-object with one read line
- # undef if end of file
- my $fh = shift;
- my $prepend = undef;
- my $arg;
- do {{
- # This makes 10% faster
- if(not ($arg = <$fh>)) {
- if(defined $prepend) {
- return Arg->new($prepend);
- } else {
- return undef;
- }
- }
- # ::debug("run", "read $arg\n");
- # Remove delimiter
- $arg =~ s:$/$::;
- if($Global::end_of_file_string and
- $arg eq $Global::end_of_file_string) {
- # Ignore the rest of input file
- close $fh;
- ::debug("run", "EOF-string ($arg) met\n");
- if(defined $prepend) {
- return Arg->new($prepend);
- } else {
- return undef;
- }
- }
- if(defined $prepend) {
- $arg = $prepend.$arg; # For line continuation
- $prepend = undef; #undef;
- }
- if($Global::ignore_empty) {
- if($arg =~ /^\s*$/) {
- redo; # Try the next line
- }
- }
- if($Global::max_lines) {
- if($arg =~ /\s$/) {
- # Trailing space => continued on next line
- $prepend = $arg;
- redo;
- }
- }
- }} while (1 == 0); # Dummy loop {{}} for redo
- if(defined $arg) {
- return Arg->new($arg);
- } else {
- ::die_bug("multiread arg undefined");
- }
- }
- sub expand_combinations {
- # Input:
- # ([xmin,xmax], [ymin,ymax], ...)
- # Returns: ([x,y,...],[x,y,...])
- # where xmin <= x <= xmax and ymin <= y <= ymax
- my $minmax_ref = shift;
- my $xmin = $$minmax_ref[0];
- my $xmax = $$minmax_ref[1];
- my @p;
- if(@_) {
- # If there are more columns: Compute those recursively
- my @rest = expand_combinations(@_);
- for(my $x = $xmin; $x <= $xmax; $x++) {
- push @p, map { [$x, @$_] } @rest;
- }
- } else {
- for(my $x = $xmin; $x <= $xmax; $x++) {
- push @p, [$x];
- }
- }
- return @p;
- }
- package Arg;
- sub new {
- my $class = shift;
- my $orig = shift;
- my @hostgroups;
- if($opt::hostgroups) {
- if($orig =~ s:@(.+)::) {
- # We found hostgroups on the arg
- @hostgroups = split(/\+/, $1);
- if(not grep { defined $Global::hostgroups{$_} } @hostgroups) {
- ::warning("No such hostgroup (@hostgroups)\n");
- @hostgroups = (keys %Global::hostgroups);
- }
- } else {
- @hostgroups = (keys %Global::hostgroups);
- }
- }
- return bless {
- 'orig' => $orig,
- 'hostgroups' => \@hostgroups,
- }, ref($class) || $class;
- }
- sub replace {
- # Calculates the corresponding value for a given perl expression
- # Returns:
- # The calculated string (quoted if asked for)
- my $self = shift;
- my $perlexpr = shift; # E.g. $_=$_ or s/.gz//
- my $quote = (shift) ? 1 : 0; # should the string be quoted?
- # This is actually a CommandLine-object,
- # but it looks nice to be able to say {= $job->slot() =}
- my $job = shift;
- $perlexpr =~ s/^-?\d+ //; # Positional replace treated as normal replace
- if(not defined $self->{"rpl",0,$perlexpr}) {
- local $_;
- if($Global::trim eq "n") {
- $_ = $self->{'orig'};
- } else {
- $_ = trim_of($self->{'orig'});
- }
- ::debug("replace", "eval ", $perlexpr, " ", $_, "\n");
- if(not $Global::perleval{$perlexpr}) {
- # Make an anonymous function of the $perlexpr
- # And more importantly: Compile it only once
- if($Global::perleval{$perlexpr} =
- eval('sub { no strict; no warnings; my $job = shift; '.
- $perlexpr.' }')) {
- # All is good
- } else {
- # The eval failed. Maybe $perlexpr is invalid perl?
- ::error("Cannot use $perlexpr: $@\n");
- ::wait_and_exit(255);
- }
- }
- # Execute the function
- $Global::perleval{$perlexpr}->($job);
- $self->{"rpl",0,$perlexpr} = $_;
- }
- if(not defined $self->{"rpl",$quote,$perlexpr}) {
- $self->{"rpl",1,$perlexpr} =
- ::shell_quote_scalar($self->{"rpl",0,$perlexpr});
- }
- return $self->{"rpl",$quote,$perlexpr};
- }
- sub orig {
- my $self = shift;
- return $self->{'orig'};
- }
- sub trim_of {
- # Removes white space as specifed by --trim:
- # n = nothing
- # l = start
- # r = end
- # lr|rl = both
- # Returns:
- # string with white space removed as needed
- my @strings = map { defined $_ ? $_ : "" } (@_);
- my $arg;
- if($Global::trim eq "n") {
- # skip
- } elsif($Global::trim eq "l") {
- for my $arg (@strings) { $arg =~ s/^\s+//; }
- } elsif($Global::trim eq "r") {
- for my $arg (@strings) { $arg =~ s/\s+$//; }
- } elsif($Global::trim eq "rl" or $Global::trim eq "lr") {
- for my $arg (@strings) { $arg =~ s/^\s+//; $arg =~ s/\s+$//; }
- } else {
- ::error("--trim must be one of: r l rl lr.\n");
- ::wait_and_exit(255);
- }
- return wantarray ? @strings : "@strings";
- }
- package TimeoutQueue;
- sub new {
- my $class = shift;
- my $delta_time = shift;
- my ($pct);
- if($delta_time =~ /(\d+(\.\d+)?)%/) {
- # Timeout in percent
- $pct = $1/100;
- $delta_time = 1_000_000;
- }
- return bless {
- 'queue' => [],
- 'delta_time' => $delta_time,
- 'pct' => $pct,
- 'remedian_idx' => 0,
- 'remedian_arr' => [],
- 'remedian' => undef,
- }, ref($class) || $class;
- }
- sub delta_time {
- my $self = shift;
- return $self->{'delta_time'};
- }
- sub set_delta_time {
- my $self = shift;
- $self->{'delta_time'} = shift;
- }
- sub remedian {
- my $self = shift;
- return $self->{'remedian'};
- }
- sub set_remedian {
- # Set median of the last 999^3 (=997002999) values using Remedian
- #
- # Rousseeuw, Peter J., and Gilbert W. Bassett Jr. "The remedian: A
- # robust averaging method for large data sets." Journal of the
- # American Statistical Association 85.409 (1990): 97-104.
- my $self = shift;
- my $val = shift;
- my $i = $self->{'remedian_idx'}++;
- my $rref = $self->{'remedian_arr'};
- $rref->[0][$i%999] = $val;
- $rref->[1][$i/999%999] = (sort @{$rref->[0]})[$#{$rref->[0]}/2];
- $rref->[2][$i/999/999%999] = (sort @{$rref->[1]})[$#{$rref->[1]}/2];
- $self->{'remedian'} = (sort @{$rref->[2]})[$#{$rref->[2]}/2];
- }
- sub update_delta_time {
- # Update delta_time based on runtime of finished job if timeout is
- # a percentage
- my $self = shift;
- my $runtime = shift;
- if($self->{'pct'}) {
- $self->set_remedian($runtime);
- $self->{'delta_time'} = $self->{'pct'} * $self->remedian();
- ::debug("run", "Timeout: $self->{'delta_time'}s ");
- }
- }
- sub process_timeouts {
- # Check if there was a timeout
- my $self = shift;
- # $self->{'queue'} is sorted by start time
- while (@{$self->{'queue'}}) {
- my $job = $self->{'queue'}[0];
- if($job->endtime()) {
- # Job already finished. No need to timeout the job
- # This could be because of --keep-order
- shift @{$self->{'queue'}};
- } elsif($job->timedout($self->{'delta_time'})) {
- # Need to shift off queue before kill
- # because kill calls usleep that calls process_timeouts
- shift @{$self->{'queue'}};
- $job->kill();
- } else {
- # Because they are sorted by start time the rest are later
- last;
- }
- }
- }
- sub insert {
- my $self = shift;
- my $in = shift;
- push @{$self->{'queue'}}, $in;
- }
- package Semaphore;
- # This package provides a counting semaphore
- #
- # If a process dies without releasing the semaphore the next process
- # that needs that entry will clean up dead semaphores
- #
- # The semaphores are stored in ~/.parallel/semaphores/id-<name> Each
- # file in ~/.parallel/semaphores/id-<name>/ is the process ID of the
- # process holding the entry. If the process dies, the entry can be
- # taken by another process.
- sub new {
- my $class = shift;
- my $id = shift;
- my $count = shift;
- $id=~s/([^-_a-z0-9])/unpack("H*",$1)/ige; # Convert non-word chars to hex
- $id="id-".$id; # To distinguish it from a process id
- my $parallel_dir = $ENV{'HOME'}."/.parallel";
- -d $parallel_dir or mkdir_or_die($parallel_dir);
- my $parallel_locks = $parallel_dir."/semaphores";
- -d $parallel_locks or mkdir_or_die($parallel_locks);
- my $lockdir = "$parallel_locks/$id";
- my $lockfile = $lockdir.".lock";
- if($count < 1) { ::die_bug("semaphore-count: $count"); }
- return bless {
- 'lockfile' => $lockfile,
- 'lockfh' => Symbol::gensym(),
- 'lockdir' => $lockdir,
- 'id' => $id,
- 'idfile' => $lockdir."/".$id,
- 'pid' => $$,
- 'pidfile' => $lockdir."/".$$.'@'.::hostname(),
- 'count' => $count + 1 # nlinks returns a link for the 'id-' as well
- }, ref($class) || $class;
- }
- sub acquire {
- my $self = shift;
- my $sleep = 1; # 1 ms
- my $start_time = time;
- while(1) {
- $self->atomic_link_if_count_less_than() and last;
- ::debug("sem", "Remove dead locks");
- my $lockdir = $self->{'lockdir'};
- for my $d (glob "$lockdir/*") {
- ::debug("sem", "Lock $d $lockdir\n");
- $d =~ m:$lockdir/([0-9]+)\@([-\._a-z0-9]+)$:o or next;
- my ($pid, $host) = ($1, $2);
- if($host eq ::hostname()) {
- if(not kill 0, $1) {
- ::debug("sem", "Dead: $d");
- unlink $d;
- } else {
- ::debug("sem", "Alive: $d");
- }
- }
- }
- # try again
- $self->atomic_link_if_count_less_than() and last;
- # Retry slower and slower up to 1 second
- $sleep = ($sleep < 1000) ? ($sleep * 1.1) : ($sleep);
- # Random to avoid every sleeping job waking up at the same time
- ::usleep(rand()*$sleep);
- if(defined($opt::timeout) and
- $start_time + $opt::timeout > time) {
- # Acquire the lock anyway
- if(not -e $self->{'idfile'}) {
- open (my $fh, ">", $self->{'idfile'}) or
- ::die_bug("timeout_write_idfile: $self->{'idfile'}");
- close $fh;
- }
- link $self->{'idfile'}, $self->{'pidfile'};
- last;
- }
- }
- ::debug("sem", "acquired $self->{'pid'}\n");
- }
- sub release {
- my $self = shift;
- unlink $self->{'pidfile'};
- if($self->nlinks() == 1) {
- # This is the last link, so atomic cleanup
- $self->lock();
- if($self->nlinks() == 1) {
- unlink $self->{'idfile'};
- rmdir $self->{'lockdir'};
- }
- $self->unlock();
- }
- ::debug("run", "released $self->{'pid'}\n");
- }
- sub _release {
- my $self = shift;
- unlink $self->{'pidfile'};
- $self->lock();
- my $nlinks = $self->nlinks();
- ::debug("sem", $nlinks, "<", $self->{'count'});
- if($nlinks-- > 1) {
- unlink $self->{'idfile'};
- open (my $fh, ">", $self->{'idfile'}) or
- ::die_bug("write_idfile: $self->{'idfile'}");
- print $fh "#"x$nlinks;
- close $fh;
- } else {
- unlink $self->{'idfile'};
- rmdir $self->{'lockdir'};
- }
- $self->unlock();
- ::debug("sem", "released $self->{'pid'}\n");
- }
- sub atomic_link_if_count_less_than {
- # Link $file1 to $file2 if nlinks to $file1 < $count
- my $self = shift;
- my $retval = 0;
- $self->lock();
- ::debug($self->nlinks(), "<", $self->{'count'});
- if($self->nlinks() < $self->{'count'}) {
- -d $self->{'lockdir'} or mkdir_or_die($self->{'lockdir'});
- if(not -e $self->{'idfile'}) {
- open (my $fh, ">", $self->{'idfile'}) or
- ::die_bug("write_idfile: $self->{'idfile'}");
- close $fh;
- }
- $retval = link $self->{'idfile'}, $self->{'pidfile'};
- }
- $self->unlock();
- ::debug("run", "atomic $retval");
- return $retval;
- }
- sub _atomic_link_if_count_less_than {
- # Link $file1 to $file2 if nlinks to $file1 < $count
- my $self = shift;
- my $retval = 0;
- $self->lock();
- my $nlinks = $self->nlinks();
- ::debug("sem", $nlinks, "<", $self->{'count'});
- if($nlinks++ < $self->{'count'}) {
- -d $self->{'lockdir'} or mkdir_or_die($self->{'lockdir'});
- if(not -e $self->{'idfile'}) {
- open (my $fh, ">", $self->{'idfile'}) or
- ::die_bug("write_idfile: $self->{'idfile'}");
- close $fh;
- }
- open (my $fh, ">", $self->{'idfile'}) or
- ::die_bug("write_idfile: $self->{'idfile'}");
- print $fh "#"x$nlinks;
- close $fh;
- $retval = link $self->{'idfile'}, $self->{'pidfile'};
- }
- $self->unlock();
- ::debug("sem", "atomic $retval");
- return $retval;
- }
- sub nlinks {
- my $self = shift;
- if(-e $self->{'idfile'}) {
- ::debug("sem", "nlinks", (stat(_))[3], "size", (stat(_))[7], "\n");
- return (stat(_))[3];
- } else {
- return 0;
- }
- }
- sub lock {
- my $self = shift;
- my $sleep = 100; # 100 ms
- my $total_sleep = 0;
- $Global::use{"Fcntl"} ||= eval "use Fcntl qw(:DEFAULT :flock); 1;";
- my $locked = 0;
- while(not $locked) {
- if(tell($self->{'lockfh'}) == -1) {
- # File not open
- open($self->{'lockfh'}, ">", $self->{'lockfile'})
- or ::debug("run", "Cannot open $self->{'lockfile'}");
- }
- if($self->{'lockfh'}) {
- # File is open
- chmod 0666, $self->{'lockfile'}; # assuming you want it a+rw
- if(flock($self->{'lockfh'}, LOCK_EX()|LOCK_NB())) {
- # The file is locked: No need to retry
- $locked = 1;
- last;
- } else {
- if ($! =~ m/Function not implemented/) {
- ::warning("flock: $!");
- ::warning("Will wait for a random while\n");
- ::usleep(rand(5000));
- # File cannot be locked: No need to retry
- $locked = 2;
- last;
- }
- }
- }
- # Locking failed in first round
- # Sleep and try again
- $sleep = ($sleep < 1000) ? ($sleep * 1.1) : ($sleep);
- # Random to avoid every sleeping job waking up at the same time
- ::usleep(rand()*$sleep);
- $total_sleep += $sleep;
- if($opt::semaphoretimeout) {
- if($total_sleep/1000 > $opt::semaphoretimeout) {
- # Timeout: bail out
- ::warning("Semaphore timed out. Ignoring timeout.");
- $locked = 3;
- last;
- }
- } else {
- if($total_sleep/1000 > 30) {
- ::warning("Semaphore stuck for 30 seconds. Consider using --semaphoretimeout.");
- }
- }
- }
- ::debug("run", "locked $self->{'lockfile'}");
- }
- sub unlock {
- my $self = shift;
- unlink $self->{'lockfile'};
- close $self->{'lockfh'};
- ::debug("run", "unlocked\n");
- }
- sub mkdir_or_die {
- # If dir is not writable: die
- my $dir = shift;
- my @dir_parts = split(m:/:,$dir);
- my ($ddir,$part);
- while(defined ($part = shift @dir_parts)) {
- $part eq "" and next;
- $ddir .= "/".$part;
- -d $ddir and next;
- mkdir $ddir;
- }
- if(not -w $dir) {
- ::error("Cannot write to $dir: $!\n");
- ::wait_and_exit(255);
- }
- }
- # Keep perl -w happy
- $opt::x = $Semaphore::timeout = $Semaphore::wait =
- $Job::file_descriptor_warning_printed = 0;
|