Feature/cli enhancement (#81)

* feat(wren-launcher): makefile

* feat(wren-launcher): detect os

* fix(wren-launcher): fix panic error recovery and windows open docker

* fix(wren-launcher): use exe extension

* feat(wren-launcher): find available port

* feat(wren-launcher): testing list process of docker API

* feat(wren-launcher): check port not used and if it's used, check if wren-ui is using it
This commit is contained in:
william chang(張仲威)
2024-04-08 14:12:56 +08:00
committed by GitHub
parent eb8c2cb73d
commit efc7a2f675
8 changed files with 223 additions and 35 deletions
+4
View File
@@ -11,3 +11,7 @@ trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
# Makefile
[Makefile]
indent_style = tab
+3
View File
@@ -67,3 +67,6 @@ yarn-error.log*
## typescript
*.tsbuildinfo
next-env.d.ts
# wren-launcher
wren-launcher/dist
+12
View File
@@ -0,0 +1,12 @@
BINARY_NAME=wren-launcher
build:
env GOARCH=amd64 GOOS=darwin CGO_ENABLED=1 go build -o dist/${BINARY_NAME}-darwin main.go
env GOARCH=amd64 GOOS=linux go build -o dist/${BINARY_NAME}-linux main.go
env GOARCH=amd64 GOOS=windows go build -o dist/${BINARY_NAME}-windows.exe main.go
clean:
go clean
rm -rf dist
rebuild: clean build
+16 -22
View File
@@ -4,9 +4,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"runtime"
"strings"
"time"
@@ -61,23 +59,15 @@ func askForAPIKey() (string, error) {
return result, nil
}
func openbrowser(url string) error {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
func Launch() {
// recover from panic
defer func() {
if r := recover(); r != nil {
pterm.Error.Println("An error occurred:", r)
fmt.Scanf("h")
}
}()
// print WrenAI header
fmt.Println(strings.Repeat("=", 55))
myFigure := figure.NewFigure("WrenAI", "", true)
@@ -90,7 +80,7 @@ func Launch() {
if err != nil {
pterm.Error.Println("Failed to get API key")
return
panic(err)
}
// check if docker daemon is running, if not, open it and loop to check again
@@ -116,7 +106,10 @@ func Launch() {
// download docker-compose file and env file template for WrenAI
pterm.Info.Println("Downloading docker-compose file and env file")
err = utils.PrepareDockerFiles(apiKey, projectDir)
// find an available port
defaultPort := 3000
port := utils.FindAvailablePort(defaultPort)
err = utils.PrepareDockerFiles(apiKey, port, projectDir)
if err != nil {
panic(err)
}
@@ -131,6 +124,7 @@ func Launch() {
// wait for 10 seconds
pterm.Info.Println("WrenAI is starting, please wait for a moment...")
url := fmt.Sprintf("http://localhost:%d", port)
// wait until checking if CheckWrenAIStarted return without error
// if timeout 2 minutes, panic
timeoutTime := time.Now().Add(2 * time.Minute)
@@ -140,7 +134,7 @@ func Launch() {
}
// check if WrenAI is started
err = utils.CheckWrenAIStarted()
err = utils.CheckWrenAIStarted(url)
if err == nil {
break
}
@@ -149,7 +143,7 @@ func Launch() {
// open browser
pterm.Info.Println("Opening browser")
openbrowser("http://localhost:3000")
utils.Openbrowser(url)
pterm.Info.Println("You can now safely close this terminal window")
fmt.Scanf("h")
+65 -13
View File
@@ -6,7 +6,6 @@ import (
"io"
"net/http"
"os"
"os/exec"
"path"
"regexp"
@@ -15,6 +14,8 @@ import (
cmdCompose "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pterm/pterm"
)
@@ -23,9 +24,15 @@ const (
DOCKER_COMPOSE_ENV_URL string = "https://gist.githubusercontent.com/wwwy3y3/5fee68a54458a07abbeb573711652292/raw/c8965ceba6eae274d2eec0595fd12b0989880ba3/.env.example"
)
func replaceEnvFileContent(content string, OpenaiApiKey string) string {
func replaceEnvFileContent(content string, OpenaiApiKey string, port int) string {
// replace OPENAI_API_KEY
reg := regexp.MustCompile(`OPENAI_API_KEY=sk-(.*)`)
return reg.ReplaceAllString(content, "OPENAI_API_KEY="+OpenaiApiKey)
str := reg.ReplaceAllString(content, "OPENAI_API_KEY="+OpenaiApiKey)
// replace PORT
reg = regexp.MustCompile(`HOST_PORT=(.*)`)
str = reg.ReplaceAllString(str, "HOST_PORT="+fmt.Sprintf("%d", port))
return str
}
func downloadFile(filepath string, url string) error {
@@ -69,7 +76,7 @@ func CheckDockerDaemonRunning() (bool, error) {
return true, nil
}
func PrepareDockerFiles(OpenaiApiKey string, projectDir string) error {
func PrepareDockerFiles(openaiApiKey string, port int, projectDir string) error {
// download docker-compose file
composeFile := path.Join(projectDir, "docker-compose.yaml")
pterm.Info.Println("Downloading docker-compose file to", composeFile)
@@ -94,7 +101,7 @@ func PrepareDockerFiles(OpenaiApiKey string, projectDir string) error {
}
// replace the content with regex
newEnvFileContent := replaceEnvFileContent(string(envFileContent), OpenaiApiKey)
newEnvFileContent := replaceEnvFileContent(string(envFileContent), openaiApiKey, port)
newEnvFile := path.Join(projectDir, ".env")
// write the file
err = os.WriteFile(newEnvFile, []byte(newEnvFileContent), 0644)
@@ -159,19 +166,64 @@ func RunDockerCompose(projectName string, projectDir string) error {
return nil
}
func OpenDockerDaemon() error {
// open docker daemon with command
cmd := exec.Command("open", "-a", "Docker")
if err := cmd.Run(); err != nil {
return err
func listProcess() ([]types.Container, error) {
ctx := context.Background()
dockerCli, err := command.NewDockerCli()
if err != nil {
return nil, err
}
return nil
err = dockerCli.Initialize(flags.NewClientOptions())
if err != nil {
return nil, err
}
containerListOptions := container.ListOptions{
All: true,
}
containers, err := dockerCli.Client().ContainerList(ctx, containerListOptions)
if err != nil {
return nil, err
}
return containers, nil
}
func CheckWrenAIStarted() error {
func findWrenUIContainer() (types.Container, error) {
containers, err := listProcess()
if err != nil {
return types.Container{}, err
}
for _, container := range containers {
// return if com.docker.compose.project == wrenai && com.docker.compose.service=wren-ui
if container.Labels["com.docker.compose.project"] == "wrenai" && container.Labels["com.docker.compose.service"] == "wren-ui" {
return container, nil
}
}
return types.Container{}, fmt.Errorf("WrenUI container not found")
}
func IfPortUsedByWrenUI(port int) bool {
container, err := findWrenUIContainer()
if err != nil {
return false
}
for _, containerPort := range container.Ports {
if containerPort.PublicPort == uint16(port) {
return true
}
}
return false
}
func CheckWrenAIStarted(url string) error {
// check response from localhost:3000
resp, err := http.Get("http://localhost:3000")
resp, err := http.Get(url)
if err != nil {
return err
}
+21
View File
@@ -0,0 +1,21 @@
package utils
import (
"testing"
)
func TestFindWrenUIContainer(t *testing.T) {
container, error := findWrenUIContainer()
if error != nil {
t.Errorf("Error: %v", error)
}
t.Logf("Container ID: %s", container.ID)
t.Logf("Container Name: %s", container.Names[0])
for _, port := range container.Ports {
t.Logf("Container IP: %s", port.IP)
t.Logf("Container Port Type: %s", port.Type)
t.Logf("Container PublicPort: %d", port.PublicPort)
t.Logf("Container PrivatePort: %d", port.PrivatePort)
}
}
+34
View File
@@ -0,0 +1,34 @@
package utils
import (
"fmt"
"net"
"github.com/pterm/pterm"
)
func ifPortAvailable(port int) bool {
// listen on port to check if it's used
_, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
return err != nil
}
func FindAvailablePort(defaultPort int) int {
// Find an available port
// Start from the default port and increment by 1
// until a port is found that is not in use
for port := defaultPort; port < defaultPort+100; port++ {
pterm.Info.Printf("Checking if port %d is available\n", port)
if ifPortAvailable(port) {
// Return the port if it's not used
return port
} else if IfPortUsedByWrenUI(port) {
// Return the port if it's used, but used by wrenAI
return port
}
}
// If no port is available, return 0
return 0
}
+68
View File
@@ -0,0 +1,68 @@
package utils
import (
"fmt"
"os/exec"
"runtime"
)
type OS int
const (
// Windows is the Windows operating system.
Windows OS = iota
// Darwin is the Apple operating system.
Darwin
// Linux is the Linux operating system.
Linux
// Unknown is an unknown operating system.
Unknown
)
func DetectOS() OS {
switch runtime.GOOS {
case "windows":
return Windows
case "darwin":
return Darwin
case "linux":
return Linux
default:
return Unknown
}
}
func Openbrowser(url string) error {
var err error
switch DetectOS() {
case Linux:
err = exec.Command("xdg-open", url).Start()
case Windows:
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case Darwin:
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
func OpenDockerDaemon() error {
var err error
switch DetectOS() {
case Linux:
// systemctl --user start docker-desktop
err = exec.Command("systemctl", "--user", "start", "docker-desktop").Run()
case Windows:
// C:\Program Files\Docker\Docker\Docker Desktop.exe
err = exec.Command("C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe").Run()
case Darwin:
cmd := exec.Command("open", "-a", "Docker")
err = cmd.Run()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}