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