cmd/hakurei/print: use new store interface
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Test / Create distribution (push) Successful in 35s
				
			
		
			
				
	
				Test / Sandbox (push) Successful in 2m15s
				
			
		
			
				
	
				Test / Hakurei (push) Successful in 3m11s
				
			
		
			
				
	
				Test / Hpkg (push) Successful in 4m2s
				
			
		
			
				
	
				Test / Sandbox (race detector) (push) Successful in 4m11s
				
			
		
			
				
	
				Test / Hakurei (race detector) (push) Successful in 5m3s
				
			
		
			
				
	
				Test / Flake checks (push) Successful in 1m40s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 35s
				
			Test / Sandbox (push) Successful in 2m15s
				
			Test / Hakurei (push) Successful in 3m11s
				
			Test / Hpkg (push) Successful in 4m2s
				
			Test / Sandbox (race detector) (push) Successful in 4m11s
				
			Test / Hakurei (race detector) (push) Successful in 5m3s
				
			Test / Flake checks (push) Successful in 1m40s
				
			This removes the final uses of the compat interfaces. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
		
							parent
							
								
									0edcb7c1d3
								
							
						
					
					
						commit
						1c168babf2
					
				@ -20,7 +20,6 @@ import (
 | 
			
		||||
	"hakurei.app/internal"
 | 
			
		||||
	"hakurei.app/internal/env"
 | 
			
		||||
	"hakurei.app/internal/outcome"
 | 
			
		||||
	"hakurei.app/internal/store"
 | 
			
		||||
	"hakurei.app/message"
 | 
			
		||||
	"hakurei.app/system/dbus"
 | 
			
		||||
)
 | 
			
		||||
