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