Skip to content

Commit

Permalink
🤘
Browse files Browse the repository at this point in the history
  • Loading branch information
evanphx committed Jul 27, 2016
0 parents commit 8b9700e
Show file tree
Hide file tree
Showing 89 changed files with 26,860 additions and 0 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Puma-dev: A development server for OS X

Puma-dev is the emotional successor to pow. It provides a quick and easy way to manage apps in development on OS X.

## Highlights

* Easy startup and idle shutdown of rack/rails apps
* Easy access to the apps using the `.pdev` subdomain **(configurable)**


### Why not just use pow?

Pow doesn't support rack.hijack and thus not websockets and thus not actioncable. So for all those new Rails 5 apps, pow is a no-go. Puma-dev fills that hole.

### Options

Run: `puma-dev -h`

You have the ability to configure most of the values that you'll use day-to-day.

### Setup

Run: `sudo puma-dev -setup`.

This will configure the bits that require root access. If you're currently using pow and want to just try out puma-dev, I suggest using `sudo puma-dev -setup -setup-skip-80` to not install the port 80 redirect rule that will conflict with pow. You can still access apps, you'll just need to add port `9280` to your requests, such as `http://test.pdev:9280`.

### Quickstart

Run: `puma-dev`

Puma-dev will startup by default using the directory `~/.puma-dev`, looking for symlinks to apps just like pow. Drop a symlink to your app in there as: `cd ~/.puma-dev; ln -s test /path/to/my/app`. You can now access your app as `test.pdev`.

### Coming from Pow

By default, puma-dev uses the domain `.pdev` to manage your apps, so that it doesn't interfer with a pow installation. If you want to have puma-dev take over for pow entirely, just run `puma-dev -pow`. Puma-dev will now use the `.dev` domain and look for apps in `~/.pow`.
83 changes: 83 additions & 0 deletions src/cmd/puma-dev/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"strings"
"time"

"github.com/mitchellh/go-homedir"

"puma/dev"
)

var (
fDomains = flag.String("d", "pdev", "domains to handle, separate with :")
fPort = flag.Int("dns-port", 9253, "port to listen on dns for")
fHTTPPort = flag.Int("http-port", 9280, "port to listen on http for")
fDir = flag.String("dir", "~/.puma-dev", "directory to watch for apps")
fTimeout = flag.Duration("timeout", 15*60*time.Second, "how long to let an app idle for")
fPow = flag.Bool("pow", false, "Mimic pow's settings")

fSetup = flag.Bool("setup", false, "Run system setup")
fSetupSkipHTTP = flag.Bool("setup-skip-80", false, "Indicate if a firewall rule to redirect port 80 to our port should be skipped")
)

func main() {
flag.Parse()

if *fSetup {
err := dev.Setup(*fSetupSkipHTTP)
if err != nil {
log.Fatalf("Unable to configure OS X resolver: %s", err)
}
return
}

if *fPow {
*fDomains = "dev"
*fDir = "~/.pow"
}

dir, err := homedir.Expand(*fDir)
if err != nil {
log.Fatalf("Unable to expand dir: %s", err)
}

err = os.MkdirAll(dir, 0755)
if err != nil {
log.Fatalf("Unable to create dir '%s': %s", dir, err)
}

var pool dev.AppPool
pool.Dir = dir
pool.IdleTime = *fTimeout

domains := strings.Split(*fDomains, ":")

err = dev.ConfigureResolver(domains, *fPort)
if err != nil {
log.Fatalf("Unable to configure OS X resolver: %s", err)
}

fmt.Printf("* Directory for apps: %s\n", dir)
fmt.Printf("* Domains: %s\n", strings.Join(domains, ", "))
fmt.Printf("* DNS Server port: %d\n", *fPort)
fmt.Printf("* HTTP Server port: %d\n", *fHTTPPort)

var dns dev.DNSResponder

dns.Address = fmt.Sprintf("127.0.0.1:%d", *fPort)

go dns.Serve(domains)

var http dev.HTTPServer

http.Address = fmt.Sprintf("127.0.0.1:%d", *fHTTPPort)
http.Pool = &pool

fmt.Printf("! Puma dev listening\n")
http.Serve()
}
215 changes: 215 additions & 0 deletions src/puma/dev/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package dev

