List ecs-isp@2rosenthals.com Arkiverade meddelande #1036

Från: "Steven Levine" <ecs-isp@2rosenthals.com> Meddelandehuvud
Oavkodat meddelande
Ämne: Re: [eCS-ISP] (uacme) certificate renew issue
Datum: Mon, 02 Dec 2024 20:27:37 -0800
Till: "eCS ISP Mailing List" <ecs-isp@2rosenthals.com>

In <list-11290759@2rosenthals.com>, on 12/02/24
   at 08:59 PM, "Massimo S." <ecs-isp@2rosenthals.com> said:

Hi massimo,

>should be possible to add the detection of this situation (here below) in
>the hook rexx script?

You can't fix this inside the hook script.   What you need is a wrapper
script that retries the failing uacme issue request.

I've appended a copy of uacme-renew.cmd that Dan and I use.  It uses my
uacme-hook.cmd, so you will not be able to the script without adapting it
to work with your hook scripts.

>I guess this is some sort of random outage or overload of LE services

You are using the internet.  Failures happen and your tools need to be
able handle the known failure modes.

For the curious, Dan has some dual homed domains.  For example

  [d:\tmp]dig +short dnacih.com
  64.60.60.123
  47.180.217.132

This is good for reliability, but complicates cert renewals because one
cannot predict which server will be asked to return the token file.
uacme-renew.cmd knows which domains are dual homed and knows how to push
the token files to all the relevant servers.

uacme-renew.cmd does not currently push the updated certs to the other
servers.  This is handled by a simple rsync scrpt.

Steven

--
----------------------------------------------------------------------
"Steven Levine" <steve53@earthlink.net>  Warp/DIY/BlueLion etc.
www.scoug.com www.arcanoae.com www.warpcave.com
----------------------------------------------------------------------
/* uacme-renew - renew Let's Encrypt certificates
   read uacme-renew.domains
   try to renew
   log results

   FIXME to explain how to swap staging and production private keys
   FIXME to verify using staging key if staging requested

   Copyright (c) 2024 Steven Levine and Associates, Inc.
   All rights reserved.

   This Source Code Form is subject to the terms of the Mozilla Public
   License, v. 2.0. If a copy of the MPL was not distributed with this
   file, You can obtain one at http://mozilla.org/MPL/2.0/.

   2024-11-05 SHL Baseline
   2024-11-07 SHL Drop excess code
   2024-11-14 SHL Show certificate dates
   2024-11-19 SHL Support multiple domains per certificate
*/

signal on Error
signal on Failure name Error
signal on Halt
signal on NotReady name Error
signal on NoValue name Error
signal on Syntax name Error

gVersion = '0.1 2024-11-19'

Globals = 'gBatch gCmdName gCertsRenewed gDbgLvl gDomainListsFileName gDomainsLists.',
 'gDryRun',
 'gEnv gLogDir gLogFile',
 'gRenewalsFailed gRenewalsRequested',
 'gStaging gTmpDir gVerbose gVersion'

call Initialize

Main:

  if 0 & trace() <> '?A' then trace '?A' ; nop /* FIXME to be gone debug */

  parse arg cmdLine
  call ScanArgs cmdLine
  drop cmdLine

  /* FIXME to verify prerequisites available - uacme, hook openssl */

  /* Optionally override default log directory */
  logdir = 'd:\logs'
  if IsDir(logdir) then
    gLogDir = logDir

  /* Read domains list */
  call ReadDomainsListsFile

  if gStaging then do
    say
    call LogWriteVTSC 'Using staging account'
  end

  /* Run uacme for each domain list */

  gRenewalsRequested = 0
  gRenewalsFailed = 0
  gCertsRenewed = 0

  do listNum = 1 to gDomainsLists.0
    domainlist = gDomainsLists.listNum
    call RenewOneCert domainlist
  end

  say
  call LogWriteVTSC 'Renewal requested for' Plural(gRenewalsRequested, 'certificate')
  call LogWriteVTSC 'Renewed' Plural(gCertsRenewed, 'certificate')
  if gRenewalsFailed > 0 then
    call LogWriteVTSC 'Uacme reported' Plural(gRenewalsFailed, 'renewal failure')

  say
  say 'Results logged to' gLogFile

  exit

