*! Version 2.0.0 04feb2022 * Contact jesse.wursten@kuleuven.be for bug reports/inquiries. * Changelog ** 04feb2022: Fixed bug that would occassionally break the tracker (related to modifying list currently being looped over) ** 04jan2021: Now a bit more robust to delayed log files creation ** 15dec2020: Max parallel option, timestamps ** 09dec2020: Now minimises new windows to save space and better delay management ** 22aug2019: Fixed sleepduration issue ** 19aug2019: Added middleman finder ** 25jan2019: Changed Y:/middleman to G:/middleman (will break usage on pc) ** 04oct2018: Made file more robust to whitespaces in dofilenames ** 25jul2018: Added "`'-stripping to avoid errors on evaluating last line of log file ** 23jul2018: Changed to dofile middleman structure ** 23jan2018: Better folder management (creation and wd reset) and cleaner output. ** 08dec2017: Added notification when particular iteration finishes cap program drop batcher cap program drop batcher_saveoption program define batcher * Syntax parsing version 8.0 syntax anything(name=dofileName), [Tempfolder(string)] [STataexe(string)] Iter(numlist) [notrack] [BEtweendelay(integer 10) TRackdelay(integer 60) UPdatedelay(integer 30)] [sts(string) sts_exceptsuccess] [SAVEoptions(string)] [nostop noquit] [test debug] [SLeepduration(integer 60)] [MAXparallel(string)] [notimestamp] ** Name of sleep duration in syntax changed to trackdelay, keeping sleepduration as internal name for backwards compatibility if `trackdelay' != 60 & `sleepduration' == 60 local sleepduration = `trackdelay' ** Verify parallel is integer if "`maxparallel'" != "" { capture confirm integer number `maxparallel' if _rc != 0 { di as error "`maxparallel' is not recognised as integer" error 198 } } ** Saving options (to profile.do) if `"`saveoptions'"' != "" { if `"`sts'"' != "" & strpos("`saveoptions'", "sts") > 0 batcher_saveoption, name(sts) value(`"`sts'"') if `"`sts_exceptsuccess'"' != ""& strpos("`saveoptions'", "sts_exceptsuccess") > 0 batcher_saveoption, name(sts_exceptsuccess) value(`"`sts_exceptsuccess'"') if `"`tempfolder'"' != "" & strpos("`saveoptions'", "tempfolder") > 0 batcher_saveoption, name(tempfolder) value(`"`tempfolder'"') if `"`sleepduration'"' != "60" & strpos("`saveoptions'", "sleepduration") > 0 batcher_saveoption, name(sleepduration) value(`"`sleepduration'"') if `"`betweendelay'"' != "10" & strpos("`saveoptions'", "betweendelay") > 0 batcher_saveoption, name(betweendelay) value(`"`betweendelay'"') if `"`updatedelay'"' != "30" & strpos("`saveoptions'", "updatedelay") > 0 batcher_saveoption, name(updatedelay) value(`"`updatedelay'"') if `"`maxparallel'"' != "" & strpos("`saveoptions'", "maxparallel") > 0 batcher_saveoption, name(maxparallel) value(`"`maxparallel'"') if `"`timestamp'"' != "" & strpos("`saveoptions'", "timestamp") > 0 batcher_saveoption, name(timestamp) value(`"`timestamp'"') if `"`stop'"' != "" & strpos("`saveoptions'", "nostop") > 0 batcher_saveoption, name(stop) value(`"`stop'"') if `"`quit'"' != "" & strpos("`saveoptions'", "noquit") > 0 batcher_saveoption, name(quit) value(`"`quit'"') } ** Loading options *** sts local stsDefined "" // 3. Default is empty if "${batcher_sts}" != "" local stsDefined `"${batcher_sts}"' // 2. Saved sts url if `"`sts'"' != "" local stsDefined `"`sts'"' // 1. Specified sts url if `"`sts'"' == "overwrite" local stsDefined "" // 0. Empty if user wants to overwrite saved sts url *** sts_exceptsuccess local sts_exceptsuccessDefined "" // 3. Default is empty if "${batcher_sts_exceptsuccess}" != "" local sts_exceptsuccessDefined `"${batcher_sts_exceptsuccess}"' // 2. Saved sts_exceptsuccess if `"`sts_exceptsuccess'"' != "" local sts_exceptsuccessDefined `"`sts_exceptsuccess'"' // 1. Specified sts_exceptsuccess if `"`sts_exceptsuccess'"' == "overwrite" local sts_exceptsuccessDefined "" // 0. Empty if user wants to overwrite saved sts_exceptsuccess *** tempfolder local tempfolderDefined "" // 3. Default is empty if "${batcher_tempfolder}" != "" local tempfolderDefined `"${batcher_tempfolder}"' // 2. Saved tempfolder if `"`tempfolder'"' != "" local tempfolderDefined `"`tempfolder'"' // 1. Specified tempfolder if `"`tempfolder'"' == "overwrite" local tempfolderDefined "" // 0. Empty if user wants to overwrite saved tempfolder *** sleepduration local sleepdurationDefined "60" // 3. Default is 60 if "${batcher_sleepduration}" != "" local sleepdurationDefined `"${batcher_sleepduration}"' // 2. Saved sleepduration url if `"`sleepduration'"' != "60" local sleepdurationDefined `"`sleepduration'"' // 1. Specified sleepduration url if `"`sleepduration'"' == "overwrite" local sleepdurationDefined "60" // 0. 60 if user wants to overwrite saved sleepduration with default *** betweendelay local betweendelayDefined "10" // 3. Default is 10 if "${batcher_betweendelay}" != "" local betweendelayDefined `"${batcher_betweendelay}"' // 2. Saved betweendelay if `"`betweendelay'"' != "10" local betweendelayDefined `"`betweendelay'"' // 1. Specified betweendelay if `"`betweendelay'"' == "overwrite" local betweendelayDefined "10" // 0. 10 if user wants to overwrite saved betweendelay with default *** updatedelay local updatedelayDefined "30" // 3. Default is 30 if "${batcher_updatedelay}" != "" local updatedelayDefined `"${batcher_updatedelay}"' // 2. Saved updatedelay if `"`updatedelay'"' != "30" local updatedelayDefined `"`updatedelay'"' // 1. Specified updatedelay if `"`updatedelay'"' == "overwrite" local updatedelayDefined "30" // 0. 30 if user wants to overwrite saved updatedelay with default *** maxparallel local maxparallelDefined "" // 3. Default is "" if "${batcher_maxparallel}" != "" local maxparallelDefined `"${batcher_maxparallel}"' // 2. Saved maxParallel if `"`maxparallel'"' != "" local maxparallelDefined `"`maxparallel'"' // 1. Specified maxParallel if `"`maxparallel'"' == "overwrite" local maxparallelDefined "" // 0. if user wants to overwrite saved maxParallel with default ("") *** notimestamp local timestampDefined "" // 3. Default is to timestamp if "${batcher_timestamp}" != "" local timestampDefined `"${batcher_timestamp}"' // 2. Saved timestamp url if `"`timestamp'"' != "" local timestampDefined `"`timestamp'"' // 1. Specified timestamp url if `"`timestamp'"' == "overwrite" local timestampDefined "" // 0. Empty if user wants to overwrite saved timestamp *** nostop local stopDefined "stop" // 3. Default is to stop on errors if "${batcher_stop}" != "" local stopDefined `"${batcher_stop}"' // 2. Saved stop url if `"`stop'"' != "" local stopDefined `"`stop'"' // 1. Specified stop url if `"`stop'"' == "overwrite" local stopDefined "" // 0. Empty if user wants to overwrite saved stop *** noquit local quitDefined "quit" // 3. Default is to quit on errors if "${batcher_quit}" != "" local quitDefined `"${batcher_quit}"' // 2. Saved quit url if `"`quit'"' != "" local quitDefined `"`quit'"' // 1. Specified quit url if `"`quit'"' == "overwrite" local quitDefined "" // 0. Empty if user wants to overwrite saved quit url ** Parsing options if `"`tempfolderDefined'"' == "" { di as text "No tempfolder specified nor saved. Using current working directory: " as result c(pwd) local tempfolderDefined = c(pwd) } local dofileName : subinstr local dofileName `"""' "", all local sleepduration_ms = `sleepdurationDefined'*1000 local betweendelay_ms = `betweendelayDefined'*1000 local updatedelay_ms = `updatedelayDefined'*1000 if "`stataexe'" == "" { if c(flavor) == "IC" local flavor "IC" if c(SE) == 1 local flavor "SE" if c(MP) == 1 local flavor "MP" if c(bit) == 64 local bit "64" else local bit "32" } local stataexe "`c(sysdir_stata)'Stata`flavor'-`bit'" local numberOfIterations = wordcount("`iter'") * Find middleman qui findfile batcher.ado local middlemanPath = r(fn) local middlemanPath = subinstr("`middlemanPath'", "batcher.ado", "batcher_middleman.do", .) if "`test'" == "test" local middlemanPath = "G:\Other\SSC programs\batcher\Versions\toEdit\batcher_middleman.do" * Test report if "`test'" == "test" { di as text "Using test ado" } * Identify iterations (limit-aware) ** Is there a concurrency limit (e.g. due to number of processors) /* 2: # processors */ if c(processors_mach) != . local limit = c(processors_mach) /* 1: option set */ if "`maxparallelDefined'" != "" local limit = `maxparallelDefined' /* 0: overwrite */ if "`maxparallelDefined'" == "0" local limit = . ** Does concurrency bind? numlist "`iter'" local iterationsAll = r(numlist) local iterationsAllCount : list sizeof iterationsAll *** If so, split into initial and remainder batch if `limit' != . & `iterationsAllCount' > `limit'{ local iterationsCurrent = "" local iterationsRemainder = "`iterationsAll'" forvalues i = 1/`limit' { local iterationToConsider : word `i' of `iterationsAll' local iterationsCurrent : list iterationsCurrent | iterationToConsider local iterationsRemainder : list iterationsRemainder - iterationToConsider } } *** Else, it's all initial batch else { local iterationsCurrent = "`iterationsAll'" local iterationsRemainder = "" } * Start dofiles cap mkdir "`tempfolderDefined'" noisily di as result `"Starting `dofileName'"' foreach iteration of local iterationsCurrent { * Start new stata process to perform the dofile (note, repeated twice below) if "`timestamp'" == "" noisily di _col(3) as text "iteration `iteration' (`c(current_date)' `c(current_time)')" else noisily di _col(3) as text "iteration `iteration'" winexec "`stataexe'" do "`middlemanPath'" "0`dofileName'0" `iteration' "`tempfolderDefined'" "`stopDefined'" "`quitDefined'" sleep `betweendelay_ms' } * Assess whether finished if "`track'" != "notrack" { noisily di as result "Starting tracking in `sleepdurationDefined' seconds. Refreshing every `updatedelayDefined' seconds." sleep `sleepduration_ms' local true "false" noisily di as text _col(4) "Finished: " _continue local finishedCount = 0 local somethingFailed = 0 local failures "" while "`true'" != "true" { if "`debug'" == "debug" noisily di as text "-while-" local restartedLoop "no" foreach iteration of local iterationsCurrent { if "`debug'" == "debug" noisily di as text "Checking `iteration' of `iterationsCurrent'" tempname log ** Try to open log capture confirm file `"`tempfolderDefined'/iteration`iteration'.log"' if _rc == 0 file open `log' using `"`tempfolderDefined'/iteration`iteration'.log"', read else { local counter = 0 while `counter' <= 4 { capture confirm file `"`tempfolderDefined'/iteration`iteration'.log"' if _rc == 0 { file open `log' using `"`tempfolderDefined'/iteration`iteration'.log"', read local counter = 999 } else { local counter = `counter' + 1 sleep 10000 } } if `counter' > 4 & `counter' != 999 { di as error "Could not find logfile of iteration `iteration'" error 999 } } file seek `log' eof local posToStart = `r(loc)' - 27 file seek `log' `posToStart' file read `log' line file close `log' local line = subinstr(`"`macval(line)'"', char(34), "", .) local line = subinstr(`"`macval(line)'"', char(39), "", .) local line = subinstr(`"`macval(line)'"', char(96), "", .) * Success if `"`line'"' == "Execution report: Success" & "`finished_`iteration''" != "1" { * Update tracker to say iteration is done local finished_`iteration' = 1 local finishedCount = `finishedCount' + 1 local iterationsCurrent : list iterationsCurrent - iteration noisily di as result " `iteration' " _continue if "`stsDefined'" != "" & "`sts_exceptsuccessDefined'" == "" qui sendtoslack, url(`stsDefined') message("Iteration `iteration' finished.") * Optionally start new iteration if "`iterationsRemainder'" != "" { ** Identify new iteration local newIteration : word 1 of `iterationsRemainder' ** Update lists local iterationsRemainder : list iterationsRemainder - newIteration local iterationsCurrent : list iterationsCurrent | newIteration ** Start it if "`timestamp'" == "" noisily di _col(3) as text `" -starting `newIteration' (`c(current_date)' `c(current_time)')- "' _continue else noisily di _col(3) as text `" -starting `newIteration'- "' _continue winexec "`stataexe'" do "`middlemanPath'" "0`dofileName'0" `newIteration' "`tempfolderDefined'" "`stopDefined'" "`quitDefined'" sleep `betweendelay_ms' } * Restart loop (this prevents a bug from popping up, related to changing the macro list currently being looped over) local restartedLoop "yes" continue, break } * Failure if `"`line'"' == "Execution report: Failure" & "`finished_`iteration''" != "1" { local finished_`iteration' = 1 local finishedCount = `finishedCount' + 1 local iterationsCurrent : list iterationsCurrent - iteration local somethingFailed = 1 local failures = trim("`failures' `iteration'") noisily di as error " `iteration' " _continue if "`stsDefined'" != "" qui sendtoslack, url(`stsDefined') message("ERROR! Iteration `iteration' failed!") * Optionally start new iteration if "`iterationsRemainder'" != "" { ** Identify new iteration local newIteration : word 1 of `iterationsRemainder' ** Update lists local iterationsRemainder : list iterationsRemainder - newIteration local iterationsCurrent : list iterationsCurrent | newIteration ** Start it if "`timestamp'" == "" noisily di _col(3) as text `" -starting `newIteration' (`c(current_date)' `c(current_time)')- "' _continue else noisily di _col(3) as text `" -starting `newIteration'- "' _continue winexec "`stataexe'" do "`middlemanPath'" "0`dofileName'0" `newIteration' "`tempfolderDefined'" "`stopDefined'" "`quitDefined'" sleep `betweendelay_ms' local startedNewIteration "yes" } * Restart loop (this prevents a bug from popping up, related to changing the macro list currently being looped over) local restartedLoop "yes" continue, break } if "`startedNewIteration'" == "yes" { local startedNewIteration "no" sleep `sleepduration_ms' } } if "`finishedCount'" == "`numberOfIterations'" local true "true" if ("`true'" != "true") & ("`restartedLoop'" == "no"){ noisily di as txt "x" _continue sleep `updatedelay_ms' } } if "`somethingFailed'" == "0" { noisily di as result " OK" noisily di _newline as result "Batch job has finished." if "`stsDefined'" != "" sendtoslack, url(`stsDefined') message("Batch job has finished.") col(4) } if "`somethingFailed'" == "1" { noisily di as error " Something failed!" noisily di _newline as result "Batch job has finished, but with " as error "failures" as result "!" noisily di as result "Failed iterations: " as error "`failures'" if "`stsDefined'" != "" sendtoslack, url(`stsDefined') message("Batch job has finished with failures: iterations `failures'.") col(4) } } end program define batcher_saveoption syntax, name(string) value(string) * Determine whether profile.do exists cap findfile profile.do ** If profile.do does not exist yet ** Create profile.do (asking permission) if _rc == 601 { di "Profile.do does not exist yet." di "Do you want to allow this program to create one for you? y: yes, n: no" _newline "(enter below)" _request(_createPermission) if "`createPermission'" == "y" { di "Creating profile.do as `c(sysdir_oldplace)'profile.do" tempname createdProfileDo file open `createdProfileDo' using `"`c(sysdir_oldplace)'profile.do"', write file close `createdProfileDo' } if "`createPermission'" != "y" { di "User did not give permission to create profile.do, aborting program." exit } } * Write in global for url ** Verify if global is already defined (if so, give warning) *** Find location of profile.do qui findfile profile.do local profileDofilePath "`r(fn)'" *** Open tempname profileDofile file open `profileDofile' using "`profileDofilePath'", read text file read `profileDofile' line *** Loop over profile.do until ... *** you reached the end *** found the global we want to define local keepGoing = 1 while `keepGoing' == 1 { if strpos(`"`macval(line)'"', "sts_`name'") > 0 { di as error "Global was already defined in profile.do" di as result "The program will add the new definition at the bottom." di "You might want to open profile.do and remove the old entry." di "This is not required, but prevents clogging your profile.do." di "To do so, type: " as txt "doed `profileDofilePath'" _newline local keepGoing = 0 } file read `profileDofile' line if r(eof) == 1 local keepGoing = 0 } file close `profileDofile' ** Write in the global file open `profileDofile' using "`profileDofilePath'", write text append file write `profileDofile' _newline `"global batcher_`name' `"`value'"'"' file close `profileDofile' ** Define it now too, as profile.do changes only take place once it has ran global batcher_`name' `"`value'"' * Report back to user di as text "Added a default " as result "`name'" as text " to " as result "`profileDofilePath'" di as text "On this PC, " as result `"`name'(`value')"' as text " will now be used even if no " as result "`name'" as text " option was specified for the batcher command." di as text "In other words, you can now type " as result "batcher" as text " and it will execute " as result `"batcher, `name'(`value')"' as text " (+ any other saved options)." _newline end