-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8b9700e
Showing
89 changed files
with
26,860 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.