├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github └── workflows │ └── build-and-release.yaml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── LICENSE.md ├── Makefile ├── README.md ├── add-wslclocktask.ps1 ├── docs └── unblock.png ├── go.mod ├── go.sum ├── internal └── pkg │ ├── logging │ └── logging.go │ └── wsl │ ├── common.go │ ├── common_test.go │ ├── wsl_notwindows.go │ └── wsl_windows.go ├── main.go ├── remove-wslclocktask.ps1 └── scripts └── ci_release.sh /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM golang:1.14-stretch 7 | 8 | # Avoid warnings by switching to noninteractive 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # Configure apt, install packages and tools 12 | RUN apt-get update \ 13 | && apt-get -y install --no-install-recommends apt-utils dialog nano \ 14 | # 15 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 16 | && apt-get -y install git iproute2 procps lsb-release \ 17 | # Install Release Tools 18 | # 19 | # --> RPM used by goreleaser 20 | && apt install -y rpm \ 21 | # Clean up 22 | && apt-get autoremove -y \ 23 | && apt-get clean -y \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 27 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 28 | # will be updated to match your local UID/GID (when using the dockerFile property). 29 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 30 | ARG USERNAME=vscode 31 | ARG USER_UID=1000 32 | ARG USER_GID=$USER_UID 33 | 34 | # Set env for tracking that we're running in a devcontainer 35 | ENV DEVCONTAINER=true 36 | 37 | # Enable go modules 38 | ENV GO111MODULE=on 39 | 40 | # Install Go tools 41 | RUN \ 42 | # --> Delve for debugging 43 | go get github.com/go-delve/delve/cmd/dlv@v1.5.0 \ 44 | # --> Go language server 45 | && go get golang.org/x/tools/gopls@v0.2.1 \ 46 | # --> Go symbols and outline for go to symbol support and test support 47 | && go get github.com/acroca/go-symbols@v0.1.1 && go get github.com/ramya-rao-a/go-outline@7182a932836a71948db4a81991a494751eccfe77 \ 48 | # --> GolangCI-lint 49 | && curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sed 's/tar -/tar --no-same-owner -/g' | sh -s -- -b $(go env GOPATH)/bin \ 50 | # --> Go releaser 51 | && curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- "v0.132.1"\ 52 | # --> Install junit converter 53 | && go get github.com/jstemmer/go-junit-report@v0.9.1 \ 54 | && rm -rf /go/src/ && rm -rf /go/pkg 55 | 56 | # Switch back to dialog for any ad-hoc use of apt-get 57 | ENV DEBIAN_FRONTEND=dialog 58 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "wsl-clock", 5 | "dockerFile": "Dockerfile", 6 | "runArgs": [ 7 | // Uncomment the next line to use a non-root user. On Linux, this will prevent 8 | // new files getting created as root, but you may need to update the USER_UID 9 | // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000. 10 | // "-u", "vscode", 11 | "--cap-add=SYS_PTRACE", 12 | "--security-opt", 13 | "seccomp=unconfined", 14 | 15 | // Mount go mod cache 16 | "-v", "wsl-clock-gomodcache:/go/pkg", 17 | // Keep command history 18 | "-v", "wsl-clock-bashhistory:/root/commandhistory", 19 | // Mount docker socket for docker builds 20 | "-v", "/var/run/docker.sock:/var/run/docker.sock", 21 | // Use host network 22 | "--network=host" 23 | ], 24 | 25 | // Set *default* container specific settings.json values on container create. 26 | "settings": { 27 | "terminal.integrated.shell.linux": "/bin/bash", 28 | "go.gopath": "/go", 29 | "go.useLanguageServer": true, 30 | "[go]": { 31 | "editor.snippetSuggestions": "none", 32 | "editor.formatOnSave": true, 33 | "editor.codeActionsOnSave": { 34 | "source.organizeImports": true, 35 | } 36 | }, 37 | "gopls": { 38 | "usePlaceholders": true, // add parameter placeholders when completing a function 39 | // Experimental settings 40 | "completeUnimported": true, // autocomplete unimported packages 41 | "watchFileChanges": true, // watch file changes outside of the editor 42 | "deepCompletion": true, // enable deep completion 43 | }, 44 | "files.eol": "\n", // formatting only supports LF line endings 45 | }, 46 | 47 | // Add the IDs of extensions you want installed when the container is created. 48 | "extensions": [ 49 | "golang.go" 50 | ] 51 | 52 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 53 | // "forwardPorts": [], 54 | 55 | // Use 'postCreateCommand' to run commands after the container is created. 56 | // "postCreateCommand": "go version", 57 | 58 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 59 | // "remoteUser": "vscode" 60 | } 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf 4 | *.{ps1,[pP][sS][1]} text eol=crlf 5 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yaml: -------------------------------------------------------------------------------- 1 | name: build-and-release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - 'README.md' 8 | - 'docs/**' 9 | pull_request: 10 | branches: [ main ] 11 | paths-ignore: 12 | - 'README.md' 13 | - 'docs/**' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-18.04 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: CI Tooling 23 | run: sudo apt update && sudo apt install build-essential -y 24 | 25 | - name: Build devcontainer for tooling 26 | run: sudo -E make devcontainer 27 | 28 | - name: Run the release 29 | run: sudo -E make devcontainer-release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | BUILD_NUMBER: ${{ github.run_id }} 33 | IS_CI: 1 34 | IS_PR: ${{ github.head_ref }} 35 | BRANCH: ${{ github.ref }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /wsl-clock 2 | /wsl-clock.exe 3 | /dist -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | binary: wsl-clock 5 | goos: 6 | - windows 7 | goarch: 8 | - 386 9 | - amd64 10 | main: . 11 | ldflags: 12 | - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} -X "main.goversion={{.Env.GOVERSION}}" -H windowsgui 13 | 14 | archives: 15 | - id: zip 16 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 17 | format_overrides: 18 | - goos: windows 19 | format: zip 20 | files: 21 | - README.md 22 | - LICENSE.md 23 | - add-wslclocktask.ps1 24 | - remove-wslclocktask.ps1 25 | 26 | changelog: 27 | sort: asc 28 | filters: 29 | exclude: 30 | - '^docs:' 31 | - '^test:' 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | }, 16 | { 17 | "name": "Launch a test", 18 | "type": "go", 19 | "request": "launch", 20 | "mode": "auto", 21 | "program": "${file}", 22 | // "args": [ 23 | // "-test.v", 24 | // "-test.run", 25 | // "^${selectedText}$" 26 | // ], 27 | "env": { 28 | "GOOS": "windows", 29 | }, 30 | "buildFlags": "-v -tags=all", 31 | "showLog": true, 32 | "envFile": "${workspaceFolder}/.env" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Stuart Leeks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOOS=windows go build . 3 | 4 | lint: build 5 | GOOS=windows golangci-lint run 6 | 7 | devcontainer: 8 | docker build -f ./.devcontainer/Dockerfile ./.devcontainer -t wsl-clock 9 | 10 | devcontainer-release: 11 | ifdef DEVCONTAINER 12 | $(error This target can only be run outside of the devcontainer as it mounts files and this fails within a devcontainer. Don't worry all it needs is docker) 13 | endif 14 | @docker run -v ${PWD}:${PWD} \ 15 | -e BUILD_NUMBER="${BUILD_NUMBER}" \ 16 | -e IS_CI="${IS_CI}" \ 17 | -e IS_PR="${IS_PR}" \ 18 | -e BRANCH="${BRANCH}" \ 19 | -e GITHUB_TOKEN="${GITHUB_TOKEN}" \ 20 | --entrypoint /bin/bash \ 21 | --workdir "${PWD}" \ 22 | wsl-clock \ 23 | -c "${PWD}/scripts/ci_release.sh" 24 | 25 | 26 | test: 27 | GOOS=windows go test -v ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wsl-clock 2 | 3 | **IMPORTANT: This is not an official solution and whilst it worked on my machine this is an unsupported workaround :-)** 4 | 5 | There is an issue with WSL where the clock in WSL isn't updated when the host resumes from sleep/hibernate. E.g. [this issue](https://github.com/microsoft/WSL/issues/4245) 6 | 7 | This repo has a workaround that creates a scheduled task that is triggered by Windows Events for resuming from sleep/hibernation. When the scheduled task executes it resets the clock in the WSL VM. 8 | 9 | For the background to this repo, see [this blog post](https://stuartleeks.com/posts/fixing-clock-skew-with-wsl-2/). The implementation discussed in that blog post is based on PowerShell and that implementation is still available [here](https://github.com/stuartleeks/wsl-clock/tree/powershell). The implementation has now been replaced with a program written in Go, with the following advantages: 10 | 11 | * The application runs as a windowless app so there is no longer the flash of a powershell window when the task runs 12 | * The logic has been updated to test for a running WSL v2 distro before checking whether to reset the clock (rather than _any_ running distro). This removes the potential for spinning up a WSL 2 distro when there wasn't one running (i.e. when there was no need to reset the clock) 13 | * The logic now uses an existing running distro for executing the time checks and reset steps, rather than the default distro. This avoids situations where an extra distro may have been started, as well as avoiding issues when the default distro was configured as WSL v1 14 | 15 | ## Setup 16 | 17 | Download the ZIP file for a [prebuilt release](https://github.com/stuartleeks/wsl-clock/releases/latest) and unzip to a local folder. 18 | 19 | After unzipping, unblock each to the `.ps1` and `.exe` files by right-clicking, selecting 'Properties', ticking 'Unblock' and clicking 'OK': 20 | 21 | ![](docs/unblock.png) 22 | 23 | To set up the scheduled task, run `add-wslclocktask.ps1` in the content you just unzipped. This will set up a scheduled task triggered on Hibernation Resume events to run the `wsl-clock.exe` to check for clock drift on resuming from hibernation. 24 | 25 | ## Cleanup 26 | 27 | To remove the scheduled task, run `remove-wslclocktask.ps1` 28 | 29 | ## Logs/Troubleshooting 30 | 31 | The program invoked by the scheduled task logs output to `~/.wsl-clock.log` 32 | 33 | ## Building from source 34 | 35 | The simplest way to build from source is to use [Visual Studio Code](https://code.visualstudio.com) and open as a [devcontainer](https://code.visualstudio.com/docs/remote/containers). This will run the development environment with the required version of Go and allow you to run `make build` to build the binary. 36 | 37 | -------------------------------------------------------------------------------- /add-wslclocktask.ps1: -------------------------------------------------------------------------------- 1 | $taskCommand = $PSScriptRoot + "\wsl-clock.exe" 2 | 3 | schtasks /Create /TN wsl-clock /TR $taskCommand /SC ONEVENT /EC System /MO "*[System[Provider[@Name='Microsoft-Windows-Kernel-Power'] and (EventID=107 or EventID=507)]]" /F -------------------------------------------------------------------------------- /docs/unblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuartleeks/wsl-clock/54e62402d4db293cfa0de6828fe643dcc494fd0d/docs/unblock.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stuartleeks/wsl-clock 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /internal/pkg/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | ) 9 | 10 | var message string 11 | 12 | const maxLogFileSize int64 = 5 * 1024 * 1024 13 | 14 | func AddMessage(newMessage string, a ...interface{}) { 15 | if message != "" { 16 | message += "\n" 17 | } 18 | timestamp := time.Now().UTC().Format("2006-01-02 15:04:05 ") 19 | message += timestamp + fmt.Sprintf(newMessage, a...) 20 | } 21 | func WriteLog() { 22 | userProfile := os.Getenv("USERPROFILE") 23 | logPath := filepath.Join(userProfile, ".wsl-clock.log") 24 | backupLogPath := filepath.Join(userProfile, ".wsl-clock.old.log") 25 | 26 | handleLogFileRotation(logPath, backupLogPath) 27 | 28 | file, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) 29 | if err != nil { 30 | fmt.Printf("Error opening log file %q: %s", logPath, err) 31 | panic(err) 32 | } 33 | defer file.Close() 34 | 35 | _, err = file.WriteString(message + "\n") 36 | if err != nil { 37 | fmt.Printf("Error writing to log file %q: %s", logPath, err) 38 | panic(err) 39 | } 40 | } 41 | 42 | func handleLogFileRotation(logPath string, backupLogPath string) { 43 | size, err := getFileSize(logPath) 44 | if err != nil { 45 | if err != nil { 46 | fmt.Printf("Error getting log file size %q: %s", logPath, err) 47 | panic(err) 48 | } 49 | } 50 | if size > maxLogFileSize { 51 | if _, err = os.Stat(backupLogPath); err != nil { 52 | if !os.IsNotExist(err) { 53 | fmt.Printf("Error checking backup log path %q: %s", backupLogPath, err) 54 | panic(err) 55 | } 56 | } else { 57 | os.Remove(backupLogPath) 58 | } 59 | if err = os.Rename(logPath, backupLogPath); err != nil { 60 | fmt.Printf("Error renaming log to backup %q: %s", backupLogPath, err) 61 | panic(err) 62 | } 63 | } 64 | } 65 | 66 | func getFileSize(path string) (int64, error) { 67 | info, err := os.Stat(path) 68 | if err != nil { 69 | if os.IsNotExist(err) { 70 | return 0, nil // treat not found as empty for this use-case 71 | } 72 | return 0, err 73 | } 74 | return info.Size(), nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/pkg/wsl/common.go: -------------------------------------------------------------------------------- 1 | package wsl 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | /* 10 | NAME STATE VERSION 11 | * Ubuntusl Running 2 12 | Ubuntu-18.04 Running 2 13 | Ubuntu-20.04 Running 2 14 | golang Stopped 2 15 | dotnet-test Stopped 2 16 | docker-desktop-data Running 2 17 | docker-desktop Running 2 18 | */ 19 | 20 | type WslDistro struct { 21 | IsDefault bool 22 | Name string 23 | State string 24 | Version string 25 | } 26 | 27 | var listHeaderRegex *regexp.Regexp = regexp.MustCompile("[A-Z]+") 28 | 29 | func parseDistroOutput(listOutput string) ([]WslDistro, error) { 30 | 31 | lines := strings.Split(listOutput, "\n") 32 | 33 | header := lines[0] 34 | lines = lines[1:] 35 | 36 | headerMatches := listHeaderRegex.FindAllIndex([]byte(header), -1) 37 | 38 | checkHeader := func(index int, expectedValue string) error { 39 | value := strings.TrimSpace(header[headerMatches[index][0]:headerMatches[index][1]]) 40 | if value != expectedValue { 41 | return fmt.Errorf("Expected header %d to be %q, got %q", index, expectedValue, value) 42 | } 43 | return nil 44 | } 45 | if len(headerMatches) != 3 { 46 | return []WslDistro{}, fmt.Errorf("Unexpected headers (expected 3, got %d): %q", len(headerMatches), header) 47 | } 48 | if err := checkHeader(0, "NAME"); err != nil { 49 | return []WslDistro{}, err 50 | } 51 | if err := checkHeader(1, "STATE"); err != nil { 52 | return []WslDistro{}, err 53 | } 54 | if err := checkHeader(2, "VERSION"); err != nil { 55 | return []WslDistro{}, err 56 | } 57 | 58 | getValue := func(line string, start int, end int) string { 59 | return strings.TrimSpace(line[start:end]) 60 | } 61 | distros := []WslDistro{} 62 | for _, line := range lines { 63 | if strings.TrimSpace(line) == "" { 64 | continue 65 | } 66 | distro := WslDistro{} 67 | if getValue(line, 0, headerMatches[0][0]) == "*" { 68 | distro.IsDefault = true 69 | } 70 | distro.Name = getValue(line, headerMatches[0][0], headerMatches[1][0]) 71 | distro.State = getValue(line, headerMatches[1][0], headerMatches[2][0]) 72 | distro.Version = getValue(line, headerMatches[2][0], len(line)) 73 | distros = append(distros, distro) 74 | } 75 | 76 | return distros, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/pkg/wsl/common_test.go: -------------------------------------------------------------------------------- 1 | package wsl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWslParsing(t *testing.T) { 10 | input := ` NAME STATE VERSION 11 | * Ubuntusl Running 2 12 | Ubuntu-18.04 Running 1 13 | Ubuntu-20.04 Stopped 2 14 | ` 15 | 16 | distros, err := parseDistroOutput(input) 17 | assert.NoError(t, err) 18 | 19 | assert.Equal(t, 3, len(distros)) 20 | 21 | if len(distros) >= 1 { 22 | distro := distros[0] 23 | assert.Equal(t, true, distro.IsDefault) 24 | assert.Equal(t, "Ubuntusl", distro.Name) 25 | assert.Equal(t, "Running", distro.State) 26 | assert.Equal(t, "2", distro.Version) 27 | } 28 | 29 | if len(distros) >= 2 { 30 | distro := distros[1] 31 | assert.Equal(t, false, distro.IsDefault) 32 | assert.Equal(t, "Ubuntu-18.04", distro.Name) 33 | assert.Equal(t, "Running", distro.State) 34 | assert.Equal(t, "1", distro.Version) 35 | } 36 | 37 | if len(distros) >= 3 { 38 | distro := distros[2] 39 | assert.Equal(t, false, distro.IsDefault) 40 | assert.Equal(t, "Ubuntu-20.04", distro.Name) 41 | assert.Equal(t, "Stopped", distro.State) 42 | assert.Equal(t, "2", distro.Version) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /internal/pkg/wsl/wsl_notwindows.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package wsl 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func GetRunningV2Distros() ([]string, error) { 11 | return []string{}, fmt.Errorf("Not implemented") 12 | } 13 | func GetWslTime(distroName string) (time.Time, error) { 14 | return time.Date(0, 0, 0, 0, 0, 0, 0, nil), fmt.Errorf("Not implemented") 15 | } 16 | func ResetWslClock(distroName string) error { 17 | return fmt.Errorf("Not implemented") 18 | } 19 | -------------------------------------------------------------------------------- /internal/pkg/wsl/wsl_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package wsl 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "os/exec" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | func GetRunningV2Distros() ([]string, error) { 15 | // TODO - consider changing this to us verbose listing and test for any running v2 instances 16 | // and then use that to determine an instance to run the remaining WSL commands in 17 | output, err := execCmd("wsl.exe", "--list", "--verbose") 18 | if err != nil { 19 | return []string{}, err 20 | } 21 | 22 | distros, err := parseDistroOutput(string(output)) 23 | if err != nil { 24 | return []string{}, err 25 | } 26 | 27 | v2Distros := []string{} 28 | for _, distro := range distros { 29 | if distro.Version == "2" && distro.State == "Running" { 30 | v2Distros = append(v2Distros, distro.Name) 31 | } 32 | } 33 | return v2Distros, nil 34 | } 35 | func GetWslTime(distroName string) (time.Time, error) { 36 | output, err := execCmdToLines("wsl.exe", "-d", distroName, "sh", "-c", "date -Iseconds") 37 | if err != nil { 38 | return time.Time{}, fmt.Errorf("Failed to call WSL to get current time: %s", err) 39 | } 40 | 41 | timeString := output[0] 42 | timeValue, err := time.Parse(time.RFC3339, timeString) 43 | if err != nil { 44 | return time.Time{}, fmt.Errorf("Failed to parse time %q: %s", timeString, err) 45 | } 46 | 47 | return timeValue, nil 48 | } 49 | func ResetWslClock(distroName string) error { 50 | _, err := execCmdToLines("wsl.exe", "-d", distroName, "-u", "root", "sh", "-c", "hwclock -s") 51 | if err != nil { 52 | return fmt.Errorf("Failed to call WSL to reset clock: %s", err) 53 | } 54 | return nil 55 | } 56 | 57 | func execCmd(program string, arg ...string) ([]byte, error) { 58 | cmd := exec.Command(program, arg...) 59 | 60 | const CREATE_NO_WINDOW = 0x08000000 61 | sysAttr := syscall.SysProcAttr{} 62 | sysAttr.CreationFlags = CREATE_NO_WINDOW 63 | sysAttr.HideWindow = true 64 | cmd.SysProcAttr = &sysAttr 65 | 66 | outputTemp, err := cmd.Output() 67 | if err != nil { 68 | return []byte{}, err 69 | } 70 | 71 | output := outputTemp 72 | if len(outputTemp) >= 2 && outputTemp[1] == 0 { 73 | output = make([]byte, len(outputTemp)/2) 74 | for i := 0; i < len(output); i++ { 75 | output[i] = outputTemp[2*i] 76 | } 77 | } 78 | 79 | return output, nil 80 | } 81 | 82 | func execCmdToLines(program string, arg ...string) ([]string, error) { 83 | 84 | output, err := execCmd(program, arg...) 85 | if err != nil { 86 | return []string{}, err 87 | } 88 | 89 | reader := bytes.NewReader(output) 90 | scanner := bufio.NewScanner(reader) 91 | if scanner == nil { 92 | return []string{}, fmt.Errorf("Failed to parse stdout") 93 | } 94 | lines := []string{} 95 | for scanner.Scan() { 96 | lines = append(lines, scanner.Text()) 97 | } 98 | if err := scanner.Err(); err != nil { 99 | return []string{}, fmt.Errorf("Error reading stdout: %s", err) 100 | } 101 | 102 | return lines, nil 103 | } 104 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "time" 8 | 9 | "github.com/stuartleeks/wsl-clock/internal/pkg/logging" 10 | "github.com/stuartleeks/wsl-clock/internal/pkg/wsl" 11 | ) 12 | 13 | // TODO 14 | // - allow clock tolerance to be specified as an arg 15 | // - allow max log size to be specified as an arg 16 | 17 | // Overridden via ldflags 18 | var ( 19 | version = "99.0.1-devbuild" 20 | commit = "unknown" 21 | date = "unknown" 22 | goversion = "unknown" 23 | ) 24 | 25 | const toleratedTimeDiffInSeconds float64 = 30 // allow for time executing the commands between comparisons 26 | 27 | func main() { 28 | err := execute() 29 | if err != nil { 30 | logging.AddMessage(err.Error()) 31 | } 32 | logging.WriteLog() 33 | 34 | if err != nil { 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | func execute() error { 40 | logging.AddMessage("********************************") 41 | logging.AddMessage("*** wsl-clock starting...") 42 | logging.AddMessage("*** Version : %s", version) 43 | logging.AddMessage("*** Commit : %s", commit) 44 | logging.AddMessage("*** Date : %s", date) 45 | logging.AddMessage("*** Go version: %s", goversion) 46 | 47 | runningDistros, err := wsl.GetRunningV2Distros() 48 | if err != nil { 49 | return fmt.Errorf("Failed to get running distros: %s", err) 50 | } 51 | if len(runningDistros) == 0 { 52 | logging.AddMessage("No running distros - quitting") 53 | return nil 54 | } 55 | distroName := runningDistros[0] 56 | logging.AddMessage("Running commands in distro %q", distroName) 57 | 58 | originalTime, err := wsl.GetWslTime(distroName) 59 | if err != nil { 60 | return fmt.Errorf("Failed to get original time: %s", err) 61 | } 62 | 63 | currentTime := time.Now() 64 | diff := currentTime.Sub(originalTime) 65 | absDiffSeconds := math.Abs(diff.Seconds()) 66 | 67 | if absDiffSeconds < toleratedTimeDiffInSeconds { 68 | logging.AddMessage("Time diff (%0.fs) within tolerance (%0.fs) - quitting", absDiffSeconds, toleratedTimeDiffInSeconds) 69 | return nil 70 | } 71 | 72 | err = wsl.ResetWslClock(distroName) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | newTime, err := wsl.GetWslTime(distroName) 78 | if err != nil { 79 | return fmt.Errorf("Failed to get new time: %s", err) 80 | } 81 | 82 | logging.AddMessage("Time correction (seconds): %.0f", newTime.Sub(originalTime).Seconds()) 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /remove-wslclocktask.ps1: -------------------------------------------------------------------------------- 1 | schtasks.exe /Delete /TN wsl-clock /F -------------------------------------------------------------------------------- /scripts/ci_release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "ci_release - starting..." 4 | 5 | # Fail if build number not set 6 | if [ -z "$BUILD_NUMBER" ]; then 7 | echo "Env var 'BUILD_NUMBER' must be set for this script to work correctly" 8 | exit 1 9 | fi 10 | 11 | # If running inside CI login to docker 12 | if [ -z ${IS_CI} ]; then 13 | echo "Not running in CI, skipping CI setup" 14 | else 15 | if [ -z $IS_PR ] && [[ $BRANCH == "refs/heads/main" ]]; then 16 | echo "On main setting PUBLISH=true" 17 | export PUBLISH=true 18 | else 19 | echo "Skipping publish as is from PR: $PR_NUMBER or not 'refs/heads/main' BRANCH: $BRANCH" 20 | fi 21 | fi 22 | 23 | # Set version for release (picked up later by goreleaser) 24 | git tag -f v0.1.$BUILD_NUMBER 25 | 26 | export GOVERSION=$(go version) 27 | 28 | make lint 29 | 30 | if [ -z ${PUBLISH} ]; then 31 | echo "Running with --skip-publish as PUBLISH not set" 32 | goreleaser --skip-publish --rm-dist 33 | else 34 | echo "Publishing release" 35 | goreleaser 36 | fi --------------------------------------------------------------------------------