/* end Main */

/*=== ReadDomainsListsFile() read domains lists into gDomainsLists stem ===*/

ReadDomainsListsFile: procedure expose (Globals)

  if stream(gDomainListsFileName, 'C', 'QUERY EXISTS') == '' then
    call Die 'Cannot access' gDomainListsFileName

  call LogWriteVTSC 'Reading' gDomainListsFileName
  call stream gDomainListsFileName, 'C', 'OPEN READ'

  /* Support # and ; comments and blank lines and missing EOL */

  gDomainsLists.0 = 0
  drop gErrCondition
  do while lines(gDomainListsFileName)
    call on NotReady name CatchError /* Avoid death on missing NL */
    line = linein(gDomainListsFileName)
    signal on NotReady name Error

    line = strip(line)
    if line == '' then iterate
    ch = left(line, 1)
    if ch == '#' | ch == ';' then iterate

    listNum = gDomainsLists.0 + 1
    gDomainsLists.listNum = line
    gDomainsLists.0 = listNum

    if symbol('gErrCondition') == 'VAR' then
      leave /* Last line missing NL */
  end /* while lines */

  call stream gDomainListsFileName, 'C', 'CLOSE'

  call LogWriteVTSC 'Read' Plural(gDomainsLists.0, 'domain') 'from' gDomainListsFileName
  return

/* end ReadDomainsListsFile */

/*=== RenewOneCert(domains) Try to renew one domain ===*/

RenewOneCert: procedure expose (Globals)

  parse arg domainlist

  /* Domain list is a list of space separated domains
     The first is the primary domain with determines file locations
  */
  domain = word(domainlist, 1)

  call LogWriteVTSC 'Preparing for' domainlist 'domains'

  /* Verify cert accessible */
  certdir = MakePath(GetEnv('UNIXROOT'), '\etc\ssl\uacme', domain)
  if \ IsDir(certdir) then
    call Die 'Cannot access' certdir 'certificate directory for' domain 'domain'

  certfile = MakePath(certdir, 'cert.pem')
  if \ IsFile(certfile) then
    call Die 'Cannot access' certfile 'certificate file for' domain 'domain'

  cmd = 'openssl x509 -in' certfile '-subject -dates -noout'
  say
  say 'Checking certificate dates'
  cmd

  if \ gBatch then do
    call AskYNQ 'Try to renew' domain
    if RESULT >= 2 then exit
    if RESULT \= 0 then return
  end

  call LogWriteVTSC 'Trying to renew' domain 'domain'

  gRenewalsRequested = gRenewalsRequested + 1

  retries = 2 /* FIXME to be more maybe */

  /* uacme return codes
     0 - Success
     1 - Certificate not reissued because it is still current
     2 - Failure (syntax or usage error; configuration error; processing failure; unexpected error)
  */

  do retry = 1 to retries

    /* FIXME to capture output for log */
    if gStaging then
      cmd = 'uacme --staging --verbose --hook uacme-hook.cmd issue' domainlist
    else
      cmd = 'uacme --verbose --hook uacme-hook.cmd issue' domainlist
    say
    err = RunExtCmdNoDie(cmd)

    select
    when err = 0 then do
      call LogWriteVTSC domain 'renew request succeeded on' retry 'of' retries
      gCertsRenewed = gCertsRenewed + 1
      leave
    end
    when err = 1 then do
      call LogWriteVTSC 'Renewal request bypessed because certificate is current on retry' retry 'of' retries
      leave
    end
    when err = 2 then do
      call LogWriteVTSC 'Renewal request failed with error' err 'on retry' retry 'of' retries
      if retry < retries then
