From: "Steven Levine" Received: from [192.168.100.201] (HELO mail.2rosenthals.com) by 2rosenthals.com (CommuniGate Pro SMTP 5.4.10) with ESMTP id 11291240 for ecs-isp@2rosenthals.com; Mon, 02 Dec 2024 23:50:09 -0500 Received: from secmgr-va.randr ([192.168.200.201]:59547 helo=mail2.2rosenthals.com) by mail.2rosenthals.com with esmtp (Exim 4.97.1) (envelope-from ) id 1tIKrI-000000005GR-1GqN for ecs-isp@2rosenthals.com; Mon, 02 Dec 2024 23:50:00 -0500 Received: from mta-101a.earthlink-vadesecure.net ([51.81.61.60]:58697) by mail2.2rosenthals.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.97.1) (envelope-from ) id 1tIKr8-000000007RG-22F0 for ecs-isp@2rosenthals.com; Mon, 02 Dec 2024 23:49:51 -0500 DKIM-Signature: v=1; a=rsa-sha256; bh=1x3vB8NyPO1kGr0KzRM1wxV6N2WRvaRlifu3d2 JbbIM=; c=relaxed/relaxed; d=earthlink.net; h=from:reply-to:subject: date:to:cc:resent-date:resent-from:resent-to:resent-cc:in-reply-to: references:list-id:list-help:list-unsubscribe:list-unsubscribe-post: list-subscribe:list-post:list-owner:list-archive; q=dns/txt; s=dk12062016; t=1733201390; x=1733806190; b=k25JCfns2GqIgUT/FaKbIQU4kMA gVhzrMbuQ/++jCkM9OufMogncpWF3cw+8ouC4gyJjxJPxZ2XM6BE+PRyX+thF+/BK+StJFO jAi1bdM/J9+6McAZ7D6uxWfu/Z6iGlH0ugHF15Yv67rrz93TF+oT8RZVLKpCsUvBF55GrQi +DWXSwj0XtBag2QnLT3RLg03QlE22oU/ObGNXyOiiXfTWlkXWVafi7eIjcqc+h0jsUs07SB 7ubSJQsi0ALJNDSLy0gshE/0gFTq3DXeDAz7Q0nVS9oQ6WgTgfFBcPJbIdXSCcnXSUXM0eh 06JUmoUDSJDVT0C7KIdXtBHGR52Jd4g== Received: from slamain ([172.56.178.196]) by vsel1nmtao01p.internal.vadesecure.com with ngmta id 727e8d4d-180d917b31a92f91; Tue, 03 Dec 2024 04:49:50 +0000 Message-ID: <674e88b9.9.mr2ice.fgrirsq@earthlink.net> Date: Mon, 02 Dec 2024 20:27:37 -0800 To: "eCS ISP Mailing List" In-Reply-To: Subject: Re: [eCS-ISP] (uacme) certificate renew issue Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="===_674E8DEC_==" X-Mailer: MR/2 Internet Cruiser Edition for OS/2 v3.00.11.24/60 This is a MIME encapsulated message. --===_674E8DEC_== Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit In , on 12/02/24 at 08:59 PM, "Massimo S." 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" 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 */ --===_674E8DEC_==--