Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"piswitch/internal/provider"
"piswitch/internal/system"
"piswitch/internal/updater"
"piswitch/internal/wsl"
)

const appVersion = "0.0.0.14"
Expand Down Expand Up @@ -242,6 +243,14 @@ func (a *App) GetAppState() (config.AppState, error) {
}, nil
}

func (a *App) GetWSLDetection() (wsl.Detection, error) {
return wsl.Detect()
}

func (a *App) GetWSLPiDetection(distro string) (wsl.PiDetection, error) {
return wsl.DetectPi(distro)
}

func (a *App) ListProviders() ([]provider.Config, error) {
cfg, err := a.coordinator.Load()
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions frontend/wailsjs/go/main/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {system} from '../models';
import {updater} from '../models';
import {provider} from '../models';
import {config} from '../models';
import {wsl} from '../models';
import {pi} from '../models';

export function CheckEnvVar(arg1:string):Promise<system.EnvCheckResult>;
Expand All @@ -20,6 +21,10 @@ export function FetchModels(arg1:string):Promise<Array<provider.ModelInfo>>;

export function GetAppState():Promise<config.AppState>;

export function GetWSLDetection():Promise<wsl.Detection>;

export function GetWSLPiDetection(arg1:string):Promise<wsl.PiDetection>;

export function ImportModels(arg1:string,arg2:Array<provider.ModelInfo>):Promise<void>;

export function InstallUpdate():Promise<void>;
Expand Down
8 changes: 8 additions & 0 deletions frontend/wailsjs/go/main/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export function GetAppState() {
return window['go']['main']['App']['GetAppState']();
}

export function GetWSLDetection() {
return window['go']['main']['App']['GetWSLDetection']();
}

export function GetWSLPiDetection(arg1) {
return window['go']['main']['App']['GetWSLPiDetection'](arg1);
}

export function ImportModels(arg1, arg2) {
return window['go']['main']['App']['ImportModels'](arg1, arg2);
}
Expand Down
45 changes: 45 additions & 0 deletions frontend/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,48 @@ export namespace updater {

}

export namespace wsl {

export class Detection {
detected: boolean;
distros: string[];

static createFrom(source: any = {}) {
return new Detection(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.detected = source["detected"];
this.distros = source["distros"];
}
}
export class PiDetection {
distro: string;
home: string;
piAvailable: boolean;
piPath: string;
piHome: string;
piHomeExists: boolean;
settingsExists: boolean;
modelsExists: boolean;

static createFrom(source: any = {}) {
return new PiDetection(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.distro = source["distro"];
this.home = source["home"];
this.piAvailable = source["piAvailable"];
this.piPath = source["piPath"];
this.piHome = source["piHome"];
this.piHomeExists = source["piHomeExists"];
this.settingsExists = source["settingsExists"];
this.modelsExists = source["modelsExists"];
}
}

}

54 changes: 54 additions & 0 deletions internal/wsl/detection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package wsl

import (
"errors"
"strings"
"unicode/utf16"
)

type Detection struct {
Detected bool `json:"detected"`
Distros []string `json:"distros"`
}

func parseDistroList(data []byte) ([]string, error) {
if len(data) >= 2 && data[0] == 0xff && data[1] == 0xfe {
data = data[2:]
}
if len(data)%2 != 0 {
return nil, errors.New("WSL 发行版列表不是有效的 UTF-16LE 输出")
}

units := make([]uint16, len(data)/2)
for i := range units {
units[i] = uint16(data[i*2]) | uint16(data[i*2+1])<<8
}
if err := validateUTF16(units); err != nil {
return nil, err
}

distros := make([]string, 0)
for _, line := range strings.Split(string(utf16.Decode(units)), "\n") {
distro := strings.TrimSpace(strings.Trim(line, "\x00"))
if distro != "" {
distros = append(distros, distro)
}
}
return distros, nil
}

func validateUTF16(units []uint16) error {
for index, unit := range units {
switch {
case unit >= 0xd800 && unit <= 0xdbff:
if index+1 >= len(units) || units[index+1] < 0xdc00 || units[index+1] > 0xdfff {
return errors.New("WSL 发行版列表包含无效的 UTF-16 代理项")
}
case unit >= 0xdc00 && unit <= 0xdfff:
if index == 0 || units[index-1] < 0xd800 || units[index-1] > 0xdbff {
return errors.New("WSL 发行版列表包含无效的 UTF-16 代理项")
}
}
}
return nil
}
7 changes: 7 additions & 0 deletions internal/wsl/detection_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package wsl

func Detect() (Detection, error) {
return Detection{Distros: []string{}}, nil
}
109 changes: 109 additions & 0 deletions internal/wsl/detection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package wsl

import (
"encoding/json"
"reflect"
"runtime"
"testing"
"unicode/utf16"
)

func TestParseDistroList(t *testing.T) {
tests := []struct {
name string
data []byte
want []string
}{
{
name: "one distro",
data: utf16LE("Ubuntu\r\n", false),
want: []string{"Ubuntu"},
},
{
name: "multiple distros",
data: utf16LE("Ubuntu\r\nDebian\r\n", false),
want: []string{"Ubuntu", "Debian"},
},
{
name: "BOM whitespace and spaces in name",
data: utf16LE("\r\n Ubuntu \r\nUbuntu 24.04 LTS\r\n", true),
want: []string{"Ubuntu", "Ubuntu 24.04 LTS"},
},
{
name: "supplementary plane Unicode",
data: utf16LE("Ubuntu \U0001F680\r\n", false),
want: []string{"Ubuntu \U0001F680"},
},
{
name: "terminal NUL",
data: utf16LE("Debian\r\n\x00", false),
want: []string{"Debian"},
},
{
name: "empty output",
data: []byte{},
want: []string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseDistroList(test.data)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("parseDistroList() = %#v, want %#v", got, test.want)
}
})
}
}

func TestParseDistroList_InvalidUTF16(t *testing.T) {
tests := [][]byte{
{0x55},
{0x00, 0xd8},
{0x00, 0xdc},
}

for _, data := range tests {
if _, err := parseDistroList(data); err == nil {
t.Errorf("parseDistroList(% x) error = nil, want error", data)
}
}
}

func TestDetectionJSONEmptyDistros(t *testing.T) {
data, err := json.Marshal(Detection{Distros: []string{}})
if err != nil {
t.Fatal(err)
}
if string(data) != `{"detected":false,"distros":[]}` {
t.Errorf("json.Marshal(Detection{}) = %s, want empty distros array", data)
}
}

func TestDetect_NonWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the Windows implementation requires a WSL installation")
}