@ -340,7 +339,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
 | 
			
		||||
		c.NewCommand("ps", "List active instances", func(args []string) error {
 | 
			
		||||
			var sc hst.Paths
 | 
			
		||||
			env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
 | 
			
		||||
			printPs(os.Stdout, time.Now().UTC(), store.NewMulti(msg, sc.SharePath), flagShort, flagJSON)
 | 
			
		||||
			printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
 | 
			
		||||
			return errSuccess
 | 
			
		||||
		}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"maps"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@ -170,52 +169,52 @@ func printShowInstance(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// printPs writes a representation of active instances to output.
 | 
			
		||||
func printPs(output io.Writer, now time.Time, s store.Compat, short, flagJSON bool) {
 | 
			
		||||
	var entries map[hst.ID]*hst.State
 | 
			
		||||
	if e, err := store.Join(s); err != nil {
 | 
			
		||||
		log.Fatalf("cannot join store: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		entries = e
 | 
			
		||||
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
 | 
			
		||||
	f := func(a func(eh *store.EntryHandle)) {
 | 
			
		||||
		entries, copyError := s.All()
 | 
			
		||||
		for eh := range entries {
 | 
			
		||||
			a(eh)
 | 
			
		||||
		}
 | 
			
		||||
		if err := copyError(); err != nil {
 | 
			
		||||
			msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !short && flagJSON {
 | 
			
		||||
		es := slices.Collect(maps.Values(entries))
 | 
			
		||||
		slices.SortFunc(es, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
 | 
			
		||||
		encodeJSON(log.Fatal, output, short, es)
 | 
			
		||||
	if short { // short output requires identifier only
 | 
			
		||||
		var identifiers []*hst.ID
 | 
			
		||||
		f(func(eh *store.EntryHandle) {
 | 
			
		||||
			if _, err := eh.Load(nil); err != nil { // passes through decode error
 | 
			
		||||
				msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			identifiers = append(identifiers, &eh.ID)
 | 
			
		||||
		})
 | 
			
		||||
		slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
 | 
			
		||||
 | 
			
		||||
		if flagJSON {
 | 
			
		||||
			encodeJSON(log.Fatal, output, short, identifiers)
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, id := range identifiers {
 | 
			
		||||
				mustPrintln(output, shortIdentifier(id))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// sort state entries by id string to ensure consistency between runs
 | 
			
		||||
	exp := make([]*expandedStateEntry, 0, len(entries))
 | 
			
		||||
	for id, instance := range entries {
 | 
			
		||||
		// gracefully skip nil states
 | 
			
		||||
		if instance == nil {
 | 
			
		||||
			log.Printf("got invalid state entry %s", id.String())
 | 
			
		||||
			continue
 | 
			
		||||
	// long output requires full instance state
 | 
			
		||||
	var instances []*hst.State
 | 
			
		||||
	f(func(eh *store.EntryHandle) {
 | 
			
		||||
		var state hst.State
 | 
			
		||||
		if _, err := eh.Load(&state); err != nil { // passes through decode error
 | 
			
		||||
			msg.GetLogger().Println(getMessage("cannot load state entry:", err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		instances = append(instances, &state)
 | 
			
		||||
	})
 | 
			
		||||
	slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
 | 
			
		||||
 | 
			
		||||
		// gracefully skip inconsistent states
 | 
			
		||||
		if id != instance.ID {
 | 
			
		||||
			log.Printf("possible store corruption: entry %s has id %s",
 | 
			
		||||
				id.String(), instance.ID.String())
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
 | 
			
		||||
	}
 | 
			
		||||
	slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
 | 
			
		||||
 | 
			
		||||
	if short {
 | 
			
		||||
		if flagJSON {
 | 
			
		||||
			v := make([]string, len(exp))
 | 
			
		||||
			for i, e := range exp {
 | 
			
		||||
				v[i] = e.s
 | 
			
		||||
			}
 | 
			
		||||
			encodeJSON(log.Fatal, output, short, v)
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, e := range exp {
 | 
			
		||||
				mustPrintln(output, shortIdentifierString(e.s))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	if flagJSON {
 | 
			
		||||
		encodeJSON(log.Fatal, output, short, instances)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -223,33 +222,21 @@ func printPs(output io.Writer, now time.Time, s store.Compat, short, flagJSON bo
 | 
			
		||||
	defer t.MustFlush()
 | 
			
		||||
 | 
			
		||||
	t.Println("\tInstance\tPID\tApplication\tUptime")
 | 
			
		||||
	for _, e := range exp {
 | 
			
		||||
		if len(e.s) != 1<<5 {
 | 
			
		||||
			// unreachable
 | 
			
		||||
			log.Printf("possible store corruption: invalid instance string %s", e.s)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	for _, instance := range instances {
 | 
			
		||||
		as := "(No configuration information)"
 | 
			
		||||
		if e.Config != nil {
 | 
			
		||||
			as = strconv.Itoa(e.Config.Identity)
 | 
			
		||||
			id := e.Config.ID
 | 
			
		||||
		if instance.Config != nil {
 | 
			
		||||
			as = strconv.Itoa(instance.Config.Identity)
 | 
			
		||||
			id := instance.Config.ID
 | 
			
		||||
			if id == "" {
 | 
			
		||||
				id = "app.hakurei." + shortIdentifierString(e.s)
 | 
			
		||||
				id = "app.hakurei." + shortIdentifier(&instance.ID)
 | 
			
		||||
			}
 | 
			
		||||
			as += " (" + id + ")"
 | 
			
		||||
		}
 | 
			
		||||
		t.Printf("\t%s\t%d\t%s\t%s\n",
 | 
			
		||||
			shortIdentifierString(e.s), e.PID, as, now.Sub(e.Time).Round(time.Second).String())
 | 
			
		||||
			shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// expandedStateEntry stores [hst.State] alongside a string representation of its [hst.ID].
 | 
			
		||||
type expandedStateEntry struct {
 | 
			
		||||
	s string
 | 
			
		||||
	*hst.State
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newPrinter returns a configured, wrapped [tabwriter.Writer].
 | 
			
		||||
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"log"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
@ -9,6 +10,7 @@ import (
 | 
			
		||||
	"hakurei.app/container/check"
 | 
			
		||||
	"hakurei.app/hst"
 | 
			
		||||
	"hakurei.app/internal/store"
 | 
			
		||||
	"hakurei.app/message"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@ -18,13 +20,30 @@ var (
 | 
			
		||||
		0x4c, 0xf0, 0x73, 0xbd,
 | 
			
		||||
		0xb4, 0x6e, 0xb5, 0xc1,
 | 
			
		||||
	}
 | 
			
		||||
	testState = &hst.State{
 | 
			
		||||
	testState = hst.State{
 | 
			
		||||
		ID:      testID,
 | 
			
		||||
		PID:     0xcafebabe,
 | 
			
		||||
		ShimPID: 0xdeadbeef,
 | 
			
		||||
		Config:  hst.Template(),
 | 
			
		||||
		Time:    testAppTime,
 | 
			
		||||
	}
 | 
			
		||||
	testStateSmall = hst.State{
 | 
			
		||||
		ID:      (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))),
 | 
			
		||||
		PID:     0xbeef,
 | 
			
		||||
		ShimPID: 0xcafe,
 | 
			
		||||
		Config: &hst.Config{
 | 
			
		||||
			Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
 | 
			
		||||
			Identity:    1,
 | 
			
		||||
			Container: &hst.ContainerConfig{
 | 
			
		||||
				Shell: check.MustAbs("/bin/sh"),
 | 
			
		||||
				Home:  check.MustAbs("/data/data/uk.gensokyo.cat"),
 | 
			
		||||
				Path:  check.MustAbs("/usr/bin/cat"),
 | 
			
		||||
				Args:  []string{"cat"},
 | 
			
		||||
				Flags: hst.FUserns,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Time: time.Unix(0, 0xdeadbeef).UTC(),
 | 
			
		||||
	}
 | 
			
		||||
	testTime    = time.Unix(3752, 1).UTC()
 | 
			
		||||
	testAppTime = time.Unix(0, 9).UTC()
 | 
			
		||||
)
 | 
			
		||||
@ -133,7 +152,7 @@ Session bus
 | 
			
		||||
 | 
			
		||||
`, false},
 | 
			
		||||
 | 
			
		||||
		{"instance", testState, hst.Template(), false, false, `State
 | 
			
		||||
		{"instance", &testState, hst.Template(), false, false, `State
 | 
			
		||||
 Instance:    8e2c76b066dabe574cf073bdb46eb5c1 (3405691582 -> 3735928559)
 | 
			
		||||
 Uptime:      1h2m32s
 | 
			
		||||
 | 
			
		||||
@ -173,7 +192,7 @@ System bus
 | 
			
		||||
 Talk:      ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
 | 
			
		||||
 | 
			
		||||
`, true},
 | 
			
		||||
		{"instance pd", testState, new(hst.Config), false, false, `Error: configuration missing container state!
 | 
			
		||||
		{"instance pd", &testState, new(hst.Config), false, false, `Error: configuration missing container state!
 | 
			
		||||
 | 
			
		||||
State
 | 
			
		||||
 Instance:    8e2c76b066dabe574cf073bdb46eb5c1 (3405691582 -> 3735928559)
 | 
			
		||||
@ -187,7 +206,7 @@ App
 | 
			
		||||
 | 
			
		||||
		{"json nil", nil, nil, false, true, `null
 | 
			
		||||
`, true},
 | 
			
		||||
		{"json instance", testState, nil, false, true, `{
 | 
			
		||||
		{"json instance", &testState, nil, false, true, `{
 | 
			
		||||
  "instance": "8e2c76b066dabe574cf073bdb46eb5c1",
 | 
			
		||||
  "pid": 3405691582,
 | 
			
		||||
  "shim_pid": 3735928559,
 | 
			
		||||
@ -515,41 +534,27 @@ func TestPrintPs(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name        string
 | 
			
		||||
		entries     map[hst.ID]*hst.State
 | 
			
		||||
		data        []hst.State
 | 
			
		||||
		short, json bool
 | 
			
		||||
		want        string
 | 
			
		||||
		want, log   string
 | 
			
		||||
	}{
 | 
			
		||||
		{"no entries", make(map[hst.ID]*hst.State), false, false, "    Instance    PID    Application    Uptime\n"},
 | 
			
		||||
		{"no entries short", make(map[hst.ID]*hst.State), true, false, ""},
 | 
			
		||||
		{"nil instance", map[hst.ID]*hst.State{testID: nil}, false, false, "    Instance    PID    Application    Uptime\n"},
 | 
			
		||||
		{"state corruption", map[hst.ID]*hst.State{hst.ID{}: testState}, false, false, "    Instance    PID    Application    Uptime\n"},
 | 
			
		||||
		{"no entries", []hst.State{}, false, false, "    Instance    PID    Application    Uptime\n", ""},
 | 
			
		||||
		{"no entries short", []hst.State{}, true, false, "", ""},
 | 
			
		||||
 | 
			
		||||
		{"valid pd", map[hst.ID]*hst.State{testID: {ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, `    Instance    PID    Application                 Uptime
 | 
			
		||||
    4cf073bd    256    0 (app.hakurei.4cf073bd)    1h2m32s
 | 
			
		||||
`},
 | 
			
		||||
		{"invalid config", []hst.State{{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, "    Instance    PID    Application    Uptime\n", "check: configuration missing container state\n"},
 | 
			
		||||
 | 
			
		||||
		{"valid", map[hst.ID]*hst.State{testID: testState}, false, false, `    Instance    PID           Application                  Uptime
 | 
			
		||||
		{"valid", []hst.State{testStateSmall, testState}, false, false, `    Instance    PID           Application                  Uptime
 | 
			
		||||
    4cf073bd    3405691582    9 (org.chromium.Chromium)    1h2m32s
 | 
			
		||||
`},
 | 
			
		||||
		{"valid short", map[hst.ID]*hst.State{testID: testState}, true, false, "4cf073bd\n"},
 | 
			
		||||
		{"valid json", map[hst.ID]*hst.State{testID: testState, (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))): {
 | 
			
		||||
			ID:      (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))),
 | 
			
		||||
			PID:     0xbeef,
 | 
			
		||||
			ShimPID: 0xcafe,
 | 
			
		||||
			Config: &hst.Config{
 | 
			
		||||
				ID:          "uk.gensokyo.cat",
 | 
			
		||||
				Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
 | 
			
		||||
				Identity:    1,
 | 
			
		||||
				Container: &hst.ContainerConfig{
 | 
			
		||||
					Shell: check.MustAbs("/bin/sh"),
 | 
			
		||||
					Home:  check.MustAbs("/data/data/uk.gensokyo.cat"),
 | 
			
		||||
					Path:  check.MustAbs("/usr/bin/cat"),
 | 
			
		||||
					Args:  []string{"cat"},
 | 
			
		||||
					Flags: hst.FUserns,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Time: time.Unix(0, 0xdeadbeef).UTC(),
 | 
			
		||||
		}}, false, true, `[
 | 
			
		||||
    aaaaaaaa    48879         1 (app.hakurei.aaaaaaaa)     1h2m28s
 | 
			
		||||
`, ""},
 | 
			
		||||
		{"valid single", []hst.State{testState}, false, false, `    Instance    PID           Application                  Uptime
 | 
			
		||||
    4cf073bd    3405691582    9 (org.chromium.Chromium)    1h2m32s
 | 
			
		||||
`, ""},
 | 
			
		||||
 | 
			
		||||
		{"valid short", []hst.State{testStateSmall, testState}, true, false, "4cf073bd\naaaaaaaa\n", ""},
 | 
			
		||||
		{"valid short single", []hst.State{testState}, true, false, "4cf073bd\n", ""},
 | 
			
		||||
 | 
			
		||||
		{"valid json", []hst.State{testState, testStateSmall}, false, true, `[
 | 
			
		||||
  {
 | 
			
		||||
    "instance": "8e2c76b066dabe574cf073bdb46eb5c1",
 | 
			
		||||
    "pid": 3405691582,
 | 
			
		||||
@ -707,7 +712,7 @@ func TestPrintPs(t *testing.T) {
 | 
			
		||||
    "instance": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
 | 
			
		||||
    "pid": 48879,
 | 
			
		||||
    "shim_pid": 51966,
 | 
			
		||||
    "id": "uk.gensokyo.cat",
 | 
			
		||||
    "id": "",
 | 
			
		||||
    "enablements": {
 | 
			
		||||
      "wayland": true,
 | 
			
		||||
      "pulse": true
 | 
			
		||||
@ -729,30 +734,44 @@ func TestPrintPs(t *testing.T) {
 | 
			
		||||
    "time": "1970-01-01T00:00:03.735928559Z"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
`},
 | 
			
		||||
		{"valid short json", map[hst.ID]*hst.State{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"]
 | 
			
		||||
`},
 | 
			
		||||
`, ""},
 | 
			
		||||
		{"valid short json", []hst.State{testStateSmall, testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
 | 
			
		||||
`, ""},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		s := store.New(check.MustAbs(t.TempDir()).Append("store"))
 | 
			
		||||
		for i := range tc.data {
 | 
			
		||||
			if h, err := s.Handle(tc.data[i].Identity); err != nil {
 | 
			
		||||
				t.Fatalf("Handle: error = %v", err)
 | 
			
		||||
			} else {
 | 
			
		||||
				var unlock func()
 | 
			
		||||
				if unlock, err = h.Lock(); err != nil {
 | 
			
		||||
					t.Fatalf("Lock: error = %v", err)
 | 
			
		||||
				}
 | 
			
		||||
				_, err = h.Save(&tc.data[i])
 | 
			
		||||
				unlock()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("Save: error = %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// store must not be written to beyond this point
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			t.Parallel()
 | 
			
		||||
 | 
			
		||||
			output := new(strings.Builder)
 | 
			
		||||
			printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
 | 
			
		||||
			if got := output.String(); got != tc.want {
 | 
			
		||||
				t.Errorf("printPs: got\n%s\nwant\n%s",
 | 
			
		||||
					got, tc.want)
 | 
			
		||||
			var printBuf, logBuf bytes.Buffer
 | 
			
		||||
			msg := message.NewMsg(log.New(&logBuf, "check: ", 0))
 | 
			
		||||
			msg.SwapVerbose(true)
 | 
			
		||||
			printPs(msg, &printBuf, testTime, s, tc.short, tc.json)
 | 
			
		||||
			if got := printBuf.String(); got != tc.want {
 | 
			
		||||
				t.Errorf("printPs:\n%s\nwant\n%s", got, tc.want)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if got := logBuf.String(); got != tc.log {
 | 
			
		||||
				t.Errorf("msg:\n%s\nwant\n%s", got, tc.log)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// stubStore implements [state.Store] and returns test samples via [state.Joiner].
 | 
			
		||||
type stubStore map[hst.ID]*hst.State
 | 
			
		||||
 | 
			
		||||
func (s stubStore) Join() (map[hst.ID]*hst.State, error)       { return s, nil }
 | 
			
		||||
func (s stubStore) Do(int, func(c store.Cursor)) (bool, error) { panic("unreachable") }
 | 
			
		||||
func (s stubStore) List() ([]int, error)                       { panic("unreachable") }
 | 
			
		||||
func (s stubStore) Close() error                               { return nil }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user