Repository: pingcap/tiup

Issue:
`tiup status` kills `tiup playground`
## Bug Report

Please answer these questions before submitting your issue. Thanks!

1. What did you do?
<!--If possible, provide a recipe for reproducing the error.-->

In one terminal, run `tiup playground --mode tikv-slim --without-monitor` (minimal example, this also works with plain `tiup playground` and wait for the playground to start up. In another terminal, run `tiup status` . Then you'll see 

2. What did you expect to see?

A list of alive things.

3. What did you see instead?

```
tikv quit: exit status 1
pd quit: exit status 1
Error: exit status 1
```

4. What version of TiUP are you using (`tiup --version`)?
```
1.16.3 v1.16.2-nightly-47
Go Version: go1.24.1
Git Ref: master
GitHash: fab52bf51a19f5127bd49ffff3e7c82122aa047e
```

Full log of of `tiup playground`:

```
$ tiup playground --mode tikv-slim --without-monitor

Note: Version constraint  is resolved to v8.5.3. If you'd like to use other versions:

    Use exact version:      tiup playground v7.1.0
    Use version range:      tiup playground ^5
    Use nightly:            tiup playground nightly

Start pd instance: v8.5.3
Start tikv instance: v8.5.3
PD Endpoints:   127.0.0.1:2379
tikv quit: exit status 1
pd quit: exit status 1
Error: exit status 1
```

Discussion:
@ti-chi-bot: Welcome @bkane-msft! It looks like this is your first issue to pingcap/tiup 🎉@bkane-msft: This also happens with nightly:

```
$ tiup playground nightly --mode tikv-slim --without-monitor
The component `pd` version v9.0.0-beta.2.pre-nightly is not installed; downloading from repository.
download https://tiup-mirrors.pingcap.com/pd-v9.0.0-beta.2.pre-nightly-linux-amd64.tar.gz 60.57 MiB / 60.57 MiB 100.00% 25.10 MiB/s                                                                                  
Start pd instance: v9.0.0-beta.2.pre-nightly
The component `tikv` version v9.0.0-beta.2.pre-nightly is not installed; downloading from repository.
download https://tiup-mirrors.pingcap.com/tikv-v9.0.0-beta.2.pre-nightly-linux-amd64.tar.gz 298.69 MiB / 298.69 MiB 100.00% 37.02 MiB/s                                                                              
Start tikv instance: v9.0.0-beta.2.pre-nightly
PD Endpoints:   127.0.0.1:2379
tikv quit: exit status 1
pd quit: exit status 1
Error: exit status 1
```@dveeden: Note that `tiup playground display` works. And I can confirm that `tiup status` causes the playground to stop in a weird way.

Solution patch:
diff --git a/pkg/localdata/profile.go b/pkg/localdata/profile.go
index 917f9c59..1712536f 100644
--- a/pkg/localdata/profile.go
+++ b/pkg/localdata/profile.go
@@ -118,17 +118,19 @@ func (p *Profile) ComponentInstalledPath(component string, version utils.Version
 }
 
 func (p *Profile) saveTo(path string, data []byte, perm os.FileMode) error {
-	fullPath := filepath.Join(p.root, path)
+	if !filepath.IsAbs(path) {
+		path = filepath.Join(p.root, path)
+	}
 	// create sub directory if needed
-	if err := utils.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
+	if err := utils.MkdirAll(filepath.Dir(path), 0755); err != nil {
 		return errors.Trace(err)
 	}
-	return utils.WriteFile(fullPath, data, perm)
+	return utils.WriteFile(path, data, perm)
 }
 
 // WriteMetaFile writes process meta to instance/MetaFilename.
 func (p *Profile) WriteMetaFile(instance string, data *Process) error {
-	metaFile := filepath.Join(DataParentDir, instance, MetaFilename)
+	metaFile := filepath.Join(instance, MetaFilename)
 	jsonData, err := json.MarshalIndent(data, "", "  ")
 	if err != nil {
 		return errors.Trace(err)



Test patch:
diff --git a/pkg/localdata/profile_test.go b/pkg/localdata/profile_test.go
index a06d9c23..d76bb6ff 100644
--- a/pkg/localdata/profile_test.go
+++ b/pkg/localdata/profile_test.go
@@ -16,19 +16,16 @@ package localdata
 import (
 	"os"
 	"path"
+	"path/filepath"
 	"testing"
 
-	"github.com/google/uuid"
 	"github.com/pingcap/tiup/pkg/utils"
 	"github.com/stretchr/testify/require"
 )
 
 func TestResetMirror(t *testing.T) {
-	uuid := uuid.New().String()
-	root := path.Join("/tmp", uuid)
-	_ = os.Mkdir(root, 0o755)
-	_ = os.Mkdir(path.Join(root, "bin"), 0o755)
-	defer os.RemoveAll(root)
+	root := t.TempDir()
+	require.NoError(t, os.Mkdir(path.Join(root, "bin"), 0o755))
 
 	cfg, _ := InitConfig(root)
 	profile := NewProfile(root, cfg)
@@ -43,3 +40,50 @@ func TestResetMirror(t *testing.T) {
 	require.Error(t, profile.ResetMirror(root, ""))
 	require.NoError(t, profile.ResetMirror(root, path.Join(root, "mock-mirror", "root.json")))
 }
+
+func TestWriteMetaFile_abs(t *testing.T) {
+	tmpdir := t.TempDir()
+
+	instance := filepath.Join(tmpdir, "testinstance")
+	cfg, _ := InitConfig(tmpdir)
+	profile := NewProfile(tmpdir, cfg)
+
+	p := Process{}
+	err := profile.WriteMetaFile(instance, &p)
+	require.NoError(t, err)
+
+	_, err = os.Stat(instance)
+	require.NoError(t, err)
+}
+
+func TestWriteMetaFile_rel(t *testing.T) {
+	tmpdir := t.TempDir()
+
+	instance := "testinstance"
+	cfg, _ := InitConfig(tmpdir)
+	profile := NewProfile(tmpdir, cfg)
+
+	p := Process{}
+	err := profile.WriteMetaFile(instance, &p)
+	require.NoError(t, err)
+
+	_, err = os.Stat(profile.Path(instance))
+	require.NoError(t, err)
+}
+
+func TestWriteMetaFile_readback(t *testing.T) {
+	tmpdir := t.TempDir()
+
+	instance := filepath.Join(tmpdir, DataParentDir, "testinstance")
+	cfg, _ := InitConfig(tmpdir)
+	profile := NewProfile(tmpdir, cfg)
+
+	p := Process{}
+	err := profile.WriteMetaFile(instance, &p)
+	require.NoError(t, err)
+
+	p2, err := profile.ReadMetaFile(filepath.Base(instance))
+	require.NoError(t, err)
+	require.NotNil(t, p2)
+	require.Equal(t, p, *p2)
+}