├── .gitignore ├── client-gtk3 ├── icon.rc ├── icon.ico ├── icon.syso ├── tray.go ├── icon.go ├── icon.go.h ├── tray.go.h └── main.go ├── thlink-client-gtk.png ├── screenshot └── screenshot-v0.0.10-amd64-windows.png ├── glg-go ├── AUTHORS ├── NEWS ├── ChangeLog ├── glg_go.go ├── glg_cairo.h └── README.md ├── utils ├── version.go ├── common.go ├── stream_test.go ├── stream.go ├── tunnel_test.go └── tunnel.go ├── thlink-client-gtk.desktop ├── thlink-broker.service ├── client ├── lib │ ├── hyouibana_test.go │ ├── client.go │ ├── hisoutensoku.go │ └── hyouibana.go └── main.go ├── Makefile ├── broker ├── main.go └── lib │ ├── broker_test.go │ └── broker.go ├── go.mod ├── go.sum └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea 3 | build 4 | 5 | -------------------------------------------------------------------------------- /client-gtk3/icon.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "icon.ico" 2 | -------------------------------------------------------------------------------- /client-gtk3/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weilinfox/youmu-thlink/HEAD/client-gtk3/icon.ico -------------------------------------------------------------------------------- /client-gtk3/icon.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weilinfox/youmu-thlink/HEAD/client-gtk3/icon.syso -------------------------------------------------------------------------------- /thlink-client-gtk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weilinfox/youmu-thlink/HEAD/thlink-client-gtk.png -------------------------------------------------------------------------------- /screenshot/screenshot-v0.0.10-amd64-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weilinfox/youmu-thlink/HEAD/screenshot/screenshot-v0.0.10-amd64-windows.png -------------------------------------------------------------------------------- /glg-go/AUTHORS: -------------------------------------------------------------------------------- 1 | Main Body of Code and Packaging: 2 | * James Scott, Jr. 3 | 4 | Go binding 5 | * weilinfox 6 | -------------------------------------------------------------------------------- /utils/version.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | // Version application version 5 | Version = "0.0.12" 6 | // Channel empty or dev or something else 7 | Channel = "" 8 | ) 9 | -------------------------------------------------------------------------------- /thlink-client-gtk.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ThLink Client Gtk 3 | Comment=Touhou project game netplay tool 4 | Comment[zh_CN]=通用的东方联机器 5 | Exec=thlink-client-gtk 6 | Icon=thlink-client-gtk 7 | Terminal=false 8 | Type=Application 9 | Categories=Network;Game 10 | -------------------------------------------------------------------------------- /thlink-broker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ThLink Broker 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=on-failure 8 | RestartSec=5s 9 | ExecStart=/usr/bin/thlink-broker 10 | # ExecStart=/usr/bin/thlink-broker -u thlink.inuyasha.love:4646 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /client/lib/hyouibana_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestZlib(t *testing.T) { 8 | l, z := zlibDataEncodeConf() 9 | if l == 0 || z == nil { 10 | t.Fatal("Zlib compress data error") 11 | } 12 | 13 | if z[0] != 0x78 || z[1] != 0x9c { 14 | t.Fatalf("Zlib returned wrong data %d %d %d", l, z[0], z[1]) 15 | } 16 | /* 17 | fmt.Println(l) 18 | for i := 0; i < int(l); i++ { 19 | fmt.Printf("%x, ", z[i]) 20 | } 21 | */ 22 | } 23 | -------------------------------------------------------------------------------- /client-gtk3/tray.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // #cgo pkg-config: gdk-3.0 atk gtk+-3.0 4 | // #include "tray.go.h" 5 | import "C" 6 | 7 | import "github.com/gotk3/gotk3/gtk" 8 | 9 | func onStatusIconSetup(window *gtk.ApplicationWindow) { 10 | C.status_icon_setup((C.gpointer)(window.ToWidget().Native())) 11 | setStatusIconText(appName) 12 | } 13 | 14 | func setStatusIconText(text string) { 15 | C.status_icon_text_set(C.CString(text)) 16 | } 17 | 18 | func setStatusIconHide() { 19 | C.status_icon_hide() 20 | } 21 | -------------------------------------------------------------------------------- /client-gtk3/icon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "unsafe" 6 | 7 | "github.com/gotk3/gotk3/gdk" 8 | "github.com/gotk3/gotk3/glib" 9 | ) 10 | 11 | // #cgo pkg-config: gdk-3.0 12 | // #include 13 | // #include "icon.go.h" 14 | import "C" 15 | 16 | func getIcon() (*gdk.Pixbuf, error) { 17 | 18 | c := C.gdk_pixbuf_new_from_xpm_data(&C.thlink_client_gtk_xpm[0]) 19 | if c == nil { 20 | return nil, errors.New("get icon error") 21 | } 22 | obj := &glib.Object{GObject: glib.ToGObject(unsafe.Pointer(c))} 23 | p := &gdk.Pixbuf{Object: obj} 24 | 25 | return p, nil 26 | } 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | VERSION:=0.0.12 3 | BUILD_ARCH=$(shell go env GOARCH) 4 | BUILD_OS=$(shell go env GOOS) 5 | 6 | all: 7 | export GOPATH=${HOME}/go 8 | # go build -o ./build/thlink-broker ./broker/ 9 | CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o ./build/thlink-broker-v${VERSION}-${BUILD_ARCH}-${BUILD_OS} ./broker/ 10 | go build -o ./build/thlink-client-v${VERSION}-${BUILD_ARCH}-${BUILD_OS} ./client/ 11 | go build -o ./build/thlink-client-gtk-v${VERSION}-${BUILD_ARCH}-${BUILD_OS} ./client-gtk3/ 12 | 13 | test: 14 | go test ./utils 15 | go test ./broker/lib 16 | go test ./client/lib 17 | 18 | clean: 19 | rm -rf ./build 20 | -------------------------------------------------------------------------------- /broker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | broker "github.com/weilinfox/youmu-thlink/broker/lib" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func main() { 13 | 14 | listenHost := flag.String("s", "0.0.0.0:4646", "listen hostname") 15 | upperHost := flag.String("u", "", "upper broker hostname") 16 | debug := flag.Bool("d", false, "debug mode") 17 | 18 | flag.Parse() 19 | 20 | if *debug { 21 | logrus.SetLevel(logrus.DebugLevel) 22 | } else { 23 | logrus.SetLevel(logrus.InfoLevel) 24 | } 25 | 26 | broker.Main(*listenHost, *upperHost) 27 | 28 | fmt.Println("Enter to quit") 29 | _, _ = fmt.Scanln() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weilinfox/youmu-thlink 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gotk3/gotk3 v0.6.1 7 | github.com/quic-go/quic-go v0.32.0 8 | github.com/sirupsen/logrus v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 13 | github.com/golang/mock v1.6.0 // indirect 14 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 15 | github.com/onsi/ginkgo/v2 v2.2.0 // indirect 16 | github.com/quic-go/qtls-go1-18 v0.2.0 // indirect 17 | github.com/quic-go/qtls-go1-19 v0.2.0 // indirect 18 | github.com/quic-go/qtls-go1-20 v0.1.0 // indirect 19 | golang.org/x/crypto v0.4.0 // indirect 20 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect 21 | golang.org/x/mod v0.6.0 // indirect 22 | golang.org/x/net v0.4.0 // indirect 23 | golang.org/x/sys v0.3.0 // indirect 24 | golang.org/x/tools v0.2.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /glg-go/NEWS: -------------------------------------------------------------------------------- 1 | * Tue Jan 10, 2023 Release 0.5.0 2 | - Move some deprecated function 3 | - Add go binding 4 | - Upgrade license to GPL3.0 5 | 6 | * Thur May 26, 2016 Release 0.4.2 7 | - Migrated to GTK+-3.0 8 | - contains: (glg) widget style routines for easier integration 9 | - fixed draw performance problem caused by '360 * M_PI' 10 | 11 | * Wed Jul 04, 2007 Release 0.3.1-0 12 | - Major rewrite of widget components 13 | - contains: (glg) widget style routines for easier integration 14 | enhanced with Cairo Graphics Engine support 15 | 16 | * Tue Jul 03, 2007 Release 0.3.0-0 17 | - Major rewrite of widget components 18 | - contains: (glg) widget style routines for easier integration 19 | considering an enhancement to support Cairo Engine 20 | 21 | * Wed May 24, 2006 Release 0.2.0-0 22 | - First public release of glinegraph package 23 | - contains: (g_lgraph) regular routines for direct integration 24 | (sklinegraph) widget style routines for easier integration 25 | 26 | -------------------------------------------------------------------------------- /utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "math/big" 10 | ) 11 | 12 | const nextProto = "myonTHlink" 13 | 14 | // GenerateTLSConfig setup a bare-bones TLS config for the server 15 | func GenerateTLSConfig() (*tls.Config, error) { 16 | key, err := rsa.GenerateKey(rand.Reader, 1024) 17 | if err != nil { 18 | return nil, err 19 | } 20 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 21 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 22 | if err != nil { 23 | return nil, err 24 | } 25 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 26 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 27 | 28 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &tls.Config{ 33 | Certificates: []tls.Certificate{tlsCert}, 34 | NextProtos: []string{nextProto}, 35 | }, nil 36 | } 37 | 38 | // LittleIndia2Int cover little india byte array into int 39 | func LittleIndia2Int(b []byte) int { 40 | return int(b[0]) | int(b[1])<<8 | int(b[2])<<16 | int(b[3])<<24 41 | } 42 | -------------------------------------------------------------------------------- /client-gtk3/icon.go.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef __ICON_GO_H__ 3 | #define __ICON_GO_H__ 4 | 5 | #pragma once 6 | 7 | /* XPM */ 8 | const char * thlink_client_gtk_xpm[] = { 9 | "32 32 3 1", 10 | " c None", 11 | ". c #FFFFFF", 12 | "> c #000000", 13 | " ...................... ", 14 | " .......................... ", 15 | " ............................ ", 16 | " .............................. ", 17 | " .............................. ", 18 | "..>>>>>>>>>.>>.......>.>>>>>>...", 19 | "..>>>>>>>>>.>>.......>.>>>>.....", 20 | ".......>>...>>>>>>>>>>..>.......", 21 | ".......>>...>>.......>..>.......", 22 | ".....>>>>...>>.......>..>>......", 23 | ".....>>>>...............>.......", 24 | ".......>>.>>>>>>>>>>>>>.>.......", 25 | ".......>>.>>............>.......", 26 | ".......>>...............>.......", 27 | ".......>>...............>.......", 28 | ".......>>...............>.......", 29 | ".......>>...............>.......", 30 | ".......>>..........>>>..>.......", 31 | ".......>>...............>.......", 32 | ".......>>...............>.......", 33 | ".......>>.......>>>>....>.......", 34 | ".......>>...............>.......", 35 | ".......>>...............>.......", 36 | ".......>>...............>.......", 37 | ".......>>.....>>>>>.....>.......", 38 | ".......>>.....>>>>>.....>.......", 39 | ".......>>.......................", 40 | " .............................. ", 41 | " .............................. ", 42 | " ............................ ", 43 | " ..............>>>>>>...... ", 44 | " ............>>>>>>.... "}; 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /glg-go/ChangeLog: -------------------------------------------------------------------------------- 1 | * Tue Jan 10, 2023 Release 0.5.0-0 weilinfox 2 | - Move some deprecated function and add go binding 3 | - Upgrade license to GPL3.0 4 | 5 | * Fri Jul 20, 2007 Release 0.3.2-0 James Scott Jr. 6 | - Fine tuned title positions around plot.box and tweaked tooltips 7 | 8 | * Tue Jul 03, 2007 Release 0.3.0-0 James Scott Jr. 9 | - ReWritten to be simple gtk widget using cairo graphics and to use g_object_set properties 10 | 11 | * Fri May 26, 2006 Release 0.2.3-0 James Scott Jr. 12 | - use this command with autogen 13 | ./autogen.sh --enable-gtk-doc 14 | - Changed the dir structure to match gtk-docs abilities. 15 | 16 | * Fri May 26, 2006 Release 0.2.2-0 James Scott Jr. 17 | - Added api documentation for SkLinegraph widget using GTK-DOC 18 | located in ./docs/reference directory 19 | 20 | * Thur May 25, 2006 Release 0.2.1-0 James Scott Jr. 21 | - Found it necassary to change the root type name to SkLinegraph to comply with 22 | Gtk naming space standards; from SkLineGraph. 23 | - SkLinegraph and sk_linegraph_*() is now the correct syntax. 24 | 25 | * Wed May 24, 2006 Release 0.2.0-0 James Scott Jr. 26 | - First public release of glinegraph package 27 | - contains: (glinegraph) regular routines for direct integration 28 | (SkLinegraph) widget style routines for easier integration 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /utils/stream_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestStream(t *testing.T) { 9 | data := make([]byte, TransBufSize) 10 | 11 | for i := 0; i < 10; i++ { 12 | // compressible random bytes 13 | tmp := make([]byte, rand.Intn(10)+10) 14 | for i := 0; i < len(tmp); i++ { 15 | tmp[i] = byte(rand.Int()) 16 | } 17 | for i := 0; i < TransBufSize; { 18 | b := rand.Intn(len(tmp)) 19 | for j := 0; j < b && i < TransBufSize; { 20 | data[i] = tmp[j] 21 | i++ 22 | j++ 23 | } 24 | } 25 | 26 | t.Log("Test compressible random bytes round ", i) 27 | checkData(data, t) 28 | } 29 | 30 | for i := 0; i < 10; i++ { 31 | // incompressible random bytes 32 | for i := 0; i < TransBufSize; i++ { 33 | data[i] = byte(rand.Int()) 34 | } 35 | 36 | t.Log("Test incompressible random bytes round ", i) 37 | checkData(data, t) 38 | } 39 | } 40 | 41 | func checkData(data []byte, t *testing.T) { 42 | dataStream := NewDataStream() 43 | dataStream.Append(NewDataFrame(DATA, data)) 44 | if dataStream.Parse() { 45 | if dataStream.Len() == TransBufSize { 46 | for i := 0; i < TransBufSize; i++ { 47 | if dataStream.Data()[i] != data[i] { 48 | t.Error("Compressible DataStream parse result not match original data") 49 | break 50 | } 51 | } 52 | } else { 53 | t.Error("Compressible DataStream parse length not match") 54 | } 55 | } else { 56 | t.Error("Compressible DataStream parse failed") 57 | } 58 | 59 | t.Log("DataStream compression rate ", dataStream.CompressRateAva()) 60 | } 61 | -------------------------------------------------------------------------------- /client-gtk3/tray.go.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef __TRAY_GO_H__ 3 | #define __TRAY_GO_H__ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | const char * thlink_client_gtk_tray_xpm[] = { 11 | "32 32 3 1", 12 | " c None", 13 | ". c #FFFFFF", 14 | "> c #000000", 15 | " ...................... ", 16 | " .......................... ", 17 | " ............................ ", 18 | " .............................. ", 19 | " .............................. ", 20 | "..>>>>>>>>>.>>.......>.>>>>>>...", 21 | "..>>>>>>>>>.>>.......>.>>>>.....", 22 | ".......>>...>>>>>>>>>>..>.......", 23 | ".......>>...>>.......>..>.......", 24 | ".....>>>>...>>.......>..>>......", 25 | ".....>>>>...............>.......", 26 | ".......>>.>>>>>>>>>>>>>.>.......", 27 | ".......>>.>>............>.......", 28 | ".......>>...............>.......", 29 | ".......>>...............>.......", 30 | ".......>>...............>.......", 31 | ".......>>...............>.......", 32 | ".......>>..........>>>..>.......", 33 | ".......>>...............>.......", 34 | ".......>>...............>.......", 35 | ".......>>.......>>>>....>.......", 36 | ".......>>...............>.......", 37 | ".......>>...............>.......", 38 | ".......>>...............>.......", 39 | ".......>>.....>>>>>.....>.......", 40 | ".......>>.....>>>>>.....>.......", 41 | ".......>>.......................", 42 | " .............................. ", 43 | " .............................. ", 44 | " ............................ ", 45 | " ..............>>>>>>...... ", 46 | " ............>>>>>>.... "}; 47 | 48 | GtkStatusIcon *_icon; 49 | 50 | void _activate_signal_callback( GObject* trayIcon, gpointer window ) 51 | { 52 | if (gtk_widget_get_visible(GTK_WIDGET(window))) { 53 | gtk_widget_hide(GTK_WIDGET(window)); 54 | } else { 55 | gtk_widget_show_all(GTK_WIDGET(window)); 56 | } 57 | } 58 | 59 | void _status_icon_activate_signal_connect( GtkStatusIcon *trayIcon, gpointer window ) 60 | { 61 | g_signal_connect(GTK_STATUS_ICON (trayIcon), "activate", G_CALLBACK(_activate_signal_callback), GTK_WIDGET(window)); 62 | } 63 | 64 | void status_icon_setup( gpointer window ) 65 | { 66 | if (_icon != NULL) { 67 | return; 68 | } 69 | _icon = gtk_status_icon_new_from_pixbuf(gdk_pixbuf_new_from_xpm_data(thlink_client_gtk_tray_xpm)); 70 | _status_icon_activate_signal_connect( _icon, window ); 71 | gtk_status_icon_set_visible( _icon, TRUE ); 72 | 73 | // where is name shown ? 74 | gtk_status_icon_set_name( _icon, "ThLink" ); 75 | gtk_status_icon_set_tooltip_text(_icon, "ThLink client"); 76 | gtk_status_icon_set_has_tooltip( _icon, TRUE ); 77 | } 78 | 79 | void status_icon_text_set( const char *text ) 80 | { 81 | // kde show title 82 | gtk_status_icon_set_title(_icon, text); 83 | // windows show tooltip 84 | gtk_status_icon_set_tooltip_text(_icon, text); 85 | } 86 | 87 | void status_icon_hide() 88 | { 89 | gtk_status_icon_set_visible( _icon, FALSE ); 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /glg-go/glg_go.go: -------------------------------------------------------------------------------- 1 | package glgo 2 | 3 | import ( 4 | "errors" 5 | "unsafe" 6 | 7 | "github.com/gotk3/gotk3/glib" 8 | "github.com/gotk3/gotk3/gtk" 9 | ) 10 | 11 | /* 12 | #cgo pkg-config: gtk+-3.0 13 | #include 14 | #include "glg_cairo.h" 15 | 16 | GlgLineGraph * my_glg_line_graph_new() 17 | { 18 | return glg_line_graph_new ("chart-set-elements", 19 | GLG_TOOLTIP | GLG_TITLE_T | GLG_TITLE_X | GLG_TITLE_Y | GLG_GRID_MAJOR_X | GLG_GRID_MAJOR_Y | GLG_GRID_MINOR_X | GLG_GRID_MINOR_Y | GLG_GRID_LABELS_X | GLG_GRID_LABELS_Y, 20 | "range-tick-minor-x", 1, 21 | "range-tick-major-x", 10, 22 | "range-scale-minor-x", 0, 23 | "range-scale-major-x", 40, 24 | "range-tick-minor-y", 2, 25 | "range-tick-major-y", 10, 26 | "range-scale-minor-y", 0, 27 | "range-scale-major-y", 120, 28 | "series-line-width", 2, 29 | "graph-title-foreground", "black", 30 | "graph-scale-foreground", "black", 31 | "graph-chart-background", "light gray", 32 | "graph-window-background", "white", 33 | "text-title-main", "Tunnel Delay Line Chart", 34 | "text-title-yaxis", "delay(ms)", 35 | "text-title-xaxis", "Click mouse button 1 to toggle popup legend.", 36 | NULL); 37 | } 38 | */ 39 | import "C" 40 | 41 | type GlgLineGraph struct { 42 | gtk.Bin 43 | 44 | // rangeScaleMajorY int 45 | } 46 | 47 | func GlgLineGraphNew() (*GlgLineGraph, error) { 48 | glg := C.my_glg_line_graph_new() 49 | if glg == nil { 50 | return nil, errors.New("cgo returned unexpected nil pointer") 51 | } 52 | 53 | obj := glib.Take(unsafe.Pointer(glg)) 54 | 55 | return &GlgLineGraph{Bin: gtk.Bin{Container: gtk.Container{Widget: gtk.Widget{InitiallyUnowned: glib.InitiallyUnowned{Object: obj}}}}}, nil 56 | } 57 | 58 | func (g *GlgLineGraph) GlgLineGraphDataSeriesAdd(legend string, color string) bool { 59 | 60 | cLegend := C.CString(legend) 61 | cColor := C.CString(color) 62 | defer C.free(unsafe.Pointer(cLegend)) 63 | defer C.free(unsafe.Pointer(cColor)) 64 | 65 | return C.glg_line_graph_data_series_add((*C.GlgLineGraph)(unsafe.Pointer(g.Native())), cLegend, cColor) == C.TRUE 66 | } 67 | 68 | func (g *GlgLineGraph) GlgLineGraphDataSeriesAddValue(series int, value float64) bool { 69 | defer g.glgLineGraphRedraw() 70 | 71 | // auto scale 72 | /*if value > float64(g.rangeScaleMajorY) { 73 | g.glgLineGraphChartSetYRanges((int(math.Floor(value/10)) + 1) * 10) 74 | }*/ 75 | 76 | return C.glg_line_graph_data_series_add_value((*C.GlgLineGraph)(unsafe.Pointer(g.Native())), *(*C.int)(unsafe.Pointer(&series)), *(*C.double)(unsafe.Pointer(&value))) == C.TRUE 77 | } 78 | 79 | func (g *GlgLineGraph) glgLineGraphRedraw() { 80 | C.glg_line_graph_redraw((*C.GlgLineGraph)(unsafe.Pointer(g.Native()))) 81 | } 82 | 83 | // cannot set range more than once 84 | /*func (g *GlgLineGraph) glgLineGraphChartSetYRanges(yScaleMax int) { 85 | C.glg_line_graph_chart_set_y_ranges((*C.GlgLineGraph)(unsafe.Pointer(g.Native())), 2, 10, 0, *(*C.gint)(unsafe.Pointer(&yScaleMax))) 86 | }*/ 87 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "sort" 6 | 7 | client "github.com/weilinfox/youmu-thlink/client/lib" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var logger = logrus.WithField("client", "main") 13 | 14 | func main() { 15 | 16 | localPort := flag.Int("p", client.DefaultLocalPort, "local port will connect to") 17 | server := flag.String("s", client.DefaultServerHost, "hostname of server") 18 | tunnelType := flag.String("t", client.DefaultTunnelType, "tunnel type, support tcp and quic") 19 | autoSelect := flag.Bool("a", true, "auto select broker in network with lowest latency") 20 | noAutoSelect := flag.Bool("na", false, "DO NOT auto select broker in network with lowest latency (override -a)") 21 | plugin := flag.Int("l", 0, "enable plugin, 123 for hisoutensoku spectacle support, 155 for hyouibana spectacle support") 22 | debug := flag.Bool("d", false, "debug mode") 23 | 24 | flag.Parse() 25 | 26 | if *debug { 27 | logrus.SetLevel(logrus.DebugLevel) 28 | } else { 29 | logrus.SetLevel(logrus.InfoLevel) 30 | } 31 | 32 | chooseBroker := *server 33 | if *autoSelect && !*noAutoSelect { 34 | 35 | // ping delays 36 | serverDelays, err := client.NetBrokerDelay(*server) 37 | if err != nil { 38 | logger.WithError(err).Fatal("Get broker delay in network failed") 39 | } 40 | 41 | // int=>string 42 | delayServers := make(map[int]string) 43 | sortDelay := make([]int, len(serverDelays)) 44 | i := 0 45 | for k, v := range serverDelays { 46 | delayServers[v] = k 47 | sortDelay[i] = v 48 | i++ 49 | } 50 | 51 | // sort ping delays 52 | sort.Ints(sortDelay) 53 | 54 | // print 5 of low latency brokers 55 | for i = 0; i < 5 && i < len(delayServers); i++ { 56 | // drop >200ms 57 | if sortDelay[i] >= 1000000*200 { 58 | break 59 | } 60 | logger.Infof("%.3fms %s", float64(sortDelay[i])/1000000, delayServers[sortDelay[i]]) 61 | } 62 | chooseBroker = delayServers[sortDelay[0]] 63 | } 64 | 65 | c, err := client.New(*localPort, chooseBroker, *tunnelType) 66 | if err != nil { 67 | logger.WithError(err).Fatal("Start client error") 68 | } 69 | defer c.Close() 70 | 71 | tunnelVersion, version, channel := c.Version() 72 | if channel != "" { 73 | version += "-" + channel 74 | } 75 | logger.Info("Client v", version, " with tunnel version ", tunnelVersion) 76 | brokerTVersion, brokerVersion := c.BrokerVersion() 77 | logger.Info("Broker v", brokerVersion, " with tunnel version ", brokerTVersion) 78 | if tunnelVersion != brokerTVersion { 79 | logger.Warn("Broker tunnel version code not match, there may have compatible issue") 80 | } 81 | 82 | bStatus := c.BrokerStatus() 83 | logger.Infof("Currently %d user(s) on broker", bStatus.UserCount) 84 | 85 | err = c.Connect() 86 | if err != nil { 87 | logger.WithError(err).Fatal("Client connect error") 88 | } 89 | 90 | switch *plugin { 91 | case 123: 92 | logger.Info("Append th12.3 hisoutensoku plugin") 93 | h := client.NewHisoutensoku() 94 | err = c.Serve(h.ReadFunc, h.WriteFunc, h.GoroutineFunc, h.SetQuitFlag) 95 | case 155: 96 | logger.Info("Append th15.5 hyouibana plugin") 97 | h := client.NewHyouibana() 98 | err = c.Serve(h.ReadFunc, h.WriteFunc, h.GoroutineFunc, h.SetQuitFlag) 99 | default: 100 | err = c.Serve(nil, nil, nil, nil) 101 | } 102 | if err != nil { 103 | logger.WithError(err).Fatal("Serve client error") 104 | } 105 | 106 | // fmt.Println("Enter to quit") 107 | // _, _ = fmt.Scanln() 108 | } 109 | -------------------------------------------------------------------------------- /utils/stream.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/lzw" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const ( 11 | CmdBufSize = 64 // command frame size 12 | TransBufSize = 2048 - 3 // forward frame size 13 | BrokersCntMax = 40 // max broker count 14 | ) 15 | 16 | var loggerStream = logrus.WithField("utils", "stream") 17 | 18 | // NewDataFrame build data frame, b can be nil 19 | // 20 | // +------+--------+--------------+ 21 | // | type | length | raw data | 22 | // | 0 7 | 8 23 | 24 < 2047 | 23 | // +------+--------+--------------+ 24 | // 25 | // type is defined in DataType 26 | func NewDataFrame(t DataType, b []byte) []byte { 27 | if b == nil || len(b) == 0 { 28 | return []byte{byte(t), 0x00, 0x00} 29 | } 30 | 31 | // emmm just remove this feature due to performance concern 32 | /*if t == DATA { 33 | useLZW := true 34 | 35 | // lzw compression 36 | result := bytes.NewBuffer(nil) 37 | lw := lzw.NewWriter(result, lzw.LSB, 8) 38 | n, err := lw.Write(b) 39 | lw.Close() 40 | if n != len(b) || err != nil { 41 | loggerStream.WithError(err).Error("LZW compression error") 42 | useLZW = false 43 | } else if result.Len() >= len(b) { 44 | useLZW = false 45 | } 46 | 47 | if useLZW { 48 | return append([]byte{byte(LZW_DATA), byte(result.Len() >> 8), byte(result.Len())}, result.Bytes()...) 49 | } else { 50 | return append([]byte{byte(DATA), byte(len(b) >> 8), byte(len(b))}, b...) 51 | } 52 | }*/ 53 | 54 | return append([]byte{byte(t), byte(len(b) >> 8), byte(len(b))}, b...) 55 | } 56 | 57 | // DataStream parser to receive and parse data stream 58 | type DataStream struct { 59 | cache []byte 60 | cachedDataLen int 61 | cachedDataType int 62 | rawData []byte 63 | dataLength int 64 | dataType DataType 65 | 66 | totalData float64 67 | totalDecode float64 68 | } 69 | 70 | // DataType 4bit type of data frame 71 | type DataType int 72 | 73 | const ( 74 | DATA DataType = iota // DATA pure data 75 | PING // PING ping 76 | TUNNEL // TUNNEL ask for new tunnel 77 | LZW_DATA // LZW_DATA lzw compressed data 78 | NET_INFO // NET_INFO ask for all broker address in this net 79 | NET_INFO_UPDATE // NET_INFO_UPDATE add or delete broker address in net 80 | BROKER_INFO // BROKER_INFO info of this broker 81 | VERSION // VERSION of tunnel 82 | RUBBISH // RUBBISH nobody care about this package 83 | BROKER_STATUS // BROKER_STATUS status of broker 84 | ) 85 | 86 | // NewDataStream return a empty data stream parser 87 | func NewDataStream() *DataStream { 88 | return &DataStream{ 89 | cachedDataType: -1, 90 | cachedDataLen: -1, 91 | rawData: nil, 92 | } 93 | } 94 | 95 | // Append append new data to data stream 96 | func (c *DataStream) Append(b []byte) { 97 | if b != nil && len(b) != 0 { 98 | c.cache = append(c.cache, b...) 99 | } 100 | } 101 | 102 | // Parse when return true, new parsed data frame will sign to rawData, dataLength and dataType 103 | func (c *DataStream) Parse() bool { 104 | // get protocol header 105 | if c.cachedDataType < 0 && len(c.cache) >= 3 { 106 | 107 | c.cachedDataType = int(c.cache[0]) 108 | c.cachedDataLen = int(c.cache[1])<<8 + int(c.cache[2]) 109 | c.cache = c.cache[3:] 110 | 111 | } 112 | 113 | // get command body 114 | if c.cachedDataType >= 0 && len(c.cache) >= c.cachedDataLen { 115 | 116 | c.rawData = c.cache[:c.cachedDataLen] 117 | c.dataLength, c.dataType = c.cachedDataLen, DataType(c.cachedDataType) 118 | 119 | c.totalData += float64(c.cachedDataLen) 120 | 121 | if c.dataType == LZW_DATA { 122 | 123 | // lzw decompress 124 | result := make([]byte, TransBufSize) 125 | lr := lzw.NewReader(bytes.NewReader(c.rawData), lzw.LSB, 8) 126 | n, err := lr.Read(result) 127 | lr.Close() 128 | if err != nil { 129 | loggerStream.WithError(err).Error("LZW decompression error") 130 | } 131 | 132 | c.rawData = result[:n] 133 | c.dataLength = n 134 | c.dataType = DATA 135 | 136 | c.totalDecode += float64(n) 137 | 138 | } else { 139 | 140 | c.totalDecode += float64(c.cachedDataLen) 141 | 142 | } 143 | 144 | c.cache = c.cache[c.cachedDataLen:] 145 | c.cachedDataLen = -1 146 | c.cachedDataType = -1 147 | 148 | return true 149 | } 150 | 151 | return false 152 | } 153 | 154 | // CompressRateAva average compress rate (calculated from decompressed data) 155 | func (c *DataStream) CompressRateAva() float64 { 156 | 157 | if c.totalData == 0 { 158 | return 0 159 | } 160 | 161 | return c.totalData / c.totalDecode 162 | } 163 | 164 | func (c *DataStream) Type() DataType { 165 | return c.dataType 166 | } 167 | 168 | func (c *DataStream) Len() int { 169 | return c.dataLength 170 | } 171 | 172 | func (c *DataStream) Data() []byte { 173 | return c.rawData 174 | } 175 | -------------------------------------------------------------------------------- /utils/tunnel_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | "strconv" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func TestQuicTunnel(t *testing.T) { 15 | 16 | logrus.SetLevel(logrus.DebugLevel) 17 | 18 | // tunnel0 19 | t.Log("Setup quic tunnel 0") 20 | tunnel0, err := NewTunnel(&TunnelConfig{ 21 | Type: ListenQuicListenUdp, 22 | Address0: "0.0.0.0:0", 23 | Address1: "0.0.0.0:0", 24 | }) 25 | if err != nil { 26 | t.Fatal("New quic tunnel 0 error: ", err) 27 | } 28 | port00, port01 := tunnel0.Ports() 29 | defer tunnel0.Close() 30 | 31 | go tunnel0.Serve(nil, nil, nil, nil) 32 | 33 | // udpConn 34 | t.Log("Setup to quic tunnel udpConn") 35 | udpAddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0") 36 | if err != nil { 37 | t.Fatal("ResolveUDPAddr error: ", err) 38 | } 39 | udpConn, err := net.ListenUDP("udp", udpAddr) 40 | if err != nil { 41 | t.Fatal("ListenUDP error: ", err) 42 | } 43 | defer udpConn.Close() 44 | _, sUdpPort, _ := net.SplitHostPort(udpConn.LocalAddr().String()) 45 | udpPort64, _ := strconv.ParseInt(sUdpPort, 10, 32) 46 | 47 | // tunnel1 48 | t.Log("Setup quic tunnel 1") 49 | tunnel1, err := NewTunnel(&TunnelConfig{ 50 | Type: DialQuicDialUdp, 51 | Address0: "localhost:" + strconv.Itoa(port00), 52 | Address1: "localhost:" + strconv.Itoa(int(udpPort64)), 53 | }) 54 | if err != nil { 55 | t.Fatal("New quic tunnel 1 error: ", err) 56 | } 57 | defer tunnel1.Close() 58 | 59 | go tunnel1.Serve(nil, nil, nil, nil) 60 | 61 | testTunnel(t, udpConn, port01) 62 | 63 | } 64 | 65 | func TestTcpTunnel(t *testing.T) { 66 | 67 | logrus.SetLevel(logrus.DebugLevel) 68 | 69 | // tunnel0 70 | t.Log("Setup tcp tunnel 0") 71 | tunnel0, err := NewTunnel(&TunnelConfig{ 72 | Type: ListenTcpListenUdp, 73 | Address0: "0.0.0.0:0", 74 | Address1: "0.0.0.0:0", 75 | }) 76 | if err != nil { 77 | t.Fatal("New tcp tunnel 0 error: ", err) 78 | } 79 | port00, port01 := tunnel0.Ports() 80 | defer tunnel0.Close() 81 | 82 | go tunnel0.Serve(nil, nil, nil, nil) 83 | 84 | // udpConn 85 | t.Log("Setup to tcp tunnel udpConn") 86 | udpAddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0") 87 | if err != nil { 88 | t.Fatal("ResolveUDPAddr error: ", err) 89 | } 90 | udpConn, err := net.ListenUDP("udp", udpAddr) 91 | if err != nil { 92 | t.Fatal("ListenUDP error: ", err) 93 | } 94 | defer udpConn.Close() 95 | _, sUdpPort, _ := net.SplitHostPort(udpConn.LocalAddr().String()) 96 | udpPort64, _ := strconv.ParseInt(sUdpPort, 10, 32) 97 | 98 | // tunnel1 99 | t.Log("Setup tcp tunnel 1") 100 | tunnel1, err := NewTunnel(&TunnelConfig{ 101 | Type: DialTcpDialUdp, 102 | Address0: "localhost:" + strconv.Itoa(port00), 103 | Address1: "localhost:" + strconv.Itoa(int(udpPort64)), 104 | }) 105 | if err != nil { 106 | t.Fatal("New tcp tunnel 1 error: ", err) 107 | } 108 | defer tunnel1.Close() 109 | 110 | go tunnel1.Serve(nil, nil, nil, nil) 111 | 112 | testTunnel(t, udpConn, port01) 113 | 114 | } 115 | 116 | // testTunnel goroutine0 <--> udpConn <--> tunnel1 <--> tunnel0 <--> goroutine1 117 | func testTunnel(t *testing.T, udpConn *net.UDPConn, port01 int) { 118 | 119 | // test data 120 | var wg sync.WaitGroup 121 | wg.Add(2) 122 | 123 | // compressible random bytes 124 | data := make([]byte, TransBufSize) 125 | tmp := make([]byte, rand.Intn(10)+10) 126 | for i := 0; i < len(tmp); i++ { 127 | tmp[i] = byte(rand.Int()) 128 | } 129 | for i := 0; i < TransBufSize; { 130 | b := rand.Intn(len(tmp)) 131 | for j := 0; j < b && i < TransBufSize; { 132 | data[i] = tmp[j] 133 | i++ 134 | j++ 135 | } 136 | } 137 | 138 | // read data from udpConn 139 | go func() { 140 | defer func() { 141 | wg.Done() 142 | }() 143 | 144 | buf := make([]byte, TransBufSize) 145 | 146 | for i := 0; i < 5; i++ { 147 | _ = udpConn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 148 | cnt, udpAddr, err := udpConn.ReadFromUDP(buf) 149 | _ = udpConn.SetReadDeadline(time.Time{}) 150 | if err != nil { 151 | t.Error("Read from udpConn error", err) 152 | } 153 | _ = udpConn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 154 | cnt1, err := udpConn.WriteToUDP(buf[:cnt], udpAddr) 155 | _ = udpConn.SetWriteDeadline(time.Time{}) 156 | if err != nil { 157 | t.Error("Write to udpConn error", err) 158 | } 159 | if cnt != cnt1 { 160 | t.Errorf("Write data count not match: %d != %d", cnt, cnt1) 161 | } 162 | } 163 | 164 | }() 165 | 166 | // send data to port01 167 | go func() { 168 | defer func() { 169 | wg.Done() 170 | }() 171 | 172 | time.Sleep(time.Millisecond * 100) 173 | buf := make([]byte, TransBufSize) 174 | 175 | udpAddr, err := net.ResolveUDPAddr("udp", "localhost:"+strconv.Itoa(port01)) 176 | if err != nil { 177 | t.Error("Resolve tunnel0 addr error: ", err) 178 | } else { 179 | 180 | udpConn, err := net.DialUDP("udp", nil, udpAddr) 181 | if err != nil { 182 | t.Error("Dial tunnel0 error: ", err) 183 | } else { 184 | defer udpConn.Close() 185 | 186 | t.Log("Connect to UDP ", udpConn.RemoteAddr()) 187 | 188 | for i := 0; i < 5; i++ { 189 | _ = udpConn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 190 | cnt, err := udpConn.Write(data[:TransBufSize-1]) 191 | _ = udpConn.SetWriteDeadline(time.Time{}) 192 | if err != nil { 193 | t.Error("Write to tunnel0 error") 194 | } 195 | _ = udpConn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 196 | cnt1, err := udpConn.Read(buf) 197 | _ = udpConn.SetReadDeadline(time.Time{}) 198 | if err != nil { 199 | t.Error("Read from tunnel0 error") 200 | } 201 | if cnt != cnt1 { 202 | t.Errorf("Write data count not match: %d != %d", cnt, cnt1) 203 | } 204 | 205 | for i := 0; i < TransBufSize-1; i++ { 206 | if buf[i] != data[i] { 207 | t.Error("Transfer data not count") 208 | break 209 | } 210 | } 211 | } 212 | 213 | } 214 | 215 | } 216 | 217 | }() 218 | 219 | wg.Wait() 220 | } 221 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 8 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 9 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 10 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 11 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 12 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 13 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 14 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 15 | github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo= 16 | github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= 17 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 18 | github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= 19 | github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 20 | github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= 24 | github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= 25 | github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk= 26 | github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= 27 | github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI= 28 | github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= 29 | github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA= 30 | github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= 31 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 32 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 35 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 36 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= 41 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 42 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= 43 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 44 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 45 | golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= 46 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 50 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 51 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 62 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 64 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 65 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 66 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 69 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 70 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= 71 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | -------------------------------------------------------------------------------- /glg-go/glg_cairo.h: -------------------------------------------------------------------------------- 1 | /* $Id: glg_cairo.h,v 1.35 2007/07/25 16:41:07 jscott Exp $ 2 | * ---------------------------------------------- 3 | * 4 | * A GTK+ widget that implements a XY line graph 5 | * 6 | * (c) 2007, 2016 James Scott Jr 7 | * 8 | * Authors: 9 | * James Scott Jr 10 | * 11 | * (c) 2023, 2023 weilinfox 12 | * Date: 1/2023 13 | * 14 | * Contributors: 15 | * weilinfox 16 | * 17 | * This library is free software; you can redistribute it and/or 18 | * modify it under the terms of the GNU General Public License 19 | * as published by the Free Software Foundation; either 20 | * version 3 of the License, or (at your option) any later version. 21 | * 22 | * This library is distributed in the hope that it will be useful, 23 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 25 | * General Public License for more details. 26 | * 27 | * You should have received a copy of the GNU General Public License 28 | * along with this library; if not, see . 29 | */ 30 | 31 | 32 | #ifndef __GLG_LINE_GRAPH_H__ 33 | #define __GLG_LINE_GRAPH_H__ 34 | 35 | G_BEGIN_DECLS 36 | 37 | /** 38 | * Constants: 39 | * @GLG_USER_MODEL_X: Minimum graph width before auto-scaling. 40 | * @GLG_USER_MODEL_Y: Minimum graph height before auto-scaling. 41 | */ 42 | #define GLG_USER_MODEL_X 570 /* Minimum width */ 43 | #define GLG_USER_MODEL_Y 270 /* Minimum height */ 44 | 45 | /** 46 | * @GLG_MAX_STRING: Maximum gchar string size for any api. 47 | */ 48 | #define GLG_MAX_STRING 256 /* Size of a text string */ 49 | 50 | typedef struct _GlgLineGraph GlgLineGraph; 51 | typedef struct _GlgLineGraphClass GlgLineGraphClass; 52 | typedef struct _GlgLineGraphPrivate GlgLineGraphPrivate; 53 | 54 | 55 | /** 56 | * GlgLineGraphClass: 57 | * @point-selected: signal to return the y value under or near the mouse. 58 | * @graph: pointer to a #GlgLineGraph widget 59 | * @x_value: x scale value 60 | * @y_value: y scale value 61 | * @point_y_pos: y value pixel position on chart 62 | * @mouse_y_pos: actual mouse y position on chart 63 | * 64 | * Main widget Class structure 65 | */ 66 | struct _GlgLineGraphClass 67 | { 68 | GtkWidgetClass parent_class; 69 | 70 | void (* point_selected) (GlgLineGraph *graph, double x_value, double y_value, double point_y_pos, double mouse_y_pos); 71 | /* Padding for future expansion */ 72 | void (*_glg_reserved1) (void); 73 | void (*_glg_reserved2) (void); 74 | void (*_glg_reserved3) (void); 75 | void (*_glg_reserved4) (void); 76 | }; 77 | 78 | /** 79 | * GlgLineGraph: 80 | * 81 | * Main widget structure 82 | */ 83 | struct _GlgLineGraph 84 | { 85 | GtkWidget parent; 86 | 87 | /* < private > */ 88 | GlgLineGraphPrivate *priv; 89 | }; 90 | 91 | 92 | /** 93 | * GLGElementID: 94 | * @GLG_TITLE_X: Enables display of the bottom chart title 95 | * @GLG_NO_TITLE_X: Disables display of the bottom chart title 96 | * @GLG_TITLE_Y: Enables display of the left/vertical chart title 97 | * @GLG_NO_TITLE_Y: Disables display of the left/vertical chart title 98 | * @GLG_TITLE_T: Enables display of the top chart title 99 | * @GLG_NO_TITLE_T: Disables display of the top chart title 100 | * @GLG_GRID_LABELS_X: 101 | * @GLG_NO_GRID_LABELS_X: 102 | * @GLG_GRID_LABELS_Y: 103 | * @GLG_NO_GRID_LABELS_Y: 104 | * @GLG_TOOLTIP: 105 | * @GLG_NO_TOOLTIP: 106 | * @GLG_GRID_LINES: 107 | * @GLG_NO_GRID_LINES: 108 | * @GLG_GRID_MINOR_X: 109 | * @GLG_NO_GRID_MINOR_X: 110 | * @GLG_GRID_MAJOR_X: 111 | * @GLG_NO_GRID_MAJOR_X: 112 | * @GLG_GRID_MINOR_Y: 113 | * @GLG_NO_GRID_MINOR_Y: 114 | * @GLG_GRID_MAJOR_Y: 115 | * @GLG_NO_GRID_MAJOR_Y: 116 | * @GLG_SCALE: chart color key -- used to change chart scale/labels color 117 | * @GLG_TITLE: chart color key -- used to change top title color 118 | * @GLG_WINDOW: chart color key -- used to change window color 119 | * @GLG_CHART: chart color key -- used to change chart color 120 | * 121 | * Communication params for interface APIs 122 | */ 123 | typedef enum _GLG_Graph_Elements { 124 | /* enable chart flags and title keys */ 125 | GLG_ELEMENT_NONE = 1 << 0, 126 | GLG_TITLE_X = 1 << 1, 127 | GLG_NO_TITLE_X = 0 << 1, 128 | GLG_TITLE_Y = 1 << 2, 129 | GLG_NO_TITLE_Y = 0 << 2, 130 | GLG_TITLE_T = 1 << 3, 131 | GLG_NO_TITLE_T = 0 << 3, 132 | 133 | /* enable chart attributes flag */ 134 | GLG_GRID_LABELS_X = 1 << 4, 135 | GLG_NO_GRID_LABELS_X = 0 << 4, 136 | GLG_GRID_LABELS_Y = 1 << 5, 137 | GLG_NO_GRID_LABELS_Y = 0 << 5, 138 | 139 | /* enable tooltip flag */ 140 | GLG_TOOLTIP = 1 << 6, 141 | GLG_NO_TOOLTIP = 0 << 6, 142 | 143 | /* enabled chart attributes */ 144 | GLG_GRID_LINES = 1 << 7, 145 | GLG_NO_GRID_LINES = 0 << 7, 146 | GLG_GRID_MINOR_X = 1 << 8, 147 | GLG_NO_GRID_MINOR_X = 0 << 8, 148 | GLG_GRID_MAJOR_X = 1 << 9, 149 | GLG_NO_GRID_MAJOR_X = 0 << 9, 150 | GLG_GRID_MINOR_Y = 1 << 10, 151 | GLG_NO_GRID_MINOR_Y = 0 << 10, 152 | GLG_GRID_MAJOR_Y = 1 << 11, 153 | GLG_NO_GRID_MAJOR_Y = 0 << 11, 154 | 155 | /* chart color key -- used to change window color only */ 156 | GLG_SCALE = 1 << 12, 157 | GLG_TITLE = 1 << 13, 158 | GLG_WINDOW = 1 << 14, 159 | GLG_CHART = 1 << 15, 160 | 161 | /* Reserved */ 162 | GLG_RESERVED_OFF = 0 << 16, 163 | GLG_RESERVED_ON = 1 << 16 164 | } GLGElementID; 165 | 166 | #define GLG_TYPE_LINE_GRAPH (glg_line_graph_get_type ()) 167 | #define GLG_LINE_GRAPH(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GLG_TYPE_LINE_GRAPH, GlgLineGraph)) 168 | #define GLG_IS_LINE_GRAPH(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GLG_TYPE_LINE_GRAPH)) 169 | 170 | 171 | /* 172 | * Public Interfaces 173 | */ 174 | extern GlgLineGraph * glg_line_graph_new (const gchar *first_property_name, ...); 175 | extern GType glg_line_graph_get_type (void) G_GNUC_CONST; 176 | extern void glg_line_graph_redraw (GlgLineGraph *graph); 177 | extern GLGElementID glg_line_graph_chart_get_elements ( GlgLineGraph *graph); 178 | extern void glg_line_graph_chart_set_elements ( GlgLineGraph *graph, GLGElementID element); 179 | extern gboolean glg_line_graph_chart_set_text (GlgLineGraph *graph, GLGElementID element, const gchar *pch_text); 180 | extern gboolean glg_line_graph_chart_set_color (GlgLineGraph *graph, GLGElementID element, const gchar *pch_color); 181 | extern void glg_line_graph_chart_set_ranges (GlgLineGraph *graph, 182 | gint x_tick_minor, gint x_tick_major, 183 | gint x_scale_min, gint x_scale_max, 184 | gint y_tick_minor, gint y_tick_major, 185 | gint y_scale_min, gint y_scale_max); 186 | extern void glg_line_graph_chart_set_x_ranges (GlgLineGraph *graph, 187 | gint x_tick_minor, gint x_tick_major, 188 | gint x_scale_min, gint x_scale_max); 189 | extern void glg_line_graph_chart_set_y_ranges (GlgLineGraph *graph, 190 | gint y_tick_minor, gint y_tick_major, 191 | gint y_scale_min, gint y_scale_max); 192 | 193 | 194 | extern gint glg_line_graph_data_series_add (GlgLineGraph *graph, const gchar *pch_legend_text, const gchar *pch_color_text); 195 | extern gboolean glg_line_graph_data_series_add_value (GlgLineGraph *graph, gint i_series_number, gdouble y_value); 196 | 197 | G_END_DECLS 198 | 199 | #endif 200 | -------------------------------------------------------------------------------- /broker/lib/broker_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "math/rand" 7 | "net" 8 | "strconv" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/weilinfox/youmu-thlink/utils" 14 | 15 | "github.com/quic-go/quic-go" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | serverHost = "localhost" 21 | serverAddress = serverHost + ":4646" 22 | serverAddress2 = serverHost + ":4647" 23 | ) 24 | 25 | func TestRun(t *testing.T) { 26 | t.Log("Run broker") 27 | 28 | logrus.SetLevel(logrus.DebugLevel) 29 | go Main("127.0.0.1:4646", "") 30 | go Main("127.0.0.1:4647", serverAddress) 31 | //time.Sleep(time.Second) 32 | } 33 | 34 | func TestLongData(t *testing.T) { 35 | 36 | buf := make([]byte, utils.CmdBufSize+1) 37 | 38 | // test long data 39 | for i := 0; i < utils.CmdBufSize+1; i++ { 40 | buf[i] = byte(i) 41 | } 42 | 43 | serveTcpAddr, _ := net.ResolveTCPAddr("tcp4", serverAddress) 44 | for i := 5; i >= 0; i-- { 45 | conn, err := net.DialTCP("tcp4", nil, serveTcpAddr) 46 | if err != nil { 47 | t.Fatal("Fail to connect to server: ", err.Error()) 48 | } 49 | 50 | n, err := conn.Write(buf) 51 | if err != nil { 52 | t.Error("Fail to send data: ", err.Error()) 53 | } 54 | 55 | if n != utils.CmdBufSize+1 { 56 | t.Error("Send data length not matched: ", n) 57 | } 58 | 59 | conn.Close() 60 | } 61 | } 62 | 63 | func TestPing(t *testing.T) { 64 | serveTcpAddr, _ := net.ResolveTCPAddr("tcp4", serverAddress) 65 | conn, err := net.DialTCP("tcp4", nil, serveTcpAddr) 66 | if err != nil { 67 | t.Fatal("Fail to connect to server: ", err.Error()) 68 | } 69 | defer conn.Close() 70 | 71 | buf := make([]byte, utils.CmdBufSize) 72 | 73 | // test ping 74 | _, err = conn.Write(utils.NewDataFrame(utils.PING, nil)) 75 | if err != nil { 76 | t.Fatal("Fail to send ping: ", err.Error()) 77 | } 78 | 79 | conn.SetReadDeadline(time.Now().Add(time.Second)) 80 | n, err := conn.Read(buf) 81 | conn.SetReadDeadline(time.Time{}) 82 | if err != nil { 83 | t.Fatal("Cannot read from server: ", err.Error()) 84 | } 85 | 86 | dataStream := utils.NewDataStream() 87 | dataStream.Append(buf[:n]) 88 | if !dataStream.Parse() || dataStream.Type() != utils.PING { 89 | t.Error("Not a ping response: ", buf[:n]) 90 | } else { 91 | t.Log("Ping test passed") 92 | } 93 | } 94 | 95 | const packageCnt = 64 96 | 97 | func TestUDP(t *testing.T) { 98 | brokerTcpAddr, _ := net.ResolveTCPAddr("tcp4", serverAddress) 99 | conn, err := net.DialTCP("tcp4", nil, brokerTcpAddr) 100 | if err != nil { 101 | t.Fatal("Fail to connect to server: ", err.Error()) 102 | } 103 | defer conn.Close() 104 | 105 | buf := make([]byte, utils.TransBufSize) 106 | 107 | // test udp 108 | _, err = conn.Write(utils.NewDataFrame(utils.TUNNEL, []byte{'u', 'q'})) 109 | if err != nil { 110 | t.Fatal("Fail to send new udp tunnel command: ", err.Error()) 111 | } 112 | 113 | //conn.SetReadDeadline(time.Now().Add(time.Second)) 114 | n, err := conn.Read(buf) 115 | if err != nil { 116 | t.Fatal("Cannot read from server: ", err.Error()) 117 | } 118 | 119 | dataStream := utils.NewDataStream() 120 | dataStream.Append(buf[:n]) 121 | if !dataStream.Parse() || dataStream.Type() != utils.TUNNEL { 122 | t.Fatal("Not a new udp tunnel response: ", buf[:n]) 123 | } 124 | 125 | port1 := int(dataStream.Data()[0])<<8 + int(dataStream.Data()[1]) 126 | port2 := int(dataStream.Data()[2])<<8 + int(dataStream.Data()[3]) 127 | if port1 <= 0 || port1 > 65535 || port2 <= 0 || port2 > 65535 { 128 | t.Fatal("Invalid port peer", port1, port2) 129 | } 130 | 131 | // QUIC 132 | tlsConfig := &tls.Config{ 133 | InsecureSkipVerify: true, 134 | NextProtos: []string{"myonTHlink"}, 135 | } 136 | if err != nil { 137 | t.Fatal("Generate TLS Config error ", err) 138 | } 139 | qConn, err := quic.DialAddr(serverHost+":"+strconv.Itoa(port1), tlsConfig, nil) 140 | if err != nil { 141 | t.Fatal("QUIC connection failed ", err) 142 | } 143 | qStream, err := qConn.OpenStreamSync(context.Background()) 144 | if err != nil { 145 | t.Fatal("QUIC stream open error", err) 146 | } 147 | defer qStream.Close() 148 | 149 | // UDP 150 | serveUdpAddr, _ := net.ResolveUDPAddr("udp", serverHost+":"+strconv.Itoa(port2)) 151 | uConn, err := net.DialUDP("udp", nil, serveUdpAddr) 152 | if err != nil { 153 | t.Fatal("UDP connection failed") 154 | } 155 | defer uConn.Close() 156 | 157 | testBrokerInfo(t) 158 | 159 | var writeQuicCnt, readQuicCnt, writeUdpCnt, readUdpCnt int 160 | var wg sync.WaitGroup 161 | 162 | wg.Add(4) 163 | 164 | writeQuicCnt = utils.TransBufSize / 2 * packageCnt 165 | writeUdpCnt = utils.TransBufSize / 2 * packageCnt 166 | 167 | // write udp 168 | go func() { 169 | 170 | defer wg.Done() 171 | 172 | buf := make([]byte, utils.TransBufSize/2) 173 | // compressible random bytes 174 | tmp := make([]byte, rand.Intn(10)+10) 175 | for i := 0; i < len(tmp); i++ { 176 | tmp[i] = byte(rand.Int()) 177 | } 178 | for i := 0; i < utils.TransBufSize/2; { 179 | b := rand.Intn(len(tmp)) 180 | for j := 0; j < b && i < utils.TransBufSize/2; { 181 | buf[i] = tmp[j] 182 | i++ 183 | j++ 184 | } 185 | } 186 | 187 | for i := 0; i < packageCnt; i++ { 188 | n, err := uConn.Write(buf) 189 | if n != utils.TransBufSize/2 || err != nil { 190 | t.Fatal("Error write to udp: ", err, " count ", strconv.Itoa(n)) 191 | } 192 | time.Sleep(time.Millisecond) 193 | } 194 | 195 | t.Log("UDP send finish") 196 | 197 | }() 198 | 199 | // write quic 200 | go func() { 201 | 202 | defer wg.Done() 203 | 204 | buf := make([]byte, utils.TransBufSize/2) 205 | // incompressible random bytes 206 | for i := 0; i < utils.TransBufSize/2; i++ { 207 | buf[i] = byte(rand.Int()) 208 | } 209 | 210 | for i := 0; i < packageCnt; i++ { 211 | n, err := qStream.Write(utils.NewDataFrame(utils.DATA, append([]byte{0}, buf...))) 212 | if n-3 != utils.TransBufSize/2 || err != nil { 213 | //t.Fatal("Error write to quic: ", err, " count ", strconv.Itoa(n)) 214 | } 215 | time.Sleep(time.Millisecond) 216 | } 217 | 218 | t.Log("QUIC send finish") 219 | 220 | }() 221 | 222 | // read udp 223 | go func() { 224 | 225 | defer wg.Done() 226 | 227 | buf := make([]byte, utils.TransBufSize) 228 | 229 | for i := 0; i < packageCnt; i++ { 230 | if readUdpCnt == writeUdpCnt { 231 | break 232 | } 233 | uConn.SetReadDeadline(time.Now().Add(time.Second)) 234 | n, err := uConn.Read(buf) 235 | if err != nil { 236 | t.Log("Error read from udp: ", err, " count ", strconv.Itoa(n)) 237 | if n == 0 { 238 | break 239 | } 240 | } 241 | 242 | readUdpCnt += n 243 | } 244 | 245 | t.Log("UDP resv finish") 246 | 247 | }() 248 | 249 | // read quic 250 | go func() { 251 | 252 | defer wg.Done() 253 | 254 | dataStream := utils.NewDataStream() 255 | buf := make([]byte, utils.TransBufSize) 256 | 257 | for i := 0; i < packageCnt; i++ { 258 | if readQuicCnt == writeQuicCnt { 259 | break 260 | } 261 | n, err := qStream.Read(buf) 262 | if err != nil { 263 | t.Log("Error read from quic: ", err, " count ", strconv.Itoa(n)) 264 | if n == 0 { 265 | break 266 | } 267 | } 268 | 269 | dataStream.Append(buf[:n]) 270 | for dataStream.Parse() { 271 | if dataStream.Type() != utils.DATA { 272 | t.Error("Not a DATA frame") 273 | } 274 | readQuicCnt += dataStream.Len() - 1 275 | } 276 | 277 | } 278 | 279 | t.Log("QUIC resv finish") 280 | t.Logf("Average compress rate %.3f", dataStream.CompressRateAva()) 281 | 282 | }() 283 | 284 | wg.Wait() 285 | 286 | if writeQuicCnt != readQuicCnt { 287 | t.Errorf("QUIC write read bytes not match write %d read %d", writeQuicCnt, readQuicCnt) 288 | } else { 289 | t.Log("QUIC write read bytes matched", writeQuicCnt) 290 | } 291 | if writeUdpCnt != readUdpCnt { 292 | t.Errorf("UDP write read bytes not match write %d read %d", writeUdpCnt, readUdpCnt) 293 | } else { 294 | t.Log("UDP write read bytes matched ", writeUdpCnt) 295 | } 296 | 297 | } 298 | 299 | func testBrokerInfo(t *testing.T) { 300 | 301 | brokerTcpAddr, _ := net.ResolveTCPAddr("tcp4", serverAddress) 302 | conn, err := net.DialTCP("tcp4", nil, brokerTcpAddr) 303 | if err != nil { 304 | t.Fatal("Fail to connect to server: ", err.Error()) 305 | } 306 | defer conn.Close() 307 | 308 | _, err = conn.Write(utils.NewDataFrame(utils.BROKER_INFO, nil)) 309 | if err != nil { 310 | t.Fatal("Fail to send broker info command: ", err.Error()) 311 | } 312 | 313 | buf := make([]byte, utils.TransBufSize) 314 | n, err := conn.Read(buf) 315 | if err != nil { 316 | t.Fatal("Fail to read broker response: ", err.Error()) 317 | } 318 | 319 | dataStream := utils.NewDataStream() 320 | dataStream.Append(buf[:n]) 321 | if !dataStream.Parse() { 322 | t.Fatal("Fail to parse broker response") 323 | } 324 | 325 | if dataStream.Len() != 8 { 326 | t.Fatal("Broker info response length is not 8: ", dataStream.Len()) 327 | } 328 | count := int64(dataStream.Data()[0])<<56 + int64(dataStream.Data()[1])<<48 + int64(dataStream.Data()[2])<<40 + int64(dataStream.Data()[3])<<32 + 329 | int64(dataStream.Data()[4])<<24 + int64(dataStream.Data()[5])<<16 + int64(dataStream.Data()[6])<<8 + int64(dataStream.Data()[7]) 330 | if count != 1 { 331 | t.Error("Broker info data is not 1: ", count) 332 | } 333 | 334 | } 335 | 336 | func TestNetInfo(t *testing.T) { 337 | testNetInfo(serverAddress, "127.0.0.1:4647", t) 338 | testNetInfo(serverAddress2, "127.0.0.1:4646", t) 339 | } 340 | 341 | func testNetInfo(addr string, ans string, t *testing.T) { 342 | 343 | brokerTcpAddr, _ := net.ResolveTCPAddr("tcp4", addr) 344 | conn, err := net.DialTCP("tcp4", nil, brokerTcpAddr) 345 | if err != nil { 346 | t.Fatal("Fail to connect to server: ", err.Error()) 347 | } 348 | defer conn.Close() 349 | 350 | _, err = conn.Write(utils.NewDataFrame(utils.NET_INFO, []byte{0, 0})) 351 | if err != nil { 352 | t.Fatal("Fail to send net info command: ", err.Error()) 353 | } 354 | 355 | buf := make([]byte, utils.TransBufSize) 356 | n, err := conn.Read(buf) 357 | if err != nil { 358 | t.Fatal("Fail to read net response: ", err.Error()) 359 | } 360 | 361 | dataStream := utils.NewDataStream() 362 | dataStream.Append(buf[:n]) 363 | if !dataStream.Parse() { 364 | t.Fatal("Fail to parse net response") 365 | } 366 | 367 | if dataStream.Len()-1 != int(dataStream.Data()[0]) { 368 | t.Error("Net response format error") 369 | } 370 | if string(dataStream.Data()[1:]) != ans { 371 | t.Error("Net response content error:", string(dataStream.Data()[1:])) 372 | } 373 | 374 | t.Log("Test", addr, ans, "finished") 375 | 376 | } 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 白玉楼製作所 ThLink 2 | 3 | [![License](https://img.shields.io/github/license/weilinfox/youmu-thlink)](https://github.com/weilinfox/youmu-thlink/blob/master/LICENSE) 4 | [![Release](https://img.shields.io/github/v/release/weilinfox/youmu-thlink)](https://github.com/weilinfox/youmu-thlink/releases) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/weilinfox/youmu-thlink)](https://goreportcard.com/report/github.com/weilinfox/youmu-thlink) 6 | 7 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fweilinfox%2Fyoumu-thlink.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fweilinfox%2Fyoumu-thlink?ref=badge_shield) 8 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fweilinfox%2Fyoumu-thlink.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2Fweilinfox%2Fyoumu-thlink?ref=badge_shield) 9 | 10 | 通用的方便自搭建的东方联机器。 11 | 12 | 默认服务器地址 ``thlink.inuyasha.love:4646`` 。 13 | 14 | 本质上是个支持 UDP 的端口转发器, thlink 客户端和 thlink 服务端之间使用可选的 QUIC 和 TCP 传输。 15 | 16 | thlink 客户端以非想天则/凭依华插件的形式实现独立于对战双方的观战, thlink 客户端将从对战双方预取观战数据,然后拦截并回应来自观战客户端的请求。 17 | 18 | 为了使客户端能够在连接任意一个服务端的情况下获知所有存在于该网络的服务端,推荐将所有服务端连接成树状, ``broker -u hostname:port`` 19 | 将指定本服务端连接到另一个服务端的地址。这样一来命令行客户端可以传入 ``-a`` 来自动选择延迟最低的服务端, 20 | gtk 客户端则可以在菜单的 ``Network Discovery`` 自主选择客户端。不过要注意客户端和服务端之间的 ``ping`` 延迟并不能完整展现对战双方的网络延迟情况, 21 | 而在打开非想天则插件 ``th123`` 的情况下,客户端会在信息栏显示对战双方交换数据的单程延迟。 22 | 23 | 初入东方对 0 萌新一只,感谢**飞翔君**带我一起玩! 24 | 25 | ![screenshot-v0.0.10-amd64-windows](screenshot/screenshot-v0.0.10-amd64-windows.png) 26 | 27 | 感谢 [JetBrains](https://www.jetbrains.com/) 不再给本项目提供开源开发许可证! [![JetBrains Main Logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/) 28 | 29 | ## 特性 30 | 31 | 1. 使用 [QUIC](https://en.wikipedia.org/wiki/QUIC)/TCP 作为传输协议 32 | 2. 可选的 QUIC 和 TCP 传输 33 | 3. 支持使用 UDP 进行联机的东方作品 34 | 4. 可配置的监听端口和服务器地址,方便自搭建 35 | 5. 支持去中心化的多服务器结构 36 | 6. 支持非想天则观战,观战支持的原理见 [hisoutensoku-spectacle](https://github.com/weilinfox/youmu-hisoutensoku-spectacle) 37 | 7. 支持凭依华观战,观战支持的原理见 [hyouibana-spectacle](https://github.com/weilinfox/youmu-hyouibana-spectacle) 38 | 8. 使用 [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) 压缩,节约少量带宽 39 | 9. 符合习惯的命令行客户端和还算易用的 gtk3 图形客户端 40 | 10. Linux 下以 [AppImage](https://appimage.org/) 格式发布图形客户端 41 | 11. 代码乱七八糟的,就是说,这个东西,被我写得很糟糕 42 | 12. 我的英文很差很差,注释将就看吧别来打我(缩) 43 | 44 | ## TODO 45 | 46 | 1. 实现更多对战和观战 47 | 2. 支持 TCP 转发 48 | 49 | ## 预编译的二进制 50 | 51 | + [github release](https://github.com/weilinfox/youmu-thlink/releases) 52 | + [gitee release](https://gitee.com/weilinfox/youmu-thlink/releases) (镜像,如果有文件缺失那就是上传失败了) 53 | + [pling](https://www.pling.com/p/1963595/) (仅 AppImage) 54 | 55 | ### Archlinux 56 | 57 | 从 AUR 安装: 58 | 59 | ```shell 60 | $ yay -S thlink-client-gtk 61 | $ yay -S thlink-client 62 | $ yay -S thlink-broker 63 | ``` 64 | 65 | ## 客户端使用指导 66 | 67 | 一份简单 GTK 图形客户端的指导:[传送门](https://blog.inuyasha.love/linuxeveryday/thlink.html) 68 | 69 | ### TH09 70 | 71 | 作品名称: 72 | 73 | |日文|中文| 74 | |:-|:-| 75 | |東方花映塚 ~ Phantasmagoria of Flower View.|东方花映塚 ~ Phantasmagoria of Flower View.| 76 | 77 | 东方花映冢(东方花映塚)使用 DirectPlay 实现联机,故需要 adonis2 配合才能使用 thlink 联机。 78 | 79 | 在 [Maribel Hearn's Touhou Portal](https://maribelhearn.com/pofv) 的相关页面可以找到需要的工具 [Adonis2](https://maribelhearn.com/mirror/PoFV-Adonis-VPatch-Goodies.zip) 。 80 | 81 | 将其 ``files`` 目录下的所有文件直接放到缩到花映塚目录。 82 | 83 | 联机时对战双方都不要运行花映塚,而是直接运行 ``adonis2.exe`` (日文)或 ``adonis2e.exe`` (英文),按照提示操作。 84 | 85 | 主机端 adonis2 会提示监听端口,默认为 ``10800`` 。配置好 adonis2 后启动 thlink ,在端口输入部分输入 adonis2 提示的端口(或保持默认端口),其他默认或按需配置, thlink 会提示对端 IP 。 86 | 87 | 客机端使用 adonis2 连接 thlink 返回的 IP 。 88 | 89 | 联机成功后 adomis2 会自动启动花映塚。 90 | 91 | ### TH10.5 TH12.3 TH13.5 TH14.5 92 | 93 | 作品名称: 94 | 95 | |日文|中文| 96 | |:-|:-| 97 | |東方緋想天 ~ Scarlet Weather Rhapsody.|东方绯想天 ~ Scarlet Weather Rhapsody.| 98 | |東方非想天則 ~ 超弩級ギニョルの謎を追え|东方非想天则 ~ 追寻特大型人偶之谜| 99 | |東方心綺楼 ~ Hopeless Masquerade.|东方心绮楼 ~ Hopeless Masquerade.| 100 | |東方深秘録 ~ Urban Legend in Limbo.|东方深秘录 ~ Urban Legend in Limbo.| 101 | 102 | 游戏内文字: 103 | 104 | |日文|中文| 105 | |:-|:-| 106 | |対戦サーバーを立てる|建立对战服务器(主机端选择)| 107 | |IPとポートを指定してサーバーに接続|连接到指定IP和端口的服务器(客机选择)| 108 | |使用するポート|连接使用的端口号| 109 | 110 | 直接在游戏内联机即可,主机默认端口为 ``10800`` 。将 thlink 设置成一样的配置,客机输入 thlink 返回的 IP 。 111 | 112 | ### TH15.5 113 | 114 | 作品名称: 115 | 116 | |日文|中文| 117 | |:-|:-| 118 | |東方憑依華 ~ Antinomy of Common Flowers.|东方凭依华 ~ Antinomy of Common Flowers.| 119 | 120 | 游戏内文字: 121 | 122 | |日文|中文| 123 | |:-|:-| 124 | |対戦相手の接続を待つ|等待对方连接(主机端选择)| 125 | |接続先を指定して対戦相手接続に|指定对端以连接到对方(客机选择)| 126 | |観戦する|观战| 127 | |使用するポート番号|连接使用的端口号| 128 | 129 | 直接在游戏内联机即可,主机默认端口为 ``10800`` 。将 thlink 设置成一样的配置,客机输入 thlink 返回的 IP 。 130 | 131 | 若在 Wine 环境下,终端运行 ``th155.exe`` 在输出下面的内容一次后退出: 132 | 133 | ``` 134 | Allocator::Info[system] total 134217728 / free 134282760 / use 504 135 | Allocator::Info[stl] total 33554432 / free 33619464 / use 504 136 | ``` 137 | 138 | 注意这不是报错,只是单纯的症状,不知道为啥 ``th155.exe`` 没有产生任何错误信息。此时只要先 ``cd`` 到游戏所在目录,再重新尝试运行。 139 | 140 | ## 白玉楼製作所電波部 Lakey 联机器 141 | 142 | 本节的目的是提供一种低成本的 CW 练习环境。 143 | 144 | ### 练习软件 145 | 146 | 在安卓上可以使用 MorseMania 和 Morse Chat 。 MorseMania 就像多邻国一样,会提供一套教学练习,不过完整版本是需要购买的。 147 | 而 Morse Chat 是配套的聊天软件,可以使用 CW 方式在公开频道上与其他用户聊天,也可以建立房间与好友聊天。 148 | 149 | 在 Windows 上则可以使用 [Lakey](https://github.com/idirect3d/lakey) ,这是一个开源的、多功能且**完全免费**的 CW 练习/收/发软件, 150 | 可以在 bin 目录下载预编译的二进制,也可以在[这里](https://github.com/HDsky/Lakey)下载到。 151 | 152 | Lakey 也带有局域网联机功能,公网联机需要联机器的支持。 Lakey 在监听本地 3010 端口(接收)的同时,将输入的信息 153 | 用 UDP 数据包发送到地址列表的所有目标。故 Lakey 的联机并不是一对一的,理想的情况是所有通信方均处在同一网络中,可以互相 ping 到。 154 | 若需要在没有组网的情况下远程联机,则需要通信双方同时使用联机器将 Lakey 监听的本地 3010 端口映射到公网。 155 | 需要注意的是 Lakey 的“发送”选项卡中,添加主机只能使用默认端口号 3010 ,使用其他端口号则需要在配置文件中手动更改。 156 | 157 | 使用 ThLink 联机器联机: 158 | 1. 联机双方从 Network Discovery 中选择延时最小的服务器后,将 Plugin 选项选中 Lakey ,单击 Connect 连接。 159 | 连接成功后复制 IP 与端口,提供给需要联机的对方。联机双方均需要得到对方的 IP 和端口进行连接。 160 | 2. 打开 Lakey 目录下的 lakey.ini ,找到最后的 ``[NETWORK]`` 部分,默认情况下应该是下面的样子: 161 | ``` 162 | [NETWORK] 163 | nwenabled=1 164 | localport=3010 165 | hosts=0 166 | ``` 167 | 3. 在关闭 Lakey 的情况下修改 ``[NETWORK]`` 部分,添加对方的 IP ,假设为 116.255.228.774:50406 : 168 | ``` 169 | [NETWORK] 170 | nwenabled=1 171 | localport=3010 172 | hosts=1 173 | host1=116.255.228.774:50406 174 | ``` 175 | 4. 保存 lakey.ini 后打开 Lakey ,在“设置”的“发送”选项卡应该能看到添加的地址。 176 | 5. 现在开始拍发,应该能够接收到对方拍发的报文。 177 | 178 | 如果能够接收到报文,声音正常但是解码不正确,可以尝试修改“接收”选项卡中的“采样数量”和“采样间隔”参数。这里给出一组参考数据: 179 | 采样数量 1024 点,采样间隔 15 ms 。 180 | 181 | ### 硬件 182 | 183 | 电键是不能少的,因为即使使用短波电台发报也需要一个实体的电键。这里提供一种方式将电键与计算机连接。 184 | 185 | 在安卓上, MorseMania 和 Morse Chat 使用触控输入,其实也可以 OTG 连接鼠标用左键输入;而在 Windows 上 Lakey 使用鼠标左键输入。 186 | 另外 Lakey 支持自动电键,即用鼠标左键和右键来区别自动电键的两个触点。综上所述,只需要在鼠标上添加 3.5 mm 接口连接电键即可使用电键输入。 187 | 188 | 这样的改造需要注意两点: 189 | 1. 鼠标左右键的微动必须是共阳或共阴 190 | 2. 手动电键建议连接到左键的微动 191 | 3. 同时支持手动电键和自动电键,需要增加一个开关,否则使用手动电键时另一个微动会被一直短路 192 | 193 | 这种改造方式除了需要一个 3.5 mm 耳机插口和一个自锁开关,再在鼠标上钻两个孔飞几根线,并不影响鼠标日常功能,也不增加练习成本。 194 | 195 | ## 构建和部署 196 | 197 | Go >= 1.18 198 | 199 | 注意 loong64 架构从 Go 1.19 开始被支持。 200 | 201 | ### 本机构建 202 | 203 | ```shell 204 | $ make 205 | ``` 206 | 207 | 可用选项: 208 | 209 | + ``static`` 构建静态链接的二进制 210 | + ``loong64`` 构建 loong64 架构的二进制 211 | + ``windows`` 构建 windows amd64 可执行文件 212 | + ``gui`` Linux 下构建本机动态链接的图形界面客户端二进制 213 | 214 | 构建得到的二进制在 build 目录下。 215 | 216 | ### 离线构建 217 | 218 | 下载全部依赖的包: 219 | 220 | ```shell 221 | $ git mod vendor 222 | ``` 223 | 224 | 将生成的 ``./vendor`` 目录打包拷贝到别处,再解包到项目根目录运行构建: 225 | 226 | ```shell 227 | $ go mod=vendor build -o build/thlink-client-gtk ./client-gtk3 228 | ``` 229 | 230 | ### 部署 231 | 232 | broker 为服务端, client 为客户端。 233 | 234 | 若想将自己的 broker 连入其他 broker 的网络,只要连接这个网络中的任一 broker 即可,它们的地位是平行的。 235 | 236 | broker 在服务器运行即可, ``broker -h`` 查看选项; client 在本地运行, ``client -h`` 查看选项。 237 | 238 | client-gtk 没有提供特殊的命令行界面。 239 | 240 | ### Linux GTK3 GUI 241 | 242 | go1.18 需要自行下载或构建。 243 | 244 | 安装依赖(以 Debian 为例,水平有限,可能不全) 245 | 246 | ```shell 247 | $ sudo apt-get install libgtk-3-dev libcairo2-dev glib2.0-dev zlib1g-dev 248 | ``` 249 | 250 | 构建: 251 | 252 | ```shell 253 | $ make gui 254 | ``` 255 | 256 | 制作 AppImage (参见 [linuxdeploy-plugin-gtk](https://github.com/linuxdeploy/linuxdeploy-plugin-gtk) ): 257 | 258 | ```shell 259 | $ cd ./build 260 | 261 | $ wget -c "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" 262 | $ wget -c "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" 263 | $ chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-gtk.sh 264 | 265 | $ mkdir -p thlink-client-gtk.AppDir/usr/bin/ 266 | $ install -Dm775 ./thlink-client-gtk thlink-client-gtk.AppDir/usr/bin/ 267 | $ ./linuxdeploy-x86_64.AppImage --appdir thlink-client-gtk.AppDir/ --plugin gtk --output appimage --icon-file thlink-client-gtk.png --desktop-file thlink-client-gtk.desktop 268 | $ mv ThLink_Client_Gtk-x86_64.AppImage thlink-client-gtk-amd64-linux.AppImage 269 | ``` 270 | 271 | ### Windows GTK3 GUI 272 | 273 | 图形界面客户端在 Windows 上使用 [MSYS2](https://www.msys2.org/) 构建,水平有限,只能提供一个不全的指南。 274 | 275 | 也可参考 gotk3 的 [Wiki](https://github.com/gotk3/gotk3/wiki/Installing-on-Windows#chocolatey) 使用 Chocolatey 搭建环境。 276 | 277 | 注意 go 应该使用 Windows 版本而不是 MSYS2 软件源中提供的版本 ,否则可能会有构建失败的情况(待考证,我有出现 ``ld`` 找不到 ``-lmingwex`` 和 ``-lmingw32`` 的报错)。 278 | 279 | 更新到最新并安装依赖: 280 | 281 | ```shell 282 | $ pacman -Syuu 283 | $ pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-toolchain base-devel glib2-devel zlib-devel 284 | ``` 285 | 286 | 配置环境变量(根据实际情况修改),其中 ``/c/msys64/mingw64/bin`` 代表的是 Mingw64 gcc 所在目录, ``/c/Go/bin`` 则代表的是 Windows 的 go 所在目录: 287 | 288 | ```shell 289 | $ echo 'export PATH=/c/msys64/mingw64/bin:/c/Go/bin:$PATH' >> ~/.bashrc 290 | $ source ~/.bashrc 291 | ``` 292 | 293 | 修复 gdk-3.0.pc 中的一个 bug (坑了我好久,其实 gotk3 的 Wiki 有写到): 294 | 295 | ```shell 296 | $ sed -i -e 's/-Wl,-luuid/-luuid/g' /mingw64/lib/pkgconfig/gdk-3.0.pc 297 | ``` 298 | 299 | 构建图标和本体, ``-H windowsgui`` 使其运行时没有黑色终端: 300 | 301 | ```shell 302 | $ windres -o ./client-gtk3/icon.syso ./client-gtk3/icon.rc 303 | $ go build -ldflags "-H windowsgui" -o ./build/client-gtk3-windows/thlink-client-gtk.exe ./client-gtk3 304 | ``` 305 | 306 | 复制依赖库: 307 | 308 | ```shell 309 | $ ldd ./build/client-gtk3-windows/thlink-client-gtk.exe | grep -o '/mingw64/bin/[^ ]*' | xargs --replace=R -t cp R ./build/client-gtk3-windows/ 310 | ``` 311 | 312 | GTK icons: 313 | 314 | ```shell 315 | $ mkdir -p ./build/client-gtk3-windows/lib/gdk-pixbuf-2.0/2.10.0/loaders 316 | $ cp /mingw64/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-png.dll ./build/client-gtk3-windows/lib/gdk-pixbuf-2.0/2.10.0/loaders/ 317 | $ cp /mingw64/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-xpm.dll ./build/client-gtk3-windows/lib/gdk-pixbuf-2.0/2.10.0/loaders/ 318 | $ cp /mingw64/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache ./build/client-gtk3-windows/lib/gdk-pixbuf-2.0/2.10.0/ 319 | ``` 320 | 321 | 打包整个 ``./build/client-gtk3-windows`` 目录即可。 322 | 323 | ## 使用的端口 324 | 325 | broker (服务端)使用 TCP 端口 4646 作为和所有 client (客户端)交互的固定端口, client 通过这个端口向 broker 请求转发通道。 326 | 327 | ``` 328 | (dynamic) 4646 329 | +------------+ +---------------+ 330 | | 主机 Host | quic/tcp | 服务端 Server | 331 | | client | <------> | broker | 332 | +------------+ +---------------+ 333 | ``` 334 | 335 | client 请求转发通道成功后获得一个端口对( ``port1`` 和 ``port2 ``)。其中一个建立 client 和 broker 之间的连接,用作之后所有数据的交换;另一个用于客户机的连接。 336 | 337 | ``` 338 | 10800 (dynamic) port1 port2 (dynamic) 339 | +------------+ +------------+ +---------------+ +------------+ 340 | | 主机 Host | tcp/udp | 主机 Host | quic/tcp | 服务端 Server | tcp/udp | 客机 Guest | 341 | | TH Game | <-------> | client | <------> | broker | <-------> | TH Game | 342 | +------------+ +------------+ +---------------+ +------------+ 343 | ``` 344 | 345 | 通常 ``port1`` 、 ``port2`` 和其他动态端口均在 32768-65535 。 346 | 347 | ## 关于传输协议 348 | 349 | + v0.0.1 使用了 tcp ,虽然实时性不是很好(?)但是在国内网络环境下比较稳定 350 | + v0.0.3 使用了 [kcp](https://github.com/skywind3000/kcp) ,试图提升一下性能,部署以后发现从 broker 发往 client 的包都消失了…… 351 | + v0.0.5 开始使用 [quic](https://en.wikipedia.org/wiki/QUIC) ,测试效果总体来说比 kcp 稳定得多 352 | + V0.0.6 开始可以在客户端自主选择使用 quic 或 tcp 传输,增强在复杂网络环境的适应性 353 | -------------------------------------------------------------------------------- /client/lib/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/weilinfox/youmu-thlink/utils" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var logger = logrus.WithField("client", "internal") 16 | 17 | const ( 18 | DefaultLocalPort = 10800 19 | DefaultServerHost = "thlink.inuyasha.love:4646" 20 | DefaultTunnelType = "tcp" 21 | ) 22 | 23 | type Client struct { 24 | tunnel *utils.Tunnel 25 | 26 | localPort int 27 | serverHost string 28 | tunnelType string 29 | 30 | serving bool 31 | 32 | peerHost string 33 | } 34 | 35 | type BrokerStatus struct { 36 | UserCount int 37 | } 38 | 39 | // New set up new client 40 | func New(localPort int, serverHost string, tunnelType string) (*Client, error) { 41 | 42 | // check arguments 43 | if localPort <= 0 || localPort > 65535 { 44 | return nil, errors.New("Invalid port " + strconv.Itoa(localPort)) 45 | } 46 | 47 | _, port, err := net.SplitHostPort(serverHost) 48 | if err != nil { 49 | return nil, errors.New("Invalid hostname " + serverHost) 50 | } 51 | port64, err := strconv.ParseInt(port, 10, 32) 52 | if port64 <= 0 || port64 > 65535 { 53 | return nil, errors.New("Invalid port " + strconv.FormatInt(port64, 10)) 54 | } 55 | 56 | if strings.ToLower(tunnelType) != "tcp" && strings.ToLower(tunnelType) != "quic" { 57 | return nil, errors.New("Invalid tunnel type " + tunnelType) 58 | } 59 | 60 | return &Client{ 61 | localPort: localPort, 62 | serverHost: serverHost, 63 | tunnelType: tunnelType, 64 | }, nil 65 | } 66 | 67 | // NewWithDefault set up default client 68 | func NewWithDefault() *Client { 69 | return &Client{ 70 | localPort: DefaultLocalPort, 71 | serverHost: DefaultServerHost, 72 | tunnelType: DefaultTunnelType, 73 | } 74 | } 75 | 76 | // Ping get client to broker delay 77 | func (c *Client) Ping() time.Duration { 78 | 79 | buf := make([]byte, utils.CmdBufSize) 80 | 81 | // dial port 82 | conn, err := net.DialTimeout("tcp", c.serverHost, time.Millisecond*500) 83 | if err != nil { 84 | logger.WithError(err).Error("Cannot connect to broker") 85 | return time.Second 86 | } 87 | 88 | // send ping 89 | timeSend := time.Now() 90 | _ = conn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 91 | _, err = conn.Write(utils.NewDataFrame(utils.PING, nil)) 92 | _ = conn.SetWriteDeadline(time.Time{}) 93 | if err != nil { 94 | _ = conn.Close() 95 | logger.WithError(err).Error("Send ping failed") 96 | return time.Second 97 | } 98 | _ = conn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 99 | n, err := conn.Read(buf) 100 | _ = conn.SetReadDeadline(time.Time{}) 101 | if err != nil { 102 | _ = conn.Close() 103 | logger.WithError(err).Error("Get ping response failed") 104 | return time.Second 105 | } 106 | _ = conn.Close() 107 | timeResp := time.Now() 108 | 109 | // parse response 110 | dataStream := utils.NewDataStream() 111 | dataStream.Append(buf[:n]) 112 | if !dataStream.Parse() || dataStream.Type() != utils.PING { 113 | logger.Error("Invalid PING response from server") 114 | return time.Second 115 | } 116 | 117 | delay := timeResp.Sub(timeSend) 118 | 119 | return delay 120 | 121 | } 122 | 123 | // BrokerStatus get broker status data 124 | func (c *Client) BrokerStatus() BrokerStatus { 125 | 126 | buf := make([]byte, utils.CmdBufSize) 127 | status := BrokerStatus{UserCount: -1} 128 | 129 | // dial port 130 | conn, err := net.DialTimeout("tcp", c.serverHost, time.Millisecond*500) 131 | if err != nil { 132 | logger.WithError(err).Error("Cannot connect to broker") 133 | return status 134 | } 135 | 136 | // send BROKER_STATUS 137 | _ = conn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 138 | _, err = conn.Write(utils.NewDataFrame(utils.BROKER_STATUS, nil)) 139 | _ = conn.SetWriteDeadline(time.Time{}) 140 | if err != nil { 141 | _ = conn.Close() 142 | logger.WithError(err).Error("Send BROKER_STATUS failed") 143 | return status 144 | } 145 | _ = conn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 146 | n, err := conn.Read(buf) 147 | _ = conn.SetReadDeadline(time.Time{}) 148 | if err != nil { 149 | _ = conn.Close() 150 | logger.Info("Broker may not support BROKER_STATUS command") 151 | return status 152 | } 153 | _ = conn.Close() 154 | 155 | // parse response 156 | dataStream := utils.NewDataStream() 157 | dataStream.Append(buf[:n]) 158 | if !dataStream.Parse() || dataStream.Type() != utils.BROKER_STATUS { 159 | logger.Error("Invalid PING response from server") 160 | return status 161 | } 162 | 163 | data := dataStream.Data() 164 | if n >= 4 { 165 | status.UserCount = int(data[0])<<24 | int(data[1])<<16 | int(data[2])<<8 | int(data[3]) 166 | } 167 | 168 | return status 169 | 170 | } 171 | 172 | // Version get self tunnel version 173 | func (c *Client) Version() (byte, string, string) { 174 | return utils.TunnelVersion, utils.Version, utils.Channel 175 | } 176 | 177 | // BrokerVersion get broker tunnel version 178 | func (c *Client) BrokerVersion() (byte, string) { 179 | 180 | buf := make([]byte, utils.CmdBufSize) 181 | 182 | // dial port 183 | conn, err := net.DialTimeout("tcp", c.serverHost, time.Millisecond*500) 184 | if err != nil { 185 | logger.WithError(err).Error("Cannot connect to broker") 186 | return 0, "0.0.0" 187 | } 188 | 189 | // send version request 190 | _ = conn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 191 | _, err = conn.Write(utils.NewDataFrame(utils.VERSION, nil)) 192 | _ = conn.SetWriteDeadline(time.Time{}) 193 | if err != nil { 194 | _ = conn.Close() 195 | logger.WithError(err).Error("Send version request failed") 196 | return 0, "0.0.0" 197 | } 198 | _ = conn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 199 | n, err := conn.Read(buf) 200 | _ = conn.SetReadDeadline(time.Time{}) 201 | if err != nil { 202 | _ = conn.Close() 203 | logger.WithError(err).Error("Get version response failed") 204 | return 0, "0.0.0" 205 | } 206 | _ = conn.Close() 207 | 208 | // parse response 209 | dataStream := utils.NewDataStream() 210 | dataStream.Append(buf[:n]) 211 | if !dataStream.Parse() || dataStream.Type() != utils.VERSION || len(dataStream.Data()) < 6 { 212 | logger.Error("Invalid version response from server") 213 | return 0, "0.0.0" 214 | } 215 | 216 | // tunnel version code and version 217 | return dataStream.Data()[0], string(dataStream.Data()[1:]) 218 | 219 | } 220 | 221 | // Connect ask new tunnel and connect 222 | func (c *Client) Connect() error { 223 | 224 | logger.Info("Will connect to local port ", c.localPort) 225 | logger.Info("Will connect to broker address ", c.serverHost) 226 | 227 | host, _, err := net.SplitHostPort(c.serverHost) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | buf := make([]byte, utils.CmdBufSize) 233 | 234 | // new tunnel command 235 | logger.Info("Ask for new udp tunnel") 236 | conn, err := net.DialTimeout("tcp", c.serverHost, time.Millisecond*500) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | _, err = conn.Write(utils.NewDataFrame(utils.TUNNEL, []byte{'u', c.tunnelType[0]})) 242 | if err != nil { 243 | return err 244 | } 245 | defer conn.Close() 246 | n, err := conn.Read(buf) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | // new tunnel command response 252 | dataStream := utils.NewDataStream() 253 | dataStream.Append(buf[:n]) 254 | if !dataStream.Parse() || dataStream.Type() != utils.TUNNEL { 255 | return errors.New("invalid TUNNEL response from server") 256 | } 257 | 258 | var port1, port2 int 259 | port1 = int(dataStream.Data()[0])<<8 + int(dataStream.Data()[1]) 260 | port2 = int(dataStream.Data()[2])<<8 + int(dataStream.Data()[3]) 261 | if port1 <= 0 || port1 > 65535 || port2 <= 0 || port2 > 65535 { 262 | return errors.New("Invalid port peer " + strconv.Itoa(port1) + "-" + strconv.Itoa(port2)) 263 | } 264 | 265 | // Set up tunnel 266 | config := utils.TunnelConfig{ 267 | Address0: host + ":" + strconv.Itoa(port1), 268 | Address1: "localhost:" + strconv.Itoa(c.localPort), 269 | } 270 | switch c.tunnelType[0] { 271 | case 't' | 'T': 272 | config.Type = utils.DialTcpDialUdp 273 | case 'q' | 'Q': 274 | config.Type = utils.DialQuicDialUdp 275 | } 276 | 277 | c.tunnel, err = utils.NewTunnel(&config) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | hostIP, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 283 | c.peerHost = hostIP + ":" + strconv.Itoa(port2) 284 | 285 | logger.Infof("Tunnel established for remote " + c.peerHost) 286 | 287 | return nil 288 | } 289 | 290 | func (c *Client) Serve(readFunc, writeFunc utils.PluginCallback, plRoutine utils.PluginGoroutine, plQuit utils.PluginSetQuitFlag) error { 291 | if c.serving { 292 | return errors.New("already serving") 293 | } 294 | c.serving = true 295 | return c.tunnel.Serve(readFunc, writeFunc, plRoutine, plQuit) 296 | } 297 | 298 | // Close stop this tunnel 299 | func (c *Client) Close() { 300 | if c.tunnel != nil { 301 | c.tunnel.Close() 302 | } 303 | c.serving = false 304 | } 305 | 306 | // TunnelDelay ping delay between client and broker 307 | func (c *Client) TunnelDelay() time.Duration { 308 | return c.tunnel.PingDelay() 309 | } 310 | 311 | // LocalPort get client config local port 312 | func (c *Client) LocalPort() int { 313 | return c.localPort 314 | } 315 | 316 | // TunnelType get client config tunnel type tcp/quic 317 | func (c *Client) TunnelType() string { 318 | return c.tunnelType 319 | } 320 | 321 | // TunnelStatus get tunnel status 322 | func (c *Client) TunnelStatus() utils.TunnelStatus { 323 | return c.tunnel.Status() 324 | } 325 | 326 | // ServerHost get client config server host 327 | func (c *Client) ServerHost() string { 328 | return c.serverHost 329 | } 330 | 331 | // PeerHost get client peer host 332 | func (c *Client) PeerHost() string { 333 | return c.peerHost 334 | } 335 | 336 | func (c *Client) Serving() bool { 337 | return c.serving 338 | } 339 | 340 | // NetBrokerDelay broker delay nanoseconds in network 341 | func NetBrokerDelay(server string) (map[string]int, error) { 342 | 343 | // ask for net info 344 | logger.Info("Get broker list") 345 | tcpAddr, err := net.ResolveTCPAddr("tcp", server) 346 | if err != nil { 347 | return nil, err 348 | } 349 | 350 | tcpConn, err := net.DialTCP("tcp", nil, tcpAddr) 351 | if err != nil { 352 | return nil, err 353 | } 354 | defer tcpConn.Close() 355 | 356 | _, err = tcpConn.Write(utils.NewDataFrame(utils.NET_INFO, []byte{0, 0})) 357 | if err != nil { 358 | return nil, err 359 | } 360 | 361 | buf := make([]byte, utils.TransBufSize) 362 | n, err := tcpConn.Read(buf) 363 | if err != nil { 364 | return nil, err 365 | } 366 | 367 | // parse broker list 368 | dataStream := utils.NewDataStream() 369 | dataStream.Append(buf[:n]) 370 | if !dataStream.Parse() { 371 | logger.Fatal("Parse net info response failed") 372 | } 373 | 374 | serverList := make([]string, utils.BrokersCntMax+1) 375 | serverList[0] = server // NET_INFO return brokers except itself 376 | serverListCnt := 1 377 | for i := 0; i < dataStream.Len() && serverListCnt < utils.BrokersCntMax+1; i++ { 378 | l := int(dataStream.Data()[i]) 379 | 380 | serverList[serverListCnt] = string(dataStream.Data()[i+1 : i+1+l]) 381 | serverListCnt++ 382 | i += l 383 | } 384 | 385 | // ping every broker *5 386 | logger.Info("Ping broker delay") 387 | const pingTimes = 5 388 | serverDelay := make([]time.Time, utils.BrokersCntMax+1) 389 | for j := 0; j < pingTimes; j++ { 390 | for i := 0; i < serverListCnt; i++ { 391 | 392 | // dial port 393 | conn, err := net.DialTimeout("tcp", serverList[i], time.Millisecond*500) 394 | if err != nil { 395 | logger.WithError(err).Warn("Cannot connect to broker") 396 | serverDelay[i] = serverDelay[i].Add(time.Second) 397 | continue 398 | } 399 | 400 | // send ping 401 | timeSend := time.Now() 402 | _ = conn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 403 | _, err = conn.Write(utils.NewDataFrame(utils.PING, nil)) 404 | _ = conn.SetWriteDeadline(time.Time{}) 405 | if err != nil { 406 | _ = conn.Close() 407 | logger.WithError(err).Warn("Send ping failed") 408 | serverDelay[i] = serverDelay[i].Add(time.Second) 409 | continue 410 | } 411 | _ = conn.SetReadDeadline(time.Now().Add(time.Millisecond * 500)) 412 | n, err := conn.Read(buf) 413 | _ = conn.SetReadDeadline(time.Time{}) 414 | if err != nil { 415 | _ = conn.Close() 416 | logger.WithError(err).Warn("Get ping response failed") 417 | serverDelay[i] = serverDelay[i].Add(time.Second) 418 | continue 419 | } 420 | _ = conn.Close() 421 | timeResp := time.Now() 422 | 423 | // parse response 424 | dataStream = utils.NewDataStream() 425 | dataStream.Append(buf[:n]) 426 | if !dataStream.Parse() || dataStream.Type() != utils.PING { 427 | logger.Debug("Invalid PING response from server") 428 | serverDelay[i] = serverDelay[i].Add(time.Second) 429 | continue 430 | } 431 | 432 | serverDelay[i] = serverDelay[i].Add(timeResp.Sub(timeSend)) 433 | } 434 | } 435 | 436 | // make ping delay map 437 | serverDelayMap := make(map[string]int) 438 | for i := 0; i < serverListCnt; i++ { 439 | serverDelayMap[serverList[i]] = serverDelay[i].Nanosecond() / pingTimes 440 | } 441 | 442 | return serverDelayMap, nil 443 | 444 | } 445 | -------------------------------------------------------------------------------- /broker/lib/broker.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/weilinfox/youmu-thlink/utils" 12 | 13 | "github.com/quic-go/quic-go" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var logger = logrus.WithField("broker", "internal") 18 | 19 | var peers = make(map[int]int) 20 | 21 | func Main(listenAddr string, upperAddr string) { 22 | 23 | var upperAddress string // upper 24 | var upperStatus = 0 // upper broker 0 health, >0 retry times 25 | var selfPort int // self port 26 | var newBrokers sync.Map // 1 jump string=>time.Time 27 | var netBrokers sync.Map // >1 jump string=>time.Time 28 | 29 | _, slistenPort, err := net.SplitHostPort(listenAddr) 30 | if err != nil { 31 | logger.WithError(err).Fatal("Address split error") 32 | } 33 | listenPort64, err := strconv.ParseInt(slistenPort, 10, 32) 34 | selfPort = int(listenPort64) 35 | tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr) 36 | if err != nil { 37 | logger.WithError(err).Fatal("Address port parse error") 38 | } 39 | if err != nil { 40 | logger.WithError(err).Fatal("Address resolve error") 41 | } 42 | 43 | // start udp command interface 44 | logger.Info("Start tcp command interface at " + tcpAddr.String()) 45 | listener, err := net.ListenTCP("tcp", tcpAddr) 46 | if err != nil { 47 | logger.WithError(err).Fatal("Address listen failed") 48 | } 49 | defer listener.Close() 50 | 51 | // net data syncing 52 | // TODO: make it more efficient 53 | go func() { 54 | 55 | for { 56 | // tell upper broker 57 | if upperAddr != "" { 58 | 59 | tcpConn, err := net.DialTimeout("tcp", upperAddr, time.Second) 60 | 61 | if err != nil { 62 | 63 | if upperAddress == "" { 64 | logger.WithError(err).Fatal("Upper broker connect error") 65 | } else { 66 | upperStatus++ 67 | logger.WithError(err).WithField("retry", upperStatus).Error("Upper broker connect error") 68 | } 69 | 70 | } else { 71 | 72 | // logger.Debug("Ping upper addr") 73 | _, err = tcpConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, []byte{byte(selfPort >> 8), byte(selfPort)})) 74 | if err != nil { 75 | if upperAddress == "" { 76 | logger.WithError(err).Fatal("Send NET_INFO_UPDATE to upper broker error") 77 | } else { 78 | upperStatus++ 79 | logger.WithError(err).WithField("retry", upperStatus).Error("Send NET_INFO_UPDATE to upper broker error") 80 | } 81 | } 82 | 83 | _ = tcpConn.Close() 84 | 85 | // first time 86 | if upperAddress == "" { 87 | upperAddress = tcpConn.RemoteAddr().String() 88 | upperStatus = 0 89 | logger.Info("Upper broker connected ", upperAddress) 90 | 91 | // sync broker list 92 | tcpConn, err := net.DialTimeout("tcp", upperAddr, time.Second) 93 | 94 | if err != nil { 95 | 96 | logger.WithError(err).Fatal("Upper broker connect for sync error") 97 | 98 | } else { 99 | 100 | logger.Info("Sync brokers in thlink network") 101 | _, err = tcpConn.Write(utils.NewDataFrame(utils.NET_INFO, []byte{byte(selfPort >> 8), byte(selfPort)})) 102 | if err != nil { 103 | logger.WithError(err).Fatal("Send NET_INFO to upper broker error") 104 | } 105 | // read broker list 106 | buf := make([]byte, utils.TransBufSize) 107 | n, err := tcpConn.Read(buf) 108 | if err != nil { 109 | logger.WithError(err).Fatal("Read NET_INFO response error") 110 | } 111 | // parse broker list 112 | dataStream := utils.NewDataStream() 113 | dataStream.Append(buf[:n]) 114 | if !dataStream.Parse() { 115 | logger.Fatal("Parse NET_INFO response error") 116 | } 117 | for i := 0; i < dataStream.Len(); { 118 | logger.Info("Sync broker: ", string(dataStream.Data()[i+1:i+1+int(dataStream.Data()[i])])) 119 | netBrokers.Store(string(dataStream.Data()[i+1:i+1+int(dataStream.Data()[i])]), time.Now()) 120 | i += 1 + int(dataStream.Data()[i]) 121 | } 122 | 123 | _ = tcpConn.Close() 124 | 125 | } 126 | 127 | } 128 | 129 | } 130 | 131 | } 132 | 133 | // find 10s timeout broker 134 | data := []byte{byte(selfPort >> 8), byte(selfPort)} 135 | newBrokers.Range(func(k, v interface{}) bool { 136 | if time.Now().Sub(v.(time.Time)).Seconds() > 10 { 137 | logger.Info("Timeout broker: ", k) 138 | newBrokers.Delete(k) 139 | data = append(data, byte(len(k.(string)))|0x80) 140 | data = append(data, []byte(k.(string))...) 141 | } 142 | return true 143 | }) 144 | if len(data) > 2 { 145 | // tell 1 jump brokers 146 | newBrokers.Range(func(k, _ interface{}) bool { 147 | bkrConn, err := net.DialTimeout("tcp", k.(string), time.Second) 148 | if err != nil { 149 | logger.WithError(err).Warn("Send new broker 1 jump broker error") 150 | return true 151 | } 152 | logger.Debug("Send timeout broker data to ", k.(string)) 153 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, data)) 154 | return true 155 | }) 156 | // tell upper broker 157 | if upperAddress != "" { 158 | bkrConn, err := net.DialTimeout("tcp", upperAddress, time.Second) 159 | if err != nil { 160 | logger.WithError(err).Warn("Send new broker to upper broker error") 161 | } else { 162 | logger.Debug("Send timeout broker data to upper broker ", upperAddress) 163 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, data)) 164 | } 165 | } 166 | } 167 | 168 | time.Sleep(time.Second) 169 | } 170 | 171 | }() 172 | 173 | for { 174 | 175 | buf := make([]byte, utils.CmdBufSize) 176 | conn, err := listener.Accept() 177 | if err != nil { 178 | logger.WithError(err).Error("TCP listen error") 179 | continue 180 | } 181 | n, err := conn.Read(buf) 182 | if err != nil { 183 | logger.WithError(err).Error("TCP read failed") 184 | conn.Close() 185 | continue 186 | } 187 | 188 | if n >= utils.CmdBufSize { 189 | logger.Warn("RawData data too long!") 190 | conn.Close() 191 | continue 192 | } 193 | 194 | // handle commands 195 | dataStream := utils.NewDataStream() 196 | dataStream.Append(buf[:n]) 197 | if !dataStream.Parse() { 198 | logger.Warn("Invalid command") 199 | continue 200 | } 201 | 202 | cmdData := dataStream.Data() 203 | cmdLen := dataStream.Len() 204 | cmdType := dataStream.Type() 205 | go func() { 206 | switch cmdType { 207 | case utils.PING: 208 | // ping 209 | _, err := conn.Write(utils.NewDataFrame(utils.PING, nil)) 210 | 211 | if err != nil { 212 | logger.WithError(err).Error("Send response failed") 213 | } 214 | 215 | case utils.TUNNEL: 216 | // new tcp/udp tunnel 217 | // t/u 218 | var port1, port2 int 219 | var err error 220 | 221 | if cmdLen > 1 { 222 | switch cmdData[0] { 223 | case 't': 224 | logger.WithField("host", conn.RemoteAddr().String()).Info("New tcp tunnel") 225 | host, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 226 | port1, port2, err = newTcpTunnel(host) 227 | case 'u': 228 | logger.WithField("host", conn.RemoteAddr().String()).Info("New udp tunnel") 229 | host, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 230 | port1, port2, err = newUdpTunnel(host, cmdData[1]) 231 | default: 232 | logger.Warn("Invalid tunnel type") 233 | } 234 | 235 | if err != nil { 236 | logger.WithError(err).Error("Failed to build new tunnel") 237 | } 238 | } 239 | 240 | _, err = conn.Write(utils.NewDataFrame(utils.TUNNEL, []byte{byte(port1 >> 8), byte(port1), byte(port2 >> 8), byte(port2)})) 241 | 242 | if err != nil { 243 | logger.WithError(err).Error("Send response failed") 244 | } 245 | 246 | case utils.BROKER_INFO: 247 | // broker info 248 | _, err := conn.Write(utils.NewDataFrame(utils.BROKER_INFO, []byte{byte(len(peers) >> 56), byte(len(peers) >> 48), byte(len(peers) >> 40), byte(len(peers) >> 32), 249 | byte(len(peers) >> 24), byte(len(peers) >> 16), byte(len(peers) >> 8), byte(len(peers))})) 250 | 251 | if err != nil { 252 | logger.WithError(err).Error("Send response failed") 253 | } 254 | 255 | case utils.NET_INFO: 256 | // broker count BrokersCntMax max, broker No bigger than BrokersCntMax will not send 257 | // NET_INFO, self port 16bit (client should be 0) 258 | if cmdLen == 2 { 259 | 260 | var data []byte 261 | var count int 262 | rp := int(cmdData[0])<<8 + int(cmdData[1]) 263 | hr, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 264 | ar := net.JoinHostPort(hr, strconv.Itoa(rp)) 265 | logger.Debug("Net info command from ", ar) 266 | newBrokers.Range(func(k, _ interface{}) bool { 267 | if count > utils.BrokersCntMax { 268 | return false 269 | } 270 | 271 | if k == ar { 272 | return true 273 | } 274 | 275 | data = append(data, byte(len(k.(string)))) 276 | data = append(data, []byte(k.(string))...) 277 | 278 | count++ 279 | return true 280 | }) 281 | netBrokers.Range(func(k, _ interface{}) bool { 282 | if count > utils.BrokersCntMax { 283 | return false 284 | } 285 | 286 | data = append(data, byte(len(k.(string)))) 287 | data = append(data, []byte(k.(string))...) 288 | 289 | count++ 290 | return true 291 | }) 292 | if count <= utils.BrokersCntMax && upperAddress != "" { 293 | data = append(data, byte(len(upperAddress))) 294 | data = append(data, []byte(upperAddress)...) 295 | } 296 | 297 | // all known broker address 298 | _, err := conn.Write(utils.NewDataFrame(utils.NET_INFO, data)) 299 | if err != nil { 300 | logger.WithError(err).Error("Send response failed") 301 | } 302 | 303 | } 304 | 305 | case utils.NET_INFO_UPDATE: 306 | // broker HANDSHAKE 307 | // NET_INFO_UPDATE, self port 16bit, address len address string, address len address string... 308 | // response with router table 309 | if cmdLen >= 2 { 310 | remotePort := int(cmdData[0])<<8 + int(cmdData[1]) 311 | addr, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 312 | peerAddress := net.JoinHostPort(addr, strconv.Itoa(remotePort)) 313 | 314 | // ping package 315 | if peerAddress != upperAddress { 316 | if _, ok := newBrokers.Load(peerAddress); ok { 317 | // old broker 318 | newBrokers.Store(peerAddress, time.Now()) 319 | } else { 320 | 321 | // new broker, no response 322 | newBrokers.Store(peerAddress, time.Now()) 323 | logger.Info("New broker connected: ", peerAddress) 324 | 325 | newData := []byte{byte(selfPort >> 8), byte(selfPort), byte(len(peerAddress))} 326 | newData = append(newData, []byte(peerAddress)...) 327 | // send to other 1 jump brokers 328 | newBrokers.Range(func(k, _ interface{}) bool { 329 | 330 | if k == peerAddress { 331 | return true 332 | } 333 | 334 | bkrConn, err := net.DialTimeout("tcp", k.(string), time.Second) 335 | if err != nil { 336 | logger.WithError(err).Warn("Send new broker 1 jump broker error") 337 | return true 338 | } 339 | logger.Debug("Send new broker to ", k.(string)) 340 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, newData)) 341 | 342 | return true 343 | }) 344 | // send to upper broker 345 | if upperAddress != "" { 346 | bkrConn, err := net.DialTimeout("tcp", upperAddress, time.Second) 347 | if err != nil { 348 | logger.WithError(err).Warn("Send new broker to upper broker error") 349 | } else { 350 | logger.Debug("Send new broker to ", upperAddress) 351 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, newData)) 352 | } 353 | } 354 | 355 | } 356 | } 357 | 358 | // not a broker ping package 359 | if cmdLen > 2 { 360 | routeData := cmdData[2:] 361 | for i := 0; i < len(routeData); i++ { 362 | // u > 0 delete; u == 0 update 363 | u := routeData[i] & 0x80 364 | l := int(routeData[i] & 0x7f) 365 | if u > 0 { 366 | logger.WithField("from", conn.RemoteAddr()).Info("Remove broker: ", string(routeData[i+1:i+1+l])) 367 | netBrokers.Delete(string(routeData[i+1 : i+1+l])) 368 | } else { 369 | logger.WithField("from", conn.RemoteAddr()).Info("New broker: ", string(routeData[i+1:i+1+l])) 370 | netBrokers.Store(string(routeData[i+1:i+1+l]), time.Now()) 371 | } 372 | i += l 373 | } 374 | 375 | // send to other 1 jump brokers 376 | newBrokers.Range(func(k, _ interface{}) bool { 377 | 378 | if k == peerAddress { 379 | return true 380 | } 381 | 382 | bkrConn, err := net.DialTimeout("tcp", k.(string), time.Second) 383 | if err != nil { 384 | logger.WithError(err).Warn("Send broker update to 1 jump broker error") 385 | return true 386 | } 387 | logger.Debug("Send new broker to ", k.(string)) 388 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, append([]byte{byte(selfPort >> 8), byte(selfPort)}, routeData...))) 389 | 390 | return true 391 | }) 392 | // send to upper broker 393 | if upperAddress != "" && peerAddress != upperAddress { 394 | bkrConn, err := net.DialTimeout("tcp", upperAddress, time.Second) 395 | if err != nil { 396 | logger.WithError(err).Warn("Send broker update to upper broker error") 397 | } else { 398 | logger.Debug("Send new broker to ", upperAddress) 399 | _, _ = bkrConn.Write(utils.NewDataFrame(utils.NET_INFO_UPDATE, append([]byte{byte(selfPort >> 8), byte(selfPort)}, routeData...))) 400 | } 401 | } 402 | } 403 | } 404 | 405 | case utils.VERSION: 406 | // tunnel VERSION 407 | version := []byte{utils.TunnelVersion} 408 | version = append(version, []byte(utils.Version)...) 409 | _, err = conn.Write(utils.NewDataFrame(utils.VERSION, version)) 410 | 411 | case utils.BROKER_STATUS: 412 | // broker status 413 | // 32bits tunnel count 414 | tunnelCount := len(peers) 415 | status := []byte{byte(tunnelCount >> 24), byte(tunnelCount >> 16), byte(tunnelCount >> 8), byte(tunnelCount)} 416 | _, err = conn.Write(utils.NewDataFrame(utils.BROKER_STATUS, status)) 417 | 418 | default: 419 | logger.Warn("RawData data invalid") 420 | } 421 | 422 | _ = conn.Close() 423 | 424 | }() 425 | 426 | } 427 | } 428 | 429 | // start new tcp tunnel 430 | func newTcpTunnel(hostIP string) (int, int, error) { 431 | 432 | serveTcpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:0") 433 | 434 | // quic tunnel between broker and client 435 | tlsConfig, err := utils.GenerateTLSConfig() 436 | hostListener, err := quic.ListenAddr(hostIP+":0", tlsConfig, nil) 437 | if err != nil { 438 | return 0, 0, err 439 | } 440 | serveListener, err := net.ListenTCP("tcp", serveTcpAddr) 441 | if err != nil { 442 | _ = hostListener.Close() 443 | return 0, 0, err 444 | } 445 | 446 | _, hostPort, _ := net.SplitHostPort(hostListener.Addr().String()) 447 | _, servePort, _ := net.SplitHostPort(serveListener.Addr().String()) 448 | iHostPort, _ := strconv.ParseInt(hostPort, 10, 32) 449 | iServePort, _ := strconv.ParseInt(servePort, 10, 32) 450 | 451 | logger.Infof("New tcp peer " + hostPort + "-" + servePort) 452 | peers[int(iHostPort)] = int(iServePort) 453 | go handleTcpTunnel(int(iHostPort), hostListener, serveListener) 454 | 455 | return int(iHostPort), int(iServePort), nil 456 | 457 | } 458 | 459 | // start new udp tunnel 460 | func newUdpTunnel(hostIP string, tunnelType byte) (int, int, error) { 461 | 462 | config := utils.TunnelConfig{} 463 | switch tunnelType { 464 | case 'q': 465 | config.Type = utils.ListenQuicListenUdp 466 | case 't': 467 | config.Type = utils.ListenTcpListenUdp 468 | default: 469 | return 0, 0, errors.New("no such tunnel type " + string(tunnelType)) 470 | } 471 | 472 | tunnel, err := utils.NewTunnel(&config) 473 | if err != nil { 474 | return 0, 0, err 475 | } 476 | 477 | port1, port2 := tunnel.Ports() 478 | peers[port1] = port2 479 | logger.Infof("New udp peer " + strconv.Itoa(port1) + "-" + strconv.Itoa(port2)) 480 | 481 | go handleUdpTunnel(tunnel) 482 | 483 | return port1, port2, nil 484 | 485 | } 486 | 487 | func handleTcpTunnel(clientPort int, hostListener quic.Listener, serveListener *net.TCPListener) { 488 | 489 | defer func() { 490 | delete(peers, clientPort) 491 | }() 492 | defer logger.Infof("End tcp peer %d-%d", clientPort, peers[clientPort]) 493 | 494 | defer hostListener.Close() 495 | defer serveListener.Close() 496 | 497 | // client connect tunnel in 10s 498 | var waitMs int64 = 0 499 | var qConn quic.Connection 500 | var err error 501 | for { 502 | switch waitMs { 503 | case 0: 504 | go func() { 505 | qConn, err = hostListener.Accept(context.Background()) 506 | }() 507 | 508 | default: 509 | if qConn == nil && err == nil { 510 | time.Sleep(time.Millisecond) 511 | } 512 | } 513 | 514 | if qConn != nil || err != nil { 515 | break 516 | } 517 | 518 | waitMs++ 519 | if waitMs > 1000*10 { 520 | logger.WithError(err).Error("Get client connection timeout") 521 | 522 | return 523 | } 524 | } 525 | if err != nil { 526 | logger.WithError(err).Error("Get client connection failed") 527 | return 528 | } 529 | 530 | qStream, err := qConn.AcceptStream(context.Background()) 531 | if err != nil { 532 | logger.WithError(err).Error("Get client stream failed") 533 | return 534 | } 535 | defer qStream.Close() 536 | 537 | conn2, err := serveListener.AcceptTCP() 538 | if err != nil { 539 | logger.WithError(err).Error("Get serve connection failed") 540 | return 541 | } 542 | 543 | _ = conn2.SetKeepAlive(true) 544 | defer conn2.Close() 545 | 546 | ch := make(chan int) 547 | go func() { 548 | defer func() { 549 | ch <- 1 550 | }() 551 | 552 | buf := make([]byte, utils.TransBufSize) 553 | 554 | for { 555 | n, err := qStream.Read(buf) 556 | 557 | if n > 0 { 558 | p := 0 559 | for { 560 | p, err = conn2.Write(buf[p:n]) 561 | 562 | if err != nil || p == n { 563 | break 564 | } 565 | } 566 | } 567 | 568 | if err != nil { 569 | break 570 | } 571 | } 572 | }() 573 | 574 | go func() { 575 | defer func() { 576 | ch <- 1 577 | }() 578 | 579 | buf := make([]byte, utils.TransBufSize) 580 | 581 | for { 582 | n, err := conn2.Read(buf) 583 | 584 | if n > 0 { 585 | p := 0 586 | for { 587 | p, err = qStream.Write(buf[p:n]) 588 | 589 | if err != nil || p == n { 590 | break 591 | } 592 | } 593 | } 594 | 595 | if err != nil { 596 | break 597 | } 598 | } 599 | }() 600 | 601 | <-ch 602 | } 603 | 604 | func handleUdpTunnel(tunnel *utils.Tunnel) { 605 | 606 | port1, port2 := tunnel.Ports() 607 | 608 | defer func() { 609 | delete(peers, port1) 610 | }() 611 | defer logger.Infof("End udp peer %d-%d", port1, port2) 612 | defer tunnel.Close() 613 | 614 | err := tunnel.Serve(nil, nil, nil, nil) 615 | if err != nil { 616 | logger.WithError(err).Error("Tunnel serve error") 617 | } 618 | 619 | } 620 | -------------------------------------------------------------------------------- /client/lib/hisoutensoku.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "io" 7 | "math" 8 | "net" 9 | "time" 10 | 11 | "github.com/weilinfox/youmu-thlink/utils" 12 | 13 | "github.com/quic-go/quic-go" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type type123pkg byte 18 | 19 | const ( 20 | HELLO_123 type123pkg = iota + 1 // 0x01 21 | PUNCH_123 // 0x02 22 | OLLEH_123 // 0x03 23 | CHAIN_123 // 0x04 24 | INIT_REQUEST_123 // 0x05 25 | INIT_SUCCESS_123 // 0x06 26 | INIT_ERROR_123 // 0x07 27 | REDIRECT_123 // 0x08 28 | QUIT_123 type123pkg = iota + 3 // 0x0b 29 | HOST_GAME_123 type123pkg = iota + 4 // 0x0d 30 | CLIENT_GAME_123 // 0x0e 31 | SOKUROLL_TIME type123pkg = iota + 5 // 0x10 32 | SOKUROLL_TIME_ACK // 0x11 33 | SOKUROLL_STATE // 0x12 34 | SOKUROLL_SETTINGS // 0x13 35 | SOKUROLL_SETTINGS_ACK // 0x14 36 | ) 37 | 38 | type data123pkg byte 39 | 40 | const ( 41 | GAME_LOADED_123 data123pkg = iota + 1 // 0x01 42 | GAME_LOADED_ACK_123 // 0x02 43 | GAME_INPUT_123 // 0x03 44 | GAME_MATCH_123 // 0x04 45 | GAME_MATCH_ACK_123 // 0x05 46 | GAME_MATCH_REQUEST_123 data123pkg = iota + 3 // 0x08 47 | GAME_REPLAY_123 // 0x09 48 | GAME_REPLAY_REQUEST_123 data123pkg = iota + 4 // 0x0b 49 | ) 50 | 51 | type Status123peer byte 52 | 53 | const ( 54 | INACTIVE_123 Status123peer = iota 55 | SUCCESS_123 56 | BATTLE_123 57 | BATTLE_WAIT_ANOTHER_123 58 | ) 59 | 60 | type spectate123type byte 61 | 62 | const ( 63 | NOSPECTATE_123 spectate123type = 0x00 64 | SPECTATE_123 spectate123type = 0x10 65 | SPECTATE_FOR_SPECTATOR_123 spectate123type = 0x11 66 | ) 67 | 68 | type hisoutensokuData struct { 69 | // PeerAddr [6]byte // appear in HELLO 70 | // TargetAddr [6]byte // appear in HELLO 71 | // GameID [16]byte // appear in INIT_REQUEST 72 | InitSuccessPkg [81]byte // copy from INIT_SUCCESS 73 | ClientProf string // parse from INIT_SUCCESS 74 | HostProf string // parse from INIT_SUCCESS 75 | Spectator bool // parse from INIT_SUCCESS 76 | SwrDisabled bool // parse from INIT_SUCCESS 77 | 78 | HostInfo [45]byte // parse from HOST_GAME GAME_MATCH 79 | ClientInfo [45]byte // parse from HOST_GAME GAME_MATCH 80 | StageId byte // parse from HOST_GAME GAME_MATCH 81 | MusicId byte // parse from HOST_GAME GAME_MATCH 82 | RandomSeeds [4]byte // parse from HOST_GAME GAME_MATCH 83 | MatchId byte // parse from HOST_GAME GAME_MATCH 84 | ReplayData map[byte][]uint16 // parse from HOST_GAME GAME_REPLAY 85 | ReplayEnd map[byte]bool // parse from HOST_GAME GAME_REPLAY 86 | } 87 | 88 | func newHisoutensokuData() *hisoutensokuData { 89 | return &hisoutensokuData{ 90 | ReplayData: make(map[byte][]uint16), 91 | ReplayEnd: make(map[byte]bool), 92 | } 93 | } 94 | 95 | type status123req byte 96 | 97 | const ( 98 | INIT_123 status123req = iota 99 | SEND_123 100 | SENT0_123 101 | SENT1_123 102 | SEND_AGAIN_123 103 | ) 104 | 105 | type Hisoutensoku struct { 106 | peerId byte // current host/client peer id (udp mutex id) 107 | PeerStatus Status123peer // current peer status 108 | peerData *hisoutensokuData // current peer data record 109 | gameId map[byte][16]byte // game id record 110 | repReqStatus status123req // GAME_REPLAY_REQUEST send status 111 | repReqTime time.Time // request send time 112 | repReqDelay time.Duration // delay between GAME_REPLAY_REQUEST and GAME_REPLAY package 113 | spectatorCount int // spectator counter 114 | 115 | quitFlag bool // plugin quit flag 116 | } 117 | 118 | var logger123 = logrus.WithField("Hisoutensoku", "internal") 119 | 120 | // NewHisoutensoku new Hisoutensoku spectating server 121 | func NewHisoutensoku() *Hisoutensoku { 122 | return &Hisoutensoku{ 123 | PeerStatus: INACTIVE_123, 124 | peerData: newHisoutensokuData(), 125 | gameId: make(map[byte][16]byte), 126 | repReqStatus: INIT_123, 127 | repReqDelay: time.Second, 128 | spectatorCount: 0, 129 | quitFlag: false, 130 | } 131 | } 132 | 133 | // WriteFunc from game host to client 134 | // orig: original data leads with 1 byte of client id 135 | func (h *Hisoutensoku) WriteFunc(orig []byte) (bool, []byte) { 136 | 137 | switch type123pkg(orig[1]) { 138 | case INIT_SUCCESS_123: 139 | if len(orig)-1 == 81 { 140 | 141 | switch spectate123type(orig[6]) { 142 | case NOSPECTATE_123, SPECTATE_123: 143 | // init success 144 | h.peerData.Spectator = spectate123type(orig[6]) == SPECTATE_123 145 | for i := 14; i <= 46; i++ { 146 | if orig[i] == 0x00 || i == 46 { 147 | h.peerData.HostProf = string(orig[14:i]) 148 | break 149 | } 150 | } 151 | for i := 46; i <= 78; i++ { 152 | if orig[i] == 0x00 || i == 78 { 153 | h.peerData.ClientProf = string(orig[46:i]) 154 | break 155 | } 156 | } 157 | h.peerData.SwrDisabled = (int(orig[78]) | int(orig[79])<<8 | int(80)<<16 | int(81)<<24) != 0 158 | 159 | logger123.Debug("INIT_SUCCESS with host profile ", h.peerData.HostProf, " client profile ", 160 | h.peerData.ClientProf, " swr disabled ", h.peerData.SwrDisabled) 161 | 162 | h.peerId = orig[0] 163 | h.PeerStatus = SUCCESS_123 164 | h.peerData.MatchId = 0 165 | 166 | logger123.Info("Th123 peer init success: spectator=", h.peerData.Spectator) 167 | 168 | case SPECTATE_FOR_SPECTATOR_123: 169 | logger123.Warn("INIT_SUCCESS type SPECTATE_FOR_SPECTATOR appears here?") 170 | default: 171 | logger123.Warn("INIT_SUCCESS spectacle type cannot recognize") 172 | } 173 | 174 | } else { 175 | logger123.Warn("INIT_SUCCESS with strange length ", len(orig)-1) 176 | } 177 | 178 | case QUIT_123: 179 | if len(orig)-1 == 1 { 180 | logger123.Info("Th123 peer quit") 181 | if orig[0] == h.peerId { 182 | h.PeerStatus = INACTIVE_123 183 | } 184 | } else { 185 | logger123.Warn("QUIT with strange length ", len(orig)-1) 186 | } 187 | 188 | case CLIENT_GAME_123: 189 | logger123.Warn("CLIENT_GAME should not appear here right? ", orig[1:]) 190 | 191 | case HOST_GAME_123: 192 | switch data123pkg(orig[2]) { 193 | case GAME_LOADED_ACK_123: 194 | if orig[3] == 0x05 { 195 | logger123.Info("Th123 battle loaded") 196 | h.PeerStatus = BATTLE_123 197 | } 198 | } 199 | } 200 | 201 | return false, orig 202 | } 203 | 204 | // ReadFunc from game client to host 205 | // orig: original data leads with 1 byte of client id 206 | func (h *Hisoutensoku) ReadFunc(orig []byte) (bool, []byte) { 207 | 208 | switch type123pkg(orig[1]) { 209 | case HELLO_123: 210 | if len(orig)-1 == 37 { 211 | if h.PeerStatus > SUCCESS_123 && orig[0] != h.peerId { 212 | return true, []byte{orig[0], byte(OLLEH_123)} 213 | } 214 | } else { 215 | logger123.Warn("HELLO with strange length ", len(orig)-1) 216 | } 217 | 218 | case CHAIN_123: 219 | if len(orig)-1 == 5 { 220 | if h.PeerStatus > SUCCESS_123 && orig[0] != h.peerId { 221 | return true, []byte{orig[0], 4, 4, 0, 0, 0} 222 | } 223 | } else { 224 | logger123.Warn("CHAIN with strange length ", len(orig)-1) 225 | } 226 | 227 | case INIT_REQUEST_123: 228 | if len(orig)-1 == 65 { 229 | 230 | var gameId [16]byte 231 | 232 | copy(gameId[:], orig[2:18]) 233 | logger123.Debug("INIT_REQUEST with game id ", gameId, " request type is ", orig[26]) 234 | 235 | h.gameId[orig[0]] = gameId 236 | 237 | if h.PeerStatus > SUCCESS_123 && orig[0] != h.peerId { 238 | // from spectator 239 | if (orig[26] == 0x00 && h.peerData.Spectator && h.peerData.MatchId == 0) || orig[26] == 0x01 { 240 | // replay request but not start or match request 241 | logger123.Debug("INIT_REQUEST for game from spectator") 242 | return true, []byte{orig[0], byte(INIT_ERROR_123), 1, 0, 0, 0} 243 | } else if orig[26] == 0x00 && !h.peerData.Spectator { 244 | // not allow spectator 245 | logger123.Info("Th123 not allow spectator") 246 | return true, []byte{orig[0], byte(INIT_ERROR_123), 0, 0, 0, 0} 247 | } else if orig[26] == 0x00 && h.peerData.MatchId > 0 { 248 | // replay request and match started 249 | logger123.Info("Th123 spectacle int request from spectator") 250 | return true, append([]byte{orig[0]}, h.peerData.InitSuccessPkg[:]...) 251 | } 252 | } 253 | 254 | } else { 255 | logger123.Warn("INIT_REQUEST with strange length ", len(orig)-1) 256 | } 257 | 258 | case INIT_SUCCESS_123: 259 | if len(orig)-1 == 81 { 260 | switch spectate123type(orig[6]) { 261 | case SPECTATE_FOR_SPECTATOR_123: 262 | copy(h.peerData.InitSuccessPkg[:], orig[1:]) 263 | h.repReqStatus = SEND_123 264 | logger123.Info("Th123 spectacle INIT_SUCCESS") 265 | 266 | return false, nil 267 | } 268 | } else { 269 | logger123.Warn("INIT_SUCCESS with strange length ", len(orig)-1) 270 | } 271 | 272 | case QUIT_123: 273 | if len(orig)-1 == 1 { 274 | logger123.Info("Th123 peer quit") 275 | if orig[0] == h.peerId { 276 | h.PeerStatus = INACTIVE_123 277 | h.repReqStatus = INIT_123 278 | } else { 279 | return false, nil 280 | } 281 | } else { 282 | logger123.Warn("QUIT with strange length ", len(orig)-1) 283 | } 284 | 285 | case HOST_GAME_123: 286 | switch data123pkg(orig[2]) { 287 | case GAME_MATCH_123: 288 | if len(orig)-1 == 99 { 289 | if orig[0] == h.peerId { 290 | // game match data 291 | copy(h.peerData.HostInfo[:], orig[3:48]) 292 | copy(h.peerData.ClientInfo[:], orig[48:93]) 293 | h.peerData.StageId = orig[93] 294 | h.peerData.MusicId = orig[94] 295 | copy(h.peerData.RandomSeeds[:], orig[95:99]) 296 | h.peerData.MatchId = orig[99] 297 | h.peerData.ReplayData[orig[99]] = make([]uint16, 1) // 填充一个 garbage 298 | h.peerData.ReplayEnd[orig[99]] = false 299 | 300 | h.repReqStatus = SEND_123 301 | 302 | logger123.Info("Th123 new match ", orig[99]) 303 | 304 | return false, nil 305 | } 306 | } else if len(orig)-1 != 59 { 307 | logger123.Warn("HOST_GAME GAME_MATCH with strange length ", len(orig)-1) 308 | } 309 | 310 | case GAME_REPLAY_123: 311 | if orig[0] == h.peerId { 312 | // game replay data 313 | if len(orig) > 4 && len(orig)-4 == int(orig[3]) { 314 | timeDelay := time.Now().Sub(h.repReqTime) 315 | 316 | r, err := zlib.NewReader(bytes.NewBuffer(orig[4:])) 317 | if err != nil { 318 | logger123.WithError(err).Error("Th123 new zlib reader error") 319 | } else { 320 | ans := make([]byte, utils.TransBufSize) 321 | n, err := r.Read(ans) 322 | _ = r.Close() 323 | 324 | if err == io.EOF { 325 | // game_inputs_count 60 MAX 326 | if n >= 10 && n-10 == int(ans[9])*2 { 327 | frameId := int(ans[0]) | int(ans[1])<<8 | int(ans[2])<<16 | int(ans[3])<<24 328 | endFrameId := int(ans[4]) | int(ans[5])<<8 | int(ans[6])<<16 | int(ans[7])<<24 329 | 330 | data := h.peerData.ReplayData[ans[8]] 331 | getDataLen := len(data) - 1 332 | if getDataLen == -1 { 333 | logger123.Error("Th123 no such match: ", ans[8]) 334 | } else if frameId-getDataLen <= int(ans[9]) { 335 | newDataLen := frameId - getDataLen 336 | 337 | if newDataLen > 0 { 338 | newData := make([]uint16, newDataLen) 339 | 340 | for i := 0; i < newDataLen; i++ { 341 | newData[newDataLen-1-i] = uint16(ans[10+i*2])<<8 | uint16(ans[11+i*2]) 342 | } 343 | 344 | h.peerData.ReplayData[ans[8]] = append(data, newData...) 345 | 346 | if len(h.peerData.ReplayData[ans[8]])-1 != frameId { 347 | logger123.Error("Th123 replay data not match after append new data") 348 | } 349 | } 350 | 351 | if endFrameId != 0 && endFrameId == frameId && !h.peerData.ReplayEnd[ans[8]] { 352 | logger123.Info("Th123 match end: ", ans[8]) 353 | h.peerData.ReplayEnd[ans[8]] = true 354 | h.PeerStatus = BATTLE_WAIT_ANOTHER_123 355 | } 356 | 357 | h.repReqTime = time.Time{} 358 | h.repReqDelay = timeDelay 359 | h.repReqStatus = SEND_123 360 | } else { 361 | logger123.Warn("Replay data package drop: frame id ", frameId, " length ", ans[9]) 362 | } 363 | } else { 364 | logger123.Error("Replay data content invalid") 365 | } 366 | } else { 367 | logger123.WithError(err).Error("Zlib decode error") 368 | } 369 | // fmt.Println(err == io.EOF, err, ans[:n]) 370 | } 371 | } else { 372 | logger123.Warn("Th123 replay data invalid") 373 | } 374 | } 375 | 376 | return false, nil 377 | } 378 | 379 | case CLIENT_GAME_123: 380 | switch data123pkg(orig[2]) { 381 | case GAME_LOADED_ACK_123: 382 | if orig[3] == 0x05 { 383 | logger123.Info("Th123 battle loaded") 384 | h.PeerStatus = BATTLE_123 385 | } 386 | 387 | case GAME_REPLAY_REQUEST_123: 388 | if len(orig)-1 == 7 { 389 | 390 | // game replay request from spectator 391 | frameId := int(orig[3]) | int(orig[4])<<8 | int(orig[5])<<16 | int(orig[6])<<24 392 | if frameId == 0xffffffff || orig[7] < h.peerData.MatchId { 393 | data := []byte{orig[0], byte(HOST_GAME_123), byte(GAME_MATCH_123)} 394 | data = append(data, h.peerData.HostInfo[:]...) 395 | data = append(data, h.peerData.ClientInfo[:]...) 396 | data = append(data, h.peerData.StageId) 397 | data = append(data, h.peerData.MusicId) 398 | data = append(data, h.peerData.RandomSeeds[:]...) 399 | data = append(data, h.peerData.MatchId) 400 | 401 | logger123.Debug("GAME_REPLAY_REQUEST reply with GAME_MATCH") 402 | logger123.Info("New spectator join") 403 | h.spectatorCount++ 404 | 405 | return true, data 406 | 407 | } else if orig[7] == h.peerData.MatchId { 408 | 409 | data := []byte{orig[0], byte(HOST_GAME_123), byte(GAME_REPLAY_123)} 410 | 411 | // replay data 412 | repData := h.peerData.ReplayData[h.peerData.MatchId] 413 | endFrameId := len(repData) - 1 414 | sendFrameId := int(math.Min(float64(endFrameId), float64(frameId+60))) 415 | var gameInput []byte 416 | if frameId <= endFrameId { 417 | // send 60 max 418 | for i := sendFrameId; i > frameId; i-- { 419 | gameInput = append(gameInput, []byte{byte(repData[i] >> 8), byte(repData[i])}...) 420 | } 421 | } 422 | if len(gameInput)%4 != 0 { 423 | logger123.Warn("Th123 game input is not time of 4 ?") 424 | } 425 | 426 | // append addition data (frameId endFrameId matchId inputCount inputs) 427 | gameInput = append([]byte{h.peerData.MatchId, byte(len(gameInput) >> 1)}, gameInput...) 428 | if h.peerData.ReplayEnd[h.peerData.MatchId] { 429 | if frameId == 0 { 430 | // when some spectator finish fetching data, 431 | // it will send 0 frame id, which lead to strange bug 432 | gameInput = []byte{h.peerData.MatchId, 0} 433 | sendFrameId = 0 434 | } 435 | gameInput = append([]byte{byte(endFrameId), byte(endFrameId >> 8), byte(endFrameId >> 16), byte(endFrameId >> 24)}, gameInput...) 436 | } else { 437 | gameInput = append([]byte{0, 0, 0, 0}, gameInput...) 438 | } 439 | gameInput = append([]byte{byte(sendFrameId), byte(sendFrameId >> 8), byte(sendFrameId >> 16), byte(sendFrameId >> 24)}, gameInput...) 440 | 441 | // zlib compress 442 | var zlibData bytes.Buffer 443 | zlibw := zlib.NewWriter(&zlibData) 444 | _, err := zlibw.Write(gameInput) 445 | if err != nil { 446 | logger123.WithError(err).Error("Th123 zlib compress error") 447 | } 448 | _ = zlibw.Close() 449 | 450 | // make data (0x09 size data) 451 | data = append(data, byte(zlibData.Len())) 452 | data = append(data, zlibData.Bytes()...) 453 | 454 | if endFrameId == sendFrameId && h.PeerStatus == INACTIVE_123 { 455 | // let spectator quit 456 | logger123.Info("Th123 quit spectator") 457 | return true, []byte{orig[0], byte(QUIT_123)} 458 | } 459 | return true, data 460 | 461 | } 462 | } else { 463 | logger123.Warn("CLIENT_GAME GAME_REPLAY_REQUEST with strange length ", len(orig)-1) 464 | } 465 | 466 | return false, nil 467 | } 468 | } 469 | 470 | return false, orig 471 | } 472 | 473 | func (h *Hisoutensoku) GoroutineFunc(tunnelConn interface{}, _ *net.UDPConn) { 474 | logger123.Info("Th123 plugin goroutine start") 475 | defer logger123.Info("Th123 plugin goroutine quit") 476 | 477 | bigLoop: 478 | for { 479 | if h.PeerStatus == BATTLE_123 { 480 | switch h.repReqStatus { 481 | case INIT_123: 482 | if h.peerData.Spectator { 483 | gameId := h.gameId[h.peerId] 484 | requestData := append([]byte{h.peerId, byte(INIT_REQUEST_123)}, gameId[:]...) // INIT_REQUEST and game id 485 | requestData = append(requestData, make([]byte, 8)...) // garbage 486 | requestData = append(requestData, 0x00) // spectacle request 487 | requestData = append(requestData, 0x00) // data length 0 488 | requestData = append(requestData, make([]byte, 38)...) // make it 65 bytes long 489 | 490 | var err error 491 | switch conn := tunnelConn.(type) { 492 | case quic.Stream: 493 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, requestData)) 494 | case *net.TCPConn: 495 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, requestData)) 496 | } 497 | if err != nil { 498 | logger123.WithError(err).Error("Th123 send INIT_REQUEST error") 499 | break 500 | } 501 | logger123.Info("Th123 send spectacle INIT_REQUEST") 502 | } 503 | 504 | case SEND_123, SEND_AGAIN_123: 505 | getId := len(h.peerData.ReplayData[h.peerData.MatchId]) - 1 506 | 507 | requestData := []byte{h.peerId, byte(CLIENT_GAME_123), byte(GAME_REPLAY_REQUEST_123), 508 | byte(getId), byte(getId >> 8), byte(getId >> 16), byte(getId >> 24), h.peerData.MatchId} 509 | 510 | h.repReqTime = time.Now() 511 | 512 | var err error 513 | switch conn := tunnelConn.(type) { 514 | case quic.Stream: 515 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, requestData)) 516 | case *net.TCPConn: 517 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, requestData)) 518 | } 519 | if err != nil { 520 | logger123.WithError(err).Error("Th123 send GAME_REPLAY_REQUEST error") 521 | break bigLoop 522 | } 523 | 524 | h.repReqStatus = SENT0_123 525 | 526 | case SENT0_123, SENT1_123: 527 | h.repReqStatus++ 528 | } 529 | } 530 | 531 | if h.quitFlag { 532 | break 533 | } 534 | 535 | // 15 request per second 536 | time.Sleep(time.Millisecond * 66) 537 | } 538 | } 539 | 540 | func (h *Hisoutensoku) GetReplayDelay() time.Duration { 541 | return h.repReqDelay 542 | } 543 | 544 | func (h *Hisoutensoku) GetSpectatorCount() int { 545 | return h.spectatorCount 546 | } 547 | 548 | func (h *Hisoutensoku) SetQuitFlag() { 549 | h.quitFlag = true 550 | } 551 | -------------------------------------------------------------------------------- /utils/tunnel.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/quic-go/quic-go" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const ( 17 | // TunnelVersion tunnel compatible version 18 | TunnelVersion byte = 2 19 | ) 20 | 21 | // Tunnel just like a bidirectional pipe 22 | type Tunnel struct { 23 | tunnelType TunnelType 24 | 25 | tunnelStatus TunnelStatus 26 | 27 | pingDelay time.Duration 28 | 29 | configPort0 int 30 | configPort1 int 31 | connection0 interface{} 32 | connection1 interface{} 33 | } 34 | 35 | // TunnelType type of tunnel Dial/Listen Address0 and Dial/Listen Address1. 36 | // Warn that Address1 is for communicate between client and broker, 37 | // that means DataStream and DataFrame(see NewDataFrame) will be used. 38 | // Address2 is for generic connection 39 | type TunnelType int 40 | 41 | const ( 42 | DialQuicDialUdp TunnelType = iota 43 | DialTcpDialUdp 44 | ListenQuicListenUdp 45 | ListenTcpListenUdp 46 | ) 47 | 48 | type TunnelStatus int 49 | 50 | const ( 51 | STATUS_INIT TunnelStatus = iota 52 | STATUS_CONNECTED 53 | STATUS_CLOSED 54 | STATUS_FAILED 55 | ) 56 | 57 | // TunnelConfig default IP is 0.0.0.0:0 58 | type TunnelConfig struct { 59 | Type TunnelType 60 | Address0 string 61 | Address1 string 62 | } 63 | 64 | var loggerTunnel = logrus.WithField("utils", "tunnel") 65 | 66 | // NewTunnel set up a new tunnel 67 | func NewTunnel(config *TunnelConfig) (*Tunnel, error) { 68 | 69 | if len(strings.TrimSpace(config.Address0)) == 0 { 70 | config.Address0 = "0.0.0.0:0" 71 | } 72 | if len(strings.TrimSpace(config.Address1)) == 0 { 73 | config.Address1 = "0.0.0.0:0" 74 | } 75 | 76 | switch config.Type { 77 | case ListenQuicListenUdp: 78 | 79 | // listen quic port 80 | tlsConfig, err := GenerateTLSConfig() 81 | if err != nil { 82 | return nil, err 83 | } 84 | quicListener, err := quic.ListenAddr(config.Address0, tlsConfig, nil) 85 | if err != nil { 86 | return nil, err 87 | } 88 | loggerTunnel.Debug("QUIC listen at ", quicListener.Addr().String()) 89 | 90 | // listen udp port 91 | udpAddr, err := net.ResolveUDPAddr("udp", config.Address1) 92 | if err != nil { 93 | _ = quicListener.Close() 94 | return nil, err 95 | } 96 | udpConn, err := net.ListenUDP("udp", udpAddr) 97 | if err != nil { 98 | _ = quicListener.Close() 99 | return nil, err 100 | } 101 | loggerTunnel.Debug("UDP listen at ", udpConn.LocalAddr().String()) 102 | 103 | _, sport0, _ := net.SplitHostPort(quicListener.Addr().String()) 104 | _, sport1, _ := net.SplitHostPort(udpConn.LocalAddr().String()) 105 | port0, _ := strconv.ParseInt(sport0, 10, 32) 106 | port1, _ := strconv.ParseInt(sport1, 10, 32) 107 | 108 | return &Tunnel{ 109 | tunnelType: config.Type, 110 | tunnelStatus: STATUS_INIT, 111 | configPort0: int(port0), 112 | connection0: quicListener, 113 | configPort1: int(port1), 114 | connection1: udpConn, 115 | }, nil 116 | 117 | case ListenTcpListenUdp: 118 | 119 | // listen tcp port 120 | tcpAddr, err := net.ResolveTCPAddr("tcp", config.Address0) 121 | if err != nil { 122 | return nil, err 123 | } 124 | tcpListener, err := net.ListenTCP("tcp", tcpAddr) 125 | if err != nil { 126 | return nil, err 127 | } 128 | loggerTunnel.Debug("TCP listen at ", tcpListener.Addr().String()) 129 | 130 | // listen udp port 131 | udpAddr, err := net.ResolveUDPAddr("udp", config.Address1) 132 | if err != nil { 133 | _ = tcpListener.Close() 134 | return nil, err 135 | } 136 | udpConn, err := net.ListenUDP("udp", udpAddr) 137 | if err != nil { 138 | _ = tcpListener.Close() 139 | return nil, err 140 | } 141 | loggerTunnel.Debug("UDP listen at ", udpConn.LocalAddr().String()) 142 | 143 | _, sport0, _ := net.SplitHostPort(tcpListener.Addr().String()) 144 | _, sport1, _ := net.SplitHostPort(udpConn.LocalAddr().String()) 145 | port0, _ := strconv.ParseInt(sport0, 10, 32) 146 | port1, _ := strconv.ParseInt(sport1, 10, 32) 147 | 148 | return &Tunnel{ 149 | tunnelType: config.Type, 150 | configPort0: int(port0), 151 | connection0: tcpListener, 152 | configPort1: int(port1), 153 | connection1: udpConn, 154 | }, nil 155 | 156 | case DialQuicDialUdp: 157 | 158 | // connect quic addr 159 | tlsConfig := &tls.Config{ 160 | InsecureSkipVerify: true, 161 | NextProtos: []string{nextProto}, 162 | } 163 | quicConn, err := quic.DialAddr(config.Address0, tlsConfig, nil) 164 | if err != nil { 165 | return nil, err 166 | } 167 | quicStream, err := quicConn.OpenStreamSync(context.Background()) 168 | if err != nil { 169 | return nil, err 170 | } 171 | loggerTunnel.Debug("QUIC dial ", quicConn.RemoteAddr()) 172 | 173 | // connect udp addr 174 | udpAddr, err := net.ResolveUDPAddr("udp", config.Address1) 175 | if err != nil { 176 | _ = quicStream.Close() 177 | return nil, err 178 | } 179 | udpConn, err := net.DialUDP("udp", nil, udpAddr) 180 | if err != nil { 181 | _ = quicStream.Close() 182 | return nil, err 183 | } 184 | loggerTunnel.Debug("UDP dial ", config.Address1) 185 | 186 | _, sport0, _ := net.SplitHostPort(config.Address0) 187 | _, sport1, _ := net.SplitHostPort(config.Address1) 188 | port0, _ := strconv.ParseInt(sport0, 10, 32) 189 | port1, _ := strconv.ParseInt(sport1, 10, 32) 190 | 191 | return &Tunnel{ 192 | tunnelType: config.Type, 193 | configPort0: int(port0), 194 | connection0: quicStream, 195 | configPort1: int(port1), 196 | connection1: udpConn, 197 | }, nil 198 | 199 | case DialTcpDialUdp: 200 | 201 | // connect tcp addr 202 | tcpAddr, err := net.ResolveTCPAddr("tcp", config.Address0) 203 | if err != nil { 204 | return nil, err 205 | } 206 | tcpConn, err := net.DialTCP("tcp", nil, tcpAddr) 207 | if err != nil { 208 | return nil, err 209 | } 210 | loggerTunnel.Debug("TCP dial ", tcpConn.RemoteAddr()) 211 | 212 | err = tcpConn.SetNoDelay(true) 213 | if err != nil { 214 | tcpConn.Close() 215 | return nil, err 216 | } 217 | 218 | // connect udp addr 219 | udpAddr, err := net.ResolveUDPAddr("udp", config.Address1) 220 | if err != nil { 221 | _ = tcpConn.Close() 222 | return nil, err 223 | } 224 | udpConn, err := net.DialUDP("udp", nil, udpAddr) 225 | if err != nil { 226 | _ = tcpConn.Close() 227 | return nil, err 228 | } 229 | loggerTunnel.Debug("UDP dial ", config.Address1) 230 | 231 | _, sport0, _ := net.SplitHostPort(config.Address0) 232 | _, sport1, _ := net.SplitHostPort(config.Address1) 233 | port0, _ := strconv.ParseInt(sport0, 10, 32) 234 | port1, _ := strconv.ParseInt(sport1, 10, 32) 235 | 236 | return &Tunnel{ 237 | tunnelType: config.Type, 238 | configPort0: int(port0), 239 | connection0: tcpConn, 240 | configPort1: int(port1), 241 | connection1: udpConn, 242 | }, nil 243 | 244 | } 245 | 246 | return nil, errors.New("no such protocol") 247 | } 248 | 249 | // Close make sure all connection be closed after use 250 | func (t *Tunnel) Close() { 251 | 252 | oldStatus := t.tunnelStatus 253 | t.tunnelStatus = STATUS_CLOSED 254 | 255 | if oldStatus != STATUS_CLOSED && t.connection0 != nil { 256 | switch tt := t.connection0.(type) { 257 | case quic.Listener: 258 | _ = tt.Close() 259 | case quic.Stream: 260 | _ = tt.Close() 261 | case *net.TCPListener: 262 | _ = tt.Close() 263 | case *net.TCPConn: 264 | _ = tt.Close() 265 | default: 266 | loggerTunnel.Errorf("I do not know how to close it: %T", tt) 267 | t.tunnelStatus = STATUS_FAILED 268 | } 269 | } 270 | 271 | if oldStatus != STATUS_CLOSED && t.connection1 != nil { 272 | switch tt := t.connection1.(type) { 273 | case quic.Listener: 274 | _ = tt.Close() 275 | case quic.Stream: 276 | _ = tt.Close() 277 | case *net.UDPConn: 278 | _ = tt.Close() 279 | case *net.TCPConn: 280 | _ = tt.Close() 281 | default: 282 | loggerTunnel.Errorf("I do not know how to close it: %T", tt) 283 | t.tunnelStatus = STATUS_FAILED 284 | } 285 | } 286 | 287 | } 288 | 289 | // PluginCallback read/write DATA, in udp tunnel, first byte is udp multiplex id 290 | // return value: reply and data; 291 | // if data is nil or length of it is 0, stop sending; 292 | // if reply is false, the data will continue to send (data could be modified); 293 | // if reply is true, the data will reverse its send direction, and send it; 294 | // caution that the first byte of data is udp multiplex id. 295 | type PluginCallback func([]byte) (bool, []byte) 296 | 297 | // PluginGoroutine goroutine for plugin 298 | // parameters are a quic.Stream or a *net.TCPConn and a *net.UDPConn, 299 | // which are two sides of a Tunnel 300 | type PluginGoroutine func(interface{}, *net.UDPConn) 301 | 302 | // PluginSetQuitFlag set quit flag and plugin will stop function when it found it 303 | type PluginSetQuitFlag func() 304 | 305 | // Serve wait for connection and sync data 306 | // readFunc, writeFunc: see syncUdp 307 | func (t *Tunnel) Serve(readFunc, writeFunc PluginCallback, plRoutine PluginGoroutine, plQuit PluginSetQuitFlag) error { 308 | 309 | switch t.tunnelType { 310 | case ListenQuicListenUdp: 311 | 312 | // accept quic stream 313 | quicConn, err := t.connection0.(quic.Listener).Accept(context.Background()) 314 | if err != nil { 315 | t.tunnelStatus = STATUS_FAILED 316 | return err 317 | } 318 | loggerTunnel.Debug("Accept quic connection from ", quicConn.RemoteAddr().String()) 319 | 320 | quicStream, err := quicConn.AcceptStream(context.Background()) 321 | if err != nil { 322 | t.tunnelStatus = STATUS_FAILED 323 | return err 324 | } 325 | loggerTunnel.Debug("Accept quic stream from ", quicConn.RemoteAddr().String()) 326 | 327 | defer quicStream.Close() 328 | 329 | t.syncUdp(quicStream, t.connection1.(*net.UDPConn), readFunc, writeFunc, plRoutine, plQuit, false, false) 330 | 331 | case ListenTcpListenUdp: 332 | 333 | // accept tcp connection 334 | err := t.connection0.(*net.TCPListener).SetDeadline(time.Now().Add(time.Second * 10)) 335 | if err != nil { 336 | t.tunnelStatus = STATUS_FAILED 337 | return err 338 | } 339 | tcpConn, err := t.connection0.(*net.TCPListener).AcceptTCP() 340 | if err != nil { 341 | t.tunnelStatus = STATUS_FAILED 342 | return err 343 | } 344 | loggerTunnel.Debug("Accept tcp connection from ", tcpConn.RemoteAddr().String()) 345 | 346 | err = tcpConn.SetNoDelay(true) 347 | if err != nil { 348 | tcpConn.Close() 349 | t.tunnelStatus = STATUS_FAILED 350 | return err 351 | } 352 | 353 | defer tcpConn.Close() 354 | 355 | t.syncUdp(tcpConn, t.connection1.(*net.UDPConn), readFunc, writeFunc, plRoutine, plQuit, false, false) 356 | 357 | case DialQuicDialUdp: 358 | 359 | t.syncUdp(t.connection0, t.connection1.(*net.UDPConn), readFunc, writeFunc, plRoutine, plQuit, true, true) 360 | 361 | case DialTcpDialUdp: 362 | 363 | t.syncUdp(t.connection0, t.connection1.(*net.UDPConn), readFunc, writeFunc, plRoutine, plQuit, true, true) 364 | 365 | } 366 | 367 | return nil 368 | } 369 | 370 | // Ports return port peer: port0, port1. 371 | func (t *Tunnel) Ports() (int, int) { 372 | return t.configPort0, t.configPort1 373 | } 374 | 375 | // Type return TunnelType. 376 | func (t *Tunnel) Type() TunnelType { 377 | return t.tunnelType 378 | } 379 | 380 | // PingDelay delay between two tunnel 381 | func (t *Tunnel) PingDelay() time.Duration { 382 | return t.pingDelay 383 | } 384 | 385 | // Status return TunnelStatus, get current tunnel status 386 | func (t *Tunnel) Status() TunnelStatus { 387 | return t.tunnelStatus 388 | } 389 | 390 | // syncUdp sync data between quic connection and udp connection. 391 | // Support quic.Stream and *net.TCPConn. 392 | // readFunc, writeFunc: PluginCallback of when read and write data into tunnel 393 | // quicPing: send ping package to avoid quic stream timeout or not; 394 | // udpConnected: udp is waiting for connection or dial to address 395 | func (t *Tunnel) syncUdp(conn interface{}, udpConn *net.UDPConn, readFunc, writeFunc PluginCallback, plRoutine PluginGoroutine, plQuit PluginSetQuitFlag, sendQuicPing, udpConnected bool) { 396 | 397 | t.tunnelStatus = STATUS_CONNECTED 398 | 399 | switch conn.(type) { 400 | case quic.Stream: 401 | case *net.TCPConn: 402 | default: 403 | loggerTunnel.Errorf("Unsupported connection type: %T", conn) 404 | return 405 | } 406 | 407 | const maxUdpRemoteNo byte = 0xFF 408 | 409 | udpRemoteID := make(map[string]byte) // remote ip record 410 | var udpRemotes []*net.UDPAddr 411 | udpVClients := make(map[byte]chan []byte) // local virtual client 412 | 413 | var pingTime time.Time 414 | ch := make(chan int, int(maxUdpRemoteNo)*2+2) 415 | 416 | if readFunc == nil { 417 | readFunc = func(data []byte) (bool, []byte) { 418 | return false, data 419 | } 420 | } 421 | if writeFunc == nil { 422 | writeFunc = func(data []byte) (bool, []byte) { 423 | return false, data 424 | } 425 | } 426 | if plQuit == nil { 427 | plQuit = func() {} 428 | } 429 | 430 | if plRoutine != nil { 431 | go plRoutine(conn, udpConn) 432 | } 433 | 434 | // PING 435 | if sendQuicPing { 436 | 437 | go func() { 438 | defer func() { 439 | ch <- 1 440 | }() 441 | 442 | for { 443 | var err error 444 | 445 | switch stream := conn.(type) { 446 | case quic.Stream: 447 | _, err = stream.Write(NewDataFrame(PING, nil)) 448 | case *net.TCPConn: 449 | _, err = stream.Write(NewDataFrame(PING, nil)) 450 | } 451 | 452 | pingTime = time.Now() 453 | if err != nil { 454 | loggerTunnel.Error("Send PING package failed") 455 | break 456 | } 457 | // no longer than 5 seconds 458 | time.Sleep(time.Second) 459 | } 460 | 461 | }() 462 | 463 | } 464 | 465 | // UDP -> QUIC 466 | udpVirtualClient := func(id byte, msg chan []byte) { 467 | 468 | if udpConnected { 469 | 470 | udpAddr, _ := net.ResolveUDPAddr("udp", udpConn.RemoteAddr().String()) 471 | myUdpConn, err := net.DialUDP("udp", nil, udpAddr) 472 | if err != nil { 473 | loggerTunnel.WithError(err).Error("New udp virtual client failed with dial udp address error") 474 | return 475 | } 476 | loggerTunnel.WithField("ID", id).Debug("New udp virtual client") 477 | 478 | go func() { 479 | defer func() { 480 | ch <- 1 481 | }() 482 | 483 | for { 484 | _, _ = myUdpConn.Write(<-msg) 485 | 486 | /*if err != nil { 487 | loggerTunnel.WithError(err).Warn("Write data to connected udp error") 488 | }*/ 489 | } 490 | 491 | }() 492 | 493 | go func() { 494 | defer func() { 495 | ch <- 1 496 | }() 497 | defer myUdpConn.Close() 498 | 499 | buf := make([]byte, TransBufSize) 500 | 501 | for { 502 | cnt, err := myUdpConn.Read(buf) 503 | if err != nil { 504 | // loggerTunnel.WithError(err).Warn("Read data from connected udp error") 505 | time.Sleep(time.Millisecond * 100) 506 | continue 507 | } 508 | 509 | if cnt != 0 { 510 | reply, data := writeFunc(append([]byte{id}, buf[:cnt]...)) 511 | if data != nil && len(data) > 0 { 512 | if reply { 513 | _, _ = myUdpConn.Write(data[1:]) 514 | } else { 515 | switch stream := conn.(type) { 516 | case quic.Stream: 517 | _, err = stream.Write(NewDataFrame(DATA, data)) 518 | case *net.TCPConn: 519 | _, err = stream.Write(NewDataFrame(DATA, data)) 520 | } 521 | 522 | if err != nil { 523 | loggerTunnel.WithError(err).Warn("Write data to tunnel error") 524 | break 525 | } 526 | } 527 | } 528 | } 529 | } 530 | 531 | }() 532 | 533 | } 534 | 535 | } 536 | 537 | // QUIC -> UDP 538 | go func() { 539 | defer func() { 540 | ch <- 1 541 | }() 542 | 543 | dataStream := NewDataStream() 544 | buf := make([]byte, TransBufSize) 545 | var cnt, wcnt int 546 | var err error 547 | 548 | for { 549 | 550 | switch stream := conn.(type) { 551 | case quic.Stream: 552 | cnt, err = stream.Read(buf) 553 | case *net.TCPConn: 554 | cnt, err = stream.Read(buf) 555 | } 556 | 557 | if err != nil { 558 | loggerTunnel.WithError(err).Warn("Read data from QUIC/TCP stream error") 559 | break 560 | } 561 | 562 | dataStream.Append(buf[:cnt]) 563 | for dataStream.Parse() { 564 | switch dataStream.Type() { 565 | 566 | case DATA: 567 | 568 | // first byte of data is 8bit guest id 569 | if udpConnected { 570 | 571 | reply, data := readFunc(dataStream.Data()) 572 | if data != nil && len(data) > 0 { 573 | if reply { 574 | switch stream := conn.(type) { 575 | case quic.Stream: 576 | _, err = stream.Write(NewDataFrame(DATA, data)) 577 | case *net.TCPConn: 578 | _, err = stream.Write(NewDataFrame(DATA, data)) 579 | } 580 | if err != nil { 581 | loggerTunnel.Error("Send reply package failed") 582 | break 583 | } 584 | } else { 585 | if ch, ok := udpVClients[data[0]]; ok { 586 | ch <- data[1:] 587 | } else { 588 | ch = make(chan []byte, 32) 589 | udpVClients[data[0]] = ch 590 | udpVirtualClient(data[0], ch) 591 | ch <- data[1:] 592 | } 593 | } 594 | } 595 | 596 | } else if len(udpRemotes) > int(dataStream.Data()[0]) { 597 | 598 | reply, data := readFunc(dataStream.Data()) 599 | if data != nil && len(data) > 0 { 600 | if reply { 601 | switch stream := conn.(type) { 602 | case quic.Stream: 603 | _, err = stream.Write(NewDataFrame(DATA, data)) 604 | case *net.TCPConn: 605 | _, err = stream.Write(NewDataFrame(DATA, data)) 606 | } 607 | if err != nil { 608 | loggerTunnel.Error("Send reply package failed") 609 | break 610 | } 611 | } else { 612 | wcnt, err = udpConn.WriteToUDP(data[1:], udpRemotes[data[0]]) 613 | if err != nil || wcnt != len(data)-1 { 614 | loggerTunnel.WithError(err).WithField("count", len(data)-1).WithField("sent", wcnt). 615 | Warn("Send data to connected udp error or send count not match") 616 | 617 | // reconnect 618 | // localAddr := udpConn.LocalAddr() 619 | // udpLocalAddr, _ := net.ResolveUDPAddr("udp", localAddr.String()) 620 | // _ = udpConn.Close() 621 | // udpConn, _ = net.DialUDP("udp", nil, udpLocalAddr) 622 | } 623 | } 624 | } 625 | 626 | } 627 | 628 | case PING: 629 | 630 | if sendQuicPing { 631 | t.pingDelay = time.Now().Sub(pingTime) 632 | loggerTunnel.Debugf("Delay %.2f ms", float64(t.pingDelay.Nanoseconds())/1000000) 633 | } else { 634 | // not sending so response it 635 | var err error 636 | 637 | switch stream := conn.(type) { 638 | case quic.Stream: 639 | _, err = stream.Write(NewDataFrame(PING, nil)) 640 | case *net.TCPConn: 641 | _, err = stream.Write(NewDataFrame(PING, nil)) 642 | } 643 | 644 | if err != nil { 645 | loggerTunnel.Error("Send PING package failed") 646 | break 647 | } 648 | } 649 | 650 | } 651 | } 652 | 653 | } 654 | 655 | loggerTunnel.Debugf("Average compress rate %.3f", dataStream.CompressRateAva()) 656 | 657 | }() 658 | 659 | // UDP -> QUIC 660 | if !udpConnected { 661 | 662 | go func() { 663 | defer func() { 664 | ch <- 1 665 | }() 666 | 667 | buf := make([]byte, TransBufSize) 668 | var cnt, wcnt int 669 | var udpAddr *net.UDPAddr 670 | var remoteNo byte = 0 671 | var err error 672 | 673 | for { 674 | 675 | cnt, udpAddr, err = udpConn.ReadFromUDP(buf) 676 | if err != nil { 677 | loggerTunnel.WithError(err).Warn("Read data from unconnected udp error") 678 | break 679 | } 680 | 681 | addrString := udpAddr.IP.String() + ":" + strconv.Itoa(udpAddr.Port) 682 | if v, ok := udpRemoteID[addrString]; ok { 683 | remoteNo = v 684 | } else { 685 | ul := len(udpRemotes) 686 | if ul > int(maxUdpRemoteNo) { 687 | // drop package 688 | continue 689 | } 690 | remoteNo = byte(ul) 691 | 692 | udpRemoteID[addrString] = remoteNo 693 | udpRemotes = append(udpRemotes, udpAddr) 694 | 695 | loggerTunnel.Debug("New UDP connection from ", udpAddr.IP.String(), " port ", udpAddr.Port) 696 | } 697 | 698 | var data []byte 699 | var reply bool 700 | 701 | // first byte of data is 8bit guest id 702 | reply, data = writeFunc(append([]byte{remoteNo}, buf[:cnt]...)) 703 | if data != nil && len(data) > 0 { 704 | if reply { 705 | _, _ = udpConn.WriteToUDP(data[1:], udpAddr) 706 | } else { 707 | data = NewDataFrame(DATA, data) 708 | 709 | switch stream := conn.(type) { 710 | case quic.Stream: 711 | wcnt, err = stream.Write(data) 712 | case *net.TCPConn: 713 | wcnt, err = stream.Write(data) 714 | } 715 | 716 | if err != nil || wcnt != len(data) { 717 | loggerTunnel.WithError(err).WithField("count", len(data)).WithField("sent", wcnt). 718 | Warn("Send data to QUIC/TCP stream error or send count not match") 719 | break 720 | } 721 | } 722 | } 723 | 724 | } 725 | 726 | }() 727 | 728 | } 729 | 730 | <-ch 731 | 732 | switch t.tunnelStatus { 733 | case STATUS_CONNECTED: 734 | loggerTunnel.Warn("Tunnel failed") 735 | t.tunnelStatus = STATUS_FAILED 736 | } 737 | 738 | plQuit() 739 | 740 | } 741 | -------------------------------------------------------------------------------- /glg-go/README.md: -------------------------------------------------------------------------------- 1 | #GlgLineGraph 2 | 3 | This is the original README file of GlgLineGraph library. 4 | 5 | Visit [here](https://github.com/skoona/glinegraph-cairo) to fetch the origin repo released under LGPLv2.0. 6 | 7 | Thanks [skoona](https://github.com/skoona) for his great work. 8 | 9 | ### (a.k.a cairo version of glinegraph ) July 2007/2016 10 | 11 | ![GLineGraph Widget](https://github.com/skoona/glinegraph-cairo/raw/master/images/glg_cairo3.png) 12 | 13 | A Gtk3/GLib2/cario GUI application which demonstrates the use of GTK+ for producing xy line graphs. This widget once created allows you to add one or more data series, then add values to those series for ploting. The X point is assumed based on arrival order. However, the Y value or position is based one the current scale and the y value itself. If the charts x scale maximum is 40, or 40 points, the 41+ value is appended to the 40th position after all points are shifted left and out through pos 0. 14 | 15 | Packaged as a gtk widget for ease of use. 16 | 17 | A lgcairo.c example program is included to demonstrate the possible use of the codeset. 18 | 19 | There is also [GtkDoc API documentation](https://skoona.github.io/glinegraph-cairo/docs/reference/html/index.html) in ./docs directory. 20 | 21 | FEATURES: 22 | 23 | * GlgLineGraph API Reference Manual in ./docs directory. 24 | * Unlimited data series support. 25 | * Accurate scaling across a wide range of X & Y scales. 26 | - Using values ranges above or below 1. 27 | * Rolling data points, if number of x points exceed x-scale. (left shift) 28 | * Ability to change chart background color, window backgrounds colors, etc. 29 | * Popup Tooltip, via mouse-button-1 click to enable/toggle. 30 | - Tooltip overlays top graph title, when present. 31 | * Data points are time stamped with current time when added. 32 | * Some key debug messages to console: $ export G_MESSAGES_DEBUG=all 33 | * Auto Size to current window size; i.e. no-scrolling. 34 | * point-selected signal tied to tooltip, to display y value under mouse. 35 | 36 | REQUIREMENTS: 37 | 38 | Gtk3/Glib2 runtime support. 39 | 40 | * the following packages may need to be installed to build program and 41 | for runtime of binaries on some platforms. 42 | 43 | glinegraph - { this package } 44 | gtk-devel - { GTK+ version 3.10 is minimum package level required} 45 | glib-devel - { version 2.40, packaged with GTK+ } 46 | 47 | 48 | DISTRIBUTION METHOD: 49 | 50 | Source tar glinegraph-{version}.tar.bz2 51 | GPL2 copyright 52 | 53 | INSTALL INFO: 54 | 55 | Configure Source with 56 | '# ./autogen.sh --enable-gtk-doc ' 57 | '# make clean ' 58 | '# make ' 59 | '# make install' 60 | 61 | -- or -- 62 | 63 | '# ./autogen.sh --disable-gtk-doc ' 64 | '# make clean ' 65 | '# make ' 66 | '# make install' 67 | 68 | 69 | INSTRUCTIONS: 70 | 71 | glinegraph -- GTK+ Cairo Programing Example Application 72 | 73 | 1. Compile program 74 | 2. Execute sample program 75 | - regular cmd: "# lgcairo" 76 | 77 | 78 | KNOWN BUGS: 79 | 80 | None. 81 | 5/23/2016 Thanks to LinuxQuestions member: norobro for helping debug the performance issue I was having. 82 | 83 | 84 | 85 | BEGIN DEBUG LOG: 86 | 87 | ### To see console log -- $ export G_MESSAGES_DEBUG=all 88 | 89 | [jscott OSX El-Capitan GLG-Cairo]$ pkg-config --modversion gtk+-3.0 90 | #==> 3.16.6 91 | 92 | [jscott OSX El-Capitan glinegraph-cairo]$ src/lgcairo 93 | 94 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_new(entered) 95 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_class_init(entered) 96 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_class_init(exited) 97 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_init(entered) 98 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_init(exited) 99 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 100 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 101 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 102 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 103 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 104 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 105 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 106 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 107 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 108 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 109 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 110 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 111 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 112 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 113 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 114 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 115 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 116 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 117 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 118 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 119 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 120 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(entered) 121 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(exited) 122 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 123 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 124 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(entered) 125 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(exited) 126 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 127 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 128 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(entered) 129 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(exited) 130 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 131 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 132 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(entered) 133 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_color(exited) 134 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 135 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 136 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(entered) 137 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(exited) 138 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 139 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 140 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(entered) 141 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(exited) 142 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 143 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(entered) 144 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(entered) 145 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_chart_set_text(exited) 146 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_set_property(exited) 147 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_new(exited) 148 | 149 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add() 150 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAdd: series=0, max_pts=40 151 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add() 152 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAdd: series=1, max_pts=40 153 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add() 154 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAdd: series=2, max_pts=40 155 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add() 156 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAdd: series=3, max_pts=40 157 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add() 158 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAdd: series=4, max_pts=40 159 | 160 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_size_allocate(entered) 161 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_size_allocate(exited) 162 | 163 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_realize(entered) 164 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_send_configure(entered) 165 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_configure_event(entered) 166 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_compute_layout(entered) 167 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_compute_layout(new width=570, height=270) 168 | ** (lgcairo:68503): DEBUG: Alloc:factors:raw:pango_layout_get_pixel_size(width=10, height=12) 169 | ** (lgcairo:68503): DEBUG: Alloc:factors:adj:pango_layout_get_pixel_size(width=10, height=20) 170 | ** (lgcairo:68503): DEBUG: Alloc:Max.Avail: plot_box.width=495, plot_box.height=175 171 | ** (lgcairo:68503): DEBUG: Alloc:Chart:Incs: x_minor=12, x_major=120, y_minor=3, y_major=15, plot_box.x=77, plot_box.y=60, plot_box.width=480, plot_box.height=165 172 | ** (lgcairo:68503): DEBUG: Alloc:Chart:Nums: x_num_minor=40, x_num_major=4, y_num_minor=55, y_num_major=11 173 | ** (lgcairo:68503): DEBUG: Alloc:Chart:Plot: x=77, y=60, width=480, height=165 174 | ** (lgcairo:68503): DEBUG: Alloc:Chart:Title: x=77, y=5, width=480, height=40 175 | ** (lgcairo:68503): DEBUG: Alloc:Chart:yLabel: x=5, y=225, width=30, height=150 176 | ** (lgcairo:68503): DEBUG: Alloc:Chart:xLabel: x=77, y=240, width=480, height=25 177 | ** (lgcairo:68503): DEBUG: Alloc:Chart:Tooltip: x=77, y=5, width=480, height=45 178 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_compute_layout(exited) 179 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(entered) 180 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#PlotArea() duration=0.281 ms. 181 | ** (lgcairo:68503): DEBUG: Chart.Surface: pg.Width=570, pg.Height=270, Plot Area x=77 y=60 width=480, height=165 182 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 183 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 184 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=5, cx=480, cy=40 185 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=203, y_pos=8, cx=228, cy=36 186 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Top-Title() duration=8.385 ms. 187 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 188 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 189 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=240, cx=480, cy=25 190 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=171, y_pos=247, cx=291, cy=16 191 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Title() duration=2.081 ms. 192 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_vertical() 193 | ** (lgcairo:68503): DEBUG: Vert:TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 194 | ** (lgcairo:68503): DEBUG: Vert.TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 195 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Title() duration=3.719 ms. 196 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_grid_lines() 197 | ** (lgcairo:68503): DEBUG: Draw.Y-GridLines: count_major=10, count_minor=54, y_minor_inc=3, y_major_inc=15 198 | ** (lgcairo:68503): DEBUG: Draw.X-GridLines: count_major=3, count_minor=39, x_minor_inc=12, x_major_inc=120 199 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#GridLines() duration=0.818 ms. 200 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_x_grid_labels() 201 | ** (lgcairo:68503): DEBUG: Scale:Labels:X small font sizes cx=13, cy=11 202 | ** (lgcairo:68503): DEBUG: Scale:Labels:X plot_box.cx=480, layout.cx=493, layout.cy=11 203 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Labels() duration=7.588 ms. 204 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_y_grid_labels() 205 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Labels() duration=0.651 ms. 206 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw_all(entered) 207 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 208 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[0]Series() duration=0.012 ms. 209 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 210 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[1]Series() duration=0.004 ms. 211 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 212 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[2]Series() duration=0.003 ms. 213 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 214 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[3]Series() duration=0.003 ms. 215 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 216 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[4]Series() duration=0.004 ms. 217 | ** (lgcairo:68503): DEBUG: glg_line_graph_data_series_draw_all(exited): #series=5, #points=0 218 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Series-All() duration=0.044 ms. 219 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_tooltip() 220 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Tooltip() duration=0.003 ms. 221 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(exited) 222 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#TOTAL-TIME() duration=23.651 ms. 223 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_configure_event(exited) 224 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_send_configure(exited) 225 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_realize(exited) 226 | 227 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_master_draw(entered) 228 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(Allocation ==> width=570, height=270, Dirty Rect ==> x=0, y=0, width=570, height=270 ) 229 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_master_draw#TOTAL-TIME() duration=2.810 ms. 230 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(exited) 231 | 232 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_master_draw(entered) 233 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(Allocation ==> width=570, height=270, Dirty Rect ==> x=0, y=0, width=570, height=270 ) 234 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_master_draw#TOTAL-TIME() duration=2.357 ms. 235 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(exited) 236 | 237 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 238 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=0, value=17.4, index=0, count=1, max_pts=40 239 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 240 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=1, value=26.1, index=0, count=1, max_pts=40 241 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 242 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=2, value=82.7, index=0, count=1, max_pts=40 243 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 244 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=3, value=53.3, index=0, count=1, max_pts=40 245 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 246 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=4, value=99.6, index=0, count=1, max_pts=40 247 | 248 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_redraw(entered) 249 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(entered) 250 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#PlotArea() duration=0.747 ms. 251 | ** (lgcairo:68503): DEBUG: Chart.Surface: pg.Width=570, pg.Height=270, Plot Area x=77 y=60 width=480, height=165 252 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 253 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 254 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=5, cx=480, cy=40 255 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=203, y_pos=8, cx=228, cy=36 256 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Top-Title() duration=0.448 ms. 257 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 258 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 259 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=240, cx=480, cy=25 260 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=171, y_pos=247, cx=291, cy=16 261 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Title() duration=0.298 ms. 262 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_vertical() 263 | ** (lgcairo:68503): DEBUG: Vert:TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 264 | ** (lgcairo:68503): DEBUG: Vert.TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 265 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Title() duration=0.463 ms. 266 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_grid_lines() 267 | ** (lgcairo:68503): DEBUG: Draw.Y-GridLines: count_major=10, count_minor=54, y_minor_inc=3, y_major_inc=15 268 | ** (lgcairo:68503): DEBUG: Draw.X-GridLines: count_major=3, count_minor=39, x_minor_inc=12, x_major_inc=120 269 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#GridLines() duration=1.428 ms. 270 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_x_grid_labels() 271 | ** (lgcairo:68503): DEBUG: Scale:Labels:X small font sizes cx=13, cy=11 272 | ** (lgcairo:68503): DEBUG: Scale:Labels:X plot_box.cx=480, layout.cx=493, layout.cy=11 273 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Labels() duration=0.329 ms. 274 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_y_grid_labels() 275 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Labels() duration=0.420 ms. 276 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw_all(entered) 277 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 278 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[0]Series() duration=0.061 ms. 279 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 280 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[1]Series() duration=0.028 ms. 281 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 282 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[2]Series() duration=0.026 ms. 283 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 284 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[3]Series() duration=0.025 ms. 285 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 286 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[4]Series() duration=0.024 ms. 287 | ** (lgcairo:68503): DEBUG: glg_line_graph_data_series_draw_all(exited): #series=5, #points=1 288 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Series-All() duration=0.203 ms. 289 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_tooltip() 290 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Tooltip() duration=0.025 ms. 291 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(exited) 292 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#TOTAL-TIME() duration=4.476 ms. 293 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_redraw(exited) 294 | 295 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_master_draw(entered) 296 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(Allocation ==> width=570, height=270, Dirty Rect ==> x=0, y=0, width=570, height=270 ) 297 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_master_draw#TOTAL-TIME() duration=3.057 ms. 298 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(exited) 299 | 300 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 301 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=0, value=9.8, index=0, count=2, max_pts=40 302 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 303 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=1, value=20.4, index=0, count=2, max_pts=40 304 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 305 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=2, value=82.7, index=0, count=2, max_pts=40 306 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 307 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=3, value=79.1, index=0, count=2, max_pts=40 308 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_add_value() 309 | ** (lgcairo:68503): DEBUG: ==>DataSeriesAddValue: series=4, value=99.3, index=0, count=2, max_pts=40 310 | 311 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_redraw(entered) 312 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(entered) 313 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#PlotArea() duration=0.858 ms. 314 | ** (lgcairo:68503): DEBUG: Chart.Surface: pg.Width=570, pg.Height=270, Plot Area x=77 y=60 width=480, height=165 315 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 316 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 317 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=5, cx=480, cy=40 318 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=203, y_pos=8, cx=228, cy=36 319 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Top-Title() duration=0.458 ms. 320 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_horizontal() 321 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Page cx=570, cy=270 322 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Orig: x=77, y=240, cx=480, cy=25 323 | ** (lgcairo:68503): DEBUG: Horiz.TextBox:Calc x_pos=171, y_pos=247, cx=291, cy=16 324 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Title() duration=0.303 ms. 325 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_text_vertical() 326 | ** (lgcairo:68503): DEBUG: Vert:TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 327 | ** (lgcairo:68503): DEBUG: Vert.TextBox: y_pos=219, x=5, y=225, cx=168, cy=30 328 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Title() duration=0.472 ms. 329 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_grid_lines() 330 | ** (lgcairo:68503): DEBUG: Draw.Y-GridLines: count_major=10, count_minor=54, y_minor_inc=3, y_major_inc=15 331 | ** (lgcairo:68503): DEBUG: Draw.X-GridLines: count_major=3, count_minor=39, x_minor_inc=12, x_major_inc=120 332 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#GridLines() duration=1.446 ms. 333 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_x_grid_labels() 334 | ** (lgcairo:68503): DEBUG: Scale:Labels:X small font sizes cx=13, cy=11 335 | ** (lgcairo:68503): DEBUG: Scale:Labels:X plot_box.cx=480, layout.cx=493, layout.cy=11 336 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#X-Labels() duration=0.304 ms. 337 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_y_grid_labels() 338 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Y-Labels() duration=0.420 ms. 339 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw_all(entered) 340 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 341 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[0]Series() duration=0.078 ms. 342 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 343 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[1]Series() duration=0.057 ms. 344 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 345 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[2]Series() duration=0.054 ms. 346 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 347 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[3]Series() duration=0.058 ms. 348 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_data_series_draw(entered) 349 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_data_series_draw#[4]Series() duration=0.055 ms. 350 | ** (lgcairo:68503): DEBUG: glg_line_graph_data_series_draw_all(exited): #series=5, #points=2 351 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Series-All() duration=0.344 ms. 352 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_tooltip() 353 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#Tooltip() duration=0.004 ms. 354 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_draw_graph(exited) 355 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_draw_graph#TOTAL-TIME() duration=4.719 ms. 356 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_redraw(exited) 357 | 358 | ** (lgcairo:68503): DEBUG: ===> glg_line_graph_master_draw(entered) 359 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(Allocation ==> width=570, height=270, Dirty Rect ==> x=0, y=0, width=570, height=270 ) 360 | ** (lgcairo:68503): DEBUG: DURATION: glg_line_graph_master_draw#TOTAL-TIME() duration=3.068 ms. 361 | ** (lgcairo:68503): DEBUG: glg_line_graph_master_draw(exited) 362 | 363 | ... 364 | 365 | ** (lgcairo:51717): DEBUG: ===> glg_line_graph_destroy(enter) 366 | ** (lgcairo:51717): DEBUG: ===> glg_line_graph_data_series_remove_all() 367 | ** (lgcairo:51717): DEBUG: ==>DataSeriesRemoveAll: number removed=5 368 | ** (lgcairo:51717): DEBUG: glg_line_graph_destroy(exit) 369 | ** (lgcairo:51717): DEBUG: ===> glg_line_graph_destroy(enter) 370 | ** (lgcairo:51717): DEBUG: glg_line_graph_destroy(exit) 371 | 372 | END DEBUG LOG: 373 | 374 | -------------------------------------------------------------------------------- /client/lib/hyouibana.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "errors" 7 | "io" 8 | "math" 9 | "math/rand" 10 | "net" 11 | "time" 12 | "unsafe" 13 | 14 | "github.com/weilinfox/youmu-thlink/utils" 15 | 16 | "github.com/quic-go/quic-go" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | /* 21 | #cgo pkg-config: zlib 22 | #include 23 | #include 24 | #include 25 | 26 | void zlib_encode(size_t len, const unsigned char *orig, size_t *retLen, unsigned char **ret) 27 | { 28 | unsigned char enc[1024]; 29 | z_stream enc_stream; 30 | 31 | enc_stream.zalloc = Z_NULL; 32 | enc_stream.zfree = Z_NULL; 33 | enc_stream.opaque = Z_NULL; 34 | 35 | enc_stream.avail_in = (uInt)len; 36 | enc_stream.next_in = (Bytef *)orig; 37 | enc_stream.avail_out = (uInt)sizeof(enc); 38 | enc_stream.next_out = (Bytef *)enc; 39 | 40 | deflateInit(&enc_stream, Z_DEFAULT_COMPRESSION); 41 | deflate(&enc_stream, Z_FINISH); 42 | deflateEnd(&enc_stream); 43 | 44 | *retLen = enc_stream.total_out; 45 | *ret = (unsigned char *)malloc(sizeof(unsigned char) * enc_stream.total_out); 46 | memcpy(*ret, enc, enc_stream.total_out); 47 | } 48 | */ 49 | import "C" 50 | 51 | var logger155 = logrus.WithField("Hyouibana", "internal") 52 | 53 | type type155pkg byte 54 | 55 | const ( 56 | CLIENT_T_ACK_155 type155pkg = iota // 0x00 57 | HOST_T_ACK_155 // 0x01 58 | INIT_ACK_155 type155pkg = iota + 2 // 0x04 59 | HOST_T_155 // 0x05 60 | CLIENT_T_155 // 0x06 61 | PUNCH_155 // 0x07 62 | INIT_155 // 0x08 63 | INIT_REQUEST_155 // 0x09 64 | INIT_SUCCESS_155 type155pkg = iota + 3 // 0x0b 65 | INIT_ERROR_155 // 0x0c 66 | HOST_QUIT_155 type155pkg = iota + 5 // 0x0f 67 | CLIENT_QUIT_155 // 0x10 68 | HOST_GAME_155 type155pkg = iota + 6 // 0x12 69 | CLIENT_GAME_155 // 0x13 70 | ) 71 | 72 | type data155pkg byte 73 | 74 | const ( 75 | GAME_SELECT_155 data155pkg = iota + 4 // 0x04 76 | GAME_INPUT_155 data155pkg = iota + 5 // 0x06 77 | GAME_REPLAY_REQUEST_155 data155pkg = iota + 7 // 0x09 78 | GAME_REPLAY_MATCH_155 // 0x0a 79 | GAME_REPLAY_DATA_155 // 0x0b 80 | GAME_REPLAY_END_155 // 0x0c 81 | ) 82 | 83 | type match155status byte 84 | 85 | const ( 86 | MATCH_WAIT_155 match155status = iota // no game start 87 | MATCH_ACCEPT_155 // peer connected 88 | MATCH_SPECT_ACK_155 89 | MATCH_SPECT_INIT_155 90 | MATCH_SPECT_SUCCESS_155 // fetching replay data 91 | MATCH_SPECT_ERROR_155 // cannot get replay data 92 | ) 93 | 94 | var th155id = [19]byte{0x57, 0x09, 0xf6, 0x67, 0xf0, 0xfd, 0x4b, 0xd0, 0xb9, 0x9a, 0x74, 0xf8, 0x38, 0x33, 0x81, 0x88, 0x00, 0x00, 0x00} 95 | 96 | // magic [6] [8] th155 0x71 th155_beta 0x72 for spectacle 97 | var th155ConfMagic = [12]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x01} 98 | 99 | // version [123:131] 100 | var th155ConfOrig = [156]C.uchar{0x10, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x00, 0x69, 0x73, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 101 | 0x08, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, 0x00, 0x65, 0x78, 0x74, 0x72, 0x61, 0x01, 0x00, 102 | 0x00, 0x01, 0x10, 0x00, 0x00, 0x08, 0x0b, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 103 | 0x68, 0x08, 0x00, 0x00, 0x01, 0x01, 0x10, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x00, 0x6e, 0x61, 0x6d, 0x65, 0x10, 0x00, 104 | 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x0a, 0x00, 0x00, 0x00, 0x62, 0x61, 0x74, 0x74, 0x6c, 0x65, 105 | 0x5f, 0x6e, 0x75, 0x6d, 0x02, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x07, 0x00, 0x00, 0x00, 106 | 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x08, 0x05, 107 | 0x00, 0x00, 0x00, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x02, 0x00, 0x00, 0x05, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01} 108 | 109 | func zlibDataDecodeError(l int, d []byte) string { 110 | if len(d) < 3 || d[0] != 0x78 || d[1] != 0x9c { 111 | return "NOT_ZLIB_DATA_ERROR" 112 | } 113 | 114 | b := bytes.NewBuffer(d) 115 | r, err := zlib.NewReader(b) 116 | if err != nil { 117 | return err.Error() 118 | } 119 | 120 | ans := make([]byte, l*2) 121 | n, err := r.Read(ans) 122 | if err != io.EOF { 123 | return err.Error() 124 | } 125 | _ = r.Close() 126 | 127 | if l != n { 128 | return "ZLIB_LENGTH_NOT_MATCH_ERROR" 129 | } 130 | 131 | dataStr := "" 132 | 133 | i, j, s := 0, 0, 0 134 | for j < n { 135 | switch s { 136 | case 0: 137 | if ans[j] == 0x10 { 138 | s++ 139 | } else { 140 | s = 0 141 | } 142 | case 1, 2: 143 | if ans[j] == 0x00 { 144 | s++ 145 | } else { 146 | s = 0 147 | } 148 | case 3: 149 | if ans[j] == 0x08 { 150 | s++ 151 | } else { 152 | s = 0 153 | } 154 | case 4: 155 | nl := utils.LittleIndia2Int(ans[j : j+4]) 156 | i = j + 4 + nl 157 | dataStr += string(ans[j+4:i]) + " " 158 | 159 | s = 0 160 | } 161 | 162 | j++ 163 | } 164 | 165 | return dataStr 166 | } 167 | 168 | func zlibDataDecodeSignConfVersion(l int, d []byte) error { 169 | if len(d) < 3 || d[0] != 0x78 || d[1] != 0x9c { 170 | return errors.New("NOT_ZLIB_DATA_ERROR") 171 | } 172 | 173 | b := bytes.NewBuffer(d) 174 | r, err := zlib.NewReader(b) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | ans := make([]byte, l*2) 180 | n, err := r.Read(ans) 181 | if err != io.EOF { 182 | return err 183 | } 184 | _ = r.Close() 185 | 186 | if l != n { 187 | return errors.New("ZLIB_LENGTH_NOT_MATCH_ERROR") 188 | } 189 | 190 | i, j, s := 0, 0, 0 191 | for j < n { 192 | switch s { 193 | case 0: 194 | if ans[j] == 0x10 { 195 | s++ 196 | } else { 197 | s = 0 198 | } 199 | case 1, 2: 200 | if ans[j] == 0x00 { 201 | s++ 202 | } else { 203 | s = 0 204 | } 205 | case 3: 206 | if ans[j] == 0x08 { 207 | s++ 208 | } else { 209 | s = 0 210 | } 211 | case 4: 212 | nl := utils.LittleIndia2Int(ans[j : j+4]) 213 | i = j + 4 + nl 214 | if string(ans[j+4:i]) == "version" { 215 | for k, l := i, 123; l < 131; { 216 | th155ConfOrig[l] = (C.uchar)(ans[k]) 217 | 218 | l++ 219 | k++ 220 | } 221 | switch th155ConfOrig[128] { 222 | case 0xaa: // th155 223 | th155ConfMagic[6] = 0x71 224 | th155ConfMagic[6] = 0x71 225 | case 0xab: // th155_beta 226 | th155ConfMagic[6] = 0x72 227 | th155ConfMagic[6] = 0x72 228 | default: 229 | logger155.Warn("Th155 plugin find unsupported version code ", th155ConfOrig[123:131]) 230 | } 231 | return nil 232 | } 233 | 234 | s = 0 235 | } 236 | 237 | j++ 238 | } 239 | 240 | return errors.New("VERSION_NOT_FOUND_ERROR") 241 | } 242 | 243 | func zlibDataEncodeConf() (int, []byte) { 244 | var cLen C.size_t = 0 245 | var cAns *C.uchar 246 | var ans []byte 247 | 248 | C.zlib_encode(156, &th155ConfOrig[0], &cLen, &cAns) 249 | cIAns := (*[utils.TransBufSize]C.uchar)(unsafe.Pointer(cAns)) 250 | for i := 0; i < int(cLen); i++ { 251 | ans = append(ans, byte(cIAns[i])) 252 | } 253 | 254 | return int(cLen), ans 255 | } 256 | 257 | type Hyouibana struct { 258 | peerId byte // current host/client peer id (udp mutex id) 259 | 260 | // peerHPTime time.Time // most resent current host/client peer HOST_T package met time 261 | // peerQuit bool // current match met HOST_QUIT or CLIENT_QUIT 262 | peerHostT []byte // spectator peer id need to send HOST_T 263 | MatchStatus match155status // current match status 264 | matchEnd bool // current match end 265 | matchId int // current match id 266 | matchInfo []byte // current match info 267 | matchRandId int // current match random id 268 | initSuccessInfo []byte // replay init success info 269 | initErrorInfo []byte // replay init error info 270 | frameId [2]int // replay frame record id 271 | frameRec [2][]byte // replay frame record 272 | timeId int64 // th155 protocol client start time in ms 273 | randId int32 // th155 protocol random id 274 | 275 | spectatorCount int // spectator counter 276 | quitFlag bool // plugin quit flag 277 | } 278 | 279 | // NewHyouibana new Hyouibana spectating server 280 | func NewHyouibana() *Hyouibana { 281 | return &Hyouibana{ 282 | // peerHPTime: time.Time{}, 283 | // peerQuit: false, 284 | MatchStatus: MATCH_WAIT_155, 285 | matchEnd: false, 286 | matchId: 0, 287 | frameId: [2]int{0, 0}, 288 | frameRec: [2][]byte{}, 289 | timeId: time.Now().UnixMilli(), 290 | randId: rand.Int31(), 291 | spectatorCount: 0, 292 | quitFlag: false, 293 | } 294 | } 295 | 296 | // WriteFunc from game host to client 297 | // orig: original data leads with 1 byte of client id 298 | func (h *Hyouibana) WriteFunc(orig []byte) (bool, []byte) { 299 | 300 | switch type155pkg(orig[1]) { 301 | 302 | case HOST_T_155: 303 | /*if len(orig)-1 == 12 { 304 | if h.MatchStatus != MATCH_WAIT_155 && orig[0] == h.peerId { 305 | h.peerHPTime = time.Now() 306 | } 307 | } else { 308 | logger155.Warn("HOST_T with strange length ", len(orig)-1) 309 | }*/ 310 | 311 | case PUNCH_155: 312 | if len(orig)-1 == 32 { 313 | if h.MatchStatus != MATCH_WAIT_155 && orig[0] == h.peerId { 314 | orig[2] = 0x02 315 | orig[3], orig[4] = 0x01, 0x00 316 | return true, orig 317 | } 318 | } else { 319 | logger155.Warn("PUNCH with strange length ", len(orig)-1) 320 | } 321 | 322 | case INIT_SUCCESS_155: 323 | if len(orig)-1 > 52 { 324 | // h.peerHPTime = time.Now() 325 | // h.peerQuit = false 326 | h.matchEnd = false 327 | h.matchId = 0 328 | // h.matchRandId = utils.LittleIndia2Int(orig[5:9]) 329 | h.frameId[0], h.frameId[1] = 0, 0 330 | h.frameRec[0], h.frameRec[1] = []byte{}, []byte{} 331 | 332 | h.peerId = orig[0] 333 | h.MatchStatus = MATCH_ACCEPT_155 334 | 335 | logger155.Info("Met INIT_SUCCESS") 336 | } else { 337 | logger155.Warn("INIT_SUCCESS with strange length ", len(orig)-1) 338 | } 339 | 340 | case HOST_QUIT_155: 341 | if len(orig)-1 == 12 { 342 | if h.matchId == 0 || h.MatchStatus == MATCH_SPECT_ERROR_155 { 343 | h.MatchStatus = MATCH_WAIT_155 344 | logger155.Info("Met HOST_QUIT and reset") 345 | } 346 | } else { 347 | logger155.Warn("HOST_QUIT ", orig[1], " with strange length ", len(orig)-1) 348 | } 349 | 350 | case CLIENT_QUIT_155: 351 | logger155.Warn("CLIENT_QUIT should not appear here") 352 | 353 | } 354 | 355 | return false, orig 356 | } 357 | 358 | // ReadFunc from game client to host 359 | // orig: original data leads with 1 byte of client id 360 | func (h *Hyouibana) ReadFunc(orig []byte) (bool, []byte) { 361 | 362 | if h.MatchStatus == MATCH_WAIT_155 { 363 | switch type155pkg(orig[1]) { 364 | 365 | case INIT_REQUEST_155: 366 | if len(orig)-1 > 40 { 367 | err := zlibDataDecodeSignConfVersion(utils.LittleIndia2Int(orig[37:41]), orig[41:]) 368 | if err != nil { 369 | logger155.WithError(err).Warn("INIT_REQUEST decode version failed") 370 | } 371 | } else { 372 | logger155.Warn("INIT_REQUEST here with strange length ", len(orig)-1) 373 | } 374 | 375 | } 376 | } else if orig[0] != h.peerId { 377 | switch type155pkg(orig[1]) { 378 | 379 | case HOST_T_ACK_155: 380 | if len(orig)-1 != 12 { 381 | logger155.Warn("HOST_T_ACK with strange length ", len(orig)-1) 382 | } 383 | 384 | case CLIENT_T_155: 385 | if len(orig)-1 == 20 { 386 | repData := []byte{orig[0], byte(CLIENT_T_ACK_155), 0x00, 0x00, 0x00} 387 | repData = append(repData, orig[5:9]...) 388 | repData = append(repData, orig[17:21]...) 389 | 390 | h.peerHostT = append(h.peerHostT, orig[0]) 391 | 392 | return true, repData 393 | } else { 394 | logger155.Warn("CLIENT_T with strange length ", len(orig)-1) 395 | } 396 | 397 | case INIT_155: 398 | if len(orig)-1 == 24 { 399 | return true, []byte{orig[0], byte(INIT_ACK_155)} 400 | } else { 401 | logger155.Warn("INIT with strange length ", len(orig)-1) 402 | } 403 | 404 | case INIT_REQUEST_155: 405 | if len(orig)-1 > 40 { 406 | switch orig[31] { 407 | 408 | case 0x6b: // battle 409 | // message busy 410 | errData := []byte{orig[0], byte(INIT_ERROR_155), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x28, 0x00, 0x00, 0x01, 411 | 0x1f, 0x00, 0x00, 0x00, 0x78, 0x9c, 0x13, 0x60, 0x60, 0xe0, 0x60, 0x67, 0x60, 0x60, 0xc8, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 412 | 0x4f, 0x15, 0x00, 0x72, 0x59, 0x80, 0xdc, 0xa4, 0xd2, 0xe2, 0x4a, 0x46, 0x06, 0x06, 0x46, 0x00, 0x4a, 0xa5, 0x04, 0xe6} 413 | logger155.Info("New spector connect error with message busy") 414 | 415 | return true, errData 416 | 417 | case 0x70, 0x71: // spectacle 418 | var repData []byte 419 | switch h.MatchStatus { 420 | 421 | case MATCH_SPECT_SUCCESS_155: 422 | repData = []byte{orig[0], byte(INIT_SUCCESS_155), 0x00, 0x00, 0x00, byte(h.matchRandId), byte(h.matchRandId >> 8), byte(h.matchRandId >> 16), byte(h.matchRandId >> 24)} 423 | repData = append(repData, h.initSuccessInfo...) 424 | 425 | h.spectatorCount++ 426 | logger155.Info("New spector connected") 427 | 428 | return true, repData 429 | 430 | case MATCH_SPECT_ERROR_155: 431 | repData = []byte{orig[0], byte(INIT_ERROR_155)} 432 | repData = append(repData, h.initErrorInfo...) 433 | logger155.Info("New spector connect error with host error message") 434 | 435 | return true, repData 436 | 437 | default: 438 | // message ready (will not reach in fact) 439 | repData = []byte{byte(INIT_ERROR_155), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x00, 0x29, 0x00, 0x00, 0x01, 440 | 0x20, 0x00, 0x00, 0x00, 0x78, 0x9c, 0x13, 0x60, 0x60, 0xe0, 0x60, 0x67, 0x60, 0x60, 0xc8, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 441 | 0x4f, 0x15, 0x00, 0x72, 0x59, 0x81, 0xdc, 0xa2, 0xd4, 0xc4, 0x94, 0x4a, 0x46, 0x06, 0x06, 0x46, 0x00, 0x51, 0x07, 0x05, 0x39} 442 | logger155.Info("New spector connect error with message ready") 443 | 444 | return true, repData 445 | } 446 | 447 | default: 448 | logger155.Warn("Th155 plugin spectator server get unknown INIT_REQUEST ", orig[1:]) 449 | } 450 | } else { 451 | logger155.Warn("INIT_REQUEST with strange length ", len(orig)-1) 452 | } 453 | 454 | case HOST_QUIT_155: 455 | logger155.Warn("HOST_QUIT should not appear here") 456 | 457 | case CLIENT_QUIT_155: 458 | if len(orig)-1 == 12 { 459 | if h.matchId == 0 || h.MatchStatus == MATCH_SPECT_ERROR_155 { 460 | h.MatchStatus = MATCH_WAIT_155 461 | logger155.Info("Met CLIENT_QUIT and reset") 462 | } 463 | } else { 464 | logger155.Warn("CLIENT_QUIT ", orig[1], " with strange length ", len(orig)-1) 465 | } 466 | 467 | case CLIENT_GAME_155: 468 | switch data155pkg(orig[3]) { 469 | 470 | case GAME_REPLAY_REQUEST_155: 471 | if len(orig)-1 == 22 || len(orig)-1 == 14 { 472 | mid, fid0, fid1 := utils.LittleIndia2Int(orig[7:11]), 0, 0 473 | if len(orig)-1 == 22 { 474 | fid0, fid1 = utils.LittleIndia2Int(orig[15:19])<<1, utils.LittleIndia2Int(orig[19:23])<<1 475 | } 476 | repData := []byte{orig[0]} 477 | 478 | if mid == 0 { 479 | if h.matchId == 0 { 480 | break 481 | } else if h.MatchStatus == MATCH_WAIT_155 { // TODO: impossible to reach this code block 482 | // HOST_QUIT 483 | logger155.Info("Quit spectator") 484 | repData = append(repData, []byte{byte(HOST_QUIT_155), 0x00, 0x00, 0x00, 485 | byte(h.matchRandId), byte(h.matchRandId >> 8), byte(h.matchRandId >> 16), byte(h.matchRandId >> 24), 486 | 0x00, 0x00, 0x00, 0x00}...) 487 | } else { 488 | // GAME_REPLAY_MATCH 489 | repData = append(repData, h.matchInfo...) 490 | } 491 | } else if mid != h.matchId || h.matchEnd { 492 | // GAME_REPLAY_END 493 | repData = append(repData, []byte{byte(HOST_GAME_155), byte(GAME_REPLAY_END_155), 0x00, 0x00, 0x00, 494 | byte(mid), byte(mid >> 8), byte(mid >> 16), byte(mid >> 24)}...) 495 | } else { 496 | fidE0, fidE1 := int(math.Min(float64(h.frameId[0]*2), float64(fid0+48))), int(math.Min(float64(h.frameId[1]*2), float64(fid1+48))) // max=24*2 497 | replay := [2][]byte{h.frameRec[0][fid0:fidE0], h.frameRec[1][fid1:fidE1]} 498 | fid0 >>= 1 499 | fid1 >>= 1 500 | fidE0 >>= 1 501 | fidE1 >>= 1 502 | repData = append(repData, []byte{byte(HOST_GAME_155), byte(GAME_REPLAY_DATA_155), 0x02, 0x00, 0x00, byte(mid), byte(mid >> 8), byte(mid >> 16), byte(mid >> 24), 503 | byte(fid0), byte(fid0 >> 8), byte(fid0 >> 16), byte(fid0 >> 24), byte(fidE0), byte(fidE0 >> 8), byte(fidE0 >> 16), byte(fidE0 >> 24)}...) 504 | repData = append(repData, replay[0]...) 505 | repData = append(repData, byte(fid1), byte(fid1>>8), byte(fid1>>16), byte(fid1>>24), byte(fidE1), byte(fidE1>>8), byte(fidE1>>16), byte(fidE1>>24)) 506 | repData = append(repData, replay[1]...) 507 | } 508 | 509 | return true, repData 510 | } else { 511 | logger155.Warn("CLIENT_GAME GAME_REPLAY_REQUEST with strange length ", len(orig)-1) 512 | } 513 | 514 | default: 515 | logger155.Warn("Th155 plugin spectator server get unexpected CLIENT_GAME ", orig[1:]) 516 | } 517 | 518 | } 519 | 520 | return false, nil 521 | } 522 | 523 | return false, orig 524 | } 525 | 526 | func (h *Hyouibana) GoroutineFunc(tunnelConn interface{}, conn *net.UDPConn) { 527 | logger155.Info("Th155 plugin goroutine start") 528 | defer logger155.Info("Th155 plugin goroutine quit") 529 | 530 | hostAddr, err := net.ResolveUDPAddr("udp", conn.RemoteAddr().String()) 531 | if err != nil { 532 | logger155.WithError(err).Error("Th155 plugin goroutine cannot resolve host udp address") 533 | return 534 | } 535 | hostConn, err := net.DialUDP("udp", nil, hostAddr) 536 | if err != nil { 537 | logger155.WithError(err).Error("Th155 plugin goroutine cannot get udp connection to host") 538 | return 539 | } 540 | hostConnClosed := false 541 | defer func() { 542 | if !hostConnClosed { 543 | _ = hostConn.Close() 544 | } 545 | }() 546 | 547 | ch := make(chan int, 2) 548 | 549 | // replay request 550 | go func() { 551 | defer func() { 552 | ch <- 1 553 | }() 554 | 555 | timeWait := 0 556 | connSucc := time.Now() 557 | bigLoop: 558 | for { 559 | 560 | if h.quitFlag { 561 | break 562 | } 563 | 564 | time.Sleep(time.Millisecond * 33) 565 | timeWait += 1 566 | timeWait %= 30 // 1s 567 | 568 | if timeWait == 0 { 569 | 570 | switch h.MatchStatus { 571 | 572 | case MATCH_WAIT_155, MATCH_SPECT_ERROR_155: 573 | connSucc = time.Now() 574 | 575 | default: 576 | timeDiff := time.Now().UnixMilli() - h.timeId 577 | specData := append([]byte{byte(CLIENT_T_155)}, []byte{0x01, 0x71, 0x00, byte(h.randId), byte(h.randId >> 8), byte(h.randId >> 16), byte(h.randId >> 24)}...) 578 | specData = append(specData, []byte{0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00}...) 579 | specData = append(specData, []byte{byte(timeDiff), byte(timeDiff >> 8), byte(timeDiff >> 16), byte(timeDiff >> 24)}...) 580 | 581 | _, err := hostConn.Write(specData) 582 | if err == nil { 583 | connSucc = time.Now() 584 | } else { 585 | logger155.WithError(err).Warn("Th155 plugin host conn write CLIENT_T error") 586 | } 587 | 588 | // spectator HOST_T 589 | var ids []byte 590 | ids, h.peerHostT = h.peerHostT, []byte{} 591 | for _, id := range ids { 592 | repData := []byte{id, byte(HOST_T_155), 0x00, 0x00, 0x00, byte(h.matchRandId), byte(h.matchRandId >> 8), byte(h.matchRandId >> 16), byte(h.matchRandId >> 24), 593 | byte(timeDiff), byte(timeDiff >> 8), byte(timeDiff >> 16), byte(timeDiff >> 24)} 594 | 595 | switch conn := tunnelConn.(type) { 596 | case quic.Stream: 597 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, repData)) 598 | case *net.TCPConn: 599 | _, err = conn.Write(utils.NewDataFrame(utils.DATA, repData)) 600 | } 601 | 602 | if err != nil { 603 | logger155.WithError(err).Warn("Th155 realize tunnel disconnected") 604 | break bigLoop 605 | } 606 | } 607 | } 608 | 609 | } else if timeWait%2 == 0 { 610 | 611 | switch h.MatchStatus { 612 | 613 | case MATCH_WAIT_155: 614 | connSucc = time.Now() 615 | 616 | case MATCH_ACCEPT_155: 617 | specData := append([]byte{byte(INIT_155)}, th155id[:]...) 618 | specData = append(specData, []byte{byte(h.randId), byte(h.randId >> 8), byte(h.randId >> 16), byte(h.randId >> 24)}...) 619 | 620 | _, err := hostConn.Write(specData) 621 | if err == nil { 622 | connSucc = time.Now() 623 | } else { 624 | logger155.WithError(err).Error("Th155 plugin host conn write INIT error") 625 | } 626 | 627 | case MATCH_SPECT_ACK_155: 628 | l, th155SpecConf := zlibDataEncodeConf() 629 | if l == 0 || th155SpecConf == nil { 630 | logger155.WithError(err).Error("Th155 plugin INIT_REQUEST zlib compression error") 631 | h.MatchStatus = MATCH_SPECT_ERROR_155 632 | break 633 | } 634 | specData := append([]byte{byte(INIT_REQUEST_155)}, th155id[:]...) 635 | specData = append(specData, byte(h.randId), byte(h.randId>>8), byte(h.randId>>16), byte(h.randId>>24)) 636 | specData = append(specData, th155ConfMagic[:]...) // spectacle 637 | specData = append(specData, 0x9c, 0x00, 0x00, 0x00) // 156 638 | specData = append(specData, th155SpecConf[:l]...) 639 | 640 | _, err = hostConn.Write(specData) 641 | if err == nil { 642 | connSucc = time.Now() 643 | } else { 644 | logger155.WithError(err).Error("Th155 plugin host conn write INIT_REQUEST error") 645 | } 646 | h.MatchStatus = MATCH_SPECT_INIT_155 647 | 648 | case MATCH_SPECT_SUCCESS_155: 649 | var specData []byte 650 | if h.matchEnd { 651 | specData = []byte{byte(CLIENT_GAME_155), 0x01, byte(GAME_REPLAY_REQUEST_155), 0x00, 0x00, 0x00, 652 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 653 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 654 | } else { 655 | specData = []byte{byte(CLIENT_GAME_155), 0x01, byte(GAME_REPLAY_REQUEST_155), 0x00, 0x00, 0x00, 656 | byte(h.matchId), byte(h.matchId >> 8), byte(h.matchId >> 16), byte(h.matchId >> 24), 657 | 0x00, 0x00, 0x00, 0x00, 658 | byte(h.frameId[0]), byte(h.frameId[0] >> 8), byte(h.frameId[0] >> 16), byte(h.frameId[0] >> 24), 659 | byte(h.frameId[1]), byte(h.frameId[1] >> 8), byte(h.frameId[1] >> 16), byte(h.frameId[1] >> 24)} 660 | } 661 | 662 | _, err := hostConn.Write(specData) 663 | if err == nil { 664 | connSucc = time.Now() 665 | } else { 666 | logger155.WithError(err).Error("Th155 plugin host conn write GAME_REPLAY_REQUEST error") 667 | } 668 | } 669 | 670 | } 671 | 672 | if h.MatchStatus != MATCH_WAIT_155 && time.Now().Sub(connSucc) > time.Second { // lost host connection 673 | logger155.Error("Th155 plugin host connection lost") 674 | h.MatchStatus = MATCH_WAIT_155 675 | h.matchEnd = true 676 | } 677 | 678 | } 679 | 680 | hostConnClosed = true 681 | _ = hostConn.Close() 682 | 683 | }() 684 | 685 | // replay record 686 | go func() { 687 | defer func() { 688 | ch <- 1 689 | }() 690 | 691 | buf := make([]byte, utils.TransBufSize) 692 | 693 | for { 694 | time.Sleep(time.Millisecond * 33) 695 | 696 | n, err := hostConn.Read(buf) 697 | 698 | if err != nil { 699 | logger155.WithError(err).Error("Th155 plugin host conn read error") 700 | break 701 | } 702 | switch type155pkg(buf[0]) { 703 | 704 | case CLIENT_T_ACK_155: 705 | 706 | case INIT_ACK_155: 707 | if n == 1 { 708 | h.MatchStatus = MATCH_SPECT_ACK_155 709 | } else { 710 | logger155.Warn("INIT_ACK with strange length ", n) 711 | } 712 | 713 | case PUNCH_155: 714 | if n == 32 { 715 | buf[1] = 0x02 716 | buf[2], buf[3] = 0x01, 0x00 717 | _, err = hostConn.Write(buf[:n]) 718 | if err != nil { 719 | logger155.WithError(err).Warn("Th155 plugin host punch reply write error") 720 | } 721 | } else { 722 | logger155.Warn("PUNCH with strange length ", n) 723 | } 724 | 725 | case HOST_T_155: 726 | if n == 12 { 727 | buf[0] = byte(HOST_T_ACK_155) 728 | _, err = hostConn.Write(buf[:n]) 729 | if err != nil { 730 | logger155.WithError(err).Warn("Th155 plugin host host_t reply write error") 731 | } 732 | } else { 733 | logger155.Warn("HOST_T with strange length ", n) 734 | } 735 | 736 | case INIT_SUCCESS_155: 737 | if n > 52 { 738 | h.MatchStatus = MATCH_SPECT_SUCCESS_155 739 | h.matchRandId = utils.LittleIndia2Int(buf[4:8]) 740 | h.initSuccessInfo = make([]byte, n-8) 741 | copy(h.initSuccessInfo, buf[8:n]) 742 | logger155.Info("Th155 plugin spectator get INIT_SUCCESS") 743 | } else { 744 | logger155.Warn("INIT_SUCCESS with strange length ", n) 745 | } 746 | 747 | case INIT_ERROR_155: 748 | if n > 20 { 749 | h.MatchStatus = MATCH_SPECT_ERROR_155 750 | h.initErrorInfo = make([]byte, n-1) 751 | copy(h.initErrorInfo, buf[1:n]) 752 | logger155.Info("Th155 plugin spectator get INIT_ERROR with ", zlibDataDecodeError(utils.LittleIndia2Int(buf[16:20]), buf[20:n])) 753 | } else { 754 | logger155.Warn("INIT_ERROR with strange length ", n) 755 | } 756 | 757 | case HOST_GAME_155: 758 | switch data155pkg(buf[1]) { 759 | 760 | case GAME_REPLAY_MATCH_155: 761 | if n > 21 { 762 | mid := utils.LittleIndia2Int(buf[5:9]) 763 | if mid != h.matchId { 764 | h.matchId = mid 765 | h.matchEnd = false 766 | h.matchInfo = make([]byte, n) 767 | copy(h.matchInfo, buf[:n]) 768 | h.frameId[0], h.frameId[1] = 0, 0 769 | h.frameRec[0], h.frameRec[1] = []byte{}, []byte{} 770 | logger155.Info("Th155 plugin spectator get new match id ", mid) 771 | } 772 | } else { 773 | logger155.Warn("HOST_GAME GAME_REPLAY_MATCH with strange length ", n) 774 | } 775 | 776 | case GAME_REPLAY_DATA_155: 777 | if n >= 24 { 778 | mid := utils.LittleIndia2Int(buf[5:9]) 779 | if mid != h.matchId { 780 | logger155.Warn("Th155 plugin spectator get invalid match id ", mid, " expect ", h.matchId) 781 | } else { 782 | fidS, fidE := utils.LittleIndia2Int(buf[9:13]), utils.LittleIndia2Int(buf[13:17]) 783 | fidL := fidE - fidS 784 | if fidS == h.frameId[0] { 785 | h.frameId[0] = fidE 786 | h.frameRec[0] = append(h.frameRec[0], buf[17:17+fidL*2]...) 787 | if len(h.frameRec[0]) != fidE*2 { 788 | logger155.Warn("Th155 plugin spectator get wrong record0 length after append new data ", len(h.frameRec[0]), " expect ", fidE*2) 789 | } 790 | } else { 791 | logger155.Warn("Th155 plugin spectator get invalid start frame id ", fidS, " expect ", h.frameId[0]) 792 | } 793 | fidS, fidE = utils.LittleIndia2Int(buf[17+fidL*2:21+fidL*2]), utils.LittleIndia2Int(buf[21+fidL*2:25+fidL*2]) 794 | if fidS == h.frameId[1] { 795 | h.frameId[1] = fidE 796 | h.frameRec[1] = append(h.frameRec[1], buf[25+fidL*2:n]...) 797 | if len(h.frameRec[1]) != fidE*2 { 798 | logger155.Warn("Th155 plugin spectator get wrong record1 length after append new data ", len(h.frameRec[1]), " expect ", fidE*2) 799 | } 800 | } else { 801 | logger155.Warn("Th155 plugin spectator get invalid start frame id ", fidS, " expect ", h.frameId[1]) 802 | } 803 | 804 | // logger155.Debug("Th155 plugin spectator get HOST_GAME GAME_REPLAY_DATA match id ", h.matchId, " frame id ", h.frameId) 805 | } 806 | } else { 807 | logger155.Warn("HOST_GAME GAME_REPLAY_DATA with strange length ", n) 808 | } 809 | 810 | case GAME_REPLAY_END_155: 811 | if n == 9 { 812 | mid := utils.LittleIndia2Int(buf[5:9]) 813 | if mid != h.matchId { 814 | logger155.Warn("Th155 plugin spectator get invalid match id ", mid, " expect ", h.matchId) 815 | } else { 816 | logger155.Info("Th155 plugin spectator get HOST_GAME GAME_REPLAY_END match id ", h.matchId) 817 | h.matchEnd = true 818 | } 819 | } else { 820 | logger155.Warn("HOST_GAME GAME_REPLAY_END with strange length ", n) 821 | } 822 | 823 | default: 824 | logger155.Warn("Th155 plugin spectator get invalid package ", buf[:n]) 825 | } 826 | 827 | case HOST_QUIT_155: 828 | logger155.Info("Th155 plugin spectator get HOST_QUIT") 829 | h.MatchStatus = MATCH_WAIT_155 // the end 830 | 831 | default: 832 | logger155.Warn("Th155 plugin spectator get invalid package ", buf[:n]) 833 | 834 | } 835 | } 836 | }() 837 | 838 | <-ch 839 | } 840 | 841 | func (h *Hyouibana) SetQuitFlag() { 842 | h.quitFlag = true 843 | } 844 | 845 | func (h *Hyouibana) GetSpectatorCount() int { 846 | return h.spectatorCount 847 | } 848 | -------------------------------------------------------------------------------- /client-gtk3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strconv" 8 | "time" 9 | 10 | client "github.com/weilinfox/youmu-thlink/client/lib" 11 | "github.com/weilinfox/youmu-thlink/glg-go" 12 | "github.com/weilinfox/youmu-thlink/utils" 13 | 14 | "github.com/gotk3/gotk3/gdk" 15 | "github.com/gotk3/gotk3/glib" 16 | "github.com/gotk3/gotk3/gtk" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var logger = logrus.WithField("client-gui", "internal") 21 | var icon *gdk.Pixbuf 22 | 23 | const appName = "白玉楼製作所 ThLink" 24 | 25 | type status struct { 26 | localPort int 27 | serverHost string 28 | tunnelType string 29 | 30 | userConfigChange bool 31 | 32 | client *client.Client 33 | plugin interface{} 34 | pluginNum int 35 | 36 | brokerTVersion byte 37 | brokerVersion string 38 | 39 | delay [40]time.Duration 40 | delayPos int 41 | delayLen int 42 | pluginDelay [40]time.Duration 43 | pluginDelayPos int 44 | pluginDelayLen int 45 | pluginDelayShow bool 46 | } 47 | 48 | // clientStatus all of this client-gui 49 | var clientStatus = status{ 50 | localPort: client.DefaultLocalPort, 51 | serverHost: client.DefaultServerHost, 52 | tunnelType: client.DefaultTunnelType, 53 | client: client.NewWithDefault(), 54 | plugin: nil, 55 | pluginNum: 0, 56 | userConfigChange: false, 57 | delayPos: 0, 58 | delayLen: 0, 59 | pluginDelayPos: 0, 60 | pluginDelayLen: 0, 61 | pluginDelayShow: false, 62 | } 63 | 64 | func main() { 65 | 66 | logrus.SetLevel(logrus.DebugLevel) 67 | 68 | const appID = "love.inuyasha.thlink" 69 | app, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE) 70 | if err != nil { 71 | logrus.WithError(err).Fatal("Could not create app") 72 | } 73 | 74 | icon, err = getIcon() 75 | if err != nil { 76 | logger.WithError(err).Error("Get icon error") 77 | } 78 | 79 | app.Connect("activate", onAppActivate) 80 | 81 | app.Run(os.Args) 82 | 83 | } 84 | 85 | var appWindow *gtk.ApplicationWindow 86 | 87 | // onAppActivate setup Main window 88 | func onAppActivate(app *gtk.Application) { 89 | 90 | var err error 91 | 92 | if appWindow != nil { 93 | if !appWindow.IsVisible() { 94 | appWindow.Show() 95 | } 96 | logger.Debug("Already running") 97 | return 98 | } 99 | 100 | clientStatus.brokerTVersion, clientStatus.brokerVersion = clientStatus.client.BrokerVersion() 101 | 102 | appWindow, err = gtk.ApplicationWindowNew(app) 103 | if err != nil { 104 | logger.WithError(err).Fatal("Could not create app window.") 105 | } 106 | 107 | // action 108 | appWindow.Connect("destroy", onAppDestroy) 109 | 110 | // simple actions 111 | aQuit := glib.SimpleActionNew("quit", nil) 112 | aQuit.Connect("activate", func() { 113 | onAppDestroy() 114 | app.Quit() 115 | }) 116 | app.AddAction(aQuit) 117 | aAbout := glib.SimpleActionNew("about", nil) 118 | aAbout.Connect("activate", showAboutDialog) 119 | app.AddAction(aAbout) 120 | 121 | appWindow.Connect("destroy", onAppDestroy) 122 | 123 | // header bar 124 | menuBtn, err := gtk.MenuButtonNew() 125 | if err != nil { 126 | logger.WithError(err).Fatal("Could not create menu button.") 127 | } 128 | header, err := gtk.HeaderBarNew() 129 | if err != nil { 130 | logger.WithError(err).Fatal("Could not create header bar.") 131 | } 132 | menu := glib.MenuNew() 133 | menu.Append("Reset config", "app.reset") 134 | menu.Append("Network discovery", "app.net-disc") 135 | menu.Append("Tunnel status", "app.t-status") 136 | menu.Append("About thlink", "app.about") 137 | menu.Append("Quit", "app.quit") 138 | menuBtn.SetMenuModel(&menu.MenuModel) 139 | header.PackStart(menuBtn) 140 | header.SetShowCloseButton(true) 141 | header.SetTitle(appName) 142 | var verText string 143 | if utils.Channel == "" { 144 | verText = "v" + utils.Version + "-" + strconv.Itoa(int(utils.TunnelVersion)) 145 | } else { 146 | verText = "v" + utils.Version + "-" + utils.Channel + "-" + strconv.Itoa(int(utils.TunnelVersion)) 147 | } 148 | header.SetSubtitle(verText) 149 | 150 | // grid 151 | mainGrid, err := gtk.GridNew() 152 | if err != nil { 153 | logger.WithError(err).Fatal("Could not create grid.") 154 | } 155 | mainGrid.SetOrientation(gtk.ORIENTATION_VERTICAL) 156 | mainGrid.SetBorderWidth(10) 157 | mainGrid.SetRowSpacing(10) 158 | 159 | mainGrid.SetHAlign(gtk.ALIGN_FILL) 160 | 161 | // server label 162 | serverLabel, err := gtk.LabelNew("Server") 163 | if err != nil { 164 | logger.WithError(err).Fatal("Could not create server label.") 165 | } 166 | serverLabel.SetHAlign(gtk.ALIGN_START) 167 | 168 | // broker address box 169 | serverEntryBuf, err := gtk.EntryBufferNew(clientStatus.serverHost, len(clientStatus.serverHost)) 170 | if err != nil { 171 | logger.WithError(err).Fatal("Could not create server entry buffer.") 172 | } 173 | serverEntry, err := gtk.EntryNewWithBuffer(serverEntryBuf) 174 | if err != nil { 175 | logger.WithError(err).Fatal("Could not create server entry.") 176 | } 177 | serverEntry.SetHExpand(true) 178 | serverEntry.Connect("changed", func() { 179 | clientStatus.userConfigChange = true 180 | host, err := serverEntry.GetText() 181 | if err != nil { 182 | logger.WithError(err).Error("Could not get server entry text.") 183 | } 184 | clientStatus.serverHost = host 185 | logger.Debug("Server host change to ", clientStatus.serverHost) 186 | }) 187 | 188 | // ping button 189 | pingBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) 190 | pingLabel, err := gtk.LabelNew("Null delay") 191 | if err != nil { 192 | logger.WithError(err).Fatal("Could not create delay label.") 193 | } 194 | pingLabel.SetHExpand(true) 195 | pingBtn, err := gtk.ButtonNewWithLabel("Ping") 196 | if err != nil { 197 | logger.WithError(err).Fatal("Could not create ping button.") 198 | } 199 | pingBtn.SetHExpand(true) 200 | 201 | setPingLabel := func(delay time.Duration) { 202 | logger.Debugf("Display new delay %.2f ms", float64(delay.Nanoseconds())/1000000) 203 | pingLabel.SetText(fmt.Sprintf("%.2f ms", float64(delay.Nanoseconds())/1000000)) 204 | 205 | clientStatus.delay[clientStatus.delayPos] = delay 206 | clientStatus.delayPos = (clientStatus.delayPos + 1) % 40 207 | if clientStatus.delayLen < 40 { 208 | clientStatus.delayLen++ 209 | } 210 | 211 | // once per second 212 | switch p := clientStatus.plugin.(type) { 213 | case *client.Hisoutensoku: 214 | clientStatus.pluginDelayShow = true 215 | if p.PeerStatus == client.BATTLE_123 { 216 | clientStatus.pluginDelay[clientStatus.pluginDelayPos] = p.GetReplayDelay() 217 | clientStatus.pluginDelayPos = (clientStatus.pluginDelayPos + 1) % 40 218 | if clientStatus.pluginDelayLen < 40 { 219 | clientStatus.pluginDelayLen++ 220 | } 221 | } 222 | } 223 | } 224 | pingBtn.Connect("clicked", func() { 225 | 226 | if !clientStatus.client.Serving() { 227 | 228 | err := onConfigUpdate() 229 | if err != nil { 230 | logger.WithError(err).Error("Update ping failed") 231 | showErrorDialog(appWindow, "Update ping error", err) 232 | return 233 | } 234 | 235 | } 236 | 237 | setPingLabel(onUpdatePing()) 238 | }) 239 | pingBox.Add(pingLabel) 240 | pingBox.Add(pingBtn) 241 | 242 | // setup label 243 | setupLabel, err := gtk.LabelNew("Tunnel info") 244 | if err != nil { 245 | logger.WithError(err).Fatal("Could not create setup label.") 246 | } 247 | setupLabel.SetHAlign(gtk.ALIGN_START) 248 | 249 | // local port 250 | localPortBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) 251 | if err != nil { 252 | logger.WithError(err).Fatal("Could not create local port box.") 253 | } 254 | localPortLabel, err := gtk.LabelNew("Local port") 255 | if err != nil { 256 | logger.WithError(err).Fatal("Could not create local port label.") 257 | } 258 | localPortBox.Add(localPortLabel) 259 | localPortDefault := strconv.Itoa(client.DefaultLocalPort) 260 | localEntryBuf, err := gtk.EntryBufferNew(localPortDefault, len(localPortDefault)) 261 | if err != nil { 262 | logger.WithError(err).Fatal("Could not create local entry buffer.") 263 | } 264 | localPortEntry, err := gtk.EntryNewWithBuffer(localEntryBuf) 265 | if err != nil { 266 | logger.WithError(err).Fatal("Could not create local entry.") 267 | } 268 | localPortEntry.SetMaxLength(5) 269 | localPortEntry.Connect("insert-text", func(_ *gtk.Entry, in []byte, length int) { 270 | for i := 0; i < length; i++ { 271 | if in[i] < '0' || in[i] > '9' { 272 | localPortEntry.StopEmission("insert-text") 273 | break 274 | } 275 | } 276 | }) 277 | localPortEntry.Connect("changed", func() { 278 | clientStatus.userConfigChange = true 279 | port, err := localPortEntry.GetText() 280 | if err != nil { 281 | logger.WithError(err).Error("Update local port failed") 282 | } 283 | port64, err := strconv.ParseInt(port, 10, 32) 284 | if err != nil { 285 | logger.WithError(err).Error("Update local port failed") 286 | } 287 | clientStatus.localPort = int(port64) 288 | logger.Debug("Local port change to ", clientStatus.localPort) 289 | }) 290 | localPortBox.Add(localPortEntry) 291 | localPortBox.SetHExpand(true) 292 | 293 | // protocol choose 294 | protoRadioBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 50) 295 | if err != nil { 296 | logger.WithError(err).Fatal("Could not create protocol radio box.") 297 | } 298 | protoRadioTcp, err := gtk.RadioButtonNewWithLabelFromWidget(nil, "TCP") 299 | if err != nil { 300 | logger.WithError(err).Fatal("Could not create protocol radio button TCP.") 301 | } 302 | protoRadioTcp.Connect("toggled", func(r *gtk.RadioButton) { 303 | if r.GetActive() { 304 | clientStatus.tunnelType = "tcp" 305 | } else { 306 | clientStatus.tunnelType = "quic" 307 | } 308 | clientStatus.userConfigChange = true 309 | logger.Debug("Protocol change to ", clientStatus.tunnelType) 310 | }) 311 | protoRadioBox.Add(protoRadioTcp) 312 | protoRadioQuic, err := gtk.RadioButtonNewWithLabelFromWidget(protoRadioTcp, "QUIC") 313 | if err != nil { 314 | logger.WithError(err).Fatal("Could not create protocol radio button QUIC.") 315 | } 316 | protoRadioBox.Add(protoRadioQuic) 317 | protoRadioBox.SetHAlign(gtk.ALIGN_CENTER) 318 | 319 | // plugin choose 320 | pluginLabel, err := gtk.LabelNew("Plugin") 321 | if err != nil { 322 | logger.WithError(err).Fatal("Could not create plugin label.") 323 | } 324 | pluginLabel.SetHAlign(gtk.ALIGN_START) 325 | 326 | pluginRadioBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) 327 | if err != nil { 328 | logger.WithError(err).Fatal("Could not create plugin radio box.") 329 | } 330 | pluginRadioOff, err := gtk.RadioButtonNewWithLabelFromWidget(nil, "OFF") 331 | if err != nil { 332 | logger.WithError(err).Fatal("Could not create plugin radio button OFF.") 333 | } 334 | pluginRadioOff.Connect("toggled", func(r *gtk.RadioButton) { 335 | if r.GetActive() { 336 | clientStatus.pluginNum = 0 337 | clientStatus.userConfigChange = true 338 | logger.Debug("Plugin change to 0") 339 | } 340 | }) 341 | pluginRadioBox.Add(pluginRadioOff) 342 | pluginRadio123, err := gtk.RadioButtonNewWithLabelFromWidget(pluginRadioOff, "TH123") 343 | if err != nil { 344 | logger.WithError(err).Fatal("Could not create plugin radio button 123.") 345 | } 346 | pluginRadio123.Connect("toggled", func(r *gtk.RadioButton) { 347 | if r.GetActive() { 348 | clientStatus.pluginNum = 123 349 | clientStatus.userConfigChange = true 350 | localPortEntry.SetText(strconv.Itoa(client.DefaultLocalPort)) 351 | logger.Debug("Plugin change to 123") 352 | } 353 | }) 354 | pluginRadioBox.Add(pluginRadio123) 355 | pluginRadio155, err := gtk.RadioButtonNewWithLabelFromWidget(pluginRadio123, "TH155") 356 | if err != nil { 357 | logger.WithError(err).Fatal("Could not create plugin radio button 155.") 358 | } 359 | pluginRadio155.Connect("toggled", func(r *gtk.RadioButton) { 360 | if r.GetActive() { 361 | clientStatus.pluginNum = 155 362 | clientStatus.userConfigChange = true 363 | localPortEntry.SetText(strconv.Itoa(client.DefaultLocalPort)) 364 | logger.Debug("Plugin change to 155") 365 | } 366 | }) 367 | pluginRadioBox.Add(pluginRadio155) 368 | pluginRadioLakey, err := gtk.RadioButtonNewWithLabelFromWidget(pluginRadio123, "Lakey") 369 | if err != nil { 370 | logger.WithError(err).Fatal("Could not create plugin radio button lakey.") 371 | } 372 | pluginRadioLakey.Connect("toggled", func(r *gtk.RadioButton) { 373 | if r.GetActive() { 374 | clientStatus.pluginNum = 1 375 | clientStatus.userConfigChange = true 376 | localPortEntry.SetText("3010") 377 | logger.Debug("Plugin change to 1") 378 | } 379 | }) 380 | pluginRadioBox.Add(pluginRadioLakey) 381 | pluginRadioBox.SetHAlign(gtk.ALIGN_CENTER) 382 | 383 | // peer address label 384 | peerLabel, err := gtk.LabelNew("Peer IP") 385 | if err != nil { 386 | logger.WithError(err).Fatal("Could not create peer address label.") 387 | } 388 | peerLabel.SetMarginTop(10) 389 | peerLabel.SetHAlign(gtk.ALIGN_START) 390 | 391 | // peer ip label 392 | addrLabel, err := gtk.LabelNew("No tunnel established") 393 | if err != nil { 394 | logger.WithError(err).Fatal("Could not create peer ip label.") 395 | } 396 | addrLabel.SetHExpand(true) 397 | 398 | // peer control button 399 | ctlBtnBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10) 400 | if err != nil { 401 | logger.WithError(err).Fatal("Could not create peer control button box.") 402 | } 403 | connBtn, err := gtk.ButtonNewWithLabel("Connect") 404 | if err != nil { 405 | logger.WithError(err).Fatal("Could not create connect button.") 406 | } 407 | connBtn.SetHExpand(true) 408 | connBtn.Connect("clicked", func() { 409 | 410 | if !clientStatus.userConfigChange && clientStatus.client.Serving() { 411 | logger.Debug("Already serving") 412 | return 413 | } 414 | 415 | err := onConfigUpdate() 416 | if err != nil { 417 | logger.WithError(err).Error("Connect failed") 418 | showErrorDialog(appWindow, "Connect failed", err) 419 | return 420 | } 421 | 422 | err = clientStatus.client.Connect() 423 | if err != nil { 424 | logger.WithError(err).Error("Connect failed") 425 | showErrorDialog(appWindow, "Connect failed", err) 426 | return 427 | } 428 | 429 | addrLabel.SetText(clientStatus.client.PeerHost()) 430 | 431 | go func() { 432 | var err error 433 | 434 | switch clientStatus.pluginNum { 435 | case 123: 436 | logger.Info("Append th12.3 hisoutensoku plugin") 437 | h := client.NewHisoutensoku() 438 | clientStatus.plugin = h 439 | err = clientStatus.client.Serve(h.ReadFunc, h.WriteFunc, h.GoroutineFunc, h.SetQuitFlag) 440 | 441 | case 155: 442 | logger.Info("Append th15.5 hyouibana plugin") 443 | h := client.NewHyouibana() 444 | clientStatus.plugin = h 445 | err = clientStatus.client.Serve(h.ReadFunc, h.WriteFunc, h.GoroutineFunc, h.SetQuitFlag) 446 | 447 | default: 448 | clientStatus.plugin = nil 449 | err = clientStatus.client.Serve(nil, nil, nil, nil) 450 | 451 | } 452 | if err != nil { 453 | logger.WithError(err).Error("Connect failed") 454 | glib.IdleAdd(func() bool { 455 | showErrorDialog(appWindow, "Connect failed", err) 456 | return false 457 | }) 458 | } 459 | 460 | }() 461 | 462 | logger.Debug("Connect") 463 | }) 464 | refreshBtn, err := gtk.ButtonNewWithLabel("Refresh") 465 | if err != nil { 466 | logger.WithError(err).Fatal("Could not create refresh button.") 467 | } 468 | refreshBtn.SetHExpand(true) 469 | refreshBtn.Connect("clicked", func() { 470 | 471 | clientStatus.client.Close() 472 | 473 | connBtn.Emit("clicked") 474 | 475 | logger.Debug("Refresh") 476 | }) 477 | copyBtn, err := gtk.ButtonNewWithLabel(" Copy ") 478 | if err != nil { 479 | logger.WithError(err).Fatal("Could not create copy button.") 480 | } 481 | copyBtn.SetHExpand(true) 482 | copyBtn.Connect("clicked", func() { 483 | 484 | if clientStatus.client.Serving() { 485 | 486 | clipBoard, err := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD) 487 | if err != nil { 488 | showErrorDialog(appWindow, "Get clipboard error", err) 489 | return 490 | } 491 | addr, err := addrLabel.GetText() 492 | if err != nil { 493 | showErrorDialog(appWindow, "Get address text error", err) 494 | return 495 | } 496 | 497 | clipBoard.SetText(addr) 498 | 499 | logger.Debug("Copy") 500 | 501 | } else { 502 | 503 | logger.Debug("Nothing to copy") 504 | 505 | } 506 | 507 | }) 508 | ctlBtnBox.Add(connBtn) 509 | ctlBtnBox.Add(refreshBtn) 510 | ctlBtnBox.Add(copyBtn) 511 | ctlBtnBox.SetHAlign(gtk.ALIGN_FILL) 512 | ctlBtnBox.SetHExpand(true) 513 | ctlBtnBox.SetMarginTop(10) 514 | 515 | // client status label 516 | statusLabel, err := gtk.LabelNew("Initializing") 517 | if err != nil { 518 | logger.WithError(err).Fatal("Could not create status label.") 519 | } 520 | statusLabel.SetMarginTop(10) 521 | statusLabel.SetHAlign(gtk.ALIGN_START) 522 | 523 | // reset action 524 | aReset := glib.SimpleActionNew("reset", nil) 525 | aReset.Connect("activate", func() { 526 | serverEntry.SetText(client.DefaultServerHost) 527 | localPortEntry.SetText(strconv.Itoa(client.DefaultLocalPort)) 528 | protoRadioTcp.SetActive(true) 529 | pluginRadioOff.SetActive(true) 530 | }) 531 | app.AddAction(aReset) 532 | 533 | // net discover action 534 | aNetDisc := glib.SimpleActionNew("net-disc", nil) 535 | aNetDisc.Connect("activate", func() { 536 | 537 | // showNetInfoDialog show network delay info dialog 538 | showNetInfoDialog := func(infoMap map[int]string) error { 539 | // prepare data 540 | sortDelay := make([]int, len(infoMap)) 541 | i := 0 542 | for k := range infoMap { 543 | sortDelay[i] = k 544 | i++ 545 | } 546 | sort.Ints(sortDelay) 547 | 548 | // setup dialog with button 549 | dialog, err := gtk.DialogNew() 550 | if err != nil { 551 | return err 552 | } 553 | dialog.SetIcon(icon) 554 | dialog.SetTitle("Network discovery") 555 | btn, err := dialog.AddButton("Close", gtk.RESPONSE_CLOSE) 556 | if err != nil { 557 | return err 558 | } 559 | btn.Connect("clicked", func() { 560 | dialog.Destroy() 561 | }) 562 | 563 | infoTreeView, err := gtk.TreeViewNew() 564 | if err != nil { 565 | return err 566 | } 567 | 568 | // setup dialog with TreeView 569 | dialogBox, err := dialog.GetContentArea() 570 | if err != nil { 571 | return err 572 | } 573 | dialogBox.Add(infoTreeView) 574 | 575 | // setup TreeView 576 | cellRenderer, err := gtk.CellRendererTextNew() 577 | if err != nil { 578 | return err 579 | } 580 | serverColumn, err := gtk.TreeViewColumnNewWithAttribute("Server", cellRenderer, "text", 0) 581 | if err != nil { 582 | return err 583 | } 584 | delayColumn, err := gtk.TreeViewColumnNewWithAttribute("Delay", cellRenderer, "text", 1) 585 | if err != nil { 586 | return err 587 | } 588 | infoListStore, err := gtk.ListStoreNew(glib.TYPE_STRING, glib.TYPE_STRING) 589 | if err != nil { 590 | return err 591 | } 592 | infoTreeView.AppendColumn(serverColumn) 593 | infoTreeView.AppendColumn(delayColumn) 594 | infoTreeView.SetModel(infoListStore) 595 | infoTreeView.Connect("row-activated", func(_ *gtk.TreeView, p *gtk.TreePath, _ *gtk.TreeViewColumn) { 596 | 597 | i := p.GetIndices()[0] 598 | logger.Debug("Net server selected ", i) 599 | 600 | serverEntry.SetText(infoMap[sortDelay[i]]) 601 | 602 | dialog.Destroy() 603 | 604 | }) 605 | 606 | // append data 607 | for _, k := range sortDelay { 608 | logger.Debug("Append server info ", infoMap[k], " delay ", k) 609 | iter := infoListStore.Append() 610 | err = infoListStore.Set(iter, []int{0, 1}, []interface{}{infoMap[k], fmt.Sprintf("%.3fms", float64(k)/1000000)}) 611 | if err != nil { 612 | return err 613 | } 614 | } 615 | 616 | // single selection 617 | infoSel, err := infoTreeView.GetSelection() 618 | if err != nil { 619 | return err 620 | } 621 | infoSel.SetMode(gtk.SELECTION_SINGLE) 622 | 623 | dialog.ShowAll() 624 | 625 | return nil 626 | } 627 | 628 | go func() { 629 | infoMap, err := client.NetBrokerDelay(client.DefaultServerHost) 630 | if err != nil { 631 | logger.WithError(err).Warn("Get network broker delay error") 632 | 633 | userServer, err := serverEntry.GetText() 634 | if err != nil { 635 | logger.WithError(err).Warn("Get server entry text error") 636 | return 637 | } 638 | 639 | infoMap, err = client.NetBrokerDelay(userServer) 640 | 641 | if err != nil { 642 | glib.IdleAdd(func() bool { 643 | showErrorDialog(appWindow, "Net discovery Failed", err) 644 | return false 645 | }) 646 | } 647 | } 648 | 649 | // show net discovery dialog 650 | infoMapCov := make(map[int]string) 651 | for k, v := range infoMap { 652 | infoMapCov[v] = k 653 | } 654 | if err == nil { 655 | logger.Debug("Show net discovery dialog") 656 | 657 | glib.IdleAdd(func() bool { 658 | err = showNetInfoDialog(infoMapCov) 659 | if err != nil { 660 | showErrorDialog(appWindow, "Show info discovery dialog error", err) 661 | } 662 | return false 663 | }) 664 | 665 | } 666 | }() 667 | 668 | }) 669 | app.AddAction(aNetDisc) 670 | 671 | // tunnel status 672 | aTStatus := glib.SimpleActionNew("t-status", nil) 673 | aTStatus.Connect("activate", func() { 674 | 675 | showTStatusDialog := func() error { 676 | 677 | // setup dialog with button 678 | dialog, err := gtk.DialogNew() 679 | if err != nil { 680 | return err 681 | } 682 | dialog.SetIcon(icon) 683 | dialog.SetTitle("Tunnel status") 684 | btn, err := dialog.AddButton("Close", gtk.RESPONSE_CLOSE) 685 | if err != nil { 686 | return err 687 | } 688 | btn.Connect("clicked", func() { 689 | dialog.Destroy() 690 | }) 691 | 692 | glg, err := glgo.GlgLineGraphNew() 693 | if err != nil { 694 | return err 695 | } 696 | 697 | // setup dialog with glgLineGraph 698 | dialogBox, err := dialog.GetContentArea() 699 | if err != nil { 700 | return err 701 | } 702 | glg.SetHExpand(true) 703 | glg.SetVExpand(true) 704 | dialogBox.Add(glg) 705 | 706 | source := glib.TimeoutAdd(1000, func() bool { 707 | 708 | pos := (clientStatus.delayPos + 39) % 40 709 | glg.GlgLineGraphDataSeriesAddValue(0, 710 | float64(clientStatus.delay[pos].Nanoseconds())/1000000) 711 | 712 | if clientStatus.pluginDelayShow { 713 | switch p := clientStatus.plugin.(type) { 714 | case *client.Hisoutensoku: 715 | if p.PeerStatus == client.BATTLE_123 { 716 | glg.GlgLineGraphDataSeriesAddValue(1, float64(p.GetReplayDelay().Nanoseconds())/1000000) 717 | } 718 | } 719 | } 720 | 721 | return true 722 | }) 723 | 724 | dialog.Connect("destroy", func() { 725 | glib.SourceRemove(source) 726 | }) 727 | 728 | dialog.SetDefaultSize(500, 300) 729 | dialog.ShowAll() 730 | 731 | glg.GlgLineGraphDataSeriesAdd("Tunnel Delay", "blue") 732 | glg.GlgLineGraphDataSeriesAdd("Peer Delay", "red") 733 | pos, l := clientStatus.delayPos, clientStatus.delayLen 734 | if l == 40 { 735 | for i := pos; i < 40; i++ { 736 | glg.GlgLineGraphDataSeriesAddValue(0, float64(clientStatus.delay[i].Nanoseconds())/1000000) 737 | } 738 | } 739 | for i := 0; i < pos; i++ { 740 | glg.GlgLineGraphDataSeriesAddValue(0, float64(clientStatus.delay[i].Nanoseconds())/1000000) 741 | } 742 | if clientStatus.pluginDelayShow { 743 | pos, l = clientStatus.pluginDelayPos, clientStatus.pluginDelayLen 744 | if l == 40 { 745 | for i := pos; i < 40; i++ { 746 | glg.GlgLineGraphDataSeriesAddValue(1, float64(clientStatus.pluginDelay[i].Nanoseconds())/1000000) 747 | } 748 | } 749 | for i := 0; i < pos; i++ { 750 | glg.GlgLineGraphDataSeriesAddValue(1, float64(clientStatus.pluginDelay[i].Nanoseconds())/1000000) 751 | } 752 | } 753 | 754 | return nil 755 | } 756 | 757 | err = showTStatusDialog() 758 | if err != nil { 759 | showErrorDialog(appWindow, "Show tunnel status dialog error", err) 760 | } 761 | }) 762 | app.AddAction(aTStatus) 763 | 764 | // add items to grid 765 | mainGrid.Add(serverLabel) 766 | mainGrid.Add(serverEntry) 767 | mainGrid.Add(pingBox) 768 | mainGrid.Add(setupLabel) 769 | mainGrid.Add(localPortBox) 770 | mainGrid.Add(protoRadioBox) 771 | mainGrid.Add(pluginLabel) 772 | mainGrid.Add(pluginRadioBox) 773 | mainGrid.Add(peerLabel) 774 | mainGrid.Add(addrLabel) 775 | mainGrid.Add(ctlBtnBox) 776 | mainGrid.Add(statusLabel) 777 | 778 | // tray icon 779 | onStatusIconSetup(appWindow) 780 | 781 | appWindow.SetTitlebar(header) 782 | if icon != nil { 783 | appWindow.SetIcon(icon) 784 | } 785 | appWindow.Add(mainGrid) 786 | appWindow.SetResizable(false) 787 | appWindow.SetDefaultSize(320, 450) 788 | appWindow.SetShowMenubar(true) 789 | appWindow.ShowAll() 790 | 791 | // auto update ping 792 | go func() { 793 | 794 | pingDelay := false 795 | pingRec := time.Duration(0) 796 | pingSameCnt := 0 797 | addrBak := "" 798 | 799 | for { 800 | time.Sleep(time.Second) 801 | 802 | glib.IdleAdd(func() bool { 803 | if clientStatus.client.Serving() { 804 | // once per second 805 | switch clientStatus.client.TunnelStatus() { 806 | case utils.STATUS_CONNECTED: 807 | delay := clientStatus.client.TunnelDelay() 808 | if delay == pingRec { 809 | pingSameCnt++ 810 | } else { 811 | pingRec = delay 812 | pingSameCnt = 0 813 | setPingLabel(delay) 814 | } 815 | if pingSameCnt > 1 { 816 | if addrBak == "" { 817 | addrBak, _ = addrLabel.GetText() 818 | addrLabel.SetText("Tunnel hanged up?") 819 | } 820 | } else if addrBak != "" { 821 | addrLabel.SetText(addrBak) 822 | addrBak = "" 823 | } 824 | case utils.STATUS_INIT: 825 | addrLabel.SetText("Tunnel init") 826 | case utils.STATUS_FAILED: 827 | addrLabel.SetText("Tunnel failed") 828 | clientStatus.client.Close() // close failed tunnel 829 | case utils.STATUS_CLOSED: 830 | addrLabel.SetText("Tunnel closed") 831 | } 832 | } else { 833 | // once each two second 834 | if !pingDelay { 835 | setPingLabel(clientStatus.client.Ping()) 836 | } 837 | pingDelay = !pingDelay 838 | } 839 | return false 840 | }) 841 | } 842 | }() 843 | 844 | // auto update status label 845 | go func() { 846 | for { 847 | 848 | if clientStatus.client.Serving() { 849 | 850 | if clientStatus.plugin != nil { 851 | 852 | switch p := clientStatus.plugin.(type) { 853 | 854 | case *client.Hisoutensoku: 855 | switch p.PeerStatus { 856 | 857 | case client.SUCCESS_123: 858 | glib.IdleAdd(func() bool { 859 | statusLabel.SetText("th12.3 game loaded") 860 | return false 861 | }) 862 | 863 | case client.BATTLE_123: 864 | glib.IdleAdd(func() bool { 865 | delay := float64(p.GetReplayDelay().Nanoseconds()) / 1000000 866 | if delay > 9999 { 867 | delay = 9999 868 | } 869 | statusLabel.SetText("th12.3 game ongoing | Delay " + 870 | fmt.Sprintf("%.2f ms", delay)) 871 | return false 872 | }) 873 | 874 | case client.BATTLE_WAIT_ANOTHER_123: 875 | glib.IdleAdd(func() bool { 876 | statusLabel.SetText(fmt.Sprintf("th12.3 game waiting | %d spectator(s)", p.GetSpectatorCount())) 877 | return false 878 | }) 879 | 880 | default: 881 | glib.IdleAdd(func() bool { 882 | tv, _, _ := clientStatus.client.Version() 883 | if tv == clientStatus.brokerTVersion { 884 | statusLabel.SetText("th12.3 game not started") 885 | } else { 886 | statusLabel.SetText("Plugin alert! Server is v" + clientStatus.brokerVersion + "-" + strconv.Itoa(int(clientStatus.brokerTVersion))) 887 | } 888 | return false 889 | }) 890 | } 891 | 892 | case *client.Hyouibana: 893 | switch p.MatchStatus { 894 | 895 | case client.MATCH_ACCEPT_155: 896 | glib.IdleAdd(func() bool { 897 | statusLabel.SetText("th15.5 game ongoing") 898 | return false 899 | }) 900 | 901 | case client.MATCH_SPECT_ACK_155: 902 | glib.IdleAdd(func() bool { 903 | statusLabel.SetText("th15.5 game ask for spectate") 904 | return false 905 | }) 906 | 907 | case client.MATCH_SPECT_INIT_155: 908 | glib.IdleAdd(func() bool { 909 | statusLabel.SetText("th15.5 host no reply | spectating disabled") 910 | return false 911 | }) 912 | 913 | case client.MATCH_SPECT_SUCCESS_155: 914 | glib.IdleAdd(func() bool { 915 | statusLabel.SetText(fmt.Sprintf("th15.5 game ongoing | %d spectator(s)", p.GetSpectatorCount())) 916 | return false 917 | }) 918 | 919 | case client.MATCH_SPECT_ERROR_155: 920 | glib.IdleAdd(func() bool { 921 | statusLabel.SetText("th15.5 game ongoing | spectating disabled") 922 | return false 923 | }) 924 | 925 | default: 926 | glib.IdleAdd(func() bool { 927 | tv, _, _ := clientStatus.client.Version() 928 | if tv == clientStatus.brokerTVersion { 929 | statusLabel.SetText("th15.5 game not started") 930 | } else { 931 | statusLabel.SetText("Plugin alert! Server is v" + clientStatus.brokerVersion + "-" + strconv.Itoa(int(clientStatus.brokerTVersion))) 932 | } 933 | return false 934 | }) 935 | } 936 | 937 | } 938 | 939 | } else { 940 | 941 | glib.IdleAdd(func() bool { 942 | tv, _, _ := clientStatus.client.Version() 943 | if tv == clientStatus.brokerTVersion { 944 | statusLabel.SetText("Connected") 945 | } else { 946 | statusLabel.SetText("Alert! Server is v" + clientStatus.brokerVersion + "-" + strconv.Itoa(int(clientStatus.brokerTVersion))) 947 | } 948 | return false 949 | }) 950 | 951 | } 952 | 953 | } else { 954 | 955 | glib.IdleAdd(func() bool { 956 | tv, _, _ := clientStatus.client.Version() 957 | if tv == clientStatus.brokerTVersion { 958 | statusLabel.SetText("Not connected") 959 | } else { 960 | statusLabel.SetText("Alert! Server is v" + clientStatus.brokerVersion + "-" + strconv.Itoa(int(clientStatus.brokerTVersion))) 961 | } 962 | return false 963 | }) 964 | 965 | } 966 | 967 | time.Sleep(time.Millisecond * 66) 968 | 969 | } 970 | }() 971 | 972 | // refresh delay 973 | _, _ = pingBtn.Emit("clicked") 974 | 975 | } 976 | 977 | // onAppDestroy close client 978 | func onAppDestroy() { 979 | setStatusIconHide() 980 | clientStatus.client.Close() 981 | } 982 | 983 | // onConfigUpdate update config and setup new client 984 | func onConfigUpdate() error { 985 | 986 | if clientStatus.client.Serving() { 987 | clientStatus.client.Close() 988 | } 989 | 990 | if clientStatus.userConfigChange { 991 | newClient, err := client.New(clientStatus.localPort, clientStatus.serverHost, clientStatus.tunnelType) 992 | if err != nil { 993 | return err 994 | } 995 | clientStatus.client = newClient 996 | clientStatus.brokerTVersion, clientStatus.brokerVersion = clientStatus.client.BrokerVersion() 997 | logger.Debugf("New client %d %s %s", clientStatus.localPort, clientStatus.serverHost, clientStatus.tunnelType) 998 | clientStatus.userConfigChange = false 999 | } 1000 | 1001 | return nil 1002 | } 1003 | 1004 | // onUpdatePing send ping via client 1005 | func onUpdatePing() time.Duration { 1006 | return clientStatus.client.Ping() 1007 | } 1008 | 1009 | // showErrorDialog show error dialog 1010 | func showErrorDialog(appWin *gtk.ApplicationWindow, msg string, err error) { 1011 | dialog := gtk.MessageDialogNew(appWin, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, "%s", err) 1012 | dialog.SetTitle(msg) 1013 | dialog.Show() 1014 | dialog.Connect("response", func() { 1015 | dialog.Destroy() 1016 | }) 1017 | } 1018 | 1019 | // showAboutDialog show about dialog 1020 | func showAboutDialog() { 1021 | 1022 | about, err := gtk.AboutDialogNew() 1023 | if err != nil { 1024 | logger.WithError(err).Error("Show about dialog error") 1025 | } 1026 | about.SetProgramName(appName) 1027 | var verText string 1028 | if utils.Channel == "" { 1029 | verText = "Client Version " + utils.Version + " Tunnel Version " + strconv.Itoa(int(utils.TunnelVersion)) 1030 | } else { 1031 | verText = "Client Version " + utils.Version + "-" + utils.Channel + " Tunnel Version " + strconv.Itoa(int(utils.TunnelVersion)) 1032 | } 1033 | about.SetVersion(verText) 1034 | about.SetAuthors([]string{"桜風の狐"}) 1035 | about.SetCopyright("https://github.com/gotk3/gotk3 ISC License\n" + 1036 | "https://github.com/quic-go/quic-go MIT License\n" + 1037 | "https://github.com/sirupsen/logrus MIT License\n" + 1038 | "https://github.com/weilinfox/youmu-thlink/glg-go GPL-3.0 License\n" + 1039 | "https://github.com/weilinfox/youmu-thlink AGPL-3.0 License\n" + 1040 | "\n2022-2023 weilinfox") 1041 | about.SetTitle("About ThLink") 1042 | if icon != nil { 1043 | about.SetIcon(icon) 1044 | about.SetLogo(icon) 1045 | } 1046 | 1047 | about.Show() 1048 | about.Connect("response", func() { 1049 | about.Destroy() 1050 | }) 1051 | 1052 | } 1053 | --------------------------------------------------------------------------------