Skip to content

Commit

Permalink
v0.2.0 - complete rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
shayne committed Oct 7, 2019
1 parent ecb4c56 commit 470ca58
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 88 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
build:
go build -o go-wsl-host.exe ./cmd/wsl2host
go build -o wsl2host.exe ./cmd/wsl2host

.PHONY: build
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# go-wsl2-host

> Take a look at https://github.com/shayne/wsl2-hacks for things like auto-starting services, running commands when the VM boots and making localhost ports accessible
> v0.2.0 is out, this drops support for `windows.local`, if this was important let me know and I can add it back in.
A workaround for accessing the WSL2 VM from the Windows host and vice versa.
A workaround for accessing the WSL2 VM from the Windows host.

This program installs as a service and runs under the local user account. It automatically updates your Windows hosts file with the WSL2 VM's IP address.

The program uses the hostname `wsl.local` for the WSL VM and `windows.local` for the Windows host.
The program uses the name of your distro, modified to be a hostname. For example "Ubuntu-18.04" becomes `ubuntu1804.wsl`. If you have more than one running distro, it will be added as well. When the distro stops it is removed from the host file.

I wrote this for my own use but thought it might be useful for others. It's not perfect but gets the job done for me.

Expand All @@ -15,11 +15,11 @@ To install and run, download a binary from the releases tab. Place it somewhere
Open an **elevated/administrator** command prompt:

```
> .\go-wsl2-host.exe install
> .\wsl2host.exe install
Windows Username: <username-you-use-to-login-to-windows>
Windows Password: <password-for-this-user>
```

The program will install a service and start it up. Launch `wsl` then from a `cmd` prompt, run `ping wsl.local`. You can check the Windows hosts file to see what was written. The service will automatically update the IP if the WSL2 VM is stopped and started again.
The program will install a service and start it up. Launch `wsl` then from a `cmd` prompt, run `ping ubuntu1804.wsl`. You can check the Windows hosts file to see what was written. The service will automatically update the IP if the WSL2 VM is stopped and started again.

