Skip to content

Commit 3b71b7a

Browse files
author
Michael Lauer
committed
passing socket to stdin working, to first order
1 parent 5307386 commit 3b71b7a

File tree

4 files changed

+139
-15
lines changed

4 files changed

+139
-15
lines changed

dialer.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package gofcgisrv
2+
3+
import (
4+
"crypto/rand"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"os"
9+
"os/exec"
10+
"path"
11+
)
12+
13+
type Dialer interface {
14+
Dial() (net.Conn, error)
15+
}
16+
17+
type TCPDialer struct {
18+
addr string
19+
}
20+
21+
func (d TCPDialer) Dial() (net.Conn, error) {
22+
return net.Dial("tcp", d.addr)
23+
}
24+
25+
// StdinDialer managers an app as a child process, creating a socket and passing it through stdin.
26+
type StdinDialer struct {
27+
app string
28+
args []string
29+
cmd *exec.Cmd
30+
stdin *os.File
31+
listener net.Listener
32+
filename string
33+
}
34+
35+
func (sd *StdinDialer) Dial() (net.Conn, error) {
36+
if sd.stdin == nil {
37+
return nil, errors.New("No file")
38+
}
39+
return net.Dial("unix", sd.filename)
40+
}
41+
42+
func (sd *StdinDialer) Start() error {
43+
// Create a socket.
44+
// We'll use the high-level net API, creating a listener that does all sorts
45+
// of socket stuff, getting its file, and passing that (really just for its FD)
46+
// to the child process.
47+
// We'll rely on crypt/rand to get a unique filename for the socket.
48+
tmpdir := os.TempDir()
49+
rnd := make([]byte, 8)
50+
n, err := rand.Read(rnd)
51+
if err != nil {
52+
return err
53+
}
54+
basename := fmt.Sprintf("fcgi%x", rnd[:n])
55+
filename := path.Join(tmpdir, basename)
56+
57+
listener, err := net.Listen("unix", filename)
58+
if err != nil {
59+
return err
60+
}
61+
socket, err := listener.(*net.UnixListener).File()
62+
if err != nil {
63+
listener.Close()
64+
return err
65+
}
66+
cmd := exec.Command(sd.app, sd.args...)
67+
cmd.Stdin = socket
68+
cmd.Stdout = os.Stdout
69+
cmd.Stderr = os.Stderr
70+
err = cmd.Start()
71+
if err != nil {
72+
socket.Close()
73+
listener.Close()
74+
return err
75+
}
76+
sd.stdin = socket
77+
sd.listener = listener
78+
sd.cmd = cmd
79+
sd.filename = filename
80+
return nil
81+
}
82+
83+
func (sd *StdinDialer) Close() {
84+
if sd.stdin != nil {
85+
sd.stdin.Close()
86+
sd.stdin = nil
87+
}
88+
if sd.listener != nil {
89+
sd.listener.Close()
90+
sd.listener = nil
91+
}
92+
if sd.cmd != nil {
93+
sd.cmd.Process.Kill()
94+
sd.cmd = nil
95+
}
96+
sd.filename = ""
97+
}

