1 #!/bin/sh 2 # 3 # Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. 4 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 # 6 # This code is free software; you can redistribute it and/or modify it 7 # under the terms of the GNU General Public License version 2 only, as 8 # published by the Free Software Foundation. 9 # 10 # This code is distributed in the hope that it will be useful, but WITHOUT 11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 13 # version 2 for more details (a copy is included in the LICENSE file that 14 # accompanied this code). 15 # 16 # You should have received a copy of the GNU General Public License version 17 # 2 along with this work; if not, write to the Free Software Foundation, 18 # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 19 # 20 # Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 21 # or visit www.oracle.com if you need additional information or have any 22 # questions. 23 # 24 25 # Shell script for a fast parallel forest/trees command 26 27 usage() { 28 echo "usage: $0 [-h|--help] [-q|--quiet] [-v|--verbose] [-s|--sequential] [--] <command> [commands...]" > ${status_output} 29 echo "command format : mercurial-command [ "jdk" ] [ extra-url ]" 30 echo "command option: jdk : used only with clone command to request just the extra repos for JDK-only builds" 31 echo "command option : extra-url : server hosting the extra repositories" 32 echo "Environment variables which modify behaviour:" 33 echo " HGFOREST_QUIET : (boolean) If 'true' then standard output is redirected to /dev/null" 34 echo " HGFOREST_VERBOSE : (boolean) If 'true' then Mercurial asked to produce verbose output" 35 echo " HGFOREST_SEQUENTIAL : (boolean) If 'true' then repos are processed sequentially. Disables concurrency" 36 echo " HGFOREST_GLOBALOPTS : (string, must begin with space) Additional Mercurial global options" 37 echo " HGFOREST_REDIRECT : (file path) Redirect standard output to specified file" 38 echo " HGFOREST_FIFOS : (boolean) Default behaviour for FIFO detection. Does not override FIFOs disabled" 39 echo " HGFOREST_CONCURRENCY: (positive integer) Number of repos to process concurrently" 40 echo " HGFOREST_DEBUG : (boolean) If 'true' then temp files are retained" 41 exit 1 42 } 43 44 global_opts="${HGFOREST_GLOBALOPTS:-}" 45 status_output="${HGFOREST_REDIRECT:-/dev/stdout}" 46 qflag="${HGFOREST_QUIET:-false}" 47 vflag="${HGFOREST_VERBOSE:-false}" 48 sflag="${HGFOREST_SEQUENTIAL:-false}" 49 while [ $# -gt 0 ] 50 do 51 case $1 in 52 -h | --help ) 53 usage 54 ;; 55 56 -q | --quiet ) 57 qflag="true" 58 ;; 59 60 -v | --verbose ) 61 vflag="true" 62 ;; 63 64 -s | --sequential ) 65 sflag="true" 66 ;; 67 68 '--' ) # no more options 69 shift; break 70 ;; 71 72 -*) # bad option 73 usage 74 ;; 75 76 * ) # non option 77 break 78 ;; 79 esac 80 shift 81 done 82 83 # debug mode 84 if [ "${HGFOREST_DEBUG:-false}" = "true" ] ; then 85 global_opts="${global_opts} --debug" 86 fi 87 88 # silence standard output? 89 if [ ${qflag} = "true" ] ; then 90 global_opts="${global_opts} -q" 91 status_output="/dev/null" 92 fi 93 94 # verbose output? 95 if [ ${vflag} = "true" ] ; then 96 global_opts="${global_opts} -v" 97 fi 98 99 # Make sure we have a command. 100 if [ ${#} -lt 1 -o -z "${1:-}" ] ; then 101 echo "ERROR: No command to hg supplied!" > ${status_output} 102 usage > ${status_output} 103 fi 104 105 # grab command 106 command="${1}"; shift 107 108 if [ ${vflag} = "true" ] ; then 109 echo "# Mercurial command: ${command}" > ${status_output} 110 fi 111 112 # At this point all command options and args are in "$@". 113 # Always use "$@" (within double quotes) to avoid breaking 114 # args with spaces into separate args. 115 116 if [ ${vflag} = "true" ] ; then 117 echo "# Mercurial command argument count: $#" > ${status_output} 118 for cmdarg in "$@" ; do 119 echo "# Mercurial command argument: ${cmdarg}" > ${status_output} 120 done 121 fi 122 123 # Clean out the temporary directory that stores the pid files. 124 tmp=/tmp/forest.$$ 125 rm -f -r ${tmp} 126 mkdir -p ${tmp} 127 128 129 if [ "${HGFOREST_DEBUG:-false}" = "true" ] ; then 130 # ignores redirection. 131 echo "DEBUG: temp files are in: ${tmp}" >&2 132 fi 133 134 # Check if we can use fifos for monitoring sub-process completion. 135 echo "1" > ${tmp}/read 136 while_subshell=1 137 while read line; do 138 while_subshell=0 139 break; 140 done < ${tmp}/read 141 rm ${tmp}/read 142 143 on_windows=`uname -s | egrep -ic -e 'cygwin|msys'` 144 145 if [ ${while_subshell} = "1" -o ${on_windows} = "1" ]; then 146 # cygwin has (2014-04-18) broken (single writer only) FIFOs 147 # msys has (2014-04-18) no FIFOs. 148 # older shells create a sub-shell for redirect to while 149 have_fifos="false" 150 else 151 have_fifos="${HGFOREST_FIFOS:-true}" 152 fi 153 154 safe_interrupt () { 155 if [ -d ${tmp} ]; then 156 if [ "`ls ${tmp}/*.pid`" != "" ]; then 157 echo "Waiting for processes ( `cat ${tmp}/.*.pid ${tmp}/*.pid 2> /dev/null | tr '\n' ' '`) to terminate nicely!" > ${status_output} 158 sleep 1 159 # Pipe stderr to dev/null to silence kill, that complains when trying to kill 160 # a subprocess that has already exited. 161 kill -TERM `cat ${tmp}/*.pid | tr '\n' ' '` 2> /dev/null 162 wait 163 echo "Interrupt complete!" > ${status_output} 164 fi 165 rm -f -r ${tmp} 166 fi 167 exit 130 168 } 169 170 nice_exit () { 171 if [ -d ${tmp} ]; then 172 if [ "`ls -A ${tmp} 2> /dev/null`" != "" ]; then 173 wait 174 fi 175 if [ "${HGFOREST_DEBUG:-false}" != "true" ] ; then 176 rm -f -r ${tmp} 177 fi 178 fi 179 } 180 181 trap 'safe_interrupt' INT QUIT 182 trap 'nice_exit' EXIT 183 184 subrepos="corba jaxp jaxws langtools jdk hotspot nashorn" 185 jdk_subrepos_extra="closed jdk/src/closed jdk/make/closed jdk/test/closed hotspot/make/closed hotspot/src/closed hotspot/test/closed" 186 subrepos_extra="$jdk_subrepos_extra deploy install sponsors pubs" 187 188 # Only look in specific locations for possible forests (avoids long searches) 189 pull_default="" 190 repos="" 191 repos_extra="" 192 if [ "${command}" = "clone" -o "${command}" = "fclone" -o "${command}" = "tclone" ] ; then 193 # we must be a clone 194 if [ ! -f .hg/hgrc ] ; then 195 echo "ERROR: Need initial repository to use this script" > ${status_output} 196 exit 1 197 fi 198 199 # the clone must know where it came from (have a default pull path). 200 pull_default=`hg paths default` 201 if [ "${pull_default}" = "" ] ; then 202 echo "ERROR: Need initial clone with 'hg paths default' defined" > ${status_output} 203 exit 1 204 fi 205 206 # determine which sub repos need to be cloned. 207 for i in ${subrepos} ; do 208 if [ ! -f ${i}/.hg/hgrc ] ; then 209 repos="${repos} ${i}" 210 fi 211 done 212 213 pull_default_tail=`echo ${pull_default} | sed -e 's@^.*://[^/]*/\(.*\)@\1@'` 214 215 if [ $# -gt 0 ] ; then 216 if [ "x${1}" = "xjdk" ] ; then 217 subrepos_extra=$jdk_subrepos_extra 218 echo "subrepos being cloned are $subrepos_extra" 219 shift 220 fi 221 # if there is an "extra sources" path then reparent "extra" repos to that path 222 if [ "x${pull_default}" = "x${pull_default_tail}" ] ; then 223 echo "ERROR: Need initial clone from non-local source" > ${status_output} 224 exit 1 225 fi 226 # assume that "extra sources" path is the first arg 227 pull_extra="${1}/${pull_default_tail}" 228 229 # determine which extra subrepos need to be cloned. 230 for i in ${subrepos_extra} ; do 231 if [ ! -f ${i}/.hg/hgrc ] ; then 232 repos_extra="${repos_extra} ${i}" 233 fi 234 done 235 else 236 if [ "x${pull_default}" = "x${pull_default_tail}" ] ; then 237 # local source repo. Clone the "extra" subrepos that exist there. 238 for i in ${subrepos_extra} ; do 239 if [ -f ${pull_default}/${i}/.hg/hgrc -a ! -f ${i}/.hg/hgrc ] ; then 240 # sub-repo there in source but not here 241 repos_extra="${repos_extra} ${i}" 242 fi 243 done 244 fi 245 fi 246 247 # Any repos to deal with? 248 if [ "${repos}" = "" -a "${repos_extra}" = "" ] ; then 249 echo "No repositories to process." > ${status_output} 250 exit 251 fi 252 253 # Repos to process concurrently. Clone does better with low concurrency. 254 at_a_time="${HGFOREST_CONCURRENCY:-2}" 255 else 256 # Process command for all of the present repos 257 for i in . ${subrepos} ${subrepos_extra} ; do 258 if [ -d ${i}/.hg ] ; then 259 repos="${repos} ${i}" 260 fi 261 done 262 263 # Any repos to deal with? 264 if [ "${repos}" = "" ] ; then 265 echo "No repositories to process." > ${status_output} 266 exit 267 fi 268 269 # any of the repos locked? 270 locked="" 271 for i in ${repos} ; do 272 if [ -h ${i}/.hg/store/lock -o -f ${i}/.hg/store/lock ] ; then 273 locked="${i} ${locked}" 274 fi 275 done 276 if [ "${locked}" != "" ] ; then 277 echo "ERROR: These repositories are locked: ${locked}" > ${status_output} 278 exit 1 279 fi 280 281 # Repos to process concurrently. 282 at_a_time="${HGFOREST_CONCURRENCY:-8}" 283 fi 284 285 # Echo out what repositories we do a command on. 286 echo "# Repositories: ${repos} ${repos_extra}" > ${status_output} 287 288 if [ "${command}" = "serve" ] ; then 289 # "serve" is run for all the repos as one command. 290 ( 291 ( 292 cwd=`pwd` 293 serving=`basename ${cwd}` 294 ( 295 echo "[web]" 296 echo "description = ${serving}" 297 echo "allow_push = *" 298 echo "push_ssl = False" 299 300 echo "[paths]" 301 for i in ${repos} ; do 302 if [ "${i}" != "." ] ; then 303 echo "/${serving}/${i} = ${i}" 304 else 305 echo "/${serving} = ${cwd}" 306 fi 307 done 308 ) > ${tmp}/serve.web-conf 309 310 echo "serving root repo ${serving}" > ${status_output} 311 312 echo "hg${global_opts} serve" > ${status_output} 313 (PYTHONUNBUFFERED=true hg${global_opts} serve -A ${status_output} -E ${status_output} --pid-file ${tmp}/serve.pid --web-conf ${tmp}/serve.web-conf; echo "$?" > ${tmp}/serve.pid.rc ) 2>&1 & 314 ) 2>&1 | sed -e "s@^@serve: @" > ${status_output} 315 ) & 316 else 317 # Run the supplied command on all repos in parallel. 318 319 # n is the number of subprocess started or which might still be running. 320 n=0 321 if [ ${have_fifos} = "true" ]; then 322 # if we have fifos use them to detect command completion. 323 mkfifo ${tmp}/fifo 324 exec 3<>${tmp}/fifo 325 fi 326 327 # iterate over all of the subrepos. 328 for i in ${repos} ${repos_extra} ; do 329 n=`expr ${n} '+' 1` 330 repopidfile=`echo ${i} | sed -e 's@./@@' -e 's@/@_@g'` 331 reponame=`echo ${i} | sed -e :a -e 's/^.\{1,20\}$/ &/;ta'` 332 pull_base="${pull_default}" 333 334 # regular repo or "extra" repo? 335 for j in ${repos_extra} ; do 336 if [ "${i}" = "${j}" ] ; then 337 # it's an "extra" 338 if [ -n "${pull_extra}" ]; then 339 # if no pull_extra is defined, assume that pull_default is valid 340 pull_base="${pull_extra}" 341 fi 342 fi 343 done 344 345 # remove trailing slash 346 pull_base="`echo ${pull_base} | sed -e 's@[/]*$@@'`" 347 348 # execute the command on the subrepo 349 ( 350 ( 351 if [ "${command}" = "clone" -o "${command}" = "fclone" -o "${command}" = "tclone" ] ; then 352 # some form of clone 353 clone_newrepo="${pull_base}/${i}" 354 parent_path="`dirname ${i}`" 355 if [ "${parent_path}" != "." ] ; then 356 times=0 357 while [ ! -d "${parent_path}" ] ; do ## nested repo, ensure containing dir exists 358 if [ "${sflag}" = "true" ] ; then 359 # Missing parent is fatal during sequential operation. 360 echo "ERROR: Missing parent path: ${parent_path}" > ${status_output} 361 exit 1 362 fi 363 times=`expr ${times} '+' 1` 364 if [ `expr ${times} '%' 10` -eq 0 ] ; then 365 echo "${parent_path} still not created, waiting..." > ${status_output} 366 fi 367 sleep 5 368 done 369 fi 370 # run the clone command. 371 echo "hg${global_opts} clone ${clone_newrepo} ${i}" > ${status_output} 372 (PYTHONUNBUFFERED=true hg${global_opts} clone ${clone_newrepo} ${i}; echo "$?" > ${tmp}/${repopidfile}.pid.rc ) 2>&1 & 373 else 374 # run the command. 375 echo "cd ${i} && hg${global_opts} ${command} ${@}" > ${status_output} 376 cd ${i} && (PYTHONUNBUFFERED=true hg${global_opts} ${command} "${@}"; echo "$?" > ${tmp}/${repopidfile}.pid.rc ) 2>&1 & 377 fi 378 379 echo $! > ${tmp}/${repopidfile}.pid 380 ) 2>&1 | sed -e "s@^@${reponame}: @" > ${status_output} 381 # tell the fifo waiter that this subprocess is done. 382 if [ ${have_fifos} = "true" ]; then 383 echo "${i}" >&3 384 fi 385 ) & 386 387 if [ "${sflag}" = "true" ] ; then 388 # complete this task before starting another. 389 wait 390 else 391 if [ "${have_fifos}" = "true" ]; then 392 # check on count of running subprocesses and possibly wait for completion 393 if [ ${n} -ge ${at_a_time} ] ; then 394 # read will block until there are completed subprocesses 395 while read repo_done; do 396 n=`expr ${n} '-' 1` 397 if [ ${n} -lt ${at_a_time} ] ; then 398 # we should start more subprocesses 399 break; 400 fi 401 done <&3 402 fi 403 else 404 # Compare completions to starts 405 completed="`(ls -a1 ${tmp}/*.pid.rc 2> /dev/null | wc -l) || echo 0`" 406 while [ `expr ${n} '-' ${completed}` -ge ${at_a_time} ] ; do 407 # sleep a short time to give time for something to complete 408 sleep 1 409 completed="`(ls -a1 ${tmp}/*.pid.rc 2> /dev/null | wc -l) || echo 0`" 410 done 411 fi 412 fi 413 done 414 415 if [ ${have_fifos} = "true" ]; then 416 # done with the fifo 417 exec 3>&- 418 fi 419 fi 420 421 # Wait for all subprocesses to complete 422 wait 423 424 # Terminate with exit 0 only if all subprocesses were successful 425 # Terminate with highest exit code of subprocesses 426 ec=0 427 if [ -d ${tmp} ]; then 428 rcfiles="`(ls -a ${tmp}/*.pid.rc 2> /dev/null) || echo ''`" 429 for rc in ${rcfiles} ; do 430 exit_code=`cat ${rc} | tr -d ' \n\r'` 431 if [ "${exit_code}" != "0" ] ; then 432 if [ ${exit_code} -gt 1 ]; then 433 # mercurial exit codes greater than "1" signal errors. 434 repo="`echo ${rc} | sed -e 's@^'${tmp}'@@' -e 's@/*\([^/]*\)\.pid\.rc$@\1@' -e 's@_@/@g'`" 435 echo "WARNING: ${repo} exited abnormally (${exit_code})" > ${status_output} 436 fi 437 if [ ${exit_code} -gt ${ec} ]; then 438 # assume that larger exit codes are more significant 439 ec=${exit_code} 440 fi 441 fi 442 done 443 fi 444 exit ${ec}