Pass 9
This commit is contained in:
@@ -169,6 +169,8 @@ import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
|
||||
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
|
||||
import { createBareOsPathconfMethods } from './lib/bare-os-pathconf.js'
|
||||
import { createBareOsLogicalFdMethods } from './lib/bare-os-logical-fd.js'
|
||||
import { createBareOsLifecycleHookMethods } from './lib/bare-os-lifecycle-hooks.js'
|
||||
import { createBareOsProcessControlMethods } from './lib/bare-os-process-control.js'
|
||||
import { createBareOsQvacBridge } from './lib/bare-os-qvac-host.mjs'
|
||||
import { bareOsQvacEnsureModelsHdms } from './lib/bare-os-qvac-models-store.mjs'
|
||||
import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
|
||||
@@ -3525,62 +3527,11 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
pendingId: String(shellEnv.BARE_OS_SYSTEM_REVISION_PENDING || '').trim(),
|
||||
slot: String(shellEnv.BARE_OS_SYSTEM_SLOT || 'a').trim()
|
||||
}),
|
||||
/** @param {() => void | Promise<void>} fn */
|
||||
bareOsRegisterSuspendHook(fn) {
|
||||
if (typeof fn === 'function') bareOsSuspendHooks.push(fn)
|
||||
return () => {
|
||||
const i = bareOsSuspendHooks.indexOf(fn)
|
||||
if (i >= 0) bareOsSuspendHooks.splice(i, 1)
|
||||
}
|
||||
},
|
||||
/** @param {() => void | Promise<void>} fn */
|
||||
bareOsRegisterResumeHook(fn) {
|
||||
if (typeof fn === 'function') bareOsResumeHooks.push(fn)
|
||||
return () => {
|
||||
const i = bareOsResumeHooks.indexOf(fn)
|
||||
if (i >= 0) bareOsResumeHooks.splice(i, 1)
|
||||
}
|
||||
},
|
||||
async bareOsInvokeSuspendHooks() {
|
||||
for (const fn of [...bareOsSuspendHooks]) {
|
||||
try {
|
||||
await fn()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
},
|
||||
async bareOsInvokeResumeHooks() {
|
||||
for (const fn of [...bareOsResumeHooks]) {
|
||||
try {
|
||||
await fn()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
},
|
||||
bareOsRequestKernelReload() {
|
||||
const err = /** @type {Error & { code?: string }} */ (
|
||||
new Error('BARE_OS_KERNEL_RELOAD')
|
||||
)
|
||||
err.code = 'BARE_OS_KERNEL_RELOAD'
|
||||
throw err
|
||||
},
|
||||
bareOsRequestKernelProfileReload() {
|
||||
const on =
|
||||
shellEnv.BARE_OS_KERNEL_PROFILE_WARM === '1' ||
|
||||
shellEnv.BARE_OS_KERNEL_PROFILE_WARM === 'true'
|
||||
if (!on) {
|
||||
throw new Error(
|
||||
'bareOsRequestKernelProfileReload: enable BARE_OS_KERNEL_PROFILE_WARM=1'
|
||||
)
|
||||
}
|
||||
const err = /** @type {Error & { code?: string }} */ (
|
||||
new Error('BARE_OS_KERNEL_PROFILE_RELOAD')
|
||||
)
|
||||
err.code = 'BARE_OS_KERNEL_PROFILE_RELOAD'
|
||||
throw err
|
||||
},
|
||||
...createBareOsLifecycleHookMethods({
|
||||
suspendHooks: bareOsSuspendHooks,
|
||||
resumeHooks: bareOsResumeHooks,
|
||||
env: shellEnv
|
||||
}),
|
||||
/**
|
||||
* Run trusted JS from the system image (kernel extensions only).
|
||||
* @param {string} logicalPath
|
||||
@@ -4190,152 +4141,11 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
line.slice(0, 4000)
|
||||
)
|
||||
},
|
||||
/**
|
||||
* POSIX-like signal dispatch over synthetic process IDs.
|
||||
* Targets: numeric pid (**1–3** session stack, **4100+** shell jobs, **5100+** initd logical rows), **negative process group** (e.g. `-300` → all running shell jobs with `pgid` 300), or `kernel` / `booter` / `shell`.
|
||||
* Signals: HUP, INT, KILL, TERM, PIPE, CHLD, USR1, USR2; `PIPE`/`CHLD`/`USR*` update state only (no session exit).
|
||||
*/
|
||||
bareOsSendSignal(target, signal = 'TERM') {
|
||||
const sig = bareOsNormalizeSignalName(signal)
|
||||
if (!bareOsIsPosixSignalName(signal)) {
|
||||
throw new Error('bareOsSendSignal: unsupported signal')
|
||||
}
|
||||
const raw = String(target == null ? '' : target).trim()
|
||||
let pid = Number.parseInt(raw, 10)
|
||||
let pgidTarget = null
|
||||
if (Number.isFinite(pid) && pid < 0) {
|
||||
pgidTarget = -pid
|
||||
pid = NaN
|
||||
}
|
||||
if (!Number.isFinite(pid)) {
|
||||
if (pgidTarget == null) {
|
||||
if (raw === 'kernel') pid = 1
|
||||
else if (raw === 'booter') pid = 2
|
||||
else if (raw === 'shell') pid = 3
|
||||
}
|
||||
}
|
||||
if (pgidTarget != null) {
|
||||
const live = bareOsInteractiveCtxRef.ctx
|
||||
const list =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
const byPgid = list.filter(
|
||||
(j) =>
|
||||
j &&
|
||||
typeof j.pgid === 'number' &&
|
||||
Math.floor(j.pgid) === Math.floor(pgidTarget)
|
||||
)
|
||||
const active = byPgid.filter((j) => !j.done)
|
||||
if (sig === '0') {
|
||||
if (byPgid.length === 0) {
|
||||
throw new Error('bareOsSendSignal: unknown process group')
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
exists: true,
|
||||
jobCount: byPgid.length
|
||||
}
|
||||
}
|
||||
if (active.length === 0) {
|
||||
throw new Error('bareOsSendSignal: unknown process group')
|
||||
}
|
||||
if (
|
||||
this.bareOsLogicalSigaction &&
|
||||
this.bareOsLogicalSigaction[sig] === 'IGNORE'
|
||||
) {
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
const atMs = Date.now()
|
||||
/** @type {number[]} */
|
||||
const pids = []
|
||||
for (const j of active) {
|
||||
const jp = 4100 + j.id
|
||||
pids.push(jp)
|
||||
deliverBareOsVirtualSignalToPid(this, jp, sig)
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: true,
|
||||
atMs,
|
||||
pids
|
||||
}
|
||||
}
|
||||
const extendedSynthetic =
|
||||
Number.isFinite(pid) && pid >= 4100 && pid < 9000
|
||||
if (!Number.isFinite(pid) || pid < 1 || (pid > 3 && !extendedSynthetic)) {
|
||||
throw new Error('bareOsSendSignal: unknown process target')
|
||||
}
|
||||
if (sig === '0') {
|
||||
return { ok: true, pid, signal: sig, delivered: false, exists: true }
|
||||
}
|
||||
const r = deliverBareOsVirtualSignalToPid(this, pid, sig)
|
||||
if (r.ignored) {
|
||||
return {
|
||||
ok: true,
|
||||
pid,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: r.atMs
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pid,
|
||||
signal: sig,
|
||||
delivered: !!r.delivered,
|
||||
atMs: r.atMs
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Logical nice adjustment for synthetic PIDs (stored on **`process_table.json`** rows; host scheduler is not affected).
|
||||
* @param {number} pid
|
||||
* @param {number} [delta]
|
||||
*/
|
||||
bareOsRenice(pid, delta = 0) {
|
||||
const p = Number(pid)
|
||||
const d = Number(delta)
|
||||
if (!Number.isFinite(p) || p < 1) {
|
||||
throw new Error('bareOsRenice: invalid pid')
|
||||
}
|
||||
if (!Number.isFinite(d)) {
|
||||
throw new Error('bareOsRenice: invalid delta')
|
||||
}
|
||||
const clampNice = (n) =>
|
||||
Math.max(-20, Math.min(19, Math.trunc(Number.isFinite(n) ? n : 0)))
|
||||
const cur = clampNice(bareOsLogicalNiceByPid.get(p) ?? 0)
|
||||
const next = clampNice(cur + d)
|
||||
bareOsLogicalNiceByPid.set(p, next)
|
||||
return {
|
||||
ok: true,
|
||||
pid: p,
|
||||
delta: d,
|
||||
nice: next,
|
||||
previousNice: cur,
|
||||
atMs: Date.now()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Try to spawn a host child via **`bare-subprocess`** when the addon is available (Bare hosts).
|
||||
* @param {string[]} argv
|
||||
* @param {Record<string, unknown>} [opts]
|
||||
*/
|
||||
...createBareOsProcessControlMethods({
|
||||
deliverBareOsVirtualSignalToPid,
|
||||
getInteractiveCtx: () => bareOsInteractiveCtxRef.ctx,
|
||||
niceByPid: bareOsLogicalNiceByPid
|
||||
}),
|
||||
async bareOsTrySpawnHostSubprocess(argv, opts) {
|
||||
const av = Array.isArray(argv) ? argv.map((x) => String(x)) : []
|
||||
const incoming = opts && typeof opts === 'object' ? { ...opts } : {}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Suspend/resume hook registry and kernel reload request methods on `ctx`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* suspendHooks: Array<() => void | Promise<void>>,
|
||||
* resumeHooks: Array<() => void | Promise<void>>,
|
||||
* env: Record<string, string | undefined>
|
||||
* }} deps
|
||||
*/
|
||||
export function createBareOsLifecycleHookMethods(deps) {
|
||||
const { suspendHooks, resumeHooks, env } = deps
|
||||
|
||||
return {
|
||||
/** @param {() => void | Promise<void>} fn */
|
||||
bareOsRegisterSuspendHook(fn) {
|
||||
if (typeof fn === 'function') suspendHooks.push(fn)
|
||||
return () => {
|
||||
const i = suspendHooks.indexOf(fn)
|
||||
if (i >= 0) suspendHooks.splice(i, 1)
|
||||
}
|
||||
},
|
||||
/** @param {() => void | Promise<void>} fn */
|
||||
bareOsRegisterResumeHook(fn) {
|
||||
if (typeof fn === 'function') resumeHooks.push(fn)
|
||||
return () => {
|
||||
const i = resumeHooks.indexOf(fn)
|
||||
if (i >= 0) resumeHooks.splice(i, 1)
|
||||
}
|
||||
},
|
||||
async bareOsInvokeSuspendHooks() {
|
||||
for (const fn of [...suspendHooks]) {
|
||||
try {
|
||||
await fn()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
},
|
||||
async bareOsInvokeResumeHooks() {
|
||||
for (const fn of [...resumeHooks]) {
|
||||
try {
|
||||
await fn()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
},
|
||||
bareOsRequestKernelReload() {
|
||||
const err = /** @type {Error & { code?: string }} */ (
|
||||
new Error('BARE_OS_KERNEL_RELOAD')
|
||||
)
|
||||
err.code = 'BARE_OS_KERNEL_RELOAD'
|
||||
throw err
|
||||
},
|
||||
bareOsRequestKernelProfileReload() {
|
||||
const on =
|
||||
env.BARE_OS_KERNEL_PROFILE_WARM === '1' ||
|
||||
env.BARE_OS_KERNEL_PROFILE_WARM === 'true'
|
||||
if (!on) {
|
||||
throw new Error(
|
||||
'bareOsRequestKernelProfileReload: enable BARE_OS_KERNEL_PROFILE_WARM=1'
|
||||
)
|
||||
}
|
||||
const err = /** @type {Error & { code?: string }} */ (
|
||||
new Error('BARE_OS_KERNEL_PROFILE_RELOAD')
|
||||
)
|
||||
err.code = 'BARE_OS_KERNEL_PROFILE_RELOAD'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Synthetic process signal and nice methods on kernel `ctx`.
|
||||
*/
|
||||
import {
|
||||
bareOsIsPosixSignalName,
|
||||
bareOsNormalizeSignalName
|
||||
} from './bare-os-posix-signals.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* deliverBareOsVirtualSignalToPid: (ctx: unknown, pid: number, sig: string) => { ignored?: boolean, delivered?: boolean, atMs?: number },
|
||||
* getInteractiveCtx: () => Record<string, unknown> | null | undefined,
|
||||
* niceByPid: Map<number, number>
|
||||
* }} deps
|
||||
*/
|
||||
export function createBareOsProcessControlMethods(deps) {
|
||||
const { deliverBareOsVirtualSignalToPid, getInteractiveCtx, niceByPid } = deps
|
||||
|
||||
return {
|
||||
/**
|
||||
* POSIX-like signal dispatch over synthetic process IDs.
|
||||
* Targets: numeric pid (**1–3** session stack, **4100+** shell jobs, **5100+** initd logical rows), **negative process group** (e.g. `-300` → all running shell jobs with `pgid` 300), or `kernel` / `booter` / `shell`.
|
||||
* Signals: HUP, INT, KILL, TERM, PIPE, CHLD, USR1, USR2; `PIPE`/`CHLD`/`USR*` update state only (no session exit).
|
||||
*/
|
||||
bareOsSendSignal(target, signal = 'TERM') {
|
||||
const sig = bareOsNormalizeSignalName(signal)
|
||||
if (!bareOsIsPosixSignalName(signal)) {
|
||||
throw new Error('bareOsSendSignal: unsupported signal')
|
||||
}
|
||||
const raw = String(target == null ? '' : target).trim()
|
||||
let pid = Number.parseInt(raw, 10)
|
||||
let pgidTarget = null
|
||||
if (Number.isFinite(pid) && pid < 0) {
|
||||
pgidTarget = -pid
|
||||
pid = NaN
|
||||
}
|
||||
if (!Number.isFinite(pid)) {
|
||||
if (pgidTarget == null) {
|
||||
if (raw === 'kernel') pid = 1
|
||||
else if (raw === 'booter') pid = 2
|
||||
else if (raw === 'shell') pid = 3
|
||||
}
|
||||
}
|
||||
if (pgidTarget != null) {
|
||||
const live = getInteractiveCtx()
|
||||
const list =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
const byPgid = list.filter(
|
||||
(j) =>
|
||||
j &&
|
||||
typeof j.pgid === 'number' &&
|
||||
Math.floor(j.pgid) === Math.floor(pgidTarget)
|
||||
)
|
||||
const active = byPgid.filter((j) => !j.done)
|
||||
if (sig === '0') {
|
||||
if (byPgid.length === 0) {
|
||||
throw new Error('bareOsSendSignal: unknown process group')
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
exists: true,
|
||||
jobCount: byPgid.length
|
||||
}
|
||||
}
|
||||
if (active.length === 0) {
|
||||
throw new Error('bareOsSendSignal: unknown process group')
|
||||
}
|
||||
if (
|
||||
this.bareOsLogicalSigaction &&
|
||||
this.bareOsLogicalSigaction[sig] === 'IGNORE'
|
||||
) {
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
const atMs = Date.now()
|
||||
/** @type {number[]} */
|
||||
const pids = []
|
||||
for (const j of active) {
|
||||
const jp = 4100 + j.id
|
||||
pids.push(jp)
|
||||
deliverBareOsVirtualSignalToPid(this, jp, sig)
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pgid: pgidTarget,
|
||||
signal: sig,
|
||||
delivered: true,
|
||||
atMs,
|
||||
pids
|
||||
}
|
||||
}
|
||||
const extendedSynthetic =
|
||||
Number.isFinite(pid) && pid >= 4100 && pid < 9000
|
||||
if (!Number.isFinite(pid) || pid < 1 || (pid > 3 && !extendedSynthetic)) {
|
||||
throw new Error('bareOsSendSignal: unknown process target')
|
||||
}
|
||||
if (sig === '0') {
|
||||
return { ok: true, pid, signal: sig, delivered: false, exists: true }
|
||||
}
|
||||
const r = deliverBareOsVirtualSignalToPid(this, pid, sig)
|
||||
if (r.ignored) {
|
||||
return {
|
||||
ok: true,
|
||||
pid,
|
||||
signal: sig,
|
||||
delivered: false,
|
||||
ignored: true,
|
||||
atMs: r.atMs
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pid,
|
||||
signal: sig,
|
||||
delivered: !!r.delivered,
|
||||
atMs: r.atMs
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Logical nice adjustment for synthetic PIDs (stored on **`process_table.json`** rows; host scheduler is not affected).
|
||||
* @param {number} pid
|
||||
* @param {number} [delta]
|
||||
*/
|
||||
bareOsRenice(pid, delta = 0) {
|
||||
const p = Number(pid)
|
||||
const d = Number(delta)
|
||||
if (!Number.isFinite(p) || p < 1) {
|
||||
throw new Error('bareOsRenice: invalid pid')
|
||||
}
|
||||
if (!Number.isFinite(d)) {
|
||||
throw new Error('bareOsRenice: invalid delta')
|
||||
}
|
||||
const clampNice = (n) =>
|
||||
Math.max(-20, Math.min(19, Math.trunc(Number.isFinite(n) ? n : 0)))
|
||||
const cur = clampNice(niceByPid.get(p) ?? 0)
|
||||
const next = clampNice(cur + d)
|
||||
niceByPid.set(p, next)
|
||||
return {
|
||||
ok: true,
|
||||
pid: p,
|
||||
delta: d,
|
||||
nice: next,
|
||||
previousNice: cur,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+89
-1099
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,9 @@ import { createVfsRouteHelpers } from './lib/vfs-route.js'
|
||||
import { createBareOsPosixFdSimMethods } from './lib/bare-os-posix-fd-sim.js'
|
||||
import { createBareOsPathconfMethods } from './lib/bare-os-pathconf.js'
|
||||
import { createBareOsLogicalFdMethods } from './lib/bare-os-logical-fd.js'
|
||||
import { createVfsPseudoFileBytes } from './lib/vfs-pseudo-bytes.js'
|
||||
import { createBareOsLifecycleHookMethods } from './lib/bare-os-lifecycle-hooks.js'
|
||||
import { createBareOsProcessControlMethods } from './lib/bare-os-process-control.js'
|
||||
|
||||
test('posix env flags and caps', (t) => {
|
||||
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
|
||||
@@ -658,3 +661,62 @@ test('posix fd sim pipe write read', (t) => {
|
||||
const probe = ctx.bareOsPosixPollProbe({ fds: [{ fd: 1, events: 'w' }] })
|
||||
t.ok(probe.ready.some((x) => x.fd === 1 && x.revents === 'w'))
|
||||
})
|
||||
|
||||
test('pseudo file bytes version and shm', (t) => {
|
||||
const shm = new Map()
|
||||
shm.set('q', new Uint8Array([9]))
|
||||
const fn = createVfsPseudoFileBytes({
|
||||
bareOsDevShm: shm,
|
||||
pseudoVersionText: () => 'Bare OS\n',
|
||||
pseudoUrandomBytes: () => new Uint8Array([1])
|
||||
})
|
||||
const ver = fn({ file: 'version', kind: 'proc' })
|
||||
t.ok(ver.byteLength > 0)
|
||||
const empty = fn({ file: 'null', kind: 'dev' })
|
||||
t.is(empty.byteLength, 0)
|
||||
const got = fn({ file: 'shm', kind: 'dev', shmName: 'q' })
|
||||
t.is(got[0], 9)
|
||||
})
|
||||
|
||||
test('lifecycle hooks register invoke and reload', async (t) => {
|
||||
const suspendHooks = []
|
||||
const resumeHooks = []
|
||||
const m = createBareOsLifecycleHookMethods({
|
||||
suspendHooks,
|
||||
resumeHooks,
|
||||
env: { BARE_OS_KERNEL_PROFILE_WARM: '1' }
|
||||
})
|
||||
let n = 0
|
||||
m.bareOsRegisterSuspendHook(() => {
|
||||
n++
|
||||
})
|
||||
await m.bareOsInvokeSuspendHooks()
|
||||
t.is(n, 1)
|
||||
t.exception(() => m.bareOsRequestKernelReload(), /BARE_OS_KERNEL_RELOAD/)
|
||||
t.exception(
|
||||
() => m.bareOsRequestKernelProfileReload(),
|
||||
/BARE_OS_KERNEL_PROFILE_RELOAD/
|
||||
)
|
||||
})
|
||||
|
||||
test('process control existence probe and renice', (t) => {
|
||||
const delivered = []
|
||||
const nice = new Map()
|
||||
const ctx = {
|
||||
bareOsLogicalSigaction: {},
|
||||
...createBareOsProcessControlMethods({
|
||||
deliverBareOsVirtualSignalToPid: (_c, pid, sig) => {
|
||||
delivered.push([pid, sig])
|
||||
return { delivered: true, atMs: 1 }
|
||||
},
|
||||
getInteractiveCtx: () => ({ shellBackgroundJobs: { list: [] } }),
|
||||
niceByPid: nice
|
||||
})
|
||||
}
|
||||
const z = ctx.bareOsSendSignal('kernel', '0')
|
||||
t.ok(z.exists)
|
||||
t.is(z.pid, 1)
|
||||
const r = ctx.bareOsRenice(1, 2)
|
||||
t.is(r.nice, 2)
|
||||
t.is(nice.get(1), 2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user