The Windows hosts file is located at: `C:\Windows\System32\drivers\etc\hosts`
7 changes: 1 addition & 6 deletions cmd/wsl2host/internal/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,9 @@ loop:
for {
select {
case <-tick:
is, err := service.IsRunning()
err := service.Run()
if err != nil {
elog.Error(1, fmt.Sprintf("%v", err))
} else if is {
err := service.UpdateIP()
if err != nil {
elog.Error(1, fmt.Sprintf("%v", err))
}
}
case c := <-r:
switch c.Cmd {
Expand Down
110 changes: 41 additions & 69 deletions cmd/wsl2host/pkg/service/service.go
Original file line number Diff line number Diff line change
@@ -1,96 +1,68 @@
package service

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"

"github.com/shayne/go-wsl2-host/pkg/wslcli"
"github.com/shayne/go-wsl2-host/pkg/hostsapi"

"github.com/shayne/go-wsl2-host/pkg/wslapi"
)

const wslHostname = "wsl.local"
const windowsHostname = "windows.local"
const tld = ".wsl"

// IsRunning returns whether or not WSL is running
func IsRunning() (bool, error) {
running, err := wslcli.Running()
if err != nil {
return false, err
}
return running, nil
}
var hostnamereg, _ = regexp.Compile("[^A-Za-z0-9]+")

func getWSLIP() (string, error) {
ip, err := wslcli.GetWSLIP()
if err != nil {
return "", err
}
return strings.TrimSpace(ip), nil
func distroNameToHostname(distroname string) string {
// Ubuntu-18.04
// => ubuntu1804.wsl
hostname := strings.ToLower(distroname)
hostname = hostnamereg.ReplaceAllString(hostname, "")
return hostname + tld
}

// UpdateIP updates the Windows hosts file
func UpdateIP() error {
wslIP, err := getWSLIP()
if err != nil {
return err
}
hostIP, err := wslcli.GetHostIP()
// Run main entry point to service logic
func Run() error {
infos, err := wslapi.GetAllInfo()
if err != nil {
return err
return fmt.Errorf("failed to get infos: %w", err)
}
f, err := os.OpenFile("c:/Windows/System32/drivers/etc/hosts", os.O_RDWR, 0600)

hapi, err := hostsapi.CreateAPI(tld)
if err != nil {
return err
return fmt.Errorf("failed to create hosts api: %w", err)
}
defer f.Close()

wslExisted := false
wslWasCorrect := false
hostExisted := false
hostWasCorrect := false
scanner := bufio.NewScanner(f)
lines := make([]string, 0, 50)

wslLine := fmt.Sprintf("%s %s", wslIP, wslHostname)
hostLine := fmt.Sprintf("%s %s", hostIP, windowsHostname)
hostentries := hapi.Entries()
for _, i := range infos {
hostname := distroNameToHostname(i.Name)
// remove stopped distros
if i.Running == false {
hapi.RemoveEntry(hostname)
continue
}

for scanner.Scan() {
line := scanner.Text()
if strings.HasSuffix(line, wslHostname) {
if strings.Contains(line, wslIP) {
wslWasCorrect = true
lines = append(lines, line)
} else {
wslExisted = true
lines = append(lines, wslLine)
}
} else if strings.HasSuffix(line, windowsHostname) {
if strings.Contains(line, hostIP) {
hostWasCorrect = true
lines = append(lines, line)
} else {
hostExisted = true
lines = append(lines, hostLine)
// update IPs of running distros
ip, err := wslapi.GetIP(i.Name)
if he, exists := hostentries[hostname]; exists {
if err != nil {
return fmt.Errorf("failed to get IP for distro %q: %w", i.Name, err)
}
he.IP = ip
} else {
lines = append(lines, line)
// add running distros not present
hapi.AddEntry(&hostsapi.HostEntry{
Hostname: hostname,
IP: ip,
})
}
}
if err := scanner.Err(); err != nil {
return err
}

if !wslWasCorrect && !wslExisted {
lines = append(lines, wslLine)
}
if !hostWasCorrect && !hostExisted {
lines = append(lines, hostLine)
}

_, err = f.WriteAt([]byte(strings.Join(lines, "\r\n")), 0)
err = hapi.Write()
if err != nil {
return err
return fmt.Errorf("failed to write hosts file: %w", err)
}

return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.12
require (
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
golang.org/x/text v0.3.0
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
171 changes: 171 additions & 0 deletions pkg/hostsapi/hostsapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package hostsapi

import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"strings"
)

const hostspath = "C:/Windows/System32/drivers/etc/hosts"

// HostEntry data structure for IP and hostnames
type HostEntry struct {
Idx int
IP string
Hostname string
}

// HostsAPI data structure
type HostsAPI struct {
filter string
hostsfile *os.File
entries map[string]*HostEntry
remidxs map[int]interface{}
}

func parseHostfileLine(idx int, line string) ([]*HostEntry, error) {
if len(line) <= 0 {
return nil, errors.New("invalid line")
}
line = strings.TrimSpace(line)
if line[0] == '#' {
return nil, errors.New("comment line")
}
fields := strings.Fields(line)
var validfields []string
for _, f := range fields {
if len(f) <= 0 {
continue
}
if f[0] == '#' { // inline comment
break // don't process any more
}
validfields = append(validfields, f)
}
if len(validfields) <= 1 {
return nil, fmt.Errorf("invalid fields for line: %q", line)
}
var entries []*HostEntry
for _, hostname := range validfields[1:] {
entries = append(entries, &HostEntry{
Idx: idx,
IP: validfields[0],
Hostname: hostname,
})
}

return entries, nil
}

func (h *HostsAPI) loadAndParse() error {
scanner := bufio.NewScanner(h.hostsfile)
idx := 0
for scanner.Scan() {
line := scanner.Text()
entries, err := parseHostfileLine(idx, line)
idx++
if err != nil {
// log.Println(err) // debug
continue
}
for _, e := range entries {
if h.filter == "" || strings.Contains(e.Hostname, h.filter) {
h.entries[e.Hostname] = e
h.remidxs[e.Idx] = nil
}
}
}
h.hostsfile.Seek(0, 0)
return nil
}

// CreateAPI creates a new instance of the hosts file API
// Call Close() when finished
// `filter` proves ability to filter by string contains
func CreateAPI(filter string) (*HostsAPI, error) {
f, err := os.Open(hostspath)
if err != nil {
return nil, fmt.Errorf("failed to open hosts file: %w", err)
}
h := &HostsAPI{
filter: filter,
remidxs: make(map[int]interface{}),
entries: make(map[string]*HostEntry),
hostsfile: f,
}
err = h.loadAndParse()
if err != nil {
return nil, fmt.Errorf("failed to parse hosts file: %w", err)
}
return h, nil
}

// Close closes the hosts file
func (h *HostsAPI) Close() error {
err := h.hostsfile.Close()
if err != nil {
return fmt.Errorf("failed to close hosts file: %w", err)
}

return nil
}

// Entries returns parsed entries of host file
func (h *HostsAPI) Entries() map[string]*HostEntry {
return h.entries
}

// RemoveEntry removes existing entry from hosts file
func (h *HostsAPI) RemoveEntry(hostname string) error {
if _, exists := h.entries[hostname]; exists {
delete(h.entries, hostname)
} else {
return fmt.Errorf("failed to remove, hostname does not exist: %s", hostname)
}
return nil
}

// AddEntry adds a new HostEntry
func (h *HostsAPI) AddEntry(entry *HostEntry) error {
if _, exists := h.entries[entry.Hostname]; exists {
return fmt.Errorf("failed to add entry, hostname already exists: %s", entry.Hostname)
}

h.entries[entry.Hostname] = entry

return nil
}

// Write
func (h *HostsAPI) Write() error {
var outbuf bytes.Buffer

// first remove all current entries
scanner := bufio.NewScanner(h.hostsfile)
for idx := 0; scanner.Scan() == true; idx++ {
line := scanner.Text()
if _, exists := h.remidxs[idx]; !exists {
outbuf.WriteString(line)
outbuf.WriteString("\r\n")
}
}

// append entries to file
for _, e := range h.entries {
outbuf.WriteString(fmt.Sprintf("%s %s # managed by wsl2-host\r\n", e.IP, e.Hostname))
}

fmt.Println(string(outbuf.Bytes()))
f, err := os.OpenFile(hostspath, os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open hosts file for writing: %w", err)
}
defer f.Close()

f.Write(outbuf.Bytes())

return nil
}
Loading

0 comments on commit 470ca58

Please sign in to comment.