Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

os.execute without inheriting parent's fds

I have a problem analogous to the one described here: Prevent fork() from copying sockets

Basically, inside my Lua script I'm spawning another script which:

  • doesn't require communicating with my script either way
  • continues running after my script had finished
  • is a 3rd party program, code of which I have no control over

The problem is that my Lua script opens a TCP socket to listen on a specific port and after it's quit and despite an explicit server:close() the child (or more specifically its children) holds onto the socket and keeps the port open (in LISTEN state) preventing my script from running again.

Here's example code that demonstrates the problem:

require('socket')

print('listening')
s = socket.bind("*", 9999)
s:settimeout(1)

while true do
    print('accepting connection')
    local c = s:accept()
    if c then
            c:settimeout(1)
            local rec = c:receive()
            print('received ' .. rec)
            c:close()
            if rec == "quit" then break end
            if rec == "exec" then 
                    print('running ping in background')
                    os.execute('sleep 10s &')
                    break
            end     
    end
end
print('closing server')
s:close()

If I run the above script and echo quit | nc localhost 9999 everything works well - program quits and port is closed.

However if I do echo exec | nc localhost 9999 the program quits but the port is blocked by the spawned sleep (as confirmed by netstat -lpn) until it exits.

How do I tackle this in the simplest possible manner, preferably without adding any extra dependencies.

like image 901
koniu Avatar asked Jan 29 '11 06:01

koniu


3 Answers

I found a much simpler solution which utilizes the fact that os.execute(cmd) runs cmd in a shell, which, as it turns out, is capable of closing file descriptors as seen here:

  • http://linux.die.net/man/1/ash (section Redirections )

  • http://www.gnu.org/software/bash/manual/bashref.html#Redirections


For example (tested in ash):

    exec 3<&-                                      # closes fd3
    exec 3<&- 4<&-                                 # closes fd3 and fd4
    eval exec `seq 1 255 | sed -e 's/.*/&<\&-/'`   # closes all file descriptors 

So in my luasocket based example it's enough to replace:

    os.execute('sleep 10s &')

with:

    os.execute("eval exec `seq 1 255 | sed -e 's/.*/&<\\&-/'`; sleep 10s &")

This closes all file descriptors, including my server socket, before executing the actual command (here sleep 10s) so that it doesn't hog the port after my script exits. It also has the bonus of taking care of stdout and stderr redirection.

This is much more compact and uncomplicated than working around Lua's limitations and doesn't require any extra dependencies. Thanks go to #uclibc where I got some brilliant help with final shell syntax from the embedded linux crew.

like image 129
koniu Avatar answered Oct 24 '22 04:10

koniu


I'm not sure if you're going to be able to do it like that if you want to keep the s:close only at the end of the whole program. You might succeed by moving it up into before os.execute, since you're breaking anyway (but you're probably not doing this in your real program). Edit for clarity: The actual problem is that the only place you're spawning a subprocess in this case, is by using os.execute(), and you have no control over the child environment of sleep, in which everything is inherited from the main program, including socket and file descriptors.

So, the canonical way to do this on POSIX is to use fork(); close(s); exec();, instead of system() (aka, os.execute) as system()/os.execute will hang on to the current process state during execution, and you won't be able to close it while blocked in the sub-process.

So, a suggestion would be to grab luaposix, and use its posix.fork() and posix.exec() functionality, and calling s:close() in the forked child process. Shouldn't be so bad, since you're already using an external package by relying upon luasocket.


EDIT: Here's heavily commented code to do it with luaposix:

require('socket')
require('posix')

print('listening')
s = socket.bind("*", 9999)
s:settimeout(1)

while true do
    print('accepting connection')
    local c = s:accept()
    if c then
            c:settimeout(1)
            local rec = c:receive()
            print('received ' .. rec)
            c:close()
            if rec == "quit" then break end
            if rec == "exec" then
                    local pid = posix.fork()
                    if pid == 0 then
                        print('child: running ping in background')
                        s:close()
                        -- exec() replaces current process, doesn't return.
                        -- execp has PATH resolution
                        rc = posix.execp('sleep','60s');
                        -- exec has no PATH resolution, probably "more secure"
                        --rc = posix.exec('/usr/bin/sleep','60s');
                        print('exec failed with rc: ' .. rc);
                    else
                        -- if you want to catch the SIGCHLD:
                        --print('parent: waiting for ping to return')
                        --posix.wait( pid )
                        print('parent: exiting loop')
                    end
                    break;
            end
    end
end
print('closing server')
s:close()

This closes the socket in the child process before calling exec, and the netstat -nlp output shows the system is correctly no longer listening on port 9999 when the parent exits.

P.S. The line print('exec failed with rc: ' .. rc); complained about a type problem once when exec failed. I don't actually know lua, so you'll have to fix that. :) Furthermore, fork() can fail, returning -1. Probably should check for that in your main code, too, for completeness.

like image 29
Sdaz MacSkibbons Avatar answered Oct 24 '22 02:10

Sdaz MacSkibbons


The POSIX approach is to set your file descriptors with the FD_CLOEXEC flag, using fcntl (2). When set, all sub-processes will not inherit the file descriptors marked with that flag.

Stock Lua doesn't have the fcntl feature but it's possible to add it with the lua posix module as introduced in previous answers. To take your example, you would have to change the starts as such:

require('socket')
require('posix')
s = socket.bind("*", 9999)
posix.setfl(s, posix.FD_CLOEXEC)
s:settimeout(1)

Note that I didn't find the FD_CLOEXEC constant in the luaposix source so you might have to add it by hand as well.

like image 1
zimbatm Avatar answered Oct 24 '22 02:10

zimbatm