369 lines
23 KiB
C
369 lines
23 KiB
C
#ifndef FROSTBYTE_SIGNAL_H
|
|
#define FROSTBYTE_SIGNAL_H
|
|
|
|
#include "types.h"
|
|
#include "isr.h"
|
|
#include "sched.h"
|
|
#include "signalnum.h"
|
|
|
|
/*
|
|
* Real (catchable) signal delivery on top of the single-address-space,
|
|
* ring0/ring3 kernel: a thread can install a handler for a signal number,
|
|
* mask/unmask signals, and have another thread (or itself) raise one --
|
|
* delivery happens the moment the target thread next returns to ring3
|
|
* through the interrupt-return path (isr.c's deliverPendingSignals), by
|
|
* rewriting its saved trapframe to enter a small shared trampoline page
|
|
* (sigInstallTrampoline below) instead of resuming where it was
|
|
* interrupted. The trampoline calls the handler, then traps back in via
|
|
* sysSigReturn, which restores the exact pre-signal register snapshot this
|
|
* kernel keeps in the Thread struct itself (sigSavedRegs) -- no signal
|
|
* frame is ever pushed onto the user stack, since there is no need to: the
|
|
* saved context already lives in kernel memory, unlike a real kernel with
|
|
* per-process address spaces where the handler must run in a stack the
|
|
* kernel does not otherwise own a copy of.
|
|
*
|
|
* Job control (SIGSTOP/SIGCONT/SIGTSTP) is real, on top of a genuine
|
|
* threadStopped ThreadState (sched.h): SIGSTOP, like SIGKILL, bypasses the
|
|
* pending/blocked/handler machinery entirely -- sigRaise delegates it
|
|
* straight to threadStop (sched.c), so it cannot be caught, blocked, or
|
|
* ignored, matching real Unix. SIGCONT is NOT bypassed the same way: it
|
|
* still sets its pending bit and runs through the normal scan (so an
|
|
* installed handler for it genuinely fires), but sigRaise ALSO
|
|
* unconditionally calls threadContinue as a side effect, independent of
|
|
* sigBlocked -- the resume itself is not something a handler or a mask can
|
|
* defer, only whether a handler additionally runs is. SIGTSTP is the
|
|
* catchable half of job control: its real-world default disposition is
|
|
* Stop rather than Terminate (sigDefaultIsStop), reported as
|
|
* sigOutcomeStop for isr.c to act on the same way it already acts on
|
|
* sigOutcomeTerminate, but -- unlike SIGSTOP -- an installed handler for it
|
|
* runs instead of stopping, and it can be blocked/ignored like any other
|
|
* catchable signal.
|
|
*
|
|
* Per-signal sa_mask (POSIX sigaction's sa_mask, not sigprocmask's whole-
|
|
* process mask): sigSetHandler takes a `mask` alongside the handler address,
|
|
* stored in Thread.sigHandlerMasks[signum] (sched.h) -- while that signum's
|
|
* handler is running, sigBlocked is the union of whatever was already
|
|
* blocked, this mask, and (unless sigFlagNoDefer -- signalnum.h -- is set
|
|
* for it) the signal itself, self-blocked like real Unix's implicit
|
|
* default. A DIFFERENT, unmasked signal arriving while a handler runs is
|
|
* therefore genuinely deliverable, not blanket-blocked the way an earlier
|
|
* version of this file (see git history) used to: sigConsumePending is
|
|
* gated by Thread.sigHandlerDepth against sigMaxNestDepth (sched.h), not by
|
|
* a single "one handler at a time" bool, so a second handler can run
|
|
* NESTED inside the first (its own context pushed onto the small
|
|
* sigSavedRegs/sigSavedBlocked stack) and the first resumes, via
|
|
* sigOnSigReturn popping that same stack, exactly where it left off once
|
|
* the nested one returns. user/nesttarget.c is the live ring-3 proof for
|
|
* two DIFFERENT signals; user/nodefertest.c is the same mechanism proving
|
|
* the SAME signal number can nest into its own still-active handler when
|
|
* registered with sigFlagNoDefer (and, by contrast, genuinely cannot
|
|
* without it); sigTest.c drives the stack directly for fully deterministic
|
|
* versions of both.
|
|
*
|
|
* Early wake of a Sleeping/Blocked target (real Unix's interruptible
|
|
* sleep/EINTR): sigRaise calls sched.c's threadWakeForSignal for any
|
|
* signal signal.c's sigWouldDeliver determines would genuinely be
|
|
* delivered (not blocked, not ignored) -- forcing the target straight to
|
|
* threadRunnable so the NEXT schedule() eventually resumes it, reaching a
|
|
* return-to-ring3 point (isr.c's deliverPendingSignals) it might otherwise
|
|
* NEVER naturally reach again (e.g. user/eintrtarget.c's read, which no
|
|
* writer will ever satisfy). A blocked or ignored signal leaves a
|
|
* Sleeping/Blocked target's wait completely undisturbed, exactly like real
|
|
* Unix -- sigWouldDeliver is checked first. threadWakeForSignal itself
|
|
* excludes threadStopped on purpose: job control's "stopped means stopped"
|
|
* must survive an ordinary signal arriving too, so only threadContinue (an
|
|
* explicit SIGCONT) may ever move a Stopped thread, the same asymmetry
|
|
* schedWakeChannel already has for the same reason.
|
|
*
|
|
* SA_RESTART / EINTR (signalnum.h's sigFlagRestart, sched.h's
|
|
* Thread.sigInRetryableSyscall): being woken early (or simply signaled
|
|
* while already Blocked) answers "does the target wake up at all", not
|
|
* "what does the interrupted syscall return once its handler is done" --
|
|
* that second question is what sa_flags settles. Before this existed,
|
|
* every retryable syscall (sysRead/sysWrite/sysWait, syscall.c) restarted
|
|
* unconditionally once a handler returned, since sysSigReturn always
|
|
* restores the exact rewound rip sigConsumePending captured. Now
|
|
* sigConsumePending checks, at the moment it is about to push a custom
|
|
* handler's context, whether the thread is genuinely mid one of those three
|
|
* (sigInRetryableSyscall) and the signal being delivered lacks
|
|
* sigFlagRestart for its own handler; if so, it rewrites the ABOUT-TO-BE-
|
|
* SAVED context in place (rip past the interrupted `int $0x80`, rax to
|
|
* errInterrupted) before pushing it, so sigOnSigReturn's later, byte-for-
|
|
* byte-faithful restore resumes the caller having genuinely observed an
|
|
* interrupted syscall, not a silently retried one -- real EINTR, not an
|
|
* approximation of it. user/restarttarget.c is the live ring-3 proof of
|
|
* both halves (with and without the flag); sigTest.c drives
|
|
* sigConsumePending directly for the deterministic one.
|
|
*
|
|
* A thread self-targeting SIGSTOP through sysSigSend (syscall.c) is
|
|
* reasoned about and exercised by
|
|
* schedTest.c's state-machine coverage of threadStop accepting
|
|
* threadRunning, but -- unlike the external-stop/continue scenario
|
|
* user/jobtest.c demonstrates live -- has no live ring-3 demo of its own
|
|
* this session: proving it would need a second, independent process to
|
|
* race-freely learn "the target has now actually stopped" before sending
|
|
* SIGCONT, which is exactly the kind of guess-free rendezvous this file's
|
|
* OWN sigtarget.c comment already had to solve once for a different race,
|
|
* and doing it again here was judged not worth the added live-boot
|
|
* surface for a self-stop path whose actual mechanism (threadStop, called
|
|
* on the current thread) is not otherwise unusual. SIGKILL is intentionally
|
|
* NOT routed through this pending/blocked machinery at all: sigRaise
|
|
* delegates it straight to the existing threadKill (sched.c), the one
|
|
* unconditional, unmaskable, uncatchable termination path this kernel
|
|
* already had (now also accepting threadStopped, since a real
|
|
* kill(pid, SIGKILL) kills a stopped process too) -- exactly like
|
|
* real kill(pid, SIGKILL) never goes through a handler either.
|
|
*/
|
|
|
|
/* Signal numbers, handler sentinels, sigSetFailed and the sigMask* "how"
|
|
values all live in signalnum.h, shared verbatim with userspace (see that
|
|
header for why it is split out). */
|
|
|
|
/* sigMaxSignals (one bit per signal number in Thread.sigPending/
|
|
sigBlocked, and the size of Thread.sigHandlers[]) is defined in sched.h,
|
|
next to the Thread struct that actually needs it as an array bound --
|
|
signal.h cannot define it itself without sched.h including signal.h
|
|
right back, which would be circular. */
|
|
|
|
/* Every signal bit set -- a plain "block literally everything" convenience
|
|
constant for callers/tests that want to exercise sigProcMask's own
|
|
sigKill/sigStop stripping, or job control's "resume happens regardless of
|
|
sigBlocked" guarantee, with the widest possible mask. Not used by
|
|
sigConsumePending's own handler-entry logic any more (see the file
|
|
comment's per-signal sa_mask paragraph) -- that is real, per-handler
|
|
Thread.sigHandlerMasks[signum] now, not this one blanket value. */
|
|
#define sigAllBlocked (~0ull)
|
|
|
|
/* Fixed virtual address of the one shared, read+execute, never-write,
|
|
user-accessible trampoline page every process's signal handlers are
|
|
entered through (sigInstallTrampoline maps it once at boot). Chosen
|
|
deliberately far from both the boot-time 1 GiB identity-mapped range
|
|
([0, 0x40000000), see boot.S's bootPd -- pagingMap refuses to touch
|
|
anything inside a huge-page-mapped range anyway) and the per-binary
|
|
elfSlotSize=0x10000000 slots USER_BASE hands out one at a time starting
|
|
at 0x40000000 (syscall.c's userSpaceMin) -- at the current growth rate
|
|
(Makefile) that sequence would need roughly three thousand more user
|
|
binaries to ever reach here, and every process already shares this one
|
|
address space regardless (see the architecture note in sched.h), so one
|
|
fixed shared page for ALL of them, like a real kernel's vDSO, is exactly
|
|
as much mapping as this needs. */
|
|
#define sigTrampolineVirtualAddr 0x0000700000000000ull
|
|
|
|
/* Fixed virtual address of the shared, read-only (never write, never
|
|
execute), user-accessible SigInfo (signalnum.h) data page(s) every
|
|
process's sigFlagSigInfo handlers are handed a pointer into --
|
|
sigInstallInfoPage maps it once at boot, right after the trampoline page
|
|
above, in the same reserved high range for the same reasons that page's
|
|
own comment already gives (nowhere near either the boot-time identity
|
|
map or any real per-binary elfSlotSize slot). One frameSize-sized page
|
|
holds frameSize/sizeof(SigInfo) slots; sigInstallInfoPage maps as many
|
|
consecutive pages, starting here, as threadCapacity()*sigMaxNestDepth
|
|
slots actually need (today: 96*4 = 384 slots, comfortably inside 2
|
|
pages) -- see sigConsumePending for how a (thread id, handler-nesting
|
|
depth) pair maps to one specific slot, the same indexing scheme
|
|
sigSavedRegs/sigSavedBlocked (sched.h) already use for the exact same
|
|
pair, just kept in a user-readable page here instead of Thread itself
|
|
since a handler running in ring 3 has no way to reach into kernel
|
|
memory. */
|
|
#define sigInfoVirtualAddr 0x0000700000010000ull
|
|
|
|
/* How many consecutive frameSize pages, starting at sigInfoVirtualAddr, a
|
|
fixed-size static array of physical frame addresses (signal.c) can hold
|
|
-- sigInstallInfoPage only ever maps as many as threadCapacity()*
|
|
sigMaxNestDepth slots actually need (2 today), so this is deliberately
|
|
generous headroom, not a tight fit: maxThreads (sched.c) has grown by a
|
|
fixed +8 roughly once per session for a long history of falas, and this
|
|
comfortably absorbs many more such bumps (8 pages * frameSize/sizeof
|
|
(SigInfo) slots-per-page / sigMaxNestDepth == room for hundreds of
|
|
threads) before ever needing to be revisited, the same "generous
|
|
headroom, not exact" reasoning sigMaxNestDepth's own comment (sched.h)
|
|
already gives for itself. */
|
|
#define sigInfoMaxPages 8
|
|
|
|
typedef enum {
|
|
sigOutcomeNone = 0,
|
|
sigOutcomeHandler = 1,
|
|
sigOutcomeTerminate = 2,
|
|
/* Default disposition of a catchable job-control signal (SIGTSTP only
|
|
-- SIGSTOP never reaches sigConsumePending at all, see the file
|
|
comment). Same contract as sigOutcomeTerminate: this function only
|
|
REPORTS it, the caller (isr.c) must actually call threadStop and
|
|
reschedule, since only schedule() can produce the next thread's own
|
|
rsp to resume with. */
|
|
sigOutcomeStop = 3
|
|
} SigOutcome;
|
|
|
|
/* Resets ALL signal state on a Thread slot to fresh defaults: no pending or
|
|
blocked signals, no installed handlers or their masks, handler-nesting
|
|
depth zero. Called by threadCreate/threadCreateUser (sched.c) on every
|
|
(re)use of a table slot,
|
|
the same explicit-field-by-field convention threadCreate/threadCreateUser
|
|
already use for every other Thread field -- threadReap/threadDiscard's
|
|
own memset() of the whole struct already guarantees this before a slot
|
|
is ever reused, so this is defensive documentation of the invariant, not
|
|
a fix for a leak that could otherwise happen (unlike the console-reader/
|
|
pipe-fd leaks this subsystem's sibling code had to actually fix, which
|
|
lived in tables OUTSIDE Thread that no memset here ever touched). */
|
|
void sigResetState(Thread* thread);
|
|
|
|
/* Raises signal `signum` against thread `targetId`. sigKill and sigStop are
|
|
both delegated straight to threadKill/threadStop (see file comment) and
|
|
never touch sigPending at all. sigCont ALSO calls threadContinue as an
|
|
unconditional side effect (resuming a Stopped target regardless of
|
|
sigBlocked) but, unlike sigKill/sigStop, still falls through to set its
|
|
own pending bit afterwards, so an installed handler still runs through
|
|
the normal scan. Any other signum just sets the pending bit, then --
|
|
if sigWouldDeliver agrees this signal is not blocked or ignored --
|
|
calls threadWakeForSignal (sched.c) to force a Sleeping/Blocked target
|
|
straight to Runnable early, real Unix's interruptible sleep/EINTR (see
|
|
file comment); actual delivery itself still only ever happens later,
|
|
off the interrupt-return path (isr.c), once that thread genuinely
|
|
resumes. Returns false, changing nothing, for an out-of-range signum, an
|
|
invalid/out-of-range targetId, or a target that is Dead/Unused (mirrors
|
|
threadKill's own validation for the non-SIGKILL path, and threadKill's/
|
|
threadStop's own for their delegated paths). */
|
|
bool sigRaise(uint32 targetId, int signum);
|
|
|
|
/* Exactly sigRaise above, plus attaching a real SigInfo (signalnum.h) to
|
|
this specific raise -- `siCode`/`siPid`/`siStatus` become
|
|
target->sigPendingInfo[signum] (sched.h) the moment the pending bit
|
|
itself is set, later handed to a sigFlagSigInfo handler by
|
|
sigConsumePending. sigRaise itself is now a thin wrapper around this
|
|
(siCodeKernel, 0, 0 -- "no specific detail available/tracked", the
|
|
honest default for the many pre-existing callers, including sigTest.c's
|
|
whole KTEST suite, that only ever cared about signum/pending-bit
|
|
semantics and never plumbed real sender/child detail through). Real
|
|
production call sites needing genuine detail (sched.c's SIGALRM/SIGCHLD
|
|
raises, syscall.c's sysSigSend) call this directly instead. Same return
|
|
value, same validation, same sigKill/sigStop delegation, same early-wake
|
|
side effect as sigRaise -- see that function's own doc comment for all
|
|
of it, not repeated here. */
|
|
bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus);
|
|
|
|
/* Real kill(-pgid, signum)/kill(0, signum)'s group-targeted half (sched.h's
|
|
Thread.pgid; userSigSend/userKill's negative/zero-pid convention,
|
|
syscall.c's sysSigSend/sysKill) and keyboard.c's own real Ctrl-C/Ctrl-Z
|
|
(SIGINT/SIGTSTP delivered to the FOREGROUND process group only, never a
|
|
single target thread): scans every currently live thread once and calls
|
|
sigRaiseInfo above against each one whose pgid matches, independently --
|
|
SIGKILL/SIGSTOP/SIGCONT's own special-cased bypasses, per-target
|
|
early-wake, and SigInfo attachment therefore all apply exactly as they
|
|
already do for a plain, single-target send, simply repeated once per
|
|
group member, rather than this function sharing or reimplementing any of
|
|
that delivery logic itself. A pgid with no currently live member at all
|
|
(nobody has ever claimed the foreground yet, or every process that once
|
|
shared it has since died) is a harmless no-op, matching real kill(2)'s
|
|
ESRCH for an empty target set -- reported back as false, exactly like a
|
|
single-target sigRaiseInfo already reports false for an invalid/dead
|
|
target. */
|
|
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid);
|
|
|
|
/* Installs `handlerAddr` (a real user-code address, or sigHandlerDefault /
|
|
sigHandlerIgnore) as thread's disposition for `signum`, `mask` as its
|
|
sa_mask -- the additional signals sigConsumePending blocks for as long as
|
|
THIS handler is active (stripped of sigKill/sigStop first, same as
|
|
sigProcMask: neither can ever be blocked, so a mask cannot "achieve" it
|
|
either) -- and `flags` (signalnum.h's sigFlagRestart, or 0) as its
|
|
sa_flags. Returns the PREVIOUS disposition (like real signal(2)/
|
|
sigaction(2)'s oldact->sa_handler) or sigSetFailed for an out-of-range
|
|
signum or an attempt to touch sigKill or sigStop (both uncatchable, like
|
|
real EINVAL on SIGKILL/SIGSTOP) -- `mask`/`flags` are stored regardless of
|
|
whether `handlerAddr` is a real handler or a sentinel, but only ever
|
|
consulted for a real one (sigHandlerDefault/sigHandlerIgnore never reach
|
|
the custom-handler branch of sigConsumePending). */
|
|
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags);
|
|
|
|
/* Blocks/unblocks/replaces thread's blocked-signal mask according to `how`
|
|
(sigMaskBlock/sigMaskUnblock/sigMaskSetMask; any other value is a no-op),
|
|
returning the mask as it was BEFORE this call (like real sigprocmask's
|
|
oldset). sigKill's and sigStop's bits are silently stripped from `mask`
|
|
first -- neither can ever be blocked, matching real Unix (sigCont CAN be
|
|
blocked here, since only whether its handler runs is gated by
|
|
sigBlocked -- the resume side effect in sigRaise never consults it). */
|
|
uint64 sigProcMask(Thread* thread, int how, uint64 mask);
|
|
|
|
/* The delivery decision, called from isr.c's deliverPendingSignals on
|
|
every return-to-ring3 point for whichever thread is about to resume.
|
|
Refuses to deliver anything (returns sigOutcomeNone, touching nothing)
|
|
once thread->sigHandlerDepth already reached sigMaxNestDepth -- the small
|
|
fixed context stack is full, so whatever is pending simply waits for a
|
|
sigOnSigReturn to free a slot; this is the only depth-independent gate,
|
|
unlike the old single "one handler at a time" bool it replaced. Otherwise
|
|
scans sigPending & ~sigBlocked from signum 1 upward. An ignored signal
|
|
(explicit sigHandlerIgnore, or sigHandlerDefault whose real-world default
|
|
is Ignore -- see sigDefaultIsIgnore in signal.c) is dismissed and the
|
|
scan continues, so it can never mask a real one behind it. Otherwise:
|
|
default disposition -> sigOutcomeStop for sigTstp (sigDefaultIsStop) or
|
|
sigOutcomeTerminate for everything else (caller must actually stop/
|
|
terminate the thread, e.g. via threadStop/threadExit, and reschedule --
|
|
this function never does, since it has no way to hand back a new rsp).
|
|
Custom handler -> the interrupted context is pushed onto
|
|
thread->sigSavedRegs[depth]/sigSavedBlocked[depth] (depth being
|
|
sigHandlerDepth's value on entry, then incremented), sigBlocked becomes
|
|
the union of what it already was, sigHandlerMasks[signum] (the sa_mask
|
|
sigSetHandler stored), and signum's own bit (self-blocked, stripped of
|
|
sigKill/sigStop like every other mask operation in this file), and `regs`
|
|
is rewritten in place to enter the trampoline page (rip), call the
|
|
handler (rax) with signum as its sole argument (rdi) -- sigOutcomeHandler.
|
|
If sigHandlerFlags[signum] also has sigFlagSigInfo set, thread->
|
|
sigPendingInfo[signum] (whatever sigRaiseInfo last stashed there for this
|
|
signum, or sigRaise's own siCodeKernel/0/0 default) is additionally
|
|
copied into this (thread id, depth) pair's slot in the shared
|
|
sigInfoVirtualAddr page (signal.h), and rsi/rdx are rewritten too -- rsi
|
|
to that slot's real user-visible address, rdx to 0 (no ucontext_t this
|
|
kernel can expose) -- so the handler genuinely receives (signum,
|
|
SigInfo*, NULL) exactly as sigaction()'s SA_SIGINFO promises. Without the
|
|
flag, rsi/rdx are left completely untouched (whatever they already held),
|
|
exactly as before this existed, since a plain one-argument handler never
|
|
reads either. Before any of that pushing happens, thread->sigInRetryableSyscall is
|
|
consumed (read, then unconditionally cleared): if it was true (this
|
|
thread is genuinely mid a sysRead/sysWrite/sysWait retry, see sched.h)
|
|
and signum's sigHandlerFlags lacks sigFlagRestart, `regs` -- the
|
|
interrupted context about to be saved -- is rewritten first (rip past
|
|
the interrupted `int $0x80`, rax to errInterrupted) so the eventual
|
|
sigOnSigReturn resumes the caller having observed a genuine EINTR-style
|
|
failure instead of transparently retrying; sigFlagRestart (or
|
|
sigInRetryableSyscall already being false, e.g. a signal that interrupts
|
|
plain running code or a nested handler) leaves `regs` untouched here,
|
|
preserving today's transparent-retry behavior exactly.
|
|
A DIFFERENT signal not covered by that union can still be delivered on a
|
|
later call while this one is active -- pushed onto depth+1 as a genuinely
|
|
NESTED handler, sigOnSigReturn popping it back off once it returns, the
|
|
interrupted (still mid-handler) context resuming exactly where it was.
|
|
Either way `*outSignum` (if non-null) is set to whichever signal was
|
|
consumed. sigStop itself never appears here at all (see file comment --
|
|
sigRaise bypasses this whole scan for it). */
|
|
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum);
|
|
|
|
/* Undoes exactly what the sigOutcomeHandler branch of sigConsumePending did
|
|
for the MOST RECENTLY entered still-active handler (LIFO, like a real call
|
|
stack of signal frames): pops thread->sigHandlerDepth, restoring `*regs`
|
|
from thread->sigSavedRegs[depth] and thread->sigBlocked from
|
|
thread->sigSavedBlocked[depth] at the popped index -- resuming the
|
|
interrupted program (which may itself be a still-active OUTER handler, if
|
|
this was a nested one) at the exact instruction, with the exact
|
|
registers, it had before that signal arrived, as if it had never been
|
|
interrupted at all.
|
|
Returns false, changing nothing, if the thread was not actually inside a
|
|
handler (a user program calling the sigreturn syscall on its own, never
|
|
having been signaled, is a misuse this kernel simply ignores rather than
|
|
trusting). */
|
|
bool sigOnSigReturn(Thread* thread, Registers* regs);
|
|
|
|
/* Allocates one physical frame, fills it with the trampoline's machine
|
|
code (sigTrampolineStart..sigTrampolineEnd, sigTrampoline.S -- position
|
|
independent by construction, see that file), and maps it read+execute+
|
|
user (never write) at sigTrampolineVirtualAddr. Called exactly once from
|
|
kmain.c, before any user thread can possibly receive a signal. */
|
|
void sigInstallTrampoline(void);
|
|
|
|
/* Allocates and maps sigInfoVirtualAddr's data page(s) -- see that
|
|
constant's own comment for the layout and sizing. Called exactly once
|
|
from kmain.c, alongside sigInstallTrampoline (before any user thread can
|
|
possibly receive a sigFlagSigInfo delivery), and before ktestRunAll so
|
|
sigTest.c's own deterministic coverage can read the same shared page a
|
|
live ring-3 handler would. */
|
|
void sigInstallInfoPage(void);
|
|
|
|
#endif
|