1266 lines
35 KiB
C++
1266 lines
35 KiB
C++
/* spawn.cc
|
|
|
|
Copyright 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,
|
|
2007, 2008, 2009, 2010, 2011, 2012 Red Hat, Inc.
|
|
|
|
This file is part of Cygwin.
|
|
|
|
This software is a copyrighted work licensed under the terms of the
|
|
Cygwin license. Please consult the file "CYGWIN_LICENSE" for
|
|
details. */
|
|
|
|
#include "winsup.h"
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <process.h>
|
|
#include <sys/wait.h>
|
|
#include <wingdi.h>
|
|
#include <winuser.h>
|
|
#include <wchar.h>
|
|
#include <ctype.h>
|
|
#include <sys/cygwin.h>
|
|
#include "cygerrno.h"
|
|
#include "security.h"
|
|
#include "sigproc.h"
|
|
#include "pinfo.h"
|
|
#include "path.h"
|
|
#include "fhandler.h"
|
|
#include "dtable.h"
|
|
#include "cygheap.h"
|
|
#include "child_info.h"
|
|
#include "environ.h"
|
|
#include "cygtls.h"
|
|
#include "tls_pbuf.h"
|
|
#include "winf.h"
|
|
#include "ntdll.h"
|
|
|
|
static suffix_info NO_COPY exe_suffixes[] =
|
|
{
|
|
suffix_info ("", 1),
|
|
suffix_info (".exe", 1),
|
|
suffix_info (".com"),
|
|
suffix_info (NULL)
|
|
};
|
|
|
|
#if 0
|
|
/* CV, 2009-11-05: Used to be used when searching for DLLs in calls to
|
|
dlopen(). However, dlopen() on other platforms never adds a suffix by
|
|
its own. Therefore we use stat_suffixes now, which only adds a .exe
|
|
suffix for symmetry. */
|
|
static suffix_info dll_suffixes[] =
|
|
{
|
|
suffix_info (".dll"),
|
|
suffix_info ("", 1),
|
|
suffix_info (".exe", 1),
|
|
suffix_info (NULL)
|
|
};
|
|
#endif
|
|
|
|
/* Add .exe to PROG if not already present and see if that exists.
|
|
If not, return PROG (converted from posix to win32 rules if necessary).
|
|
The result is always BUF.
|
|
|
|
Returns (possibly NULL) suffix */
|
|
|
|
static const char *
|
|
perhaps_suffix (const char *prog, path_conv& buf, int& err, unsigned opt)
|
|
{
|
|
const char *ext;
|
|
|
|
err = 0;
|
|
debug_printf ("prog '%s'", prog);
|
|
buf.check (prog, PC_SYM_FOLLOW | PC_NULLEMPTY,
|
|
(opt & FE_DLL) ? stat_suffixes : exe_suffixes);
|
|
|
|
if (buf.isdir ())
|
|
{
|
|
err = EACCES;
|
|
ext = NULL;
|
|
}
|
|
else if (!buf.exists ())
|
|
{
|
|
err = ENOENT;
|
|
ext = NULL;
|
|
}
|
|
else if (buf.known_suffix)
|
|
ext = buf.get_win32 () + (buf.known_suffix - buf.get_win32 ());
|
|
else
|
|
ext = strchr (buf.get_win32 (), '\0');
|
|
|
|
debug_printf ("buf %s, suffix found '%s'", (char *) buf.get_win32 (), ext);
|
|
return ext;
|
|
}
|
|
|
|
/* Find an executable name, possibly by appending known executable
|
|
suffixes to it. The win32-translated name is placed in 'buf'.
|
|
Any found suffix is returned in known_suffix.
|
|
|
|
If the file is not found and !null_if_not_found then the win32 version
|
|
of name is placed in buf and returned. Otherwise the contents of buf
|
|
is undefined and NULL is returned. */
|
|
|
|
const char * __stdcall
|
|
find_exec (const char *name, path_conv& buf, const char *mywinenv,
|
|
unsigned opt, const char **known_suffix)
|
|
{
|
|
const char *suffix = "";
|
|
debug_printf ("find_exec (%s)", name);
|
|
const char *retval;
|
|
tmp_pathbuf tp;
|
|
char *tmp = tp.c_get ();
|
|
const char *posix = (opt & FE_NATIVE) ? NULL : name;
|
|
bool has_slash = !!strpbrk (name, "/\\");
|
|
int err;
|
|
|
|
/* Check to see if file can be opened as is first.
|
|
Win32 systems always check . first, but PATH may not be set up to
|
|
do this. */
|
|
if ((has_slash || opt & FE_CWD)
|
|
&& (suffix = perhaps_suffix (name, buf, err, opt)) != NULL)
|
|
{
|
|
if (posix && !has_slash)
|
|
{
|
|
tmp[0] = '.';
|
|
tmp[1] = '/';
|
|
strcpy (tmp + 2, name);
|
|
posix = tmp;
|
|
}
|
|
retval = buf.get_win32 ();
|
|
goto out;
|
|
}
|
|
|
|
win_env *winpath;
|
|
const char *path;
|
|
const char *posix_path;
|
|
|
|
posix = (opt & FE_NATIVE) ? NULL : tmp;
|
|
|
|
if (strchr (mywinenv, '/'))
|
|
{
|
|
/* it's not really an environment variable at all */
|
|
int n = cygwin_conv_path_list (CCP_POSIX_TO_WIN_A, mywinenv, NULL, 0);
|
|
char *s = (char *) alloca (n);
|
|
if (cygwin_conv_path_list (CCP_POSIX_TO_WIN_A, mywinenv, s, n))
|
|
goto errout;
|
|
path = s;
|
|
posix_path = mywinenv - 1;
|
|
}
|
|
else if (has_slash || strchr (name, '\\') || isdrive (name)
|
|
|| !(winpath = getwinenv (mywinenv))
|
|
|| !(path = winpath->get_native ()) || *path == '\0')
|
|
/* Return the error condition if this is an absolute path or if there
|
|
is no PATH to search. */
|
|
goto errout;
|
|
else
|
|
posix_path = winpath->get_posix () - 1;
|
|
|
|
debug_printf ("%s%s", mywinenv, path);
|
|
/* Iterate over the specified path, looking for the file with and without
|
|
executable extensions. */
|
|
do
|
|
{
|
|
posix_path++;
|
|
char *eotmp = strccpy (tmp, &path, ';');
|
|
/* An empty path or '.' means the current directory, but we've
|
|
already tried that. */
|
|
if (opt & FE_CWD && (tmp[0] == '\0' || (tmp[0] == '.' && tmp[1] == '\0')))
|
|
continue;
|
|
|
|
*eotmp++ = '\\';
|
|
strcpy (eotmp, name);
|
|
|
|
debug_printf ("trying %s", tmp);
|
|
|
|
int err1;
|
|
|
|
if ((suffix = perhaps_suffix (tmp, buf, err1, opt)) != NULL)
|
|
{
|
|
if (buf.has_acls () && check_file_access (buf, X_OK, true))
|
|
continue;
|
|
|
|
if (posix == tmp)
|
|
{
|
|
eotmp = strccpy (tmp, &posix_path, ':');
|
|
if (eotmp == tmp)
|
|
*eotmp++ = '.';
|
|
*eotmp++ = '/';
|
|
strcpy (eotmp, name);
|
|
}
|
|
retval = buf.get_win32 ();
|
|
goto out;
|
|
}
|
|
}
|
|
while (*path && *++path && (posix_path = strchr (posix_path, ':')));
|
|
|
|
errout:
|
|
posix = NULL;
|
|
/* Couldn't find anything in the given path.
|
|
Take the appropriate action based on null_if_not_found. */
|
|
if (opt & FE_NNF)
|
|
retval = NULL;
|
|
else if (!(opt & FE_NATIVE))
|
|
retval = name;
|
|
else
|
|
{
|
|
buf.check (name);
|
|
retval = buf.get_win32 ();
|
|
}
|
|
|
|
out:
|
|
if (posix)
|
|
retval = buf.set_path (posix);
|
|
debug_printf ("%s = find_exec (%s)", (char *) buf.get_win32 (), name);
|
|
if (known_suffix)
|
|
*known_suffix = suffix ?: strchr (buf.get_win32 (), '\0');
|
|
if (!retval && err)
|
|
set_errno (err);
|
|
return retval;
|
|
}
|
|
|
|
/* Utility for child_info_spawn::worker. */
|
|
|
|
static HANDLE
|
|
handle (int fd, bool writing)
|
|
{
|
|
HANDLE h;
|
|
cygheap_fdget cfd (fd);
|
|
|
|
if (cfd < 0)
|
|
h = INVALID_HANDLE_VALUE;
|
|
else if (cfd->close_on_exec ())
|
|
h = INVALID_HANDLE_VALUE;
|
|
else if (!writing)
|
|
h = cfd->get_handle ();
|
|
else
|
|
h = cfd->get_output_handle ();
|
|
|
|
return h;
|
|
}
|
|
|
|
int
|
|
iscmd (const char *argv0, const char *what)
|
|
{
|
|
int n;
|
|
n = strlen (argv0) - strlen (what);
|
|
if (n >= 2 && argv0[1] != ':')
|
|
return 0;
|
|
return n >= 0 && strcasematch (argv0 + n, what) &&
|
|
(n == 0 || isdirsep (argv0[n - 1]));
|
|
}
|
|
|
|
struct pthread_cleanup
|
|
{
|
|
_sig_func_ptr oldint;
|
|
_sig_func_ptr oldquit;
|
|
sigset_t oldmask;
|
|
pthread_cleanup (): oldint (NULL), oldquit (NULL), oldmask ((sigset_t) -1) {}
|
|
};
|
|
|
|
static void
|
|
do_cleanup (void *args)
|
|
{
|
|
# define cleanup ((pthread_cleanup *) args)
|
|
if (cleanup->oldmask != (sigset_t) -1)
|
|
{
|
|
signal (SIGINT, cleanup->oldint);
|
|
signal (SIGQUIT, cleanup->oldquit);
|
|
sigprocmask (SIG_SETMASK, &(cleanup->oldmask), NULL);
|
|
}
|
|
# undef cleanup
|
|
}
|
|
|
|
child_info_spawn NO_COPY ch_spawn;
|
|
|
|
int
|
|
child_info_spawn::worker (const char *prog_arg, const char *const *argv,
|
|
const char *const envp[], int mode,
|
|
int in__stdin, int in__stdout)
|
|
{
|
|
bool rc;
|
|
pid_t cygpid;
|
|
int res = -1;
|
|
|
|
/* Check if we have been called from exec{lv}p or spawn{lv}p and mask
|
|
mode to keep only the spawn mode. */
|
|
bool p_type_exec = !!(mode & _P_PATH_TYPE_EXEC);
|
|
mode = _P_MODE (mode);
|
|
|
|
if (prog_arg == NULL)
|
|
{
|
|
syscall_printf ("prog_arg is NULL");
|
|
set_errno (EFAULT); /* As on Linux. */
|
|
return -1;
|
|
}
|
|
if (!prog_arg[0])
|
|
{
|
|
syscall_printf ("prog_arg is empty");
|
|
set_errno (ENOENT); /* Per POSIX */
|
|
return -1;
|
|
}
|
|
|
|
syscall_printf ("mode = %d, prog_arg = %.9500s", mode, prog_arg);
|
|
|
|
/* FIXME: This is no error condition on Linux. */
|
|
if (argv == NULL)
|
|
{
|
|
syscall_printf ("argv is NULL");
|
|
set_errno (EINVAL);
|
|
return -1;
|
|
}
|
|
|
|
/* FIXME: There is a small race here and FIXME: not thread safe! */
|
|
pthread_cleanup cleanup;
|
|
if (mode == _P_SYSTEM)
|
|
{
|
|
sigset_t child_block;
|
|
cleanup.oldint = signal (SIGINT, SIG_IGN);
|
|
cleanup.oldquit = signal (SIGQUIT, SIG_IGN);
|
|
sigemptyset (&child_block);
|
|
sigaddset (&child_block, SIGCHLD);
|
|
sigprocmask (SIG_BLOCK, &child_block, &cleanup.oldmask);
|
|
}
|
|
pthread_cleanup_push (do_cleanup, (void *) &cleanup);
|
|
av newargv;
|
|
linebuf one_line;
|
|
PWCHAR envblock = NULL;
|
|
path_conv real_path;
|
|
bool reset_sendsig = false;
|
|
|
|
tmp_pathbuf tp;
|
|
PWCHAR runpath = tp.w_get ();
|
|
int c_flags;
|
|
bool wascygexec;
|
|
|
|
bool null_app_name = false;
|
|
STARTUPINFOW si = {};
|
|
int looped = 0;
|
|
|
|
myfault efault;
|
|
if (efault.faulted ())
|
|
{
|
|
if (get_errno () == ENOMEM)
|
|
set_errno (E2BIG);
|
|
else
|
|
set_errno (EFAULT);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
|
|
child_info_types chtype;
|
|
if (mode == _P_OVERLAY)
|
|
chtype = _CH_EXEC;
|
|
else
|
|
chtype = _CH_SPAWN;
|
|
|
|
moreinfo = cygheap_exec_info::alloc ();
|
|
|
|
/* CreateProcess takes one long string that is the command line (sigh).
|
|
We need to quote any argument that has whitespace or embedded "'s. */
|
|
|
|
int ac;
|
|
for (ac = 0; argv[ac]; ac++)
|
|
/* nothing */;
|
|
|
|
newargv.set (ac, argv);
|
|
|
|
int err;
|
|
const char *ext;
|
|
if ((ext = perhaps_suffix (prog_arg, real_path, err, FE_NADA)) == NULL)
|
|
{
|
|
set_errno (err);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
|
|
wascygexec = real_path.iscygexec ();
|
|
res = newargv.fixup (prog_arg, real_path, ext, p_type_exec);
|
|
|
|
if (res)
|
|
goto out;
|
|
|
|
if (!real_path.iscygexec () && ::cygheap->cwd.get_error ())
|
|
{
|
|
small_printf ("Error: Current working directory %s.\n"
|
|
"Can't start native Windows application from here.\n\n",
|
|
::cygheap->cwd.get_error_desc ());
|
|
set_errno (::cygheap->cwd.get_error ());
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
|
|
if (ac == 3 && argv[1][0] == '/' && tolower (argv[1][1]) == 'c' &&
|
|
(iscmd (argv[0], "command.com") || iscmd (argv[0], "cmd.exe")))
|
|
{
|
|
real_path.check (prog_arg);
|
|
one_line.add ("\"");
|
|
if (!real_path.error)
|
|
one_line.add (real_path.get_win32 ());
|
|
else
|
|
one_line.add (argv[0]);
|
|
one_line.add ("\"");
|
|
one_line.add (" ");
|
|
one_line.add (argv[1]);
|
|
one_line.add (" ");
|
|
one_line.add (argv[2]);
|
|
real_path.set_path (argv[0]);
|
|
null_app_name = true;
|
|
}
|
|
else
|
|
{
|
|
if (wascygexec)
|
|
newargv.dup_all ();
|
|
else if (!one_line.fromargv (newargv, real_path.get_win32 (),
|
|
real_path.iscygexec ()))
|
|
{
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
|
|
|
|
newargv.all_calloced ();
|
|
moreinfo->argc = newargv.argc;
|
|
moreinfo->argv = newargv;
|
|
|
|
if (mode != _P_OVERLAY || !real_path.iscygexec ()
|
|
|| !DuplicateHandle (GetCurrentProcess (), myself.shared_handle (),
|
|
GetCurrentProcess (), &moreinfo->myself_pinfo,
|
|
0, TRUE, DUPLICATE_SAME_ACCESS))
|
|
moreinfo->myself_pinfo = NULL;
|
|
else
|
|
VerifyHandle (moreinfo->myself_pinfo);
|
|
}
|
|
WCHAR wone_line[one_line.ix + 1];
|
|
if (one_line.ix)
|
|
sys_mbstowcs (wone_line, one_line.ix + 1, one_line.buf);
|
|
else
|
|
wone_line[0] = L'\0';
|
|
|
|
PROCESS_INFORMATION pi;
|
|
pi.hProcess = pi.hThread = NULL;
|
|
pi.dwProcessId = pi.dwThreadId = 0;
|
|
|
|
/* Set up needed handles for stdio */
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = handle ((in__stdin < 0 ? 0 : in__stdin), false);
|
|
si.hStdOutput = handle ((in__stdout < 0 ? 1 : in__stdout), true);
|
|
si.hStdError = handle (2, true);
|
|
|
|
si.cb = sizeof (si);
|
|
|
|
c_flags = GetPriorityClass (GetCurrentProcess ());
|
|
sigproc_printf ("priority class %d", c_flags);
|
|
|
|
c_flags |= CREATE_SEPARATE_WOW_VDM | CREATE_UNICODE_ENVIRONMENT;
|
|
|
|
if (wincap.has_program_compatibility_assistant ())
|
|
{
|
|
/* We're adding the CREATE_BREAKAWAY_FROM_JOB flag here to workaround
|
|
issues with the "Program Compatibility Assistant (PCA) Service"
|
|
starting with Windows Vista. For some reason, when starting long
|
|
running sessions from mintty(*), the affected svchost.exe process
|
|
takes more and more memory and at one point takes over the CPU. At
|
|
this point the machine becomes unresponsive. The only way to get
|
|
back to normal is to stop the entire mintty session, or to stop the
|
|
PCA service. However, a process which is controlled by PCA is part
|
|
of a compatibility job, which allows child processes to break away
|
|
from the job. This helps to avoid this issue.
|
|
|
|
(*) Note that this is not mintty's fault. It has just been observed
|
|
with mintty in the first place. See the archives for more info:
|
|
http://cygwin.com/ml/cygwin-developers/2012-02/msg00018.html */
|
|
|
|
JOBOBJECT_BASIC_LIMIT_INFORMATION jobinfo;
|
|
|
|
/* Calling QueryInformationJobObject costs time. Starting with
|
|
Windows XP there's a function IsProcessInJob, which fetches the
|
|
information whether or not we're part of a job 20 times faster than
|
|
the call to QueryInformationJobObject. But we're still
|
|
supporting Windows 2000, so we can't just link to that function.
|
|
On the other hand, loading the function pointer at runtime is a
|
|
time comsuming operation, too. So, what we do here is to emulate
|
|
the IsProcessInJob function when called for the own process and with
|
|
a NULL job handle. In this case it just returns the value of the
|
|
lowest bit from PEB->EnvironmentUpdateCount (observed with WinDbg).
|
|
The name of this PEB member is the same in all (inofficial)
|
|
documentations of the PEB. Apparently it's a bit misleading.
|
|
As a result, we only call QueryInformationJobObject if we're on
|
|
Vista or later *and* if the PEB indicates we're running in a job.
|
|
Tested on Vista/32, Vista/64, W7/32, W7/64, W8/64. */
|
|
if ((NtCurrentTeb ()->Peb->EnvironmentUpdateCount & 1) != 0
|
|
&& QueryInformationJobObject (NULL, JobObjectBasicLimitInformation,
|
|
&jobinfo, sizeof jobinfo, NULL)
|
|
&& (jobinfo.LimitFlags & (JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
|
| JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)))
|
|
{
|
|
debug_printf ("Add CREATE_BREAKAWAY_FROM_JOB");
|
|
c_flags |= CREATE_BREAKAWAY_FROM_JOB;
|
|
}
|
|
}
|
|
|
|
if (mode == _P_DETACH)
|
|
c_flags |= DETACHED_PROCESS;
|
|
else
|
|
fhandler_console::need_invisible ();
|
|
|
|
if (mode != _P_OVERLAY)
|
|
myself->exec_sendsig = NULL;
|
|
else
|
|
{
|
|
/* Reset sendsig so that any process which wants to send a signal
|
|
to this pid will wait for the new process to become active.
|
|
Save the old value in case the exec fails. */
|
|
if (!myself->exec_sendsig)
|
|
{
|
|
myself->exec_sendsig = myself->sendsig;
|
|
myself->exec_dwProcessId = myself->dwProcessId;
|
|
myself->sendsig = NULL;
|
|
reset_sendsig = true;
|
|
}
|
|
}
|
|
|
|
if (null_app_name)
|
|
runpath = NULL;
|
|
else
|
|
{
|
|
USHORT len = real_path.get_nt_native_path ()->Length / sizeof (WCHAR);
|
|
if (RtlEqualUnicodePathPrefix (real_path.get_nt_native_path (),
|
|
&ro_u_natp, FALSE))
|
|
{
|
|
runpath = real_path.get_wide_win32_path (runpath);
|
|
/* If the executable path length is < MAX_PATH, make sure the long
|
|
path win32 prefix is removed from the path to make subsequent
|
|
not long path aware native Win32 child processes happy. */
|
|
if (len < MAX_PATH + 4)
|
|
{
|
|
if (runpath[5] == ':')
|
|
runpath += 4;
|
|
else if (len < MAX_PATH + 6)
|
|
*(runpath += 6) = L'\\';
|
|
}
|
|
}
|
|
else if (len < NT_MAX_PATH - ro_u_globalroot.Length / sizeof (WCHAR))
|
|
{
|
|
UNICODE_STRING rpath;
|
|
|
|
RtlInitEmptyUnicodeString (&rpath, runpath,
|
|
(NT_MAX_PATH - 1) * sizeof (WCHAR));
|
|
RtlCopyUnicodeString (&rpath, &ro_u_globalroot);
|
|
RtlAppendUnicodeStringToString (&rpath,
|
|
real_path.get_nt_native_path ());
|
|
}
|
|
else
|
|
{
|
|
set_errno (ENAMETOOLONG);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
}
|
|
syscall_printf ("null_app_name %d (%W, %.9500W)", null_app_name,
|
|
runpath, wone_line);
|
|
|
|
cygbench ("spawn-worker");
|
|
|
|
if (!real_path.iscygexec())
|
|
::cygheap->fdtab.set_file_pointers_for_exec ();
|
|
|
|
moreinfo->envp = build_env (envp, envblock, moreinfo->envc,
|
|
real_path.iscygexec ());
|
|
if (!moreinfo->envp || !envblock)
|
|
{
|
|
set_errno (E2BIG);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
set (chtype, real_path.iscygexec ());
|
|
__stdin = in__stdin;
|
|
__stdout = in__stdout;
|
|
record_children ();
|
|
|
|
si.lpReserved2 = (LPBYTE) this;
|
|
si.cbReserved2 = sizeof (*this);
|
|
|
|
/* Depends on set call above.
|
|
Some file types might need extra effort in the parent after CreateProcess
|
|
and before copying the datastructures to the child. So we have to start
|
|
the child in suspend state, unfortunately, to avoid a race condition. */
|
|
if (!newargv.win16_exe
|
|
&& (!iscygwin () || mode != _P_OVERLAY
|
|
|| ::cygheap->fdtab.need_fixup_before ()))
|
|
c_flags |= CREATE_SUSPENDED;
|
|
/* If a native application should be spawned, we test here if the spawning
|
|
process is running in a console and, if so, if it's a foreground or
|
|
background process. If it's a background process, we start the native
|
|
process with the CREATE_NEW_PROCESS_GROUP flag set. This lets the native
|
|
process ignore Ctrl-C by default. If we don't do that, pressing Ctrl-C
|
|
in a console will break native processes running in the background,
|
|
because the Ctrl-C event is sent to all processes in the console, unless
|
|
they ignore it explicitely. CREATE_NEW_PROCESS_GROUP does that for us. */
|
|
if (!iscygwin () && fhandler_console::exists ()
|
|
&& fhandler_console::tc_getpgid () != myself->pgid)
|
|
c_flags |= CREATE_NEW_PROCESS_GROUP;
|
|
refresh_cygheap ();
|
|
|
|
if (mode == _P_DETACH)
|
|
/* all set */;
|
|
else if (mode != _P_OVERLAY || !my_wr_proc_pipe)
|
|
prefork ();
|
|
else
|
|
wr_proc_pipe = my_wr_proc_pipe;
|
|
|
|
/* Don't allow child to inherit these handles if it's not a Cygwin program.
|
|
wr_proc_pipe will be injected later. parent won't be used by the child
|
|
so there is no reason for the child to have it open as it can confuse
|
|
ps into thinking that children of windows processes are all part of
|
|
the same "execed" process.
|
|
FIXME: Someday, make it so that parent is never created when starting
|
|
non-Cygwin processes. */
|
|
if (!iscygwin ())
|
|
{
|
|
SetHandleInformation (wr_proc_pipe, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation (parent, HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
/* FIXME: racy */
|
|
if (mode != _P_OVERLAY)
|
|
SetHandleInformation (my_wr_proc_pipe, HANDLE_FLAG_INHERIT, 0);
|
|
parent_winpid = GetCurrentProcessId ();
|
|
|
|
/* When ruid != euid we create the new process under the current original
|
|
account and impersonate in child, this way maintaining the different
|
|
effective vs. real ids.
|
|
FIXME: If ruid != euid and ruid != saved_uid we currently give
|
|
up on ruid. The new process will have ruid == euid. */
|
|
loop:
|
|
::cygheap->user.deimpersonate ();
|
|
|
|
if (!real_path.iscygexec () && mode == _P_OVERLAY)
|
|
myself->process_state |= PID_NOTCYGWIN;
|
|
|
|
if (!::cygheap->user.issetuid ()
|
|
|| (::cygheap->user.saved_uid == ::cygheap->user.real_uid
|
|
&& ::cygheap->user.saved_gid == ::cygheap->user.real_gid
|
|
&& !::cygheap->user.groups.issetgroups ()
|
|
&& !::cygheap->user.setuid_to_restricted))
|
|
{
|
|
rc = CreateProcessW (runpath, /* image name - with full path */
|
|
wone_line, /* what was passed to exec */
|
|
&sec_none_nih, /* process security attrs */
|
|
&sec_none_nih, /* thread security attrs */
|
|
TRUE, /* inherit handles from parent */
|
|
c_flags,
|
|
envblock, /* environment */
|
|
NULL,
|
|
&si,
|
|
&pi);
|
|
}
|
|
else
|
|
{
|
|
/* Give access to myself */
|
|
if (mode == _P_OVERLAY)
|
|
myself.set_acl();
|
|
|
|
WCHAR wstname[1024] = { L'\0' };
|
|
HWINSTA hwst_orig = NULL, hwst = NULL;
|
|
HDESK hdsk_orig = NULL, hdsk = NULL;
|
|
PSECURITY_ATTRIBUTES sa;
|
|
DWORD n;
|
|
|
|
hwst_orig = GetProcessWindowStation ();
|
|
hdsk_orig = GetThreadDesktop (GetCurrentThreadId ());
|
|
GetUserObjectInformationW (hwst_orig, UOI_NAME, wstname, 1024, &n);
|
|
/* Prior to Vista it was possible to start a service with the
|
|
"Interact with desktop" flag. This started the service in the
|
|
interactive window station of the console. A big security
|
|
risk, but we don't want to disable this behaviour for older
|
|
OSes because it's still heavily used by some users. They have
|
|
been warned. */
|
|
if (!::cygheap->user.setuid_to_restricted
|
|
&& wcscasecmp (wstname, L"WinSta0") != 0)
|
|
{
|
|
WCHAR sid[128];
|
|
|
|
sa = sec_user ((PSECURITY_ATTRIBUTES) alloca (1024),
|
|
::cygheap->user.sid ());
|
|
/* We're creating a window station per user, not per logon session.
|
|
First of all we might not have a valid logon session for
|
|
the user (logon by create_token), and second, it doesn't
|
|
make sense in terms of security to create a new window
|
|
station for every logon of the same user. It just fills up
|
|
the system with window stations for no good reason. */
|
|
hwst = CreateWindowStationW (::cygheap->user.get_windows_id (sid), 0,
|
|
GENERIC_READ | GENERIC_WRITE, sa);
|
|
if (!hwst)
|
|
system_printf ("CreateWindowStation failed, %E");
|
|
else if (!SetProcessWindowStation (hwst))
|
|
system_printf ("SetProcessWindowStation failed, %E");
|
|
else if (!(hdsk = CreateDesktopW (L"Default", NULL, NULL, 0,
|
|
GENERIC_ALL, sa)))
|
|
system_printf ("CreateDesktop failed, %E");
|
|
else
|
|
{
|
|
wcpcpy (wcpcpy (wstname, sid), L"\\Default");
|
|
si.lpDesktop = wstname;
|
|
debug_printf ("Desktop: %W", si.lpDesktop);
|
|
}
|
|
}
|
|
|
|
rc = CreateProcessAsUserW (::cygheap->user.primary_token (),
|
|
runpath, /* image name - with full path */
|
|
wone_line, /* what was passed to exec */
|
|
&sec_none_nih, /* process security attrs */
|
|
&sec_none_nih, /* thread security attrs */
|
|
TRUE, /* inherit handles from parent */
|
|
c_flags,
|
|
envblock, /* environment */
|
|
NULL,
|
|
&si,
|
|
&pi);
|
|
if (hwst)
|
|
{
|
|
SetProcessWindowStation (hwst_orig);
|
|
CloseWindowStation (hwst);
|
|
}
|
|
if (hdsk)
|
|
{
|
|
SetThreadDesktop (hdsk_orig);
|
|
CloseDesktop (hdsk);
|
|
}
|
|
}
|
|
|
|
if (mode != _P_OVERLAY)
|
|
SetHandleInformation (my_wr_proc_pipe, HANDLE_FLAG_INHERIT,
|
|
HANDLE_FLAG_INHERIT);
|
|
|
|
/* Set errno now so that debugging messages from it appear before our
|
|
final debugging message [this is a general rule for debugging
|
|
messages]. */
|
|
if (!rc)
|
|
{
|
|
__seterrno ();
|
|
syscall_printf ("CreateProcess failed, %E");
|
|
/* If this was a failed exec, restore the saved sendsig. */
|
|
if (reset_sendsig)
|
|
{
|
|
myself->sendsig = myself->exec_sendsig;
|
|
myself->exec_sendsig = NULL;
|
|
}
|
|
myself->process_state &= ~PID_NOTCYGWIN;
|
|
/* Reset handle inheritance to default when the execution of a non-Cygwin
|
|
process fails. Only need to do this for _P_OVERLAY since the handle will
|
|
be closed otherwise. Don't need to do this for 'parent' since it will
|
|
be closed in every case. See FIXME above. */
|
|
if (!iscygwin () && mode == _P_OVERLAY)
|
|
SetHandleInformation (wr_proc_pipe, HANDLE_FLAG_INHERIT,
|
|
HANDLE_FLAG_INHERIT);
|
|
if (wr_proc_pipe == my_wr_proc_pipe)
|
|
wr_proc_pipe = NULL; /* We still own it: don't nuke in destructor */
|
|
|
|
/* Restore impersonation. In case of _P_OVERLAY this isn't
|
|
allowed since it would overwrite child data. */
|
|
if (mode != _P_OVERLAY)
|
|
::cygheap->user.reimpersonate ();
|
|
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
|
|
/* The CREATE_SUSPENDED case is handled below */
|
|
if (iscygwin () && !(c_flags & CREATE_SUSPENDED))
|
|
strace.write_childpid (pi.dwProcessId);
|
|
|
|
/* Fixup the parent data structures if needed and resume the child's
|
|
main thread. */
|
|
if (::cygheap->fdtab.need_fixup_before ())
|
|
::cygheap->fdtab.fixup_before_exec (pi.dwProcessId);
|
|
|
|
if (mode != _P_OVERLAY)
|
|
cygpid = cygwin_pid (pi.dwProcessId);
|
|
else
|
|
cygpid = myself->pid;
|
|
|
|
/* We print the original program name here so the user can see that too. */
|
|
syscall_printf ("pid %d, prog_arg %s, cmd line %.9500s)",
|
|
rc ? cygpid : (unsigned int) -1, prog_arg, one_line.buf);
|
|
|
|
/* Name the handle similarly to proc_subproc. */
|
|
ProtectHandle1 (pi.hProcess, childhProc);
|
|
|
|
pid_t pid;
|
|
if (mode == _P_OVERLAY)
|
|
{
|
|
myself->dwProcessId = pi.dwProcessId;
|
|
strace.execing = 1;
|
|
myself.hProcess = hExeced = pi.hProcess;
|
|
real_path.get_wide_win32_path (myself->progname); // FIXME: race?
|
|
sigproc_printf ("new process name %W", myself->progname);
|
|
pid = myself->pid;
|
|
if (!iscygwin ())
|
|
close_all_files ();
|
|
}
|
|
else
|
|
{
|
|
myself->set_has_pgid_children ();
|
|
ProtectHandle (pi.hThread);
|
|
pinfo child (cygpid,
|
|
PID_IN_USE | (real_path.iscygexec () ? 0 : PID_NOTCYGWIN));
|
|
if (!child)
|
|
{
|
|
syscall_printf ("pinfo failed");
|
|
if (get_errno () != ENOMEM)
|
|
set_errno (EAGAIN);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
child->dwProcessId = pi.dwProcessId;
|
|
child.hProcess = pi.hProcess;
|
|
|
|
real_path.get_wide_win32_path (child->progname);
|
|
/* FIXME: This introduces an unreferenced, open handle into the child.
|
|
The purpose is to keep the pid shared memory open so that all of
|
|
the fields filled out by child.remember do not disappear and so there
|
|
is not a brief period during which the pid is not available.
|
|
However, we should try to find another way to do this eventually. */
|
|
DuplicateHandle (GetCurrentProcess (), child.shared_handle (),
|
|
pi.hProcess, NULL, 0, 0, DUPLICATE_SAME_ACCESS);
|
|
child->start_time = time (NULL); /* Register child's starting time. */
|
|
child->nice = myself->nice;
|
|
postfork (child);
|
|
if (!child.remember (mode == _P_DETACH))
|
|
{
|
|
/* FIXME: Child in strange state now */
|
|
CloseHandle (pi.hProcess);
|
|
ForceCloseHandle (pi.hThread);
|
|
res = -1;
|
|
goto out;
|
|
}
|
|
pid = child->pid;
|
|
}
|
|
|
|
/* Start the child running */
|
|
if (c_flags & CREATE_SUSPENDED)
|
|
{
|
|
/* Inject a non-inheritable wr_proc_pipe handle into child so that we
|
|
can accurately track when the child exits without keeping this
|
|
process waiting around for it to exit. */
|
|
if (!iscygwin ())
|
|
DuplicateHandle (GetCurrentProcess (), wr_proc_pipe, pi.hProcess, NULL,
|
|
0, false, DUPLICATE_SAME_ACCESS);
|
|
ResumeThread (pi.hThread);
|
|
if (iscygwin ())
|
|
strace.write_childpid (pi.dwProcessId);
|
|
}
|
|
ForceCloseHandle (pi.hThread);
|
|
|
|
sigproc_printf ("spawned windows pid %d", pi.dwProcessId);
|
|
|
|
bool synced;
|
|
if ((mode == _P_DETACH || mode == _P_NOWAIT) && !iscygwin ())
|
|
synced = false;
|
|
else
|
|
/* Just mark a non-cygwin process as 'synced'. We will still eventually
|
|
wait for it to exit in maybe_set_exit_code_from_windows(). */
|
|
synced = iscygwin () ? sync (pi.dwProcessId, pi.hProcess, INFINITE) : true;
|
|
|
|
switch (mode)
|
|
{
|
|
case _P_OVERLAY:
|
|
myself.hProcess = pi.hProcess;
|
|
if (!synced)
|
|
{
|
|
if (!proc_retry (pi.hProcess))
|
|
{
|
|
looped++;
|
|
goto loop;
|
|
}
|
|
close_all_files (true);
|
|
}
|
|
else
|
|
{
|
|
if (iscygwin ())
|
|
close_all_files (true);
|
|
if (!my_wr_proc_pipe
|
|
&& WaitForSingleObject (pi.hProcess, 0) == WAIT_TIMEOUT)
|
|
wait_for_myself ();
|
|
}
|
|
myself.exit (EXITCODE_NOSET);
|
|
break;
|
|
case _P_WAIT:
|
|
case _P_SYSTEM:
|
|
if (waitpid (cygpid, &res, 0) != cygpid)
|
|
res = -1;
|
|
break;
|
|
case _P_DETACH:
|
|
res = 0; /* Lost all memory of this child. */
|
|
break;
|
|
case _P_NOWAIT:
|
|
case _P_NOWAITO:
|
|
case _P_VFORK:
|
|
res = cygpid;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
out:
|
|
this->cleanup ();
|
|
if (envblock)
|
|
free (envblock);
|
|
pthread_cleanup_pop (1);
|
|
return (int) res;
|
|
}
|
|
|
|
extern "C" int
|
|
cwait (int *result, int pid, int)
|
|
{
|
|
return waitpid (pid, result, 0);
|
|
}
|
|
|
|
/*
|
|
* Helper function for spawn runtime calls.
|
|
* Doesn't search the path.
|
|
*/
|
|
|
|
extern "C" int
|
|
spawnve (int mode, const char *path, const char *const *argv,
|
|
const char *const *envp)
|
|
{
|
|
static char *const empty_env[] = { NULL };
|
|
|
|
int ret;
|
|
#ifdef NEWVFORK
|
|
vfork_save *vf = vfork_storage.val ();
|
|
|
|
if (vf != NULL && (vf->pid < 0) && mode == _P_OVERLAY)
|
|
mode = _P_NOWAIT;
|
|
else
|
|
vf = NULL;
|
|
#endif
|
|
|
|
syscall_printf ("spawnve (%s, %s, %x)", path, argv[0], envp);
|
|
|
|
if (!envp)
|
|
envp = empty_env;
|
|
|
|
switch (_P_MODE (mode))
|
|
{
|
|
case _P_OVERLAY:
|
|
ch_spawn.worker (path, argv, envp, mode);
|
|
/* Errno should be set by worker. */
|
|
ret = -1;
|
|
break;
|
|
case _P_VFORK:
|
|
case _P_NOWAIT:
|
|
case _P_NOWAITO:
|
|
case _P_WAIT:
|
|
case _P_DETACH:
|
|
case _P_SYSTEM:
|
|
ret = ch_spawn.worker (path, argv, envp, mode);
|
|
#ifdef NEWVFORK
|
|
if (vf)
|
|
{
|
|
if (ret > 0)
|
|
{
|
|
debug_printf ("longjmping due to vfork");
|
|
vf->restore_pid (ret);
|
|
}
|
|
}
|
|
#endif
|
|
break;
|
|
default:
|
|
set_errno (EINVAL);
|
|
ret = -1;
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* spawn functions as implemented in the MS runtime library.
|
|
* Most of these based on (and copied from) newlib/libc/posix/execXX.c
|
|
*/
|
|
|
|
extern "C" int
|
|
spawnl (int mode, const char *path, const char *arg0, ...)
|
|
{
|
|
int i;
|
|
va_list args;
|
|
const char *argv[256];
|
|
|
|
va_start (args, arg0);
|
|
argv[0] = arg0;
|
|
i = 1;
|
|
|
|
do
|
|
argv[i] = va_arg (args, const char *);
|
|
while (argv[i++] != NULL);
|
|
|
|
va_end (args);
|
|
|
|
return spawnve (mode, path, (char * const *) argv, cur_environ ());
|
|
}
|
|
|
|
extern "C" int
|
|
spawnle (int mode, const char *path, const char *arg0, ...)
|
|
{
|
|
int i;
|
|
va_list args;
|
|
const char * const *envp;
|
|
const char *argv[256];
|
|
|
|
va_start (args, arg0);
|
|
argv[0] = arg0;
|
|
i = 1;
|
|
|
|
do
|
|
argv[i] = va_arg (args, const char *);
|
|
while (argv[i++] != NULL);
|
|
|
|
envp = va_arg (args, const char * const *);
|
|
va_end (args);
|
|
|
|
return spawnve (mode, path, (char * const *) argv, (char * const *) envp);
|
|
}
|
|
|
|
extern "C" int
|
|
spawnlp (int mode, const char *file, const char *arg0, ...)
|
|
{
|
|
int i;
|
|
va_list args;
|
|
const char *argv[256];
|
|
path_conv buf;
|
|
|
|
va_start (args, arg0);
|
|
argv[0] = arg0;
|
|
i = 1;
|
|
|
|
do
|
|
argv[i] = va_arg (args, const char *);
|
|
while (argv[i++] != NULL);
|
|
|
|
va_end (args);
|
|
|
|
return spawnve (mode | _P_PATH_TYPE_EXEC, find_exec (file, buf),
|
|
(char * const *) argv, cur_environ ());
|
|
}
|
|
|
|
extern "C" int
|
|
spawnlpe (int mode, const char *file, const char *arg0, ...)
|
|
{
|
|
int i;
|
|
va_list args;
|
|
const char * const *envp;
|
|
const char *argv[256];
|
|
path_conv buf;
|
|
|
|
va_start (args, arg0);
|
|
argv[0] = arg0;
|
|
i = 1;
|
|
|
|
do
|
|
argv[i] = va_arg (args, const char *);
|
|
while (argv[i++] != NULL);
|
|
|
|
envp = va_arg (args, const char * const *);
|
|
va_end (args);
|
|
|
|
return spawnve (mode | _P_PATH_TYPE_EXEC, find_exec (file, buf),
|
|
(char * const *) argv, envp);
|
|
}
|
|
|
|
extern "C" int
|
|
spawnv (int mode, const char *path, const char * const *argv)
|
|
{
|
|
return spawnve (mode, path, argv, cur_environ ());
|
|
}
|
|
|
|
extern "C" int
|
|
spawnvp (int mode, const char *file, const char * const *argv)
|
|
{
|
|
path_conv buf;
|
|
return spawnve (mode | _P_PATH_TYPE_EXEC, find_exec (file, buf), argv,
|
|
cur_environ ());
|
|
}
|
|
|
|
extern "C" int
|
|
spawnvpe (int mode, const char *file, const char * const *argv,
|
|
const char * const *envp)
|
|
{
|
|
path_conv buf;
|
|
return spawnve (mode | _P_PATH_TYPE_EXEC, find_exec (file, buf), argv, envp);
|
|
}
|
|
|
|
int
|
|
av::fixup (const char *prog_arg, path_conv& real_path, const char *ext,
|
|
bool p_type_exec)
|
|
{
|
|
const char *p;
|
|
bool exeext = ascii_strcasematch (ext, ".exe");
|
|
if ((exeext && real_path.iscygexec ()) || ascii_strcasematch (ext, ".bat"))
|
|
return 0;
|
|
if (!*ext && ((p = ext - 4) > real_path.get_win32 ())
|
|
&& (ascii_strcasematch (p, ".bat") || ascii_strcasematch (p, ".cmd")
|
|
|| ascii_strcasematch (p, ".btm")))
|
|
return 0;
|
|
while (1)
|
|
{
|
|
char *pgm = NULL;
|
|
char *arg1 = NULL;
|
|
char *ptr, *buf;
|
|
OBJECT_ATTRIBUTES attr;
|
|
IO_STATUS_BLOCK io;
|
|
HANDLE h;
|
|
NTSTATUS status;
|
|
LARGE_INTEGER size;
|
|
|
|
status = NtOpenFile (&h, SYNCHRONIZE | GENERIC_READ,
|
|
real_path.get_object_attr (attr, sec_none_nih),
|
|
&io, FILE_SHARE_VALID_FLAGS,
|
|
FILE_SYNCHRONOUS_IO_NONALERT
|
|
| FILE_OPEN_FOR_BACKUP_INTENT
|
|
| FILE_NON_DIRECTORY_FILE);
|
|
if (!NT_SUCCESS (status))
|
|
{
|
|
/* File is not readable? Doesn't mean it's not executable.
|
|
Test for executability and if so, just assume the file is
|
|
a cygwin executable and go ahead. */
|
|
if (status == STATUS_ACCESS_DENIED && real_path.has_acls ()
|
|
&& check_file_access (real_path, X_OK, true) == 0)
|
|
{
|
|
real_path.set_cygexec (true);
|
|
break;
|
|
}
|
|
goto err;
|
|
}
|
|
if (!GetFileSizeEx (h, &size))
|
|
{
|
|
NtClose (h);
|
|
goto err;
|
|
}
|
|
if (size.QuadPart > wincap.allocation_granularity ())
|
|
size.LowPart = wincap.allocation_granularity ();
|
|
|
|
HANDLE hm = CreateFileMapping (h, &sec_none_nih, PAGE_READONLY,
|
|
0, 0, NULL);
|
|
NtClose (h);
|
|
if (!hm)
|
|
{
|
|
/* ERROR_FILE_INVALID indicates very likely an empty file. */
|
|
if (GetLastError () == ERROR_FILE_INVALID)
|
|
{
|
|
debug_printf ("zero length file, treat as script.");
|
|
goto just_shell;
|
|
}
|
|
goto err;
|
|
}
|
|
/* Try to map the first 64K of the image. That's enough for the local
|
|
tests, and it's enough for hook_or_detect_cygwin to compute the IAT
|
|
address. */
|
|
buf = (char *) MapViewOfFile (hm, FILE_MAP_READ, 0, 0, size.LowPart);
|
|
if (!buf)
|
|
{
|
|
CloseHandle (hm);
|
|
goto err;
|
|
}
|
|
|
|
{
|
|
myfault efault;
|
|
if (efault.faulted ())
|
|
{
|
|
UnmapViewOfFile (buf);
|
|
CloseHandle (hm);
|
|
real_path.set_cygexec (false);
|
|
break;
|
|
}
|
|
if (buf[0] == 'M' && buf[1] == 'Z')
|
|
{
|
|
WORD subsys;
|
|
unsigned off = (unsigned char) buf[0x18] | (((unsigned char) buf[0x19]) << 8);
|
|
win16_exe = off < sizeof (IMAGE_DOS_HEADER);
|
|
if (!win16_exe)
|
|
real_path.set_cygexec (!!hook_or_detect_cygwin (buf, NULL,
|
|
subsys, hm));
|
|
else
|
|
real_path.set_cygexec (false);
|
|
UnmapViewOfFile (buf);
|
|
CloseHandle (hm);
|
|
break;
|
|
}
|
|
}
|
|
CloseHandle (hm);
|
|
|
|
debug_printf ("%s is possibly a script", real_path.get_win32 ());
|
|
|
|
ptr = buf;
|
|
if (*ptr++ == '#' && *ptr++ == '!')
|
|
{
|
|
ptr += strspn (ptr, " \t");
|
|
size_t len = strcspn (ptr, "\r\n");
|
|
if (len)
|
|
{
|
|
char *namebuf = (char *) alloca (len + 1);
|
|
memcpy (namebuf, ptr, len);
|
|
namebuf[len] = '\0';
|
|
for (ptr = pgm = namebuf; *ptr; ptr++)
|
|
if (!arg1 && (*ptr == ' ' || *ptr == '\t'))
|
|
{
|
|
/* Null terminate the initial command and step over any
|
|
additional white space. If we've hit the end of the
|
|
line, exit the loop. Otherwise, we've found the first
|
|
argument. Position the current pointer on the last known
|
|
white space. */
|
|
*ptr = '\0';
|
|
char *newptr = ptr + 1;
|
|
newptr += strspn (newptr, " \t");
|
|
if (!*newptr)
|
|
break;
|
|
arg1 = newptr;
|
|
ptr = newptr - 1;
|
|
}
|
|
}
|
|
}
|
|
UnmapViewOfFile (buf);
|
|
just_shell:
|
|
if (!pgm)
|
|
{
|
|
if (!p_type_exec)
|
|
{
|
|
/* Not called from exec[lv]p. Don't try to treat as script. */
|
|
debug_printf ("%s is not a valid executable",
|
|
real_path.get_win32 ());
|
|
set_errno (ENOEXEC);
|
|
return -1;
|
|
}
|
|
if (ascii_strcasematch (ext, ".com"))
|
|
break;
|
|
pgm = (char *) "/bin/sh";
|
|
arg1 = NULL;
|
|
}
|
|
|
|
/* Check if script is executable. Otherwise we start non-executable
|
|
scripts successfully, which is incorrect behaviour. */
|
|
if (real_path.has_acls ()
|
|
&& check_file_access (real_path, X_OK, true) < 0)
|
|
return -1; /* errno is already set. */
|
|
|
|
/* Replace argv[0] with the full path to the script if this is the
|
|
first time through the loop. */
|
|
replace0_maybe (prog_arg);
|
|
|
|
/* pointers:
|
|
* pgm interpreter name
|
|
* arg1 optional string
|
|
*/
|
|
if (arg1)
|
|
unshift (arg1);
|
|
|
|
/* FIXME: This should not be using FE_NATIVE. It should be putting
|
|
the posix path on the argv list. */
|
|
find_exec (pgm, real_path, "PATH=", FE_NATIVE, &ext);
|
|
unshift (real_path.get_win32 (), 1);
|
|
}
|
|
return 0;
|
|
|
|
err:
|
|
__seterrno ();
|
|
return -1;
|
|
}
|