import (
"bufio"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"sync"
"time"

"gopkg.in/tomb.v2"
)

var ErrUnexpectedExit = errors.New("unexpected exit")

type App struct {
Name string
Port int
Command *exec.Cmd

t tomb.Tomb

listener net.Listener

stdout io.Reader
lock sync.Mutex
pool *AppPool
lastUse time.Time
}

func (a *App) Address() string {
return fmt.Sprintf("localhost:%d", a.Port)
}

func (a *App) watch() error {
c := make(chan error)

go func() {
r := bufio.NewReader(a.stdout)

for {
line, err := r.ReadString('\n')
if line != "" {
fmt.Fprintf(os.Stdout, "%s[%d]: %s", a.Name, a.Command.Process.Pid, line)
}

if err != nil {
c <- err
return
}
}
}()

var err error

select {
case err = <-c:
err = ErrUnexpectedExit
case <-a.t.Dying():
a.Command.Process.Kill()
err = nil
}

a.Command.Wait()
a.pool.remove(a)
a.listener.Close()

return err
}

func (a *App) idleMonitor() error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
if a.pool.maybeIdle(a) {
a.Command.Process.Kill()
}
return nil
case <-a.t.Dying():
return nil
}
}

return nil
}

func (a *App) UpdateUsed() {
a.lastUse = time.Now()
}

func LaunchApp(pool *AppPool, name, dir string) (*App, error) {
// Create a listener socket and inject it
l, err := net.Listen("tcp", ":0")
if err != nil {
return nil, err
}

addr := l.Addr().(*net.TCPAddr)

cmd := exec.Command("bundle", "exec", "puma", "-C-",
"--tag", fmt.Sprintf("puma-dev:%s", name),
"-b", fmt.Sprintf("tcp://127.0.0.1:%d", addr.Port))

cmd.Dir = dir

cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("PUMA_INHERIT_0=3:tcp://127.0.0.1:%d", addr.Port))

tcpListener := l.(*net.TCPListener)
socket, err := tcpListener.File()
if err != nil {
return nil, err
}

cmd.ExtraFiles = []*os.File{socket}

cmd.Stderr = os.Stderr

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}

err = cmd.Start()
if err != nil {
return nil, err
}

fmt.Printf("! Booted app '%s' on port %d\n", name, addr.Port)

app := &App{
Name: name,
Port: addr.Port,
Command: cmd,
listener: l,
stdout: stdout,
}

app.t.Go(app.watch)
app.t.Go(app.idleMonitor)

return app, nil
}

type AppPool struct {
Dir string
IdleTime time.Duration

lock sync.Mutex
apps map[string]*App
}

func (a *AppPool) maybeIdle(app *App) bool {
a.lock.Lock()
defer a.lock.Unlock()

diff := time.Since(app.lastUse)
if diff > a.IdleTime {
delete(a.apps, app.Name)
return true
}

return false
}

func (a *AppPool) App(name string) (*App, error) {
a.lock.Lock()
defer a.lock.Unlock()

if a.apps == nil {
a.apps = make(map[string]*App)
}

app, ok := a.apps[name]
if ok {
app.UpdateUsed()
return app, nil
}

path := filepath.Join(a.Dir, name)

_, err := os.Stat(path)
if os.IsNotExist(err) {
return nil, fmt.Errorf("Unknown app: %s", name)
}

app, err = LaunchApp(a, name, path)
if err != nil {
return nil, err
}

app.pool = a

app.UpdateUsed()
a.apps[name] = app

return app, nil
}

func (a *AppPool) remove(app *App) {
a.lock.Lock()
defer a.lock.Unlock()

fmt.Printf("! Shutdown app '%s'\n", app.Name)

delete(a.apps, app.Name)
}
Loading

0 comments on commit 8b9700e

Please sign in to comment.