call SysSleep 5
    end
    otherwise
      call LogWriteVTSC 'Renewal request failed with unexpected error' err 'on retry' retry 'of' retries
      gRenewalsFailed = gRenewalsFailed + 1
      leave
    end

  end /* do retry */

  return

/* end RenewOneCert */

/*=== Initialize() Initialize globals ===*/

Initialize: procedure expose (Globals)
  call SetCmdName
  call LoadRexxUtil
  gEnv = 'OS2ENVIRONMENT'
  call SetTmpDir
  return

/* end Initialize */

/*=== ScanArgsInit() ScanArgs initialization exit routine ===*/

ScanArgsInit: procedure expose (Globals) cmdTail swCtl keepQuoted

  /* Preset defaults */
  gBatch = 0 /* Batch mode flag */
  gDbgLvl = 0 /* Debug mode level */
  gDomainListsFileName = ''
  gDryRun = 0 /* Dry run - no files changed */
  gStaging = 0 /* Use staging account */
  gVerbose = 0 /* Verbose mode level */

  return

/* end ScanArgsInit */

/*=== ScanArgsSwitch() ScanArgs switch option exit routine ===*/

ScanArgsSwitch: procedure expose (Globals) curSw curSwArg

  select
  when curSw == 'b' then
    gBatch = 1
  when curSw == 'd' then
    gDbgLvl = gDbgLvl + 1
  when curSw == 'h' | curSw == '?' then
    call ScanArgsHelp
  when curSw == 'n' then
    gDryRun = 1
  when curSw == 's' then
    gStaging = 1
  when curSw == 'v' then
    gVerbose = gVerbose + 1
  when curSw == 'V' then do
    say
    say gCmdName gVersion
    exit
  end
  otherwise
    call ScanArgsUsage 'Switch "-' || curSw || '" unexpected'
  end /* select */

  return

/* end ScanArgsSwitch */

/*=== ScanArgsArg() ScanArgs argument option exit routine ===*/

ScanArgsArg: procedure expose (Globals) curArg

  call ScanArgsUsage 'Argument "' || curArg '" unexpected'

  if gDomainListsFileName \= '' then
    call ScanArgsUsage 'Domains file name already set to' gDomainListsFileName

  if \ IsFile(curArg) then
    call ScanArgsUsage 'Cannot access ' curArg 'domains file'

  gDomainListsFileName = curArg

  return

/* end ScanArgsArg */

/*=== ScanArgsTerm() ScanArgs scan end exit routine ===*/

ScanArgsTerm: procedure expose (Globals)

  if gDomainListsFileName = '' then do
    gDomainListsFileName = MakePath(gCmdName, '.domains')

    if \ IsFile(gDomainListsFileName) then
      call ScanArgsUsage 'Cannot access' gDomainListsFileName 'default domains file'

  end

  return

/* end ScanArgsTerm */

/*=== ScanArgsHelp() Display ScanArgs usage help exit routine ===*/

ScanArgsHelp:
  say
  say 'uacme certificate renew wrapper.'
  say
  say 'Usage:' gCmdName '[-b] [-d] [-h] [-n] [-s] [-v] [-V] [-?] domains-file'
  say
  say '  -b            Enable batch mode'
  say '  -d            Enable debug mode, repeat for more verbosity'
  say '  -h -?         Display this message'
  say '  -n            Dry run - no files changed'
  say '  -s            Use staging account'
  say '  -v            Enable verbose messages, repeat for more verbosity'
  say '  -V            Display version number and quit'
  say
  say '  domains-file  Domain list file, default is' gCmdName || '.domains'
  exit 255

/* end ScanArgsHelp */

/*=== ScanArgsUsage(message) Report ScanArgs usage error exit routine ===*/

ScanArgsUsage:
  parse arg msg
  say
  if msg \== '' then
    say msg
  say 'Usage:' gCmdName '[-b] [-d] [-h] [-n] [-s] [-v] [-V] [-?] domains-file'
  exit 255

