Skip to content

Commit 8248637

Browse files
authored
WSL Integration (#1031)
Adds support for connecting to local WSL installations on Windows. (also adds wshrpcmmultiproxy / connserver router)
1 parent 4e86b67 commit 8248637

File tree

31 files changed

+2101
-75
lines changed

31 files changed

+2101
-75
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* text=auto
1+
* text=auto eol=lf

cmd/server/main-server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ func shutdownActivityUpdate() {
159159

160160
func createMainWshClient() {
161161
rpc := wshserver.GetMainRpcClient()
162-
wshutil.DefaultRouter.RegisterRoute(wshutil.DefaultRoute, rpc)
162+
wshutil.DefaultRouter.RegisterRoute(wshutil.DefaultRoute, rpc, true)
163163
wps.Broker.SetClient(wshutil.DefaultRouter)
164164
localConnWsh := wshutil.MakeWshRpc(nil, nil, wshrpc.RpcContext{Conn: wshrpc.LocalConnName}, &wshremote.ServerImpl{})
165165
go wshremote.RunSysInfoLoop(localConnWsh, wshrpc.LocalConnName)
166-
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeConnectionRouteId(wshrpc.LocalConnName), localConnWsh)
166+
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeConnectionRouteId(wshrpc.LocalConnName), localConnWsh, true)
167167
}
168168

