Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement cobra cli for apps & integrate apps with merged binary compilation #1704

Merged
merged 22 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ check-windows: lint-windows test-windows ## Run linters and tests on windows ima

build: host-apps bin ## Install dependencies, build apps and binaries. `go build` with ${OPTS}

build-merged: ## Install dependencies, build apps and binaries. `go build` with ${OPTS}
${OPTS} go build ${BUILD_OPTS} -o $(BUILD_PATH)skywire ./cmd/skywire-deployment


build-windows: host-apps-windows bin-windows ## Install dependencies, build apps and binaries. `go build` with ${OPTS}

build-static: host-apps-static bin-static ## Build apps and binaries. `go build` with ${OPTS}
Expand All @@ -118,6 +122,10 @@ install-system-linux: build ## Install apps and binaries over those provided by
sudo install -Dm755 $(BUILD_PATH){skywire-cli,skywire-visor} /opt/skywire/bin/ & \
sudo install -Dm755 $(BUILD_PATH)apps/{vpn-server,vpn-client,skysocks-client,skysocks,skychat} /opt/skywire/apps/

install-system-linux-merged: build-merged ## Install apps and binaries over those provided by the linux package - linux package must be installed first!
sudo echo "sudo cache"
sudo install -Dm755 $(BUILD_PATH)skywire /opt/skywire/bin/

install-generate: ## Installs required execs for go generate.
${OPTS} go install github.com/mjibson/esc github.com/vektra/mockery/v2@latest