/* end ScanArgsUsage */

/*==============================================================================*/
/*=== SkelRexxFunc standards - Delete unused - Move modified above this mark ===*/
/*==============================================================================*/

/*=== AskYNQ([prompt][, noskip[, nofocus]])) returns 0=Yes, 1=No, 2=Quit, skips line unless noskip ===*/

AskYNQ: procedure
  parse arg msg, noskip, nofocus

  /* Take focus with 4OS2 or fail if cannot match window title */
  /* If script designed for CMD too, use nofocus arg to avoid error noise */
  signal off Error
  /* Map 1st left bracket to wild card - [pid] seems to confuse activate */
  if nofocus = '' | nofocus \= 1 then
    '@if defined _WINTITLE activate "%@replace[[,*,%_WINTITLE]"'
  signal on Error

  /* Skip line unless suppressed by noskip arg - any non-zero value requests noskip */
  if noskip = '' | noskip = 0 then
    call lineout 'STDERR', ''

  if msg == '' then
    msg = 'Continue'
  call charout 'STDERR', msg '(y/n/q) ? '
  do forever
    key = translate(SysGetKey('NOECHO'))
    if key == 'Y' | key == 'N' then do
      call lineout 'STDERR', key
      if key == 'Y' then
ynq = 0
      else
ynq = 1
      leave
    end
    if key == 'Q' | c2x(key) == '1B' then do
      call lineout 'STDERR', ''
      ynq = 2
      leave
    end
  end /* forever */
  return ynq

/* end AskYNQ */

/*=== DieVTS(message) Write message to STDOUT and timestamped message to log file and die ===*/

Die:
  /* Requires LogWriteVTS and dependents */
  /* Requires LogWriteVTSC and dependents */
  parse arg msg
  callerSIGL = SIGL
  /* Use say to avoid NotReady in case running detached - FIXME to be sure not ok to write to STDERR */
  say
  call LogWriteVTS msg
  msg = gCmdName 'aborting at line' callerSIGL || '.'
  call LogWriteVTSC msg
  call beep 200, 300
  exit 254

/* end DieVTS */

/*=== GetEnv(var) Return value for environment variable or empty string ===*/

GetEnv: procedure expose (Globals)
  parse arg var
  if var = '' then
    call Die 'GetEnv requires an argument'
  return value(var,, gEnv)

/* end GetEnv */

/*=== IsDir(dirName[, full]) return true if directory is valid, accessible directory ===*/

