Example #1
0
void
privsep_exec_set_args(FILE* fp, ArgList& args)
{
    int num_args = args.Count();
    for (int i = 0; i < num_args; i++) {
        fprintf(fp, "exec-arg<%lu>\n", (unsigned long)strlen(args.GetArg(i)));
        fprintf(fp, "%s\n", args.GetArg(i));
    }
}
Example #2
0
/** Parse the arguments line of an existing .condor.sub file, extracing
    the arguments we want to preserve when updating the .condor.sub file.
	@param subLine: the arguments line from the .condor.sub file
	@param shallowOpts: the condor_submit_dag shallow options
	@return 0 if successful, 1 if failed
*/
int
parseArgumentsLine( const MyString &subLine,
			SubmitDagShallowOptions &shallowOpts )
{
	const char *line = subLine.Value();
	const char *start = strchr( line, '"' );
	const char *end = strrchr( line, '"' );

	MyString arguments;
	if ( start && end ) {
		arguments = subLine.Substr( start - line, end - line );
	} else {
		fprintf( stderr, "Missing quotes in arguments line: <%s>\n",
					subLine.Value() );
		return 1;
	}

	ArgList arglist;
	MyString error;
	if ( !arglist.AppendArgsV2Quoted( arguments.Value(),
				&error ) ) {
		fprintf( stderr, "Error parsing arguments: %s\n", error.Value() );
		return 1;
	}

	for ( int argNum = 0; argNum < arglist.Count(); argNum++ ) {
		MyString strArg = arglist.GetArg( argNum );
		strArg.lower_case();
		(void)parsePreservedArgs( strArg, argNum, arglist.Count(),
					arglist.GetStringArray(), shallowOpts);
	}

	return 0;
}
int
GLExecPrivSepHelper::run_script(ArgList& args,MyString &error_desc)
{
	if (!proxy_valid_right_now()) {
		dprintf(D_ALWAYS, "GLExecPrivSepHelper::run_script: not invoking glexec since the proxy is not valid!\n");
		error_desc += "The job proxy is not valid.";
		return INVALID_PROXY_RC;
	}

		/* Note that set_user_priv is a no-op if condor is running as
		   non-root (the "usual" mode for invoking glexec) */
	priv_state priv_saved = set_user_priv();
	FILE* fp = my_popen(args, "r", TRUE);
	set_priv(priv_saved);
	if (fp == NULL) {
		dprintf(D_ALWAYS,
		        "GLExecPrivSepHelper::run_script: "
		            "my_popen failure on %s: errno=%d (%s)\n",
		        args.GetArg(0),
			errno,
			strerror(errno));
		return -1;
	}
	MyString str;
	while (str.readLine(fp, true));

	priv_saved = set_user_priv();
	int ret = my_pclose(fp);
	set_priv(priv_saved);

	if (ret != 0) {
		str.trim();
		dprintf(D_ALWAYS,
		        "GLExecPrivSepHelper::run_script: %s exited "
		            "with status %d and following output:\n%s\n",
		        args.GetArg(0),
		        ret,
		        str.Value());
		error_desc.formatstr_cat("%s exited with status %d and the following output: %s",
				       condor_basename(args.GetArg(0)),
				       ret,
				       str.Value());
		error_desc.replaceString("\n","; ");
	}
	return ret;
}
Example #4
0
void GF_Palette::Assign( const ArgList& inArgs ) {
	UtilStr str;

	// Mix up the rnd seed
	srand( clock() );

	// Compile and link the temp exprs.  By spec, A-vars are evaluated now
	mAVars.Compile( inArgs, 'A', mDict );
	mAVars.Evaluate();		
	
	inArgs.GetArg( 'H', str );		mH.Compile( str, mDict );
	inArgs.GetArg( 'S', str );		mS.Compile( str, mDict );
	inArgs.GetArg( 'V', str );		mV.Compile( str, mDict );
	
	mH_I_Dep = mH.IsDependent( "I" );
	mS_I_Dep = mS.IsDependent( "I" );
	mV_I_Dep = mV.IsDependent( "I" );

}
Example #5
0
void ExprArray::Compile( const ArgList& inArgs, long inID, ExpressionDict& ioDict ) {
	UtilStr str;
	unsigned long i;

	// Determine the name of this expression array
	i = inID;
	mIDStr.Wipe();
	while ( i > 0 ) {
		mIDStr.Prepend( (char) ( i & 0xFF ) );
		i = i >> 8;
	}

	// Maintain memory heap for arbitrary array size...
	mNumExprs = inArgs.GetArraySize( inID );
	if ( mNumExprs > mDimNumExprs ) {

		if ( mVals )
			delete []mVals;

		if ( mExprs )
			delete []mExprs;

		mVals	= new float[ mNumExprs + 1 ];
		mExprs	= new Expression[ mNumExprs + 1 ];
		mDimNumExprs = mNumExprs;
	}

	// Add/Insert the vars to the dict
	for ( int i = 0; i < mNumExprs; i++ ) {
		str.Assign( mIDStr );
		str.Append( (long) i );
		mVals[ i ] = 0;
		ioDict.AddVar( str, &mVals[ i ] );
	}

	// Compile each expression array element
	for ( int i = 0; i < mNumExprs; i++ ) {
		inArgs.GetArg( inID, str, i );
		mExprs[ i ].Compile( str, ioDict );
	}
}
int
GLExecPrivSepHelper::create_process(const char* path,
                                    ArgList&    args,
                                    Env&        env,
                                    const char* iwd,
                                    int         job_std_fds[3],
                                    const char* std_file_names[3],
                                    int         nice_inc,
                                    size_t*     core_size_ptr,
                                    int         reaper_id,
                                    int         dc_job_opts,
                                    FamilyInfo* family_info,
                                    int *,
									MyString *error_msg)
{
	ASSERT(m_initialized);

	if (!proxy_valid_right_now()) {
		dprintf(D_ALWAYS, "GLExecPrivSepHelper::create_process: not invoking glexec since the proxy is not valid!\n");
		if( error_msg ) {
			error_msg->formatstr_cat("The job proxy is invalid.");
		}
		return -1;
	}

	// make a copy of std FDs so we're not messing w/ our caller's
	// memory
	int std_fds[3] = {-1, -1, -1};

	ArgList modified_args;
	modified_args.AppendArg(m_run_script);
	modified_args.AppendArg(m_glexec);
	modified_args.AppendArg(m_proxy);
	modified_args.AppendArg(m_sandbox);
	modified_args.AppendArg(m_wrapper_script);
	for (int i = 0; i < 3; i++) {
		modified_args.AppendArg((job_std_fds == NULL || job_std_fds[i] == -1) ?
		                            std_file_names[i] : "-");
	}
	modified_args.AppendArg(path);
	for (int i = 1; i < args.Count(); i++) {
		modified_args.AppendArg(args.GetArg(i));
	}

	int glexec_errors = 0;
	while(1) {
		// setup a UNIX domain socket for communicating with
		// condor_glexec_wrapper (see comment above feed_wrapper()
		// for details
		//
		int sock_fds[2];
		if (socketpair(PF_UNIX, SOCK_STREAM, 0, sock_fds) == -1)
		{
			dprintf(D_ALWAYS,
			        "GLEXEC: socketpair error: %s\n",
			        strerror(errno));
			return false;
		}
		std_fds[0] = sock_fds[1];

			// now create a pipe for receiving diagnostic stdout/stderr from glexec
		int glexec_out_fds[2];
		if (pipe(glexec_out_fds) < 0) {
			dprintf(D_ALWAYS,
					"GLEXEC: pipe() error: %s\n",
					strerror(errno));
			close(sock_fds[0]);
			close(sock_fds[1]);
			return false;
		}
		std_fds[1] = glexec_out_fds[1];
		std_fds[2] = std_fds[1]; // collect glexec stderr and stdout together

		FamilyInfo fi;
		FamilyInfo* fi_ptr = (family_info != NULL) ? family_info : &fi;
		MyString proxy_path;
		proxy_path.formatstr("%s.condor/%s", m_sandbox, m_proxy);
		fi_ptr->glexec_proxy = proxy_path.Value();

			// At the very least, we need to pass the condor daemon's
			// X509_USER_PROXY to condor_glexec_run.  Currently, we just
			// pass all daemon environment.  We do _not_ run
			// condor_glexec_run in the job environment, because that
			// would be a security risk and would serve no purpose, since
			// glexec cleanses the environment anyway.
		dc_job_opts &= ~(DCJOBOPT_NO_ENV_INHERIT);

		int pid = daemonCore->Create_Process(m_run_script.Value(),
		                                     modified_args,
		                                     PRIV_USER_FINAL,
		                                     reaper_id,
		                                     FALSE,
		                                     FALSE,
		                                     NULL,
		                                     iwd,
		                                     fi_ptr,
		                                     NULL,
		                                     std_fds,
		                                     NULL,
		                                     nice_inc,
		                                     NULL,
		                                     dc_job_opts,
		                                     core_size_ptr,
											 NULL,
											 NULL,
											 error_msg);

			// close our handle to glexec's end of the diagnostic output pipe
		close(glexec_out_fds[1]);

		MyString glexec_error_msg;
		int glexec_rc = 0;
		int ret_val = feed_wrapper(pid, sock_fds, env, dc_job_opts, job_std_fds, glexec_out_fds[0], &glexec_error_msg, &glexec_rc);

			// if not closed in feed_wrapper, close the glexec error pipe now
		if( glexec_out_fds[0] != -1 ) {
			close(glexec_out_fds[0]);
		}

			// Unlike the other glexec operations where we handle
			// glexec retry inside the helper script,
			// condor_glexec_run cannot handle retry for us, because
			// it must exec glexec rather than spawning it off in a
			// new process.  Therefore, we handle here retries in case
			// of transient errors.

		if( ret_val != 0 ) {
			return ret_val; // success
		}
		bool retry = true;
		if( glexec_rc != 202 && glexec_rc != 203 ) {
				// Either a non-transient glexec error, or some other
				// non-glexec error.
			retry = false;
		}
		else {
			// This _could_ be a transient glexec issue, such as a
			// communication error with GUMS, so retry up to some
			// limit.
			glexec_errors += 1;
			if( glexec_errors > m_glexec_retries ) {
				retry = false;
			}
		}

		if( !retry ) {
				// return the most recent glexec error output
			if( error_msg ) {
				error_msg->formatstr_cat(glexec_error_msg.Value());
			}
			return 0;
		}
			// truncated exponential backoff
		int delay_rand = 1 + (get_random_int() % glexec_errors) % 100;
		int delay = m_glexec_retry_delay * delay_rand;
		dprintf(D_ALWAYS,"Glexec exited with status %d; retrying in %d seconds.\n",
				glexec_rc, delay );
		sleep(delay);
			// now try again ...
	}

		// should never get here
	return 0;
}
Example #7
0
//
// Because we fork before calling docker, we don't actually
// care if the image is stored locally or not (except to the extent that
// remote image pull violates the principle of least astonishment).
//
int DockerAPI::run(
	ClassAd &machineAd,
	ClassAd &jobAd,
	const std::string & containerName,
	const std::string & imageID,
	const std::string & command,
	const ArgList & args,
	const Env & env,
	const std::string & sandboxPath,
	const std::list<std::string> extraVolumes,
	int & pid,
	int * childFDs,
	CondorError & /* err */ )
{
	gc_image(imageID);
	//
	// We currently assume that the system has been configured so that
	// anyone (user) who can run an HTCondor job can also run docker.  It's
	// also apparently a security worry to run Docker as root, so let's not.
	//
	ArgList runArgs;
	if ( ! add_docker_arg(runArgs))
		return -1;
	runArgs.AppendArg( "run" );

	// Write out a file with the container ID.
	// FIXME: The startd can check this to clean up after us.
	// This needs to go into a directory that condor user
	// can write to.

/*
	std::string cidFileName = sandboxPath + "/.cidfile";
	runArgs.AppendArg( "--cidfile=" + cidFileName );
*/

	
	// Configure resource limits.
	
	// First cpus
	int  cpus;
	int cpuShare;

	if (machineAd.LookupInteger(ATTR_CPUS, cpus)) {
		cpuShare = 10 * cpus;
	} else {
		cpuShare = 10;
	}
	std::string cpuShareStr;
	formatstr(cpuShareStr, "--cpu-shares=%d", cpuShare);
	runArgs.AppendArg(cpuShareStr);

	// Now memory
	int memory; // in Megabytes
	if (machineAd.LookupInteger(ATTR_MEMORY, memory)) {
		std::string mem;
		formatstr(mem, "--memory=%dm", memory);
		runArgs.AppendArg(mem);
	} 

	// drop unneeded Linux capabilities
	if (param_boolean("DOCKER_DROP_ALL_CAPABILITIES", true /*default*/,
		true /*do_log*/, &machineAd, &jobAd)) {
		runArgs.AppendArg("--cap-drop=all");
			
		// --no-new-privileges flag appears in docker 1.11
		if (DockerAPI::majorVersion > 1 ||
		    DockerAPI::minorVersion > 10) {
			runArgs.AppendArg("--no-new-privileges");
		}
	}

	// Give the container a useful name
	std::string hname = makeHostname(&machineAd, &jobAd);
	runArgs.AppendArg("--hostname");
	runArgs.AppendArg(hname.c_str());

		// Now the container name
	runArgs.AppendArg( "--name" );
	runArgs.AppendArg( containerName );

	if ( ! add_env_to_args_for_docker(runArgs, env)) {
		dprintf( D_ALWAYS | D_FAILURE, "Failed to pass enviroment to docker.\n" );
		return -8;
	}

	// Map the external sanbox to the internal sandbox.
	runArgs.AppendArg( "--volume" );
	runArgs.AppendArg( sandboxPath + ":" + sandboxPath );

	// Now any extra volumes
	for (std::list<std::string>::const_iterator it = extraVolumes.begin(); it != extraVolumes.end(); it++) {
		runArgs.AppendArg("--volume");
		std::string volume = *it;
		runArgs.AppendArg(volume);
	}
	
	// Start in the sandbox.
	runArgs.AppendArg( "--workdir" );
	runArgs.AppendArg( sandboxPath );

	// Run with the uid that condor selects for the user
	// either a slot user or submitting user or nobody
	uid_t uid = 0;
	uid_t gid = 0;

	// Docker doesn't actually run on Windows, but we compile
	// on Windows because...
#ifndef WIN32
	uid = get_user_uid();
	gid = get_user_gid();
#endif
	
	if ((uid == 0) || (gid == 0)) {
		dprintf(D_ALWAYS|D_FAILURE, "Failed to get userid to run docker job\n");
		return -9;
	}

	runArgs.AppendArg("--user");
	std::string uidgidarg;
	formatstr(uidgidarg, "%d:%d", uid, gid);
	runArgs.AppendArg(uidgidarg);

	// Run the command with its arguments in the image.
	runArgs.AppendArg( imageID );

	
	// If no command given, the default command in the image will run
	if (command.length() > 0) {
		runArgs.AppendArg( command );
	}

	runArgs.AppendArgsFromArgList( args );

	MyString displayString;
	runArgs.GetArgsStringForLogging( & displayString );
	dprintf( D_ALWAYS, "Attempting to run: %s\n", displayString.c_str() );

	//
	// If we run Docker attached, we avoid a race condition where
	// 'docker logs --follow' returns before 'docker rm' knows that the
	// container is gone (and refuses to remove it).  Of course, we
	// can't block, so we have a proxy process run attached for us.
	//
	FamilyInfo fi;
	fi.max_snapshot_interval = param_integer( "PID_SNAPSHOT_INTERVAL", 15 );
	int childPID = daemonCore->Create_Process( runArgs.GetArg(0), runArgs,
		PRIV_CONDOR_FINAL, 1, FALSE, FALSE, NULL, "/",
		& fi, NULL, childFDs );

	if( childPID == FALSE ) {
		dprintf( D_ALWAYS | D_FAILURE, "Create_Process() failed.\n" );
		return -1;
	}
	pid = childPID;

	return 0;
}
int
GLExecPrivSepHelper::create_process(const char* path,
                                    ArgList&    args,
                                    Env&        env,
                                    const char* iwd,
                                    int         job_std_fds[3],
                                    const char* std_file_names[3],
                                    int         nice_inc,
                                    size_t*     core_size_ptr,
                                    int         reaper_id,
                                    int         dc_job_opts,
                                    FamilyInfo* family_info,
                                    int *,
									MyString *error_msg)
{
	ASSERT(m_initialized);

	if (!proxy_valid_right_now()) {
		dprintf(D_ALWAYS, "GLExecPrivSepHelper::create_process: not invoking glexec since the proxy is not valid!\n");
		return -1;
	}

	// make a copy of std FDs so we're not messing w/ our caller's
	// memory
	int std_fds[3] = {-1, -1, -1};

	ArgList modified_args;
	modified_args.AppendArg(m_run_script);
	modified_args.AppendArg(m_glexec);
	modified_args.AppendArg(m_proxy);
	modified_args.AppendArg(m_sandbox);
	modified_args.AppendArg(m_wrapper_script);
	for (int i = 0; i < 3; i++) {
		modified_args.AppendArg((job_std_fds == NULL || job_std_fds[i] == -1) ?
		                            std_file_names[i] : "-");
	}
	modified_args.AppendArg(path);
	for (int i = 1; i < args.Count(); i++) {
		modified_args.AppendArg(args.GetArg(i));
	}

	// setup a UNIX domain socket for communicating with
	// condor_glexec_wrapper (see comment above feed_wrapper()
	// for details
	//
	int sock_fds[2];
	if (socketpair(PF_UNIX, SOCK_STREAM, 0, sock_fds) == -1)
	{
		dprintf(D_ALWAYS,
		        "GLEXEC: socketpair error: %s\n",
		        strerror(errno));
		return false;
	}
	std_fds[0] = sock_fds[1];

		// now create a pipe for receiving diagnostic stdout/stderr from glexec
	int glexec_out_fds[2];
	if (pipe(glexec_out_fds) < 0) {
		dprintf(D_ALWAYS,
				"GLEXEC: pipe() error: %s\n",
				strerror(errno));
		close(sock_fds[0]);
		close(sock_fds[1]);
		return false;
	}
	std_fds[1] = glexec_out_fds[1];
	std_fds[2] = std_fds[1]; // collect glexec stderr and stdout together

	FamilyInfo fi;
	FamilyInfo* fi_ptr = (family_info != NULL) ? family_info : &fi;
	MyString proxy_path;
	proxy_path.formatstr("%s.condor/%s", m_sandbox, m_proxy);
	fi_ptr->glexec_proxy = proxy_path.Value();

		// At the very least, we need to pass the condor daemon's
		// X509_USER_PROXY to condor_glexec_run.  Currently, we just
		// pass all daemon environment.  We do _not_ run
		// condor_glexec_run in the job environment, because that
		// would be a security risk and would serve no purpose, since
		// glexec cleanses the environment anyway.
	dc_job_opts &= ~(DCJOBOPT_NO_ENV_INHERIT);

	int pid = daemonCore->Create_Process(m_run_script.Value(),
	                                     modified_args,
	                                     PRIV_USER_FINAL,
	                                     reaper_id,
	                                     FALSE,
	                                     NULL,
	                                     iwd,
	                                     fi_ptr,
	                                     NULL,
	                                     std_fds,
	                                     NULL,
	                                     nice_inc,
	                                     NULL,
	                                     dc_job_opts,
	                                     core_size_ptr,
										 NULL,
										 NULL,
										 error_msg);

		// close our handle to glexec's end of the diagnostic output pipe
	close(glexec_out_fds[1]);

	int ret_val = feed_wrapper(pid, sock_fds, env, dc_job_opts, job_std_fds, glexec_out_fds[0], error_msg);

		// if not closed in feed_wrapper, close the glexec error pipe now
	if( glexec_out_fds[0] != -1 ) {
		close(glexec_out_fds[0]);
	}

	return ret_val;
}
Example #9
0
std::string *NordugridJob::buildSubmitRSL()
{
	int transfer_exec = TRUE;
	std::string *rsl = new std::string;
	StringList *stage_list = NULL;
	StringList *stage_local_list = NULL;
	char *attr_value = NULL;
	std::string rsl_suffix;
	std::string iwd;
	std::string executable;

	if ( jobAd->LookupString( ATTR_NORDUGRID_RSL, rsl_suffix ) &&
						   rsl_suffix[0] == '&' ) {
		*rsl = rsl_suffix;
		return rsl;
	}

	if ( jobAd->LookupString( ATTR_JOB_IWD, iwd ) != 1 ) {
		errorString = "ATTR_JOB_IWD not defined";
		delete rsl;
		return NULL;
	}

	//Start off the RSL
	attr_value = param( "FULL_HOSTNAME" );
	formatstr( *rsl, "&(savestate=yes)(action=request)(hostname=%s)", attr_value );
	free( attr_value );
	attr_value = NULL;

	//We're assuming all job clasads have a command attribute
	jobAd->LookupString( ATTR_JOB_CMD, executable );
	jobAd->LookupBool( ATTR_TRANSFER_EXECUTABLE, transfer_exec );

	*rsl += "(executable=";
	// If we're transferring the executable, strip off the path for the
	// remote machine, since it refers to the submit machine.
	if ( transfer_exec ) {
		*rsl += condor_basename( executable.c_str() );
	} else {
		*rsl += executable;
	}

	{
		ArgList args;
		MyString arg_errors;
		MyString rsl_args;
		if(!args.AppendArgsFromClassAd(jobAd,&arg_errors)) {
			dprintf(D_ALWAYS,"(%d.%d) Failed to read job arguments: %s\n",
					procID.cluster, procID.proc, arg_errors.Value());
			formatstr(errorString,"Failed to read job arguments: %s\n",
					arg_errors.Value());
			delete rsl;
			return NULL;
		}
		if(args.Count() != 0) {
			if(args.InputWasV1()) {
					// In V1 syntax, the user's input _is_ RSL
				if(!args.GetArgsStringV1Raw(&rsl_args,&arg_errors)) {
					dprintf(D_ALWAYS,
							"(%d.%d) Failed to get job arguments: %s\n",
							procID.cluster,procID.proc,arg_errors.Value());
					formatstr(errorString,"Failed to get job arguments: %s\n",
							arg_errors.Value());
					delete rsl;
					return NULL;
				}
			}
			else {
					// In V2 syntax, we convert the ArgList to RSL
				for(int i=0;i<args.Count();i++) {
					if(i) {
						rsl_args += ' ';
					}
					rsl_args += rsl_stringify(args.GetArg(i));
				}
			}
			*rsl += ")(arguments=";
			*rsl += rsl_args;
		}
	}

	// If we're transferring the executable, tell Nordugrid to set the
	// execute bit on the transferred executable.
	if ( transfer_exec ) {
		*rsl += ")(executables=";
		*rsl += condor_basename( executable.c_str() );
	}

	if ( jobAd->LookupString( ATTR_JOB_INPUT, &attr_value ) == 1) {
		// only add to list if not NULL_FILE (i.e. /dev/null)
		if ( ! nullFile(attr_value) ) {
			*rsl += ")(stdin=";
			*rsl += condor_basename(attr_value);
		}
		free( attr_value );
		attr_value = NULL;
	}

	stage_list = buildStageInList();

	if ( stage_list->isEmpty() == false ) {
		char *file;
		stage_list->rewind();

		*rsl += ")(inputfiles=";

		while ( (file = stage_list->next()) != NULL ) {
			*rsl += "(";
			*rsl += condor_basename(file);
			if ( IsUrl( file ) ) {
				formatstr_cat( *rsl, " \"%s\")", file );
			} else {
				*rsl += " \"\")";
			}
		}
	}

	delete stage_list;
	stage_list = NULL;

	if ( jobAd->LookupString( ATTR_JOB_OUTPUT, &attr_value ) == 1) {
		// only add to list if not NULL_FILE (i.e. /dev/null)
		if ( ! nullFile(attr_value) ) {
			*rsl += ")(stdout=" REMOTE_STDOUT_NAME;
		}
		free( attr_value );
		attr_value = NULL;
	}

	if ( jobAd->LookupString( ATTR_JOB_ERROR, &attr_value ) == 1) {
		// only add to list if not NULL_FILE (i.e. /dev/null)
		if ( ! nullFile(attr_value) ) {
			*rsl += ")(stderr=" REMOTE_STDERR_NAME;
		}
		free( attr_value );
	}

	stage_list = buildStageOutList();
	stage_local_list = buildStageOutLocalList( stage_list );

	if ( stage_list->isEmpty() == false ) {
		char *file;
		char *local_file;
		stage_list->rewind();
		stage_local_list->rewind();

		*rsl += ")(outputfiles=";

		while ( (file = stage_list->next()) != NULL ) {
			local_file = stage_local_list->next();
			*rsl += "(";
			*rsl += condor_basename(file);
			if ( IsUrl( local_file ) ) {
				formatstr_cat( *rsl, " \"%s\")", local_file );
			} else {
				*rsl += " \"\")";
			}
		}
	}

	delete stage_list;
	stage_list = NULL;
	delete stage_local_list;
	stage_local_list = NULL;

	*rsl += ')';

	if ( !rsl_suffix.empty() ) {
		*rsl += rsl_suffix;
	}

dprintf(D_FULLDEBUG,"*** RSL='%s'\n",rsl->c_str());
	return rsl;
}