Expand Down Expand Up @@ -316,9 +324,24 @@ prepare:
chmod +x ./apps/*
sudo echo "sudo cache"

## Prepare to run skywire from source via cmd/skywire-deployment, without compiling binaries
prepare1:
test -d apps && rm -r apps || true
mkdir -p apps
ln ./scripts/_merged-apps/skychat ./apps/
ln ./scripts/_merged-apps/skysocks ./apps/
ln ./scripts/_merged-apps/skysocks-client ./apps/
ln ./scripts/_merged-apps/vpn-server ./apps/
ln ./scripts/_merged-apps/vpn-client ./apps/
chmod +x ./apps/*
sudo echo "sudo cache"

run-source: prepare ## Run skywire from source, without compiling binaries
go run ./cmd/skywire-cli/skywire-cli.go config gen -in | sudo go run ./cmd/skywire-visor/skywire-visor.go -n || true

run-source-merged: prepare1 ## Run skywire from source, without compiling binaries
go run ./cmd/skywire-deployment/skywire.go cli config gen -in | sudo go run ./cmd/skywire-deployment/skywire.go visor -n || true

run-systray: prepare ## Run skywire from source, with vpn server enabled
go run ./cmd/skywire-cli/skywire-cli.go config gen -ni | sudo go run ./cmd/skywire-visor/skywire-visor.go -n --systray || true

Expand Down
330 changes: 330 additions & 0 deletions cmd/apps/skychat/commands/skychat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
// Package commands cmd/apps/skychat/skychat.go
package commands

import (
"context"
"embed"
"encoding/json"
"flag"
"fmt"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"sync"
"time"

ipc "github.com/james-barrow/golang-ipc"
"github.com/spf13/cobra"

"github.com/skycoin/skywire-utilities/pkg/buildinfo"
"github.com/skycoin/skywire-utilities/pkg/cipher"
"github.com/skycoin/skywire-utilities/pkg/netutil"
"github.com/skycoin/skywire/pkg/app"
"github.com/skycoin/skywire/pkg/app/appnet"
"github.com/skycoin/skywire/pkg/app/appserver"
"github.com/skycoin/skywire/pkg/routing"
"github.com/skycoin/skywire/pkg/visor/visorconfig"
)

const (
netType = appnet.TypeSkynet
port = routing.Port(1)
)

// var addr = flag.String("addr", ":8001", "address to bind, put an * before the port if you want to be able to access outside localhost")
var r = netutil.NewRetrier(nil, 50*time.Millisecond, netutil.DefaultMaxBackoff, 5, 2)

var (
addr string
appCl *app.Client
clientCh chan string
conns map[cipher.PubKey]net.Conn // Chat connections
connsMu sync.Mutex
)

// the go embed static points to skywire/cmd/apps/skychat/static

//go:embed static
var embededFiles embed.FS

func init() {
RootCmd.Flags().StringVar(&addr, "addr", ":8001", "address to bind, put an * before the port if you want to be able to access outside localhost")
}

// RootCmd is the root command for skywire-cli
var RootCmd = &cobra.Command{
Use: "skychat",
Short: "skywire chat application",
Long: `
┌─┐┬┌─┬ ┬┌─┐┬ ┬┌─┐┌┬┐
└─┐├┴┐└┬┘│ ├─┤├─┤ │
└─┘┴ ┴ ┴ └─┘┴ ┴┴ ┴ ┴ `,
SilenceErrors: true,
SilenceUsage: true,
DisableSuggestions: true,
DisableFlagsInUseLine: true,
Version: buildinfo.Version(),
Run: func(cmd *cobra.Command, args []string) {

appCl = app.NewClient(nil)
defer appCl.Close()

if _, err := buildinfo.Get().WriteTo(os.Stdout); err != nil {
print(fmt.Sprintf("Failed to output build info: %v\n", err))
}

flag.Parse()
fmt.Println("Successfully started skychat.")

clientCh = make(chan string)
defer close(clientCh)

conns = make(map[cipher.PubKey]net.Conn)
go listenLoop()

if runtime.GOOS == "windows" {
ipcClient, err := ipc.StartClient(visorconfig.SkychatName, nil)
if err != nil {
print(fmt.Sprintf("Error creating ipc server for skychat client: %v\n", err))
setAppError(appCl, err)
os.Exit(1)
}
go handleIPCSignal(ipcClient)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

http.Handle("/", http.FileServer(getFileSystem()))
http.HandleFunc("/message", messageHandler(ctx))
http.HandleFunc("/sse", sseHandler)

url := ""
// address := *addr
address := addr
if len(address) < 5 || (address[:1] != ":" && address[:2] != "*:") {
url = "127.0.0.1:8001"
} else if address[:1] == ":" {
url = "127.0.0.1" + address
} else if address[:2] == "*:" {
url = address[1:]
} else {
url = "127.0.0.1:8001"
}

fmt.Println("Serving HTTP on", url)

if runtime.GOOS != "windows" {
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, os.Interrupt)

go func() {
<-termCh
setAppStatus(appCl, appserver.AppDetailedStatusStopped)
os.Exit(1)
}()
}
setAppStatus(appCl, appserver.AppDetailedStatusRunning)
srv := &http.Server{ //nolint gosec
Addr: url,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
err := srv.ListenAndServe()
if err != nil {
print(err.Error())
setAppError(appCl, err)
os.Exit(1)
}

},
}

// Execute executes root CLI command.
func Execute() {
if err := RootCmd.Execute(); err != nil {
log.Fatal("Failed to execute command: ", err)
}
}

func listenLoop() {
l, err := appCl.Listen(netType, port)
if err != nil {
print(fmt.Sprintf("Error listening network %v on port %d: %v\n", netType, port, err))
setAppError(appCl, err)
return
}

setAppPort(appCl, port)

for {
fmt.Println("Accepting skychat conn...")
conn, err := l.Accept()
if err != nil {
print(fmt.Sprintf("Failed to accept conn: %v\n", err))
return
}
fmt.Println("Accepted skychat conn")

raddr := conn.RemoteAddr().(appnet.Addr)
connsMu.Lock()
conns[raddr.PubKey] = conn
connsMu.Unlock()
fmt.Printf("Accepted skychat conn on %s from %s\n", conn.LocalAddr(), raddr.PubKey)

go handleConn(conn)
}
}

func handleConn(conn net.Conn) {
raddr := conn.RemoteAddr().(appnet.Addr)
for {
buf := make([]byte, 32*1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Failed to read packet:", err)
raddr := conn.RemoteAddr().(appnet.Addr)
connsMu.Lock()
delete(conns, raddr.PubKey)
connsMu.Unlock()
return
}

clientMsg, err := json.Marshal(map[string]string{"sender": raddr.PubKey.Hex(), "message": string(buf[:n])})
if err != nil {
print(fmt.Sprintf("Failed to marshal json: %v\n", err))
}
select {
case clientCh <- string(clientMsg):
fmt.Printf("Received and sent to ui: %s\n", clientMsg)
default:
fmt.Printf("Received and trashed: %s\n", clientMsg)
}
}
}

func messageHandler(ctx context.Context) func(w http.ResponseWriter, rreq *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {

data := map[string]string{}
if err := json.NewDecoder(req.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

pk := cipher.PubKey{}
if err := pk.UnmarshalText([]byte(data["recipient"])); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

addr := appnet.Addr{
Net: netType,
PubKey: pk,
Port: 1,
}
connsMu.Lock()
conn, ok := conns[pk]
connsMu.Unlock()

if !ok {
var err error
err = r.Do(ctx, func() error {
conn, err = appCl.Dial(addr)
return err
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

connsMu.Lock()
conns[pk] = conn
connsMu.Unlock()

go handleConn(conn)
}

_, err := conn.Write([]byte(data["message"]))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)

connsMu.Lock()
delete(conns, pk)
connsMu.Unlock()

return
}
}
}

func sseHandler(w http.ResponseWriter, req *http.Request) {
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")

for {
select {
case msg, ok := <-clientCh:
if !ok {
return
}
_, _ = fmt.Fprintf(w, "data: %s\n\n", msg)
f.Flush()

case <-req.Context().Done():
fmt.Print("SSE connection were closed.")
return
}
}
}

func getFileSystem() http.FileSystem {
fsys, err := fs.Sub(embededFiles, "static")
if err != nil {
panic(err)
}
return http.FS(fsys)
}

func handleIPCSignal(client *ipc.Client) {
for {
m, err := client.Read()
if err != nil {
fmt.Printf("%s IPC received error: %v", visorconfig.SkychatName, err)
}
if m.MsgType == visorconfig.IPCShutdownMessageType {
fmt.Println("Stopping " + visorconfig.SkychatName + " via IPC")
break
}
}
os.Exit(0)
}

func setAppStatus(appCl *app.Client, status appserver.AppDetailedStatus) {
if err := appCl.SetDetailedStatus(string(status)); err != nil {
print(fmt.Sprintf("Failed to set status %v: %v\n", status, err))
}
}

func setAppError(appCl *app.Client, appErr error) {
if err := appCl.SetError(appErr.Error()); err != nil {
print(fmt.Sprintf("Failed to set error %v: %v\n", appErr, err))
}
}

func setAppPort(appCl *app.Client, port routing.Port) {
if err := appCl.SetAppPort(port); err != nil {
print(fmt.Sprintf("Failed to set port %v: %v\n", port, err))
}
}
File renamed without changes
Loading
Loading