gofcgisrv.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ func (f RequesterFunc) Request(env []string, stdin io.Reader, stdout io.Writer,
3737
// Server is the external interface. It manages connections to a single FastCGI application.
3838
// A server may maintain many connections, each of which may multiplex many requests.
3939
type Server struct {
40-
applicationAddr string
41-
connections []*conn
42-
reqLock sync.Mutex
43-
reqCond *sync.Cond
44-
initialized bool
40+
dialer Dialer
41+
connections []*conn
42+
reqLock sync.Mutex
43+
reqCond *sync.Cond
44+
initialized bool
4545

4646
// Parameters of the application
4747
CanMultiplex bool
@@ -51,7 +51,18 @@ type Server struct {
5151

5252
// NewServer creates a server that will attempt to connect to the application at the given address over TCP.
5353
func NewServer(applicationAddr string) *Server {
54-
s := &Server{applicationAddr: applicationAddr}
54+
s := &Server{}
55+
s.dialer = TCPDialer{addr: applicationAddr}
56+
s.MaxConns = 1
57+
s.MaxRequests = 1
58+
s.reqCond = sync.NewCond(&s.reqLock)
59+
return s
60+
}
61+
62+
// NewFCGIStdin creates a server that runs the app and connects over stdin.
63+
func NewFCGIStdin(app string, args ...string) *Server {
64+
s := &Server{}
65+
s.dialer = &StdinDialer{app: app, args: args}
5566
s.MaxConns = 1
5667
s.MaxRequests = 1
5768
s.reqCond = sync.NewCond(&s.reqLock)
@@ -89,7 +100,7 @@ func (s *Server) processGetValuesResult(rec record) (int, error) {
89100
// PHP barfs on FCGI_GET_VALUES. I don't know why. Maybe it expects a different connection.
90101
// For now don't do it unless asked.
91102
func (s *Server) GetValues() error {
92-
c, err := net.Dial("tcp", s.applicationAddr)
103+
c, err := s.dialer.Dial()
93104
time.AfterFunc(time.Second, func() { c.Close() })
94105
if err != nil {
95106
return err
@@ -147,7 +158,7 @@ func (s *Server) Request(env []string, stdin io.Reader, stdout io.Writer, stderr
147158
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
148159
env := HTTPEnv(nil, r)
149160
buffer := bytes.NewBuffer(nil)
150-
s.Request(env, r.Body, buffer, buffer)
161+
s.Request(env, r.Body, buffer, os.Stderr)
151162

152163
// Add any headers produced by the application, and skip to the response.
153164
ProcessResponse(buffer, w, r)
@@ -170,7 +181,7 @@ func (s *Server) newRequest() (*request, error) {
170181
s.reqCond.Wait()
171182
}
172183
// We will always need to create a new connection, for now.
173-
netconn, err := net.Dial("tcp", s.applicationAddr)
184+
netconn, err := s.dialer.Dial()
174185
if err != nil {
175186
return nil, err
176187
}

py_test.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func waitForConn(addr string, timeout time.Duration) error {
3333
}
3434

3535
func TestPyServer(t *testing.T) {
36-
cmd := exec.Command("python", "./testdata/cgi_test.py", "--port=9001")
36+
cmd := exec.Command("python", "./testdata/cgi_test.py", "--host=127.0.0.1", "--port=9001")
3737
cmd.Stdout = os.Stdout
3838
cmd.Stderr = os.Stderr
3939
err := cmd.Start()
@@ -65,7 +65,7 @@ func TestPyCGI(t *testing.T) {
6565
}
6666

6767
func TestPySCGI(t *testing.T) {
68-
cmd := exec.Command("python", "./testdata/cgi_test.py", "--scgi", "--port=9002")
68+
cmd := exec.Command("python", "./testdata/cgi_test.py", "--scgi", "--host=127.0.0.1", "--port=9002")
6969
// flup barfs some output. Why?? Seems wrong to me.
7070
err := cmd.Start()
7171
if err != nil {
@@ -82,3 +82,16 @@ func TestPySCGI(t *testing.T) {
8282
expected: "This is a test",
8383
})
8484
}
85+
86+
func TestPyFcgiStdin(t *testing.T) {
87+
s := NewFCGIStdin("python", "./testdata/cgi_test.py", "--fcgi")
88+
s.dialer.(*StdinDialer).Start()
89+
defer s.dialer.(*StdinDialer).Close()
90+
testRequester(t, httpTestData{
91+
name: "py fastcgi stdin",
92+
f: s,
93+
body: strings.NewReader("This is a test"),
94+
status: 200,
95+
expected: "This is a test",
96+
})
97+
}

testdata/cgi_test.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ def resp():
1313

1414
if __name__ == '__main__':
1515
parser = argparse.ArgumentParser(description="FastCGI test client application")
16-
parser.add_argument('--host', type=str, default='127.0.0.1', help='Hostname to listen on')
17-
parser.add_argument('--port', type=int, default='9000', help='Port to listen on')
16+
parser.add_argument('--host', type=str, help='Hostname to listen on')
17+
parser.add_argument('--port', type=int, help='Port to listen on')
1818
group = parser.add_mutually_exclusive_group()
1919
group.add_argument('--fcgi', action="store_true", help='Run as a FastCGI server')
2020
group.add_argument('--cgi', action="store_true", help='Run as a CGI server')
2121
group.add_argument('--scgi', action="store_true", help='Run as an SCGI server')
2222
args = parser.parse_args()
2323

24-
kwargs = {'bindAddress' : (args.host, args.port) }
24+
if args.host and args.port:
25+
kwargs = {'bindAddress' : (args.host, args.port) }
26+
else:
27+
kwargs = {}
2528
if args.cgi:
2629
import flup.server.cgi as cgimod
2730
kwargs = {}
@@ -32,5 +35,5 @@ def resp():
3235

3336
WSGIServer = cgimod.WSGIServer
3437

35-
WSGIServer(echo, **kwargs).run()
38+
result = WSGIServer(echo, **kwargs).run()
3639

0 commit comments

Comments
 (0)