mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-05 03:33:24 +08:00
91 lines
3.9 KiB
Go
91 lines
3.9 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
// Copyright 2023 Gravitational, Inc
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"syscall"
|
|
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
k32 = syscall.NewLazyDLL("kernel32.dll")
|
|
freeConsole = k32.NewProc("FreeConsole")
|
|
attachConsole = k32.NewProc("AttachConsole")
|
|
)
|
|
|
|
// onDaemonStop implements the "tsh daemon stop" command. It handles graceful shutdown of the daemon.
|
|
//
|
|
// teleterm.Serve listens to os.Interrupt and syscall.SIGTERM and stops the deamon when one of the
|
|
// signals is received. The Electron app starts the daemon and kills it with SIGTERM before the GUI
|
|
// app itself is closed.
|
|
//
|
|
// The problem is that Windows doesn't implement Unix signals. In Node.js, calling `subprocess.kill`
|
|
// on Windows is the equivalent of using SIGKILL to kill a process, giving it no chance to perform
|
|
// any kind of cleanup before exiting. [1]
|
|
//
|
|
// However, there is a way around this problem, as found on SO [2]. A process can attach itself to
|
|
// the console of another process. Then it can send a signal to the console which sends a signal to
|
|
// all processes attached to that console to shut down. In Go, that signal is represented as
|
|
// os.Interrupt. [3]
|
|
//
|
|
// Why do it in a separate tsh command instead of in the main JavaScript process which starts the
|
|
// tsh daemon? First, calling Windows APIs in Go is much easier. Second, sending CTRL_BREAK_EVENT
|
|
// sends os.Interrupt to _all_ processes attached to a particular console. We'd have to add special
|
|
// calls in Node.js to make the main process ignore that signal. Instead, we can just spawn a new
|
|
// process and not care about ignoring the signal.
|
|
//
|
|
// [1] https://nodejs.org/docs/latest-v16.x/api/child_process.html#subprocesskillsignal
|
|
// [2] https://stackoverflow.com/a/50020028/742872
|
|
// [3] https://pkg.go.dev/os/signal#hdr-Windows
|
|
//
|
|
// Related:
|
|
// - https://blog.codetitans.pl/post/sending-ctrl-c-signal-to-another-application-on-windows/
|
|
func onDaemonStop(cf *CLIConf) error {
|
|
// A process can be attached to at most one console, so we have to deattach the current process
|
|
// from its console first.
|
|
//
|
|
// returnValue is used for syscall.Proc.Call-specific error handling, see:
|
|
// https://github.com/golang/go/blob/go1.19.5/src/syscall/dll_windows.go#L175-L188
|
|
// https://learn.microsoft.com/en-us/windows/console/freeconsole
|
|
returnValue, _, err := freeConsole.Call()
|
|
if returnValue == 0 {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// https://learn.microsoft.com/en-us/windows/console/attachconsole
|
|
returnValue, _, err = attachConsole.Call(uintptr(cf.DaemonPid))
|
|
if returnValue == 0 {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// We have to use CTRL_BREAK_EVENT instead of CTRL_C_EVENT here. For some reason, CTRL_C_EVENT
|
|
// doesn't make the other process to receive os.Interrupt.
|
|
//
|
|
// It might be because CTRL_C_EVENT is received as a signal by default, but processes can use
|
|
// SetConsoleCtrlHandler to receive CTRL_C_EVENT as keyboard input. [1] We didn't investigate if
|
|
// this is what Electron does under the hood when using spawn, but rest assured we know that
|
|
// CTRL_C_EVENT works here - as the docs say, it's always sent as a signal.
|
|
//
|
|
// [1] https://learn.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals
|
|
// https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent
|
|
err = windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, 0)
|
|
return trace.Wrap(err)
|
|
}
|