169169
func main() {

cmd/wsh/cmd/wshcmd-conn.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cmd
55

66
import (
77
"fmt"
8+
"strings"
89

910
"github.com/spf13/cobra"
1011
"github.com/wavetermdev/waveterm/pkg/remote"
@@ -25,17 +26,24 @@ func init() {
2526
}
2627

2728
func connStatus() error {
28-
resp, err := wshclient.ConnStatusCommand(RpcClient, nil)
29+
var allResp []wshrpc.ConnStatus
30+
sshResp, err := wshclient.ConnStatusCommand(RpcClient, nil)
2931
if err != nil {
30-
return fmt.Errorf("getting connection status: %w", err)
32+
return fmt.Errorf("getting ssh connection status: %w", err)
3133
}
32-
if len(resp) == 0 {
34+
allResp = append(allResp, sshResp...)
35+
wslResp, err := wshclient.WslStatusCommand(RpcClient, nil)
36+
if err != nil {
37+
return fmt.Errorf("getting wsl connection status: %w", err)
38+
}
39+
allResp = append(allResp, wslResp...)
40+
if len(allResp) == 0 {
3341
WriteStdout("no connections\n")
3442
return nil
3543
}
3644
WriteStdout("%-30s %-12s\n", "connection", "status")
3745
WriteStdout("----------------------------------------------\n")
38-
for _, conn := range resp {
46+
for _, conn := range allResp {
3947
str := fmt.Sprintf("%-30s %-12s", conn.Connection, conn.Status)
4048
if conn.Error != "" {
4149
str += fmt.Sprintf(" (%s)", conn.Error)
@@ -110,7 +118,7 @@ func connRun(cmd *cobra.Command, args []string) error {
110118
}
111119
connName = args[1]
112120
_, err := remote.ParseOpts(connName)
113-
if err != nil {
121+
if err != nil && !strings.HasPrefix(connName, "wsl://") {
114122
return fmt.Errorf("cannot parse connection name: %w", err)
115123
}
116124
}

cmd/wsh/cmd/wshcmd-connserver.go

Lines changed: 166 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,186 @@
44
package cmd
55

66
import (
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"log"
11+
"net"
712
"os"
13+
"sync/atomic"
14+
"time"
815

916
"github.com/spf13/cobra"
17+
"github.com/wavetermdev/waveterm/pkg/util/packetparser"
18+
"github.com/wavetermdev/waveterm/pkg/wavebase"
19+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
20+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
1021
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
22+
"github.com/wavetermdev/waveterm/pkg/wshutil"
1123
)
1224

1325
var serverCmd = &cobra.Command{
14-
Use: "connserver",
15-
Hidden: true,
16-
Short: "remote server to power wave blocks",
17-
Args: cobra.NoArgs,
18-
Run: serverRun,
19-
PreRunE: preRunSetupRpcClient,
26+
Use: "connserver",
27+
Hidden: true,
28+
Short: "remote server to power wave blocks",
29+
Args: cobra.NoArgs,
30+
RunE: serverRun,
2031
}
2132

33+
var connServerRouter bool
34+
2235
func init() {
36+
serverCmd.Flags().BoolVar(&connServerRouter, "router", false, "run in local router mode")
2337
rootCmd.AddCommand(serverCmd)
2438
}
2539

26-
func serverRun(cmd *cobra.Command, args []string) {
40+
func MakeRemoteUnixListener() (net.Listener, error) {
41+
serverAddr := wavebase.GetRemoteDomainSocketName()
42+
os.Remove(serverAddr) // ignore error
43+
rtn, err := net.Listen("unix", serverAddr)
44+
if err != nil {
45+
return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err)
46+
}
47+
os.Chmod(serverAddr, 0700)
48+
log.Printf("Server [unix-domain] listening on %s\n", serverAddr)
49+
return rtn, nil
50+
}
51+
52+
func handleNewListenerConn(conn net.Conn, router *wshutil.WshRouter) {
53+
var routeIdContainer atomic.Pointer[string]
54+
proxy := wshutil.MakeRpcProxy()
55+
go func() {
56+
writeErr := wshutil.AdaptOutputChToStream(proxy.ToRemoteCh, conn)
57+
if writeErr != nil {
58+
log.Printf("error writing to domain socket: %v\n", writeErr)
59+
}
60+
}()
61+
go func() {
62+
// when input is closed, close the connection
63+
defer func() {
64+
conn.Close()
65+
routeIdPtr := routeIdContainer.Load()
66+
if routeIdPtr != nil && *routeIdPtr != "" {
67+
router.UnregisterRoute(*routeIdPtr)
68+
disposeMsg := &wshutil.RpcMessage{
69+
Command: wshrpc.Command_Dispose,
70+
Data: wshrpc.CommandDisposeData{
71+
RouteId: *routeIdPtr,
72+
},
73+
Source: *routeIdPtr,
74+
AuthToken: proxy.GetAuthToken(),
75+
}
76+
disposeBytes, _ := json.Marshal(disposeMsg)
77+
router.InjectMessage(disposeBytes, *routeIdPtr)
78+
}
79+
}()
80+
wshutil.AdaptStreamToMsgCh(conn, proxy.FromRemoteCh)
81+
}()
82+
routeId, err := proxy.HandleClientProxyAuth(router)
83+
if err != nil {
84+
log.Printf("error handling client proxy auth: %v\n", err)
85+
conn.Close()
86+
return
87+
}
88+
router.RegisterRoute(routeId, proxy, false)
89+
routeIdContainer.Store(&routeId)
90+
}
91+
92+
func runListener(listener net.Listener, router *wshutil.WshRouter) {
93+
defer func() {
94+
log.Printf("listener closed, exiting\n")
95+
time.Sleep(500 * time.Millisecond)
96+
wshutil.DoShutdown("", 1, true)
97+
}()
98+
for {
99+
conn, err := listener.Accept()
100+
if err == io.EOF {
101+
break
102+
}
103+
if err != nil {
104+
log.Printf("error accepting connection: %v\n", err)
105+
continue
106+
}
107+
go handleNewListenerConn(conn, router)
108+
}
109+
}
110+
111+
func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter) (*wshutil.WshRpc, error) {
112+
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
113+
if jwtToken == "" {
114+
return nil, fmt.Errorf("no jwt token found for connserver")
115+
}
116+
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
117+
if err != nil {
118+
return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
119+
}
120+
authRtn, err := router.HandleProxyAuth(jwtToken)
121+
if err != nil {
122+
return nil, fmt.Errorf("error handling proxy auth: %v", err)
123+
}
124+
inputCh := make(chan []byte, wshutil.DefaultInputChSize)
125+
outputCh := make(chan []byte, wshutil.DefaultOutputChSize)
126+
connServerClient := wshutil.MakeWshRpc(inputCh, outputCh, *rpcCtx, &wshremote.ServerImpl{LogWriter: os.Stdout})
127+
connServerClient.SetAuthToken(authRtn.AuthToken)
128+
router.RegisterRoute(authRtn.RouteId, connServerClient, false)
129+
wshclient.RouteAnnounceCommand(connServerClient, nil)
130+
return connServerClient, nil
131+
}
132+
133+
func serverRunRouter() error {
134+
router := wshutil.NewWshRouter()
135+
termProxy := wshutil.MakeRpcProxy()
136+
rawCh := make(chan []byte, wshutil.DefaultOutputChSize)
137+
go packetparser.Parse(os.Stdin, termProxy.FromRemoteCh, rawCh)
138+
go func() {
139+
for msg := range termProxy.ToRemoteCh {
140+
packetparser.WritePacket(os.Stdout, msg)
141+
}
142+
}()
143+
go func() {
144+
// just ignore and drain the rawCh (stdin)
145+
// when stdin is closed, shutdown
146+
defer wshutil.DoShutdown("", 0, true)
147+
for range rawCh {
148+
// ignore
149+
}
150+
}()
151+
go func() {
152+
for msg := range termProxy.FromRemoteCh {
153+
// send this to the router
154+
router.InjectMessage(msg, wshutil.UpstreamRoute)
155+
}
156+
}()
157+
router.SetUpstreamClient(termProxy)
158+
// now set up the domain socket
159+
unixListener, err := MakeRemoteUnixListener()
160+
if err != nil {
161+
return fmt.Errorf("cannot create unix listener: %v", err)
162+
}
163+
client, err := setupConnServerRpcClientWithRouter(router)
164+
if err != nil {
165+
return fmt.Errorf("error setting up connserver rpc client: %v", err)
166+
}
167+
go runListener(unixListener, router)
168+
// run the sysinfo loop
169+
wshremote.RunSysInfoLoop(client, client.GetRpcContext().Conn)
170+
select {}
171+
}
172+
173+
func serverRunNormal() error {
174+
err := setupRpcClient(&wshremote.ServerImpl{LogWriter: os.Stdout})
175+
if err != nil {
176+
return err
177+
}
27178
WriteStdout("running wsh connserver (%s)\n", RpcContext.Conn)
28179
go wshremote.RunSysInfoLoop(RpcClient, RpcContext.Conn)
29-
RpcClient.SetServerImpl(&wshremote.ServerImpl{LogWriter: os.Stdout})
30-
31180
select {} // run forever
32181
}
182+
183+
func serverRun(cmd *cobra.Command, args []string) error {
184+
if connServerRouter {
185+
return serverRunRouter()
186+
} else {
187+
return serverRunNormal()
188+
}
189+
}

cmd/wsh/cmd/wshcmd-wsl.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2024, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/wavetermdev/waveterm/pkg/waveobj"
11+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
12+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
13+
)
14+
15+
var distroName string
16+
17+
var wslCmd = &cobra.Command{
18+
Use: "wsl [-d <Distro>]",
19+
Short: "connect this terminal to a local wsl connection",
20+
Args: cobra.NoArgs,
21+
Run: wslRun,
22+
PreRunE: preRunSetupRpcClient,
23+
}
24+
25+
func init() {
26+
wslCmd.Flags().StringVarP(&distroName, "distribution", "d", "", "Run the specified distribution")
27+
rootCmd.AddCommand(wslCmd)
28+
}
29+
30+
func wslRun(cmd *cobra.Command, args []string) {
31+
var err error
32+
if distroName == "" {
33+
// get default distro from the host
34+
distroName, err = wshclient.WslDefaultDistroCommand(RpcClient, nil)
35+
if err != nil {
36+
WriteStderr("[error] %s\n", err)
37+
return
38+
}
39+
}
40+
if !strings.HasPrefix(distroName, "wsl://") {
41+
distroName = "wsl://" + distroName
42+
}
43+
blockId := RpcContext.BlockId
44+
if blockId == "" {
45+
WriteStderr("[error] cannot determine blockid (not in JWT)\n")
46+
return
47+
}
48+
data := wshrpc.CommandSetMetaData{
49+
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
50+
Meta: map[string]any{
51+
waveobj.MetaKey_Connection: distroName,
52+
},
53+
}
54+
err = wshclient.SetMetaCommand(RpcClient, data, nil)
55+
if err != nil {
56+
WriteStderr("[error] setting switching connection: %v\n", err)
57+
return
58+
}
59+
WriteStderr("switched connection to %q\n", distroName)
60+
}

frontend/app/block/blockframe.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ const ChangeConnectionBlockModal = React.memo(
521521
const connStatusAtom = getConnStatusAtom(connection);
522522
const connStatus = jotai.useAtomValue(connStatusAtom);
523523
const [connList, setConnList] = React.useState<Array<string>>([]);
524+
const [wslList, setWslList] = React.useState<Array<string>>([]);
524525
const allConnStatus = jotai.useAtomValue(atoms.allConnStatus);
525526
const [rowIndex, setRowIndex] = React.useState(0);
526527
const connStatusMap = new Map<string, ConnStatus>();
@@ -540,6 +541,18 @@ const ChangeConnectionBlockModal = React.memo(
540541
prtn.then((newConnList) => {
541542
setConnList(newConnList ?? []);
542543
}).catch((e) => console.log("unable to load conn list from backend. using blank list: ", e));
544+
const p2rtn = RpcApi.WslListCommand(TabRpcClient, { timeout: 2000 });
545+
p2rtn
546+
.then((newWslList) => {
547+
console.log(newWslList);
548+
setWslList(newWslList ?? []);
549+
})
550+
.catch((e) => {
551+
// removing this log and failing silentyly since it will happen
552+
// if a system isn't using the wsl. and would happen every time the
553+
// typeahead was opened. good candidate for verbose log level.
554+
//console.log("unable to load wsl list from backend. using blank list: ", e)
555+
});
543556
}, [changeConnModalOpen, setConnList]);
544557

545558
const changeConnection = React.useCallback(
@@ -588,6 +601,15 @@ const ChangeConnectionBlockModal = React.memo(
588601
filteredList.push(conn);
589602
}
590603
}
604+
const filteredWslList: Array<string> = [];
605+
for (const conn of wslList) {
606+
if (conn === connSelected) {
607+
createNew = false;
608+
}
609+
if (conn.includes(connSelected)) {
610+
filteredWslList.push(conn);
611+
}
612+
}
591613
// priority handles special suggestions when necessary
592614
// for instance, when reconnecting
593615
const newConnectionSuggestion: SuggestionConnectionItem = {
@@ -637,6 +659,20 @@ const ChangeConnectionBlockModal = React.memo(
637659
label: localName,
638660
});
639661
}
662+
for (const wslConn of filteredWslList) {
663+
const connStatus = connStatusMap.get(wslConn);
664+
const connColorNum = computeConnColorNum(connStatus);
665+
localSuggestion.items.push({
666+
status: "connected",
667+
icon: "arrow-right-arrow-left",
668+
iconColor:
669+
connStatus?.status == "connected"
670+
? `var(--conn-icon-color-${connColorNum})`
671+
: "var(--grey-text-color)",
672+
value: "wsl://" + wslConn,
673+
label: "wsl://" + wslConn,
674+
});
675+
}
640676
const remoteItems = filteredList.map((connName) => {
641677
const connStatus = connStatusMap.get(connName);
642678
const connColorNum = computeConnColorNum(connStatus);

0 commit comments

Comments
 (0)