// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"os"
	"runtime"
	"sync"
	"time"
)

var mainTID int

func init() {
	registerInit("LockOSThreadMain", func() {
		// init is guaranteed to run on the main thread.
		mainTID = gettid()
	})
	register("LockOSThreadMain", LockOSThreadMain)

	registerInit("LockOSThreadAlt", func() {
		// Lock the OS thread now so main runs on the main thread.
		runtime.LockOSThread()
	})
	register("LockOSThreadAlt", LockOSThreadAlt)

	registerInit("LockOSThreadAvoidsStatePropagation", func() {
		// Lock the OS thread now so main runs on the main thread.
		runtime.LockOSThread()
	})
	register("LockOSThreadAvoidsStatePropagation", LockOSThreadAvoidsStatePropagation)
	register("LockOSThreadTemplateThreadRace", LockOSThreadTemplateThreadRace)
}

func LockOSThreadMain() {
	// gettid only works on Linux, so on other platforms this just
	// checks that the runtime doesn't do anything terrible.

	// This requires GOMAXPROCS=1 from the beginning to reliably
	// start a goroutine on the main thread.
	if runtime.GOMAXPROCS(-1) != 1 {
		println("requires GOMAXPROCS=1")
		os.Exit(1)
	}

	ready := make(chan bool, 1)
	go func() {
		// Because GOMAXPROCS=1, this *should* be on the main
		// thread. Stay there.
		runtime.LockOSThread()
		if mainTID != 0 && gettid() != mainTID {
			println("failed to start goroutine on main thread")
			os.Exit(1)
		}
		// Exit with the thread locked, which should exit the
		// main thread.
		ready <- true
	}()
	<-ready
	time.Sleep(1 * time.Millisecond)
	// Check that this goroutine is still running on a different
	// thread.
	if mainTID != 0 && gettid() == mainTID {
		println("goroutine migrated to locked thread")
		os.Exit(1)
	}
	println("OK")
}

func LockOSThreadAlt() {
	// This is running locked to the main OS thread.

	var subTID int
	ready := make(chan bool, 1)
	go func() {
		// This goroutine must be running on a new thread.
		runtime.LockOSThread()
		subTID = gettid()
		ready <- true
		// Exit with the thread locked.
	}()
	<-ready
	runtime.UnlockOSThread()
	for i := 0; i < 100; i++ {
		time.Sleep(1 * time.Millisecond)
		// Check that this goroutine is running on a different thread.
		if subTID != 0 && gettid() == subTID {
			println("locked thread reused")
			os.Exit(1)
		}
		exists, supported := tidExists(subTID)
		if !supported || !exists {
			goto ok
		}
	}
	println("sub thread", subTID, "still running")
	return
ok:
	println("OK")
}

func LockOSThreadAvoidsStatePropagation() {
	// This test is similar to LockOSThreadAlt in that it will detect if a thread
	// which should have died is still running. However, rather than do this with
	// thread IDs, it does this by unsharing state on that thread. This way, it
	// also detects whether new threads were cloned from the dead thread, and not
	// from a clean thread. Cloning from a locked thread is undesirable since
	// cloned threads will inherit potentially unwanted OS state.
	//
	// unshareFs, getcwd, and chdir("/tmp") are only guaranteed to work on
	// Linux, so on other platforms this just checks that the runtime doesn't
	// do anything terrible.
	//
	// This is running locked to the main OS thread.

	// GOMAXPROCS=1 makes this fail much more reliably if a tainted thread is
	// cloned from.
	if runtime.GOMAXPROCS(-1) != 1 {
		println("requires GOMAXPROCS=1")
		os.Exit(1)
	}

	if err := chdir("/"); err != nil {
		println("failed to chdir:", err.Error())
		os.Exit(1)
	}
	// On systems other than Linux, cwd == "".
	cwd, err := getcwd()
	if err != nil {
		println("failed to get cwd:", err.Error())
		os.Exit(1)
	}
	if cwd != "" && cwd != "/" {
		println("unexpected cwd", cwd, " wanted /")
		os.Exit(1)
	}

	ready := make(chan bool, 1)
	go func() {
		// This goroutine must be running on a new thread.
		runtime.LockOSThread()

		// Unshare details about the FS, like the CWD, with
		// the rest of the process on this thread.
		// On systems other than Linux, this is a no-op.
		if err := unshareFs(); err != nil {
			if err == errNotPermitted {
				println("unshare not permitted")
				os.Exit(0)
			}
			println("failed to unshare fs:", err.Error())
			os.Exit(1)
		}
		// Chdir to somewhere else on this thread.
		// On systems other than Linux, this is a no-op.
		if err := chdir(os.TempDir()); err != nil {
			println("failed to chdir:", err.Error())
			os.Exit(1)
		}

		// The state on this thread is now considered "tainted", but it
		// should no longer be observable in any other context.

		ready <- true
		// Exit with the thread locked.
	}()
	<-ready

	// Spawn yet another goroutine and lock it. Since GOMAXPROCS=1, if
	// for some reason state from the (hopefully dead) locked thread above
	// propagated into a newly created thread (via clone), or that thread
	// is actually being re-used, then we should get scheduled on such a
	// thread with high likelihood.
	done := make(chan bool)
	go func() {
		runtime.LockOSThread()

		// Get the CWD and check if this is the same as the main thread's
		// CWD. Every thread should share the same CWD.
		// On systems other than Linux, wd == "".
		wd, err := getcwd()
		if err != nil {
			println("failed to get cwd:", err.Error())
			os.Exit(1)
		}
		if wd != cwd {
			println("bad state from old thread propagated after it should have died")
			os.Exit(1)
		}
		<-done

		runtime.UnlockOSThread()
	}()
	done <- true
	runtime.UnlockOSThread()
	println("OK")
}

func LockOSThreadTemplateThreadRace() {
	// This test attempts to reproduce the race described in
	// golang.org/issue/38931. To do so, we must have a stop-the-world
	// (achieved via ReadMemStats) racing with two LockOSThread calls.
	//
	// While this test attempts to line up the timing, it is only expected
	// to fail (and thus hang) around 2% of the time if the race is
	// present.

	// Ensure enough Ps to actually run everything in parallel. Though on
	// <4 core machines, we are still at the whim of the kernel scheduler.
	runtime.GOMAXPROCS(4)

	go func() {
		// Stop the world; race with LockOSThread below.
		var m runtime.MemStats
		for {
			runtime.ReadMemStats(&m)
		}
	}()

	// Try to synchronize both LockOSThreads.
	start := time.Now().Add(10 * time.Millisecond)

	var wg sync.WaitGroup
	wg.Add(2)

	for i := 0; i < 2; i++ {
		go func() {
			for time.Now().Before(start) {
			}

			// Add work to the local runq to trigger early startm
			// in handoffp.
			go func() {}()

			runtime.LockOSThread()
			runtime.Gosched() // add a preemption point.
			wg.Done()
		}()
	}

	wg.Wait()
	// If both LockOSThreads completed then we did not hit the race.
	println("OK")
}
