2004-09-08 10:09:39 -05:00
|
|
|
/* $RCSfile: runargv.c,v $
|
2006-04-20 06:19:12 -05:00
|
|
|
-- $Revision: 1.9 $
|
|
|
|
-- last change: $Author: hr $ $Date: 2006-04-20 12:19:12 $
|
2000-09-22 09:33:37 -05:00
|
|
|
--
|
|
|
|
-- SYNOPSIS
|
|
|
|
-- Invoke a sub process.
|
|
|
|
--
|
|
|
|
-- DESCRIPTION
|
|
|
|
-- Use the standard methods of executing a sub process.
|
|
|
|
--
|
|
|
|
-- AUTHOR
|
|
|
|
-- Dennis Vadura, dvadura@dmake.wticorp.com
|
|
|
|
--
|
|
|
|
-- WWW
|
|
|
|
-- http://dmake.wticorp.com/
|
|
|
|
--
|
|
|
|
-- COPYRIGHT
|
|
|
|
-- Copyright (c) 1996,1997 by WTI Corp. All rights reserved.
|
|
|
|
--
|
|
|
|
-- This program is NOT free software; you can redistribute it and/or
|
|
|
|
-- modify it under the terms of the Software License Agreement Provided
|
|
|
|
-- in the file <distribution-root>/readme/license.txt.
|
|
|
|
--
|
|
|
|
-- LOG
|
|
|
|
-- Use cvs log to obtain detailed change logs.
|
|
|
|
*/
|
2006-04-20 06:19:12 -05:00
|
|
|
/*
|
|
|
|
This file (runargv.c) provides all the parallel process handling routines
|
|
|
|
for dmake on unix like operating systems. The following text briefly
|
|
|
|
describes the process flow.
|
|
|
|
|
|
|
|
Exec_commands() [make.c] builds the recipes associated to the given target.
|
|
|
|
They are build sequentially in a loop that calls Do_cmnd() for each of them.
|
|
|
|
|
|
|
|
Do_cmnd() [sysintf.c] feeds the given command or command group to runargv().
|
|
|
|
|
|
|
|
runargv() [unix/runargv] The actual child processes are started in this
|
|
|
|
function, even in non parallel builds (MAXPROCESS==1) child processes are
|
|
|
|
created.
|
|
|
|
If recipes for a target are currently running attach them to the process
|
|
|
|
queue (_procs[i]) of that target and return.
|
|
|
|
If the maximum number of running process queues is reached
|
|
|
|
Wait_for_child(?, -1) is used to wait for one to be available.
|
|
|
|
New child processes are started using:
|
|
|
|
spawn: posix_spawnp (POSIX) or spawnvp (cygwin).
|
|
|
|
fork/execvp: Create a client process with fork and run the command with
|
|
|
|
execvp.
|
|
|
|
The parent calls _add_child() to track the child.
|
|
|
|
|
|
|
|
_add_child() [unix/runargv] creates a new process queue and enters the child
|
|
|
|
parameters.
|
|
|
|
If Wait_for_completion (global variable) is set the function calls
|
|
|
|
Wait_for_child to waits for the new process queue to be finished.
|
|
|
|
|
|
|
|
Wait_for_child(abort_flg, pid) [unix/runargv] waits for the child processes
|
|
|
|
with pid to finish. All finished processes are handled by calling
|
|
|
|
_finished_child() for each of them.
|
|
|
|
If pid == -1 wait for the next child process to finish.
|
|
|
|
If abort_flg is TRUE no further processes will be added to the process
|
|
|
|
queue.
|
|
|
|
If the global variable Wait_for_completion is set then all finished
|
|
|
|
processes are handled until the process with the given pid is reached.
|
|
|
|
|
|
|
|
_finished_child(pid, ?) [unix/runargv] removes the finished child from its
|
|
|
|
process queue. If there are more commands in this queue start the next
|
|
|
|
with runargv().
|
|
|
|
*/
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
#include <signal.h>
|
2006-04-20 06:19:12 -05:00
|
|
|
|
|
|
|
#include "extern.h"
|
|
|
|
|
2003-01-04 06:02:44 -06:00
|
|
|
#ifdef HAVE_WAIT_H
|
2003-03-25 07:05:43 -06:00
|
|
|
# include <wait.h>
|
|
|
|
#else
|
|
|
|
# ifdef HAVE_SYS_WAIT_H
|
|
|
|
# include <sys/wait.h>
|
|
|
|
# endif
|
2003-01-04 06:02:44 -06:00
|
|
|
#endif
|
2002-10-11 07:42:49 -05:00
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
#if HAVE_SPAWN_H && ENABLE_SPAWN
|
2002-10-11 07:42:49 -05:00
|
|
|
# include <spawn.h>
|
|
|
|
#endif
|
2006-04-20 06:19:12 -05:00
|
|
|
|
|
|
|
#if __CYGWIN__ && ENABLE_SPAWN
|
|
|
|
# include <process.h>
|
|
|
|
#endif
|
|
|
|
|
2000-09-22 09:33:37 -05:00
|
|
|
#include "sysintf.h"
|
2002-10-11 07:42:49 -05:00
|
|
|
#if HAVE_ERRNO_H
|
|
|
|
# include <errno.h>
|
|
|
|
#else
|
|
|
|
extern int errno;
|
2001-05-07 06:47:09 -05:00
|
|
|
#endif
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
typedef struct prp {
|
|
|
|
char *prp_cmd;
|
|
|
|
int prp_group;
|
|
|
|
int prp_ignore;
|
|
|
|
int prp_last;
|
|
|
|
int prp_shell;
|
|
|
|
struct prp *prp_next;
|
|
|
|
} RCP, *RCPPTR;
|
|
|
|
|
|
|
|
typedef struct pr {
|
|
|
|
int pr_valid;
|
|
|
|
int pr_pid;
|
|
|
|
CELLPTR pr_target;
|
|
|
|
int pr_ignore;
|
|
|
|
int pr_last;
|
|
|
|
RCPPTR pr_recipe;
|
|
|
|
RCPPTR pr_recipe_end;
|
|
|
|
char *pr_dir;
|
|
|
|
} PR;
|
|
|
|
|
|
|
|
static PR *_procs = NIL(PR);
|
|
|
|
static int _proc_cnt = 0;
|
|
|
|
static int _abort_flg= FALSE;
|
|
|
|
static int _use_i = -1;
|
|
|
|
|
|
|
|
static void _add_child ANSI((int, CELLPTR, int, int));
|
|
|
|
static void _attach_cmd ANSI((char *, int, int, CELLPTR, int, int));
|
|
|
|
static void _finished_child ANSI((int, int));
|
|
|
|
static int _running ANSI((CELLPTR));
|
|
|
|
|
2002-10-11 07:42:49 -05:00
|
|
|
#if ! HAVE_STRERROR
|
|
|
|
static char *
|
|
|
|
private_strerror (errnum)
|
|
|
|
int errnum;
|
|
|
|
{
|
|
|
|
#ifndef __APPLE__
|
|
|
|
#ifdef arm32
|
|
|
|
extern const char * const sys_errlist[];
|
|
|
|
#else
|
|
|
|
#if defined(linux) || defined(__FreeBSD__)
|
|
|
|
extern const char * const sys_errlist[];
|
|
|
|
#else
|
|
|
|
extern char *sys_errlist[];
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
extern int sys_nerr;
|
|
|
|
|
|
|
|
if (errnum > 0 && errnum <= sys_nerr)
|
|
|
|
return sys_errlist[errnum];
|
|
|
|
return "Unknown system error";
|
|
|
|
}
|
|
|
|
#define strerror private_strerror
|
|
|
|
#endif /* HAVE_STRERROR */
|
|
|
|
|
2000-09-22 09:33:37 -05:00
|
|
|
PUBLIC int
|
|
|
|
runargv(target, ignore, group, last, shell, cmd)
|
|
|
|
CELLPTR target;
|
|
|
|
int ignore;
|
|
|
|
int group;
|
|
|
|
int last;
|
|
|
|
int shell;
|
|
|
|
char *cmd;
|
|
|
|
{
|
|
|
|
int pid;
|
2006-04-20 06:19:12 -05:00
|
|
|
int st_pq = 0; /* Current _exec_shell target process queue */
|
2000-09-22 09:33:37 -05:00
|
|
|
char **argv;
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
#if ENABLE_SPAWN && ( HAVE_SPAWN_H || __CYGWIN__ )
|
|
|
|
int old_stdout;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* Special handling for the shell function macro is required. If the currend
|
|
|
|
* command is called as part of a shell escape in a recipe make sure that all
|
|
|
|
* previous recipe lines of this target have finished. */
|
|
|
|
if( Is_exec_shell ) {
|
|
|
|
if( (st_pq = _running(Shell_exec_target)) != -1 ) {
|
|
|
|
Wait_for_child(FALSE, _procs[st_pq].pr_pid);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if( _running(target) != -1 /*&& Max_proc != 1*/ ) {
|
|
|
|
/* The command will be executed when the previous recipe
|
|
|
|
* line completes. */
|
|
|
|
_attach_cmd( cmd, group, ignore, target, last, shell );
|
|
|
|
return(1);
|
|
|
|
}
|
2000-09-22 09:33:37 -05:00
|
|
|
}
|
|
|
|
|
2002-10-11 07:42:49 -05:00
|
|
|
/* Any Fatal call can potentially loop by recursion because we
|
|
|
|
* are called from the Quit routine that Fatal indirectly calls
|
|
|
|
* since Fatal should not happen I have left this bug in here */
|
2006-04-20 06:19:12 -05:00
|
|
|
while( _proc_cnt == Max_proc ) { /* This forces sequential execution for Max_proc == 1. */
|
2002-10-11 07:42:49 -05:00
|
|
|
if( Wait_for_child(FALSE, -1) == -1 ) {
|
|
|
|
if( ! in_quit() || errno != ECHILD )
|
|
|
|
Fatal( "Lost a child %d: %s", errno, strerror( errno ) );
|
|
|
|
else {/* we are quitting and the _proc_cnt was stuffed up by ^C */
|
|
|
|
fprintf(stderr,"_proc_cnt %d, Max_proc %d\n",_proc_cnt,Max_proc);
|
|
|
|
_proc_cnt = 0;
|
|
|
|
}
|
2001-05-29 16:43:32 -05:00
|
|
|
}
|
2002-10-11 07:42:49 -05:00
|
|
|
}
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
argv = Pack_argv( group, shell, cmd );
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
#if ENABLE_SPAWN && ( HAVE_SPAWN_H || __CYGWIN__ )
|
|
|
|
/* As no other childs are started while the output is redirected this
|
|
|
|
* is save. */
|
|
|
|
if( Is_exec_shell ) {
|
|
|
|
old_stdout = dup(1);
|
|
|
|
close(1);
|
|
|
|
dup( fileno(stdout_redir) );
|
|
|
|
}
|
|
|
|
#if __CYGWIN__
|
|
|
|
pid = spawnvp(_P_NOWAIT, argv[0], (const char**) argv);
|
|
|
|
#else /* __CYGWIN__ */
|
|
|
|
if (posix_spawnp (&pid, argv[0], NULL, NULL, argv, (char *)NULL))
|
|
|
|
pid = -1; /* posix_spawn failed */
|
|
|
|
#endif /* __CYGWIN__ */
|
|
|
|
if( Is_exec_shell ) {
|
|
|
|
close(1);
|
|
|
|
dup(old_stdout);
|
|
|
|
}
|
|
|
|
if(pid == -1)
|
|
|
|
{ /* spawn failed */
|
2002-10-11 07:42:49 -05:00
|
|
|
Error("%s: %s", argv[0], strerror(errno));
|
|
|
|
Handle_result(-1, ignore, _abort_flg, target);
|
|
|
|
return(-1);
|
|
|
|
} else {
|
|
|
|
_add_child(pid, target, ignore, last);
|
|
|
|
}
|
2006-04-20 06:19:12 -05:00
|
|
|
#else /* ENABLE_SPAWN && ... */
|
2002-10-11 07:42:49 -05:00
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
fflush(stdout);
|
2000-09-22 09:33:37 -05:00
|
|
|
switch( pid=fork() ){
|
|
|
|
|
|
|
|
case -1: /* fork failed */
|
2002-10-11 07:42:49 -05:00
|
|
|
Error("%s: %s", argv[0], strerror( errno ));
|
2000-09-22 09:33:37 -05:00
|
|
|
Handle_result(-1, ignore, _abort_flg, target);
|
|
|
|
return(-1);
|
|
|
|
|
|
|
|
case 0: /* child */
|
2006-04-20 06:19:12 -05:00
|
|
|
/* redirect stdout for _exec_shell */
|
|
|
|
if( Is_exec_shell ) {
|
|
|
|
/* org_out = dup(1); */
|
|
|
|
close(1);
|
|
|
|
dup( fileno(stdout_redir) );
|
|
|
|
}
|
2000-09-22 09:33:37 -05:00
|
|
|
execvp(argv[0], argv);
|
2006-04-20 06:19:12 -05:00
|
|
|
/* restoring stdout is not needed */
|
2000-09-22 09:33:37 -05:00
|
|
|
Continue = TRUE; /* survive error message */
|
2002-10-11 07:42:49 -05:00
|
|
|
Error("%s: %s", argv[0], strerror( errno ));
|
2000-09-22 09:33:37 -05:00
|
|
|
kill(getpid(), SIGTERM);
|
|
|
|
/*NOTREACHED*/
|
|
|
|
|
|
|
|
default: /* parent */
|
|
|
|
_add_child(pid, target, ignore, last);
|
|
|
|
}
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
#endif /* ENABLE_SPAWN && ... */
|
2002-10-11 07:42:49 -05:00
|
|
|
|
2000-09-22 09:33:37 -05:00
|
|
|
return(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PUBLIC int
|
2006-04-20 06:19:12 -05:00
|
|
|
Wait_for_child( abort_flg, pid )/*
|
|
|
|
==================================
|
|
|
|
Wait for the child processes with pid to to finish. All finished processes
|
|
|
|
are handled by calling _finished_child() for each of them.
|
|
|
|
If pid == -1 wait for the next child process to finish.
|
|
|
|
If abort_flg is TRUE no further processes will be added to the process
|
|
|
|
queue.
|
|
|
|
If the global variable Wait_for_completion is set then all finished
|
|
|
|
processes are handled until the process with the given pid is reached. */
|
2000-09-22 09:33:37 -05:00
|
|
|
int abort_flg;
|
|
|
|
int pid;
|
|
|
|
{
|
|
|
|
int wid;
|
|
|
|
int status;
|
|
|
|
int waitchild;
|
2006-04-20 06:19:12 -05:00
|
|
|
int is_exec_shell_status = Is_exec_shell;
|
|
|
|
|
|
|
|
/* It is impossible that processes that were started from _exec_shell
|
|
|
|
* have follow-up commands in its process queue. Unset Is_exec_shell
|
|
|
|
* to prevent piping of child processes that are started from the
|
|
|
|
* _finished_child subroutine and reset to its original value when
|
|
|
|
* leaving this function. */
|
|
|
|
Is_exec_shell = FALSE;
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
waitchild = (pid == -1)? FALSE : Wait_for_completion;
|
|
|
|
|
|
|
|
do {
|
2002-10-11 07:42:49 -05:00
|
|
|
wid = wait(&status);
|
2006-04-20 06:19:12 -05:00
|
|
|
|
|
|
|
if( wid == -1 ) {
|
|
|
|
Is_exec_shell = is_exec_shell_status;
|
|
|
|
return(-1);
|
|
|
|
}
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
_abort_flg = abort_flg;
|
|
|
|
_finished_child(wid, status);
|
|
|
|
_abort_flg = FALSE;
|
|
|
|
}
|
|
|
|
while( waitchild && pid != wid );
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
Is_exec_shell = is_exec_shell_status;
|
2000-09-22 09:33:37 -05:00
|
|
|
return(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PUBLIC void
|
|
|
|
Clean_up_processes()
|
|
|
|
{
|
|
|
|
register int i;
|
|
|
|
|
|
|
|
if( _procs != NIL(PR) ) {
|
|
|
|
for( i=0; i<Max_proc; i++ )
|
|
|
|
if( _procs[i].pr_valid )
|
|
|
|
kill(_procs[i].pr_pid, SIGTERM);
|
|
|
|
|
|
|
|
while( Wait_for_child(TRUE, -1) != -1 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
_add_child( pid, target, ignore, last )
|
|
|
|
int pid;
|
|
|
|
CELLPTR target;
|
|
|
|
int ignore;
|
|
|
|
int last;
|
|
|
|
{
|
|
|
|
register int i;
|
|
|
|
register PR *pp;
|
|
|
|
|
|
|
|
if( _procs == NIL(PR) ) {
|
|
|
|
TALLOC( _procs, Max_proc, PR );
|
|
|
|
}
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
if( Measure & M_RECIPE )
|
|
|
|
Do_profile_output( "s", M_RECIPE, target );
|
|
|
|
|
|
|
|
/* If _use_i!=-1 then this function is called by _finished_child() */
|
2000-09-22 09:33:37 -05:00
|
|
|
if( (i = _use_i) == -1 )
|
|
|
|
for( i=0; i<Max_proc; i++ )
|
|
|
|
if( !_procs[i].pr_valid )
|
|
|
|
break;
|
|
|
|
|
|
|
|
pp = _procs+i;
|
|
|
|
|
|
|
|
pp->pr_valid = 1;
|
|
|
|
pp->pr_pid = pid;
|
|
|
|
pp->pr_target = target;
|
|
|
|
pp->pr_ignore = ignore;
|
|
|
|
pp->pr_last = last;
|
|
|
|
pp->pr_dir = DmStrDup(Get_current_dir());
|
|
|
|
|
|
|
|
Current_target = NIL(CELL);
|
|
|
|
|
|
|
|
_proc_cnt++;
|
|
|
|
|
|
|
|
if( Wait_for_completion ) Wait_for_child( FALSE, pid );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
_finished_child(pid, status)
|
|
|
|
int pid;
|
|
|
|
int status;
|
|
|
|
{
|
|
|
|
register int i;
|
|
|
|
char *dir;
|
|
|
|
|
|
|
|
for( i=0; i<Max_proc; i++ )
|
|
|
|
if( _procs[i].pr_valid && _procs[i].pr_pid == pid )
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* Some children we didn't make esp true if using /bin/sh to execute a
|
|
|
|
* a pipe and feed the output as a makefile into dmake. */
|
|
|
|
if( i == Max_proc ) return;
|
2006-04-20 06:19:12 -05:00
|
|
|
|
|
|
|
_procs[i].pr_valid = 0; /* Not a running process anymore. */
|
|
|
|
if( Measure & M_RECIPE )
|
|
|
|
Do_profile_output( "e", M_RECIPE, _procs[i].pr_target );
|
|
|
|
|
2000-09-22 09:33:37 -05:00
|
|
|
_proc_cnt--;
|
|
|
|
dir = DmStrDup(Get_current_dir());
|
|
|
|
Set_dir( _procs[i].pr_dir );
|
|
|
|
|
|
|
|
if( _procs[i].pr_recipe != NIL(RCP) && !_abort_flg ) {
|
|
|
|
RCPPTR rp = _procs[i].pr_recipe;
|
|
|
|
|
|
|
|
|
|
|
|
Current_target = _procs[i].pr_target;
|
|
|
|
Handle_result( status, _procs[i].pr_ignore, FALSE, _procs[i].pr_target );
|
|
|
|
Current_target = NIL(CELL);
|
|
|
|
|
|
|
|
if ( _procs[i].pr_target->ce_attr & A_ERROR ) {
|
|
|
|
Unlink_temp_files( _procs[i].pr_target );
|
|
|
|
_procs[i].pr_last = TRUE;
|
|
|
|
goto ABORT_REMAINDER_OF_RECIPE;
|
|
|
|
}
|
|
|
|
|
|
|
|
_procs[i].pr_recipe = rp->prp_next;
|
|
|
|
|
|
|
|
_use_i = i;
|
2006-04-20 06:19:12 -05:00
|
|
|
/* Run next recipe line. */
|
2000-09-22 09:33:37 -05:00
|
|
|
runargv( _procs[i].pr_target, rp->prp_ignore, rp->prp_group,
|
|
|
|
rp->prp_last, rp->prp_shell, rp->prp_cmd );
|
|
|
|
_use_i = -1;
|
|
|
|
|
|
|
|
FREE( rp->prp_cmd );
|
|
|
|
FREE( rp );
|
|
|
|
|
|
|
|
if( _proc_cnt == Max_proc ) Wait_for_child( FALSE, -1 );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Unlink_temp_files( _procs[i].pr_target );
|
|
|
|
Handle_result(status,_procs[i].pr_ignore,_abort_flg,_procs[i].pr_target);
|
|
|
|
|
|
|
|
ABORT_REMAINDER_OF_RECIPE:
|
|
|
|
if( _procs[i].pr_last ) {
|
|
|
|
FREE(_procs[i].pr_dir );
|
|
|
|
|
|
|
|
if( !Doing_bang ) Update_time_stamp( _procs[i].pr_target );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Set_dir(dir);
|
|
|
|
FREE(dir);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
_running( cp )
|
|
|
|
CELLPTR cp;
|
|
|
|
{
|
|
|
|
register int i;
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
if( !_procs ) return( -1 );
|
2000-09-22 09:33:37 -05:00
|
|
|
|
|
|
|
for( i=0; i<Max_proc; i++ )
|
|
|
|
if( _procs[i].pr_valid &&
|
|
|
|
_procs[i].pr_target == cp )
|
|
|
|
break;
|
|
|
|
|
2006-04-20 06:19:12 -05:00
|
|
|
return( i == Max_proc ? -1 : i );
|
2000-09-22 09:33:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
_attach_cmd( cmd, group, ignore, cp, last, shell )
|
|
|
|
char *cmd;
|
|
|
|
int group;
|
|
|
|
int ignore;
|
|
|
|
CELLPTR cp;
|
|
|
|
int last;
|
|
|
|
int shell;
|
|
|
|
{
|
|
|
|
register int i;
|
|
|
|
RCPPTR rp;
|
|
|
|
|
|
|
|
for( i=0; i<Max_proc; i++ )
|
|
|
|
if( _procs[i].pr_valid &&
|
|
|
|
_procs[i].pr_target == cp )
|
|
|
|
break;
|
|
|
|
|
|
|
|
TALLOC( rp, 1, RCP );
|
|
|
|
rp->prp_cmd = DmStrDup(cmd);
|
|
|
|
rp->prp_group = group;
|
|
|
|
rp->prp_ignore= ignore;
|
|
|
|
rp->prp_last = last;
|
|
|
|
rp->prp_shell = shell;
|
|
|
|
|
|
|
|
if( _procs[i].pr_recipe == NIL(RCP) )
|
|
|
|
_procs[i].pr_recipe = _procs[i].pr_recipe_end = rp;
|
|
|
|
else {
|
|
|
|
_procs[i].pr_recipe_end->prp_next = rp;
|
|
|
|
_procs[i].pr_recipe_end = rp;
|
|
|
|
}
|
|
|
|
}
|