detection, err := Detect()
if err != nil {
t.Fatal(err)
}
if detection.Detected || !reflect.DeepEqual(detection.Distros, []string{}) {
t.Errorf("Detect() = %#v, want unavailable empty result", detection)
}
}

func utf16LE(text string, bom bool) []byte {
data := make([]byte, 0, len(text)*2+2)
if bom {
data = append(data, 0xff, 0xfe)
}
for _, unit := range utf16.Encode([]rune(text)) {
data = append(data, byte(unit), byte(unit>>8))
}
return data
}
33 changes: 33 additions & 0 deletions internal/wsl/detection_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build windows

package wsl

import (
"errors"
"fmt"
"os/exec"
"syscall"
)

const createNoWindow uint32 = 0x08000000

func Detect() (Detection, error) {
unavailable := Detection{Distros: []string{}}

cmd := exec.Command("wsl.exe", "--list", "--quiet")
// Prevent a console window for this noninteractive WSL query.
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
output, err := cmd.Output()
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
return unavailable, nil
}
return unavailable, fmt.Errorf("查询 WSL 发行版失败: %w", err)
}

distros, err := parseDistroList(output)
if err != nil {
return unavailable, fmt.Errorf("无法解析 WSL 发行版列表: %w", err)
}
return Detection{Detected: true, Distros: distros}, nil
}
Loading