Windows 10 시간 동기화 실패 문제…(feat Golang)

 

요즘에 잠자기 전에 하는 일이 하나 있는데 바로 온라인 폐지 줍는일임.
보통 12시에 리셋 되고 경쟁이 치열하기 때문에 땡하면 클릭하는게 하루 마무리 일과임.
그래서 놋북 시계를 보고 12시 땡하면 클릭했는데 안되서 핸폰 시계를 보니 아직 11시 59분인걸 보고 시간이 틀어졌구나 생각하면서 윈도우 시간 동기화를 클릭함.

위 스샷 처럼 한달 넘게 시간 동기화가 안 이루어지면서 약 20초 정도 빨라졌던거였음.
근데 솔직히 뭐가 문제인지 모르겠는데 저 동기화 실패 문제를 해결 못함.
레지스트리 수정, 윈도우 서비스 재 시작, 공유기 없이 인터넷 직결 기타 등등 인터넷 검색해서 나온거 대부분 해봤는데 안됨.

다 해봐도 안되면 네트워크 문제일 가능성이 높다고 하는 글을 보긴 했으나 우분투나 공유기 자체 시간 동기화에선 잘 된다.

한두시간 삽질하다가 걍 포기하고 그냥 동기화 해주는 앱 하나 만들기로 했음.
사실 KRISS한국표준과학연구원에서 제공되는 UTCk3 라는 앱이 하나 있어서 받긴 했는데…

계속 동기에 실패했다는 문구만 떠서 이것도 안되네 라는 생각에 앱 만들기 시작한건데 사실 이앱이 동기에 실패하는 이유는 관리자 권한으로 실행하지 않아서 였음 -_-; (그리고 이 사실은 앱 거의 다 만들어 가는 시점에 깨달음)

아무튼 golang으로 CUI 프로그램 하나 만들었으니 여기 올려봄.

func amAdmin() bool {
    _, err := os.Open("\\\\.\\PHYSICALDRIVE0")
    if err != nil {
        //관리자 권한이 없음
        return false
    }
    //관리자 권한이 있음.
    return true
}

일단은 관리자 권한이 있는지 없는지 확인 하는 코드인데 약간(?) 문제가 있긴 있음.

func runMeElevated() {
    verb := "runas"
    exe, _ := os.Executable()
    cwd, _ := os.Getwd()
    args := strings.Join(os.Args[1:], " ")

    verbPtr, _ := syscall.UTF16PtrFromString(verb)
    exePtr, _ := syscall.UTF16PtrFromString(exe)
    cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
    argPtr, _ := syscall.UTF16PtrFromString(args)

    var showCmd int32 = 0 //SW_HIDE

    err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
    if err != nil {
        panic(err)
    }
}

이건 관리자로 승급하는 코드인데 이거 호출 이후에 위에 amAdmin코드를 호출해도 여전히 false가 나온다. amAdmin코드가 true가 나올려면 앱 실행시 마우스 우클릭후 관리자권한으로 실행을 해야지 되는데 내 추측으로는 runas 명령에도 많은 옵션이 있는데 그냥 간단하게 해서 최고(?)관리자급이 아니라서 저 물리 드라이브0을 오픈하는 거는 안되는거 같음.

runas 명령 옵션을 손봐야 하긴 하는데 뭐 어쨌던 저거만 적용해도 시스템 시간 변경은 잘 되서 일단은 놔둠.

var (
    Kernel32dll = windows.NewLazyDLL("Kernel32.dll")
    procSystemParamInfo = Kernel32dll.NewProc("SetLocalTime")
)

시스템 시간을 변경할려면 winapi를 써야 하는데 그 중 SetLocalTime 함수를 써야 함.
위와 같이 변수를 지정해주고.

type SYSTEMTIME struct {
    Year         uint16
    Month        uint16
    DayOfWeek    uint16
    Day          uint16
    Hour         uint16
    Minute       uint16
    Second       uint16
    Milliseconds uint16
}

SYSTEMTIME 구조체를 하나 선언해준다.

NTP 라이브러리는 “github.com/beevik/ntp” 를 사용함.

ntptime, err := ntp.Time("time.google.com")

ntptime을 구하고

st := SYSTEMTIME{}
st.Year = uint16(ntptime.Year())
st.Month = uint16(ntptime.Month())
st.Day = uint16(ntptime.Day())
st.Hour = uint16(ntptime.Hour())
st.Minute = uint16(ntptime.Minute())
st.Second = uint16(ntptime.Second())
st.Milliseconds = uint16(ntptime.Nanosecond() / 1000000)

procSystemParamInfo.Call(uintptr(unsafe.Pointer(&st)))

구조체에 채워준담에 호출을 하면 된다.