IsDir: procedure
  /* If arg(2) not omitted, return full directory name or empty string */
  parse arg dir, full
  newdir = ''

  do 1
    if dir == '' then do
      cwd = '' /* No restore needed */
      leave
    end
    dir = translate(dir, '\', '/') /* Convert to OS/2 slashes */
    s = strip(dir, 'T', '\') /* Chop trailing slashes unless root */
    if s \== '' & right(s, 1) \== ":" then
      dir = s /* Chop */
    drv = filespec('D', dir)
    cwd = directory() /* Remember */
    /* If have drive letter and requested directory on some other drive */
    if drv \== '' & translate(drv) \== translate(left(cwd, 2)) then do
      /* Avoid slow failures and unwanted directory changes */
      drvs = SysDriveMap('A:')
      if pos(translate(drv), drvs) = 0 then
leave /* Unknown drive */
      if SysDriveInfo(drv) == '' then
leave /* Drive not ready */
      cwd2 = directory(drv) /* Remember current directory on other drive */
      newdir = directory(dir) /* Try to change and get full path name */
      call directory cwd2 /* Restore current directory on other drive */
      leave
    end

    /* If no drive letter or same drive and not UNC name */
    if left(dir, 2) \== '\\' then do
      newdir = directory(dir) /* Try to change and get full path name */
      leave
    end

    /* UNC name - hopefully server is accessible or this will be slow
       Accept
\\server
\\server\
\\server\dir\
\\server\dir
     */
    cwd = '' /* No restore needed */
    wc = dir
    if right(wc, 1) \== '\' then
      wc = wc || '\'
    i = lastpos('\', wc)
    if substr(wc, 3, 1) == '\' then
      leave /* Malformed UNC - no server name */
    if pos('*', wc) > 0 | pos('?', wc) > 0 then
      leave /* No wildcards allowed */
    call SysFileTree wc, 'files', 'O'
    if files.0 > 0 then do
      s = files.1 /* Exists and is not empty */
      i = lastpos('\', s)
      newdir = left(s, i - 1) /* Extract directory name from full path name */
      leave
    end
    /* Try wildcarded directory name */
    wc = strip(wc, 'T', '\')
    i = lastpos('\', wc)
    base = substr(wc, i + 1)
    if base == '' then
      leave /* Should have matched above */
    wc = substr(wc, 1, i) || '*' || base || '*'
    call SysFileTree wc, 'files', 'DO'
    do fileNum = 1 to files.0
      /* Find directory name is list */
      s = files.fileNum
      i = lastpos('\', s)
      s2 = substr(s, i + 1)
      if translate(base) == translate(s2) then do
newdir = left(s, i - 1)
leave
      end
    end /* i */
  end /* 1 */

  if cwd \== '' then
    call directory cwd /* Restore original directory and drive */

  if full \== '' then
    ret = newdir /* Return full directory name or empty string */
  else
    ret = newdir \== '' /* Return true if valid and accessible */
  return ret

/* end IsDir */

/*=== IsFile(file) return true if arg is file and file exists ===*/

IsFile: procedure expose (Globals)
  parse arg file
  if file == '' then
    yes = 0
  else do
    /* '.' and '..' returns files in '.' or '..' - so avoid false positives */
    call SysFileTree file, 'files', 'F'
    if RESULT \= 0 then
      call Die 'SysFileTree' file 'failed'
    /* Assume caller knows if arg contains wildcards */
    yes = file \== '.' & file \== '..' & files.0 \= 0
  end
  return yes

/* end IsFile */

/*=== LogOpen() Open log file for append ===*/

LogOpen: procedure expose (Globals)
  /* Requires LogSetName unless gLogFile defined */
  /* Sets gLogFile if not defined */
  /* Overrides gLogFile if open fails */
  if symbol('gLogFile') \== 'VAR' then
    call LogSetName
  /* Assume closed */
  call stream gLogFile, 'C', 'OPEN WRITE'
  if stream(gLogFile) \== 'READY' then do
    gLogFile = '\' || gCmdName || '.log' /* Try root */
    call stream gLogFile, 'C', 'OPEN WRITE'
  end
  return

/* end LogOpen */

/*=== LogSetName() Set log file name ===*/

/**
 * Sets gLogFile if not defined
 * Sets gLogDir if not defined
 */

LogSetName: procedure expose (Globals)
  /* Requires LogSetDir unless gLogDir defined */
  /* Requires gCmdName */
  if symbol('gLogFile') \== 'VAR' then do
    if symbol('gLogDir') \== 'VAR' then
      call LogSetDir
    /* Ensure trailing backslash unless using current directory */
    dir = gLogDir
    if dir \== '' & right(dir, 1) \== ':' & right(dir, 1) \== '\' then
      dir = dir || '\' /* Ensure trailing backslash */
    gLogFile = dir || gCmdName'.log'
  end
  return

/* end LogSetName */

/*=== LogSetDir() Set gLogDir and provide trailing backslash if needed ===*/

/**
 * Set gLogDir if gLogDir not defined
 * Tries %LOGFILES gTmpDir %TMP
 * Falls back to current directory and returns null string
 */

LogSetDir: procedure expose (Globals)
  if symbol('gLogDir') \== 'VAR' then do
    /* Try gLogDir %LOGFILES gTmpDir %TMP */
    do 1
      /* Try %LOGFILES */
      gLogDir = value('LOGFILES',, gEnv)
      if gLogDir \== '' then leave
      /* Try gTmpDir */
      if symbol('gTmpDir') == 'VAR' then do
gLogDir = gTmpDir
leave
      end
      /* Try %TMP - return empty string if TMP not defined */
      gLogDir = value('TMP',, gEnv)
    end
  end
  return

/* end LogSetDir */

/*=== LogWriteVTS(message,...) Write multi-line message to STDOUT and timestamped message to log file ===*/

LogWriteVTS: procedure expose (Globals)
  /* Requires LogOpen */
  /* Requires MakeTimestamp */
  if symbol('gLogFile') \== 'VAR' then
    call LogOpen
  do i = 1 to arg()
    say arg(i)
    call lineout gLogFile, MakeTimestamp() arg(i)
    if symbol('gLogWrites') == 'VAR' then
      gLogWrites = gLogWrites + 1
  end
  return

/* end LogWriteVTS */

/*=== LogWriteVTSC(message,...) Write multi-line message to STDOUT and timestamped message to log file and close log ===*/

LogWriteVTSC: procedure expose (Globals)
  /* Requires LogOpen */
  /* Requires MakeTimestamp */
  if symbol('gLogFile') \== 'VAR' then
    call LogOpen
  ts = MakeTimestamp()
  do i = 1 to arg()
    say ts arg(i)
    call lineout gLogFile, ts arg(i)
    if symbol('gLogWrites') == 'VAR' then
      gLogWrites = gLogWrites + 1
  end
  call stream gLogFile, 'C', 'CLOSE'
  return

/* end LogWriteVTSC */

/*=== MakePath(pathparts,...) Make path name from parts ===*/

MakePath: procedure

  /* All parts optional - code guesses what caller means.
     If last arg begins with a dot and is not .. and does not
     contain a slash, it is assumed to be a file extension.
     To avoid this behavior, pass empty arg as last arg.
     Empty args are ignored.
     Automatically converts unix slashes to dos slashes.
     If 1st arg is drive letter, it must have trailing colon.
   */

  argCnt = arg()

  path = ''

  do argNum = 1 to argCnt
    s = arg(argNum)
    s = translate(s, '\', '/') /* Ensure DOS */
    if s == '' & argNum = argCnt then
      iterate /* Ignore nul last arg */
    if argNum = 1 then
      path = s
    else do
      lead = left(s, 1)
      tail = right(path, 1)
      if tail == ':' & argNum = 2 then
path = path || s /* Append path part to drive spec */
      else if lead == '.' & argNum = argCnt & s \== '..' & pos('\', s) = 0 then
path = path || s /* Assume extension unless .. or contains \ */
      else if tail == '\' & lead == '\' then
path = path || substr(s, 2) /* Drop extra backslash */
      else if path \== '' & tail \== '\' & lead \== '\' then
path = path || '\' || s /* Ensure have backslash */
      else
path = path || s
    end
  end /* for */

  return path

/* end MakePath */

/*=== MakeTimestamp() Convert current date/time to sorted, delimited timestamp - yyyy/mm/dd-hh:mm:ss ===*/

MakeTimestamp: procedure
  /* Return yyyy/mm/dd-hh:mm:ss */
  return translate('ABCD/EF/GH',date('S'),'ABCDEFGH')'-'time()

/* end MakeTimestamp */

/*=== Plural(cnt, units, suffix, suffix1, no) Return formatted cnt and units ===*/

/**
 * Designed for strings that will display in interior of line so 0 cnt displays as "no"
 * unless overridden
 * @param cnt is non-negative quantity
 * @param units is unit of measure for quantity 1
 * @param suffix is optional suffix for quantities other than 1, defaults to es or s
 * @param suffix1 is optional suffix for quantity 1, defaults to empty string
 * @param no is optional override for 0 cnt display, defaults to no
 * @notes tries to guess right suffix if suffix omitted
 */

Plural: procedure

  parse arg cnt, units, suffix, suffix1, no

  if cnt = 1 then do
    if suffix1 \== '' then
      units = units || suffix1
  end
  else do
    /* Not 1 */
    select
    when suffix \== '' then
      units = units || suffix
    when right(units, 1) == 's' then
      units = units || 'es'
    when right(units, 2) == 'ch' then
      units = units || 'es'
    otherwise
      units = units || 's'
    end
  end

  if cnt = 0 then do
    if no == '' then
      no = 'no' /* Assume count not at start of line */
    s = no units
  end
  else
    s = cnt units

  return s

/* end Plural */

/*=== RunExtCmdNoDie(cmd, die) Run external command that sets RC and return RC ===*/

RunExtCmdNoDie: procedure expose (Globals) SIGL

  /* Requires GetEnv */

  /* Return error code on error unless overridden */
  parse arg cmd, die

  if cmd = '' then
    call Die 'Required cmd omitted at' SIGL

  if pos('echo off', cmd) = 0 then do
    say
    '@echo on'
  end
  signal off Error
  cmd
  signal on Error
  err = RC
  '@echo off'

  /* if piped cmd of form ( extcmd %+ set E=%_? | rxqueue ) */
  if err = 0 & (pos('E=%?', cmd) > 0 | pos('E=%_?', cmd) > 0 )then
    err = GetEnv('E') /* Retrieve status */

  if err \= 0 then do
    /* Return error code unless die requested */
    die = die = 1 | die == 'die' & die \== ''
    if die then
      call Die '', cmd 'failed with error' err
  end

  return err

  return

/* end RunExtCmdNoDie */

/*==========================================================================*/
/*=== SkelRexx standards - Delete unused - Move modified above this mark ===*/
/*==========================================================================*/

/*=== Error() Set gErrCondition; report to STDOUT; trace and exit or return if called ===*/

Error:
  say
  parse source . . cmd
  gErrCondition = condition('C')
  say gErrCondition 'signaled at line' SIGL 'of' cmd || '.'
  if condition('D') \== '' then
    say 'REXX reason =' condition('D') || '.'
  if gErrCondition == 'SYNTAX' & symbol('RC') == 'VAR' then
    say 'REXX error =' RC '-' errortext(RC) || '.'
  else if symbol('RC') == 'VAR' then
    say 'RC =' RC || '.'
  say 'Source =' sourceline(SIGL)

  if condition('I') \== 'CALL' | gErrCondition == 'NOVALUE' | gErrCondition == 'SYNTAX' then do
    trace '?A'
    say 'Enter REXX commands to debug failure.  Press enter to exit script.'
    nop
    if symbol('RC') \== 'VAR' then
      RC = 255
    exit RC
  end

  return

/* end Error */

/*=== Halt() Report HALT condition to STDOUT and exit ===*/

Halt:
  say
  parse source . . cmd
  say condition('C') 'signaled at' cmd 'line' SIGL || '.'
  say 'Source =' sourceline(SIGL)
  say 'Sleeping for 2 seconds...'
  call SysSleep 2
  exit 253

/* end Halt */

/*=== LoadRexxUtil() Load RexxUtil functions ===*/

LoadRexxUtil:
  if RxFuncQuery('SysLoadFuncs') then do
    call RxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
    if RESULT then
      call Die 'Cannot load SysLoadFuncs.'
    call SysLoadFuncs
  end
  return

/* end LoadRexxUtil */

/*=== ScanArgs(cmdLine) Scan command line ===*/

ScanArgs: procedure expose (Globals)

  /* Calls user exits to process arguments and switches */

  parse arg cmdTail
  cmdTail = strip(cmdTail)

  call ScanArgsInit

  /* Ensure optional settings initialized */
  if symbol('SWCTL') \== 'VAR' then
    swCtl = '' /* Switches that take args, append ? if optional */
  if symbol('KEEPQUOTED') \== 'VAR' then
    keepQuoted = 0 /* Set to 1 to keep arguments quoted */

  /* Scan */
  curArg = '' /* Current arg string */
  curSwList = '' /* Current switch list */
  /* curSwArg = '' */ /* Current switch argument, if needed */
  noMoreSw = 0 /* End of switches */

  do while cmdTail \== '' | curArg \== '' | curSwList \== ''

    /* If arg buffer empty, refill */
    if curArg == '' then do
      qChar = left(cmdTail, 1) /* Remember quote */
      if \ verify(qChar,'''"', 'M') then
parse var cmdTail curArg cmdTail /* Not quoted */
      else do
/* Arg is quoted */
curArg = ''
do forever
 /* Parse dropping quotes */
 parse var cmdTail (qChar)quotedPart(qChar) cmdTail
 curArg = curArg || quotedPart
 /* Check for escaped quote within quoted string (i.e. "" or '') */
 if left(cmdTail, 1) \== qChar then do
   cmdTail = strip(cmdTail) /* Strip leading whitespace */
   leave /* Done with this quoted arg */
 end
 curArg = curArg || qChar /* Append quote */
 if keepQuoted then
   curArg = curArg || qChar /* Append escaped quote */
 parse var cmdTail (qChar) cmdTail /* Strip quote */
end /* do forever */
if keepQuoted then
 curArg = qChar || curArg || qChar /* requote */
      end /* if quoted */
    end /* if curArg empty */

    /* If switch buffer empty, refill */
    if curSwList == '' & \ noMoreSw then do
      if left(curArg, 1) == '-' & curArg \== '-' then do
if curArg == '--' then
 noMoreSw = 1
else
 curSwList = substr(curArg, 2) /* Remember switch string */
curArg = '' /* Mark empty */
iterate /* Refill arg buffer */
      end /* if switch */
    end /* if curSwList empty */

    /* If switch in progress */
    if curSwList \== '' then do
      curSw = left(curSwList, 1) /* Next switch */
      curSwList = substr(curSwList, 2) /* Drop from pending */
      /* Check switch allows argument, avoid matching ? */
      if pos(curSw, translate(swCtl,,'?')) \= 0 then do
if curSwList \== '' then do
 curSwArg = curSwList /* Use rest of switch string for switch argument */
 curSwList = ''
end
else if curArg \== '' & left(curArg, 1) \== '-' then do
 curSwArg = curArg /* Arg string is switch argument */
 curArg = '' /* Mark arg string empty */
end
else if pos(curSw'?', swCtl) = 0 then
 call ScanArgsUsage 'Switch "-' || curSw || '" requires an argument'
else
 curSwArg = '' /* Optional arg omitted */
      end

      call ScanArgsSwitch /* Passing curSw and curSwArg */
      drop curSwArg /* Must be used by now */
    end /* if switch */

    /* If arg */
    else if curArg \== '' then do
      noMoreSw = 1
      call ScanArgsArg /* Passing curArg */
      curArg = ''
    end

  end /* while not done */

  call ScanArgsTerm

  return

/* end ScanArgs */

/*=== SetCmdName() Set gCmdName to short script name ===*/

SetCmdName: procedure expose (Globals)
  parse source . . cmd
  cmd = filespec('N', cmd) /* Chop path */
  c = lastpos('.', cmd)
  if c > 1 then
    cmd = left(cmd, c - 1) /* Chop extension */
  gCmdName = translate(cmd, xrange('a', 'z'), xrange('A', 'Z')) /* Lowercase */
  return

/* end SetCmdName */

/*=== SetTmpDir() Set gTmpDir to %TMP with trailing backslash or empty string ===*/

SetTmpDir: procedure expose (Globals)
  s = value('TMP',,gEnv)
  if s \== '' & right(s, 1) \== ':' & right(s, 1) \== '\' then
    s = s'\' /* Stuff backslash */
  gTmpDir = s
  return

/* end SetTmpDir */

/* eof */

Prenumerera: Sändning, Uppsamling, Index.
Stoppa prenumeration
Meddelande till ListMaster