임의로 1분 가량 느리게 바꿔 준담에 실행하면 인터넷 시간과 동기화를 해준다.

다운로드 : TimeSync.zip

안에 timesync.json 파일이 있는데 거기에 NTP 주소를 추가해주면 순서대로 동기화 시도한다.

package main

import (
    "encoding/json"
    "fmt"
    "time"
    "unsafe"

    "os"
    "strings"
    "syscall"

    "github.com/beevik/ntp"
    "golang.org/x/sys/windows"
)

//https://anubissec.github.io/How-To-Call-Windows-APIs-In-Golang/#
var (
    Kernel32dll = windows.NewLazyDLL("Kernel32.dll")
    //procSystemParamInfo = Kernel32dll.NewProc("SetSystemTime")
    procSystemParamInfo = Kernel32dll.NewProc("SetLocalTime")
)

type SYSTEMTIME struct {
    Year         uint16
    Month        uint16
    DayOfWeek    uint16
    Day          uint16
    Hour         uint16
    Minute       uint16
    Second       uint16
    Milliseconds uint16
}

//---------------------------------------------------------------------------------------------
//관리자 권한 체크
func amAdmin() bool {
    _, err := os.Open("\\\\.\\PHYSICALDRIVE0")
    if err != nil {
        //관리자 권한이 없음
        return false
    }
    //관리자 권한이 있음.
    return true
}

//---------------------------------------------------------------------------------------------
func runMeElevated() {
    verb := "runas"
    exe, _ := os.Executable()
    cwd, _ := os.Getwd()
    args := strings.Join(os.Args[1:], " ")

    verbPtr, _ := syscall.UTF16PtrFromString(verb)
    exePtr, _ := syscall.UTF16PtrFromString(exe)
    cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
    argPtr, _ := syscall.UTF16PtrFromString(args)

    var showCmd int32 = 0 //SW_HIDE

    err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
    if err != nil {
        panic(err)
    }
}

//---------------------------------------------------------------------------------------------
func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

type Config struct {
    //DB관련 설정값
    NTP_URL_LIST string `json:"ntp_url_list"`
}

//---------------------------------------------------------------------------------------------
// LoadConfig 는 설정파일을 읽어들인다.
func LoadConfig(filePath string, result any) bool {

    bytes, err := os.ReadFile(filePath)
    if err != nil {
        fmt.Println(err)

        fo, err := os.Create(filePath)
        checkErr(err)
        defer fo.Close()

        jsonBytes, err := json.MarshalIndent(result, "", "	")
        checkErr(err)

        _, err = fo.Write(jsonBytes)
        checkErr(err)

        fmt.Println(filePath, "파일을 생성했습니다.")
        fmt.Println("json 파일을 열어서 각 항목을 수정하세요")
        return false
    }
    err = json.Unmarshal(bytes, &result)
    checkErr(err)

    return true
}

//---------------------------------------------------------------------------------------------
func main() {

    //설정파일 값 읽기
    cf := Config{}
    if !LoadConfig("./timesync.json", &cf) {
        //설정파일 읽기 실패시 종료.
        os.Exit(0)
    }

    //관리자 권한 요청하는곳
    if !amAdmin() {
        runMeElevated()
    }

    URLS := strings.Split(cf.NTP_URL_LIST, ",")

    for _, url := range URLS {
        url = strings.Replace(url, " ", "", -1)

        ntptime, err := ntp.Time(url)
        if err != nil {
            fmt.Println(err)
            continue
        }

        st := SYSTEMTIME{}
        st.Year = uint16(ntptime.Year())
        st.Month = uint16(ntptime.Month())
        st.Day = uint16(ntptime.Day())
        st.Hour = uint16(ntptime.Hour())
        st.Minute = uint16(ntptime.Minute())
        st.Second = uint16(ntptime.Second())
        st.Milliseconds = uint16(ntptime.Nanosecond() / 1000000)
        procSystemParamInfo.Call(uintptr(unsafe.Pointer(&st)))

        t := time.Now()
        t2 := t.Sub(ntptime)
        fmt.Printf("변경전 %s 서버와 로컬 시간 차이 : %s \n", url, t2)

        time.Sleep(time.Millisecond * 100)

        ntptime, err = ntp.Time(url)
        if err != nil {
            fmt.Println(err)
            continue
        }
        t = time.Now()
        t2 = t.Sub(ntptime)
        fmt.Printf("변경후 %s 서버와 로컬 시간 차이 : %s \n", url, t2)
        os.Exit(0)
    }

}

이건 전체 소스

참고 사이트
https://gist.github.com/jerblack/d0eb182cc5a1c1d92d92a4c4fcc416c6
https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime
https://anubissec.github.io/How-To-Call-Windows-APIs-In-Golang/#

댓글 달기

이메일 주소는 공개되지 않습니다.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.