You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
706 lines
16 KiB
706 lines
16 KiB
/*
|
|
* info_solaris.cpp
|
|
*
|
|
* Torsten Kasch <tk@Genetik.Uni-Bielefeld.DE>
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <sys/mnttab.h>
|
|
#include <kstat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/statvfs.h>
|
|
#include <time.h>
|
|
|
|
#ifdef HAVE_LIBDEVINFO_H
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/mkdev.h>
|
|
#include <sys/stat.h>
|
|
#include <devid.h>
|
|
#include <libdevinfo.h>
|
|
#endif /* HAVE_LIBDEVINFO_H */
|
|
|
|
#define INFO_CPU_AVAILABLE
|
|
#define INFO_IRQ_AVAILABLE
|
|
#define INFO_DMA_AVAILABLE
|
|
#define INFO_PCI_AVAILABLE
|
|
#define INFO_IOPORTS_AVAILABLE
|
|
#define INFO_SOUND_AVAILABLE
|
|
#define INFO_DEVICES_AVAILABLE
|
|
#define INFO_SCSI_AVAILABLE
|
|
#define INFO_PARTITIONS_AVAILABLE
|
|
#define INFO_XSERVER_AVAILABLE
|
|
|
|
|
|
bool GetInfo_CPU( TQListView *lBox ) {
|
|
|
|
kstat_ctl_t *kctl;
|
|
kstat_t *ksp;
|
|
kstat_named_t *kdata;
|
|
char cputype[16],
|
|
fputype[16];
|
|
char *timetxt;
|
|
char *ptr;
|
|
uint32_t i, ncpus;
|
|
unsigned long state_begin;
|
|
QString state;
|
|
QString mhz;
|
|
QString inst;
|
|
|
|
/*
|
|
* get a kstat handle first and update the user's kstat chain
|
|
*/
|
|
if( (kctl = kstat_open()) == NULL ) {
|
|
return false;
|
|
}
|
|
while( kstat_chain_update( kctl ) != 0 )
|
|
;
|
|
|
|
/*
|
|
* get the # of CPUs
|
|
*/
|
|
if( (ksp = kstat_lookup( kctl, "unix", 0, "system_misc" )) == NULL ) {
|
|
return false;
|
|
}
|
|
if( kstat_read( kctl, ksp, NULL ) == -1 ) {
|
|
return false;
|
|
}
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "ncpus" );
|
|
if( kdata != NULL ) {
|
|
ncpus = kdata->value.ui32;
|
|
} else {
|
|
ncpus = 0;
|
|
}
|
|
|
|
lBox->addColumn( i18n( "Instance" ));
|
|
lBox->addColumn( i18n( "CPU Type" ));
|
|
lBox->addColumn( i18n( "FPU Type" ));
|
|
lBox->addColumn( i18n( "MHz" ));
|
|
lBox->addColumn( i18n( "State" ));
|
|
|
|
/*
|
|
* get the per-processor info
|
|
*/
|
|
for( i = 0; i < ncpus; i++ ) {
|
|
|
|
if( (ksp = kstat_lookup( kctl, "cpu_info", i, NULL )) == NULL ){
|
|
return false;
|
|
}
|
|
|
|
if( kstat_read( kctl, ksp, NULL ) == -1 ) {
|
|
return false;
|
|
}
|
|
|
|
inst.setNum( i );
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "cpu_type" );
|
|
if( kdata != NULL ) {
|
|
strcpy( cputype, kdata->value.c );
|
|
} else {
|
|
sprintf( cputype, "???" );
|
|
}
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "fpu_type" );
|
|
if( kdata != NULL ) {
|
|
strcpy( fputype, kdata->value.c );
|
|
} else {
|
|
sprintf( fputype, "???" );
|
|
}
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "clock_MHz" );
|
|
if( kdata != NULL ) {
|
|
mhz.setNum( kdata->value.ul );
|
|
} else {
|
|
mhz.setNum( 0 );
|
|
}
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "state" );
|
|
if( kdata != NULL ) {
|
|
state = TQString( kdata->value.c );
|
|
} else {
|
|
state = "???";
|
|
}
|
|
kdata = (kstat_named_t *) kstat_data_lookup( ksp, "state_begin" );
|
|
if( kdata != NULL ) {
|
|
state_begin = kdata->value.i32;
|
|
if( (timetxt = ctime( (time_t *) &state_begin )) != NULL ) {
|
|
ptr = strrchr( timetxt, '\n' );
|
|
*ptr = '\0';
|
|
state += " since " + TQString( timetxt );
|
|
}
|
|
}
|
|
|
|
new TQListViewItem( lBox, inst, cputype, fputype, mhz, state );
|
|
}
|
|
|
|
// sorting_allowed = true;
|
|
lBox->setSorting( 0 );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GetInfo_IRQ( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_DMA( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_PCI( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_IO_Ports( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_Sound( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_SCSI( TQListView * ) {
|
|
return false;
|
|
}
|
|
|
|
bool GetInfo_Partitions( TQListView *lBox ) {
|
|
|
|
FILE *mnttab;
|
|
struct mnttab mnt;
|
|
struct statvfs statbuf;
|
|
fsblkcnt_t tmp;
|
|
QString total;
|
|
QString avail;
|
|
time_t mnttime;
|
|
char *timetxt;
|
|
char *ptr;
|
|
|
|
if( (mnttab = fopen( MNTTAB, "r" )) == NULL ) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* set up column headers
|
|
*/
|
|
lBox->addColumn( i18n( "Device" ));
|
|
lBox->addColumn( i18n( "Mount Point" ));
|
|
lBox->addColumn( i18n( "FS Type" ));
|
|
lBox->addColumn( i18n( "Total Size" ));
|
|
// XXX: FIXME: how do I set column alignment correctly?
|
|
lBox->setColumnAlignment( 3, 2 );
|
|
lBox->addColumn( i18n( "Free Size" ));
|
|
// XXX: FIXME: how do I set column alignment correctly?
|
|
lBox->setColumnAlignment( 4, 2 );
|
|
lBox->addColumn( i18n( "Mount Time" ));
|
|
lBox->addColumn( i18n( "Mount Options" ));
|
|
|
|
/*
|
|
* get info about mounted file systems
|
|
*/
|
|
rewind( mnttab );
|
|
while( getmntent( mnttab, &mnt ) == 0 ) {
|
|
/*
|
|
* skip fstype "nfs" and "autofs" for two reasons:
|
|
* o if the mountpoint is visible, the fs is not
|
|
* necessarily available (autofs option "-nobrowse")
|
|
* and we don't want to mount every remote fs just
|
|
* to get its size, do we?
|
|
* o the name "Partitions" for this statistics implies
|
|
* "local file systems only"
|
|
*/
|
|
if( (strcmp( mnt.mnt_fstype, "nfs" ) == 0)
|
|
|| (strcmp( mnt.mnt_fstype, "autofs" ) == 0) )
|
|
continue;
|
|
if( statvfs( mnt.mnt_mountp, &statbuf ) == 0 ) {
|
|
if( statbuf.f_blocks > 0 ) {
|
|
/*
|
|
* produce output in KB, MB, or GB for
|
|
* readability -- unfortunately, this
|
|
* breaks sorting for these columns...
|
|
*/
|
|
tmp = statbuf.f_blocks
|
|
* (statbuf.f_frsize / 1024);
|
|
if( tmp > 9999 ) {
|
|
tmp /= 1024;
|
|
if( tmp > 9999 ) {
|
|
tmp /= 1024;
|
|
total.setNum( tmp );
|
|
total += " G";
|
|
} else {
|
|
total.setNum( tmp );
|
|
total += " M";
|
|
}
|
|
} else {
|
|
total.setNum( tmp );
|
|
total += " K";
|
|
}
|
|
// avail.setNum( statbuf.f_bavail );
|
|
// avail += " K";
|
|
tmp = statbuf.f_bavail
|
|
* (statbuf.f_frsize / 1024);
|
|
if( tmp > 9999 ) {
|
|
tmp /= 1024;
|
|
if( tmp > 9999 ) {
|
|
tmp /= 1024;
|
|
avail.setNum( tmp );
|
|
avail += " G";
|
|
} else {
|
|
avail.setNum( tmp );
|
|
avail += " M";
|
|
}
|
|
} else {
|
|
avail.setNum( tmp );
|
|
avail += " K";
|
|
}
|
|
} else {
|
|
total = "-";
|
|
avail = "-";
|
|
}
|
|
} else {
|
|
total = "???";
|
|
avail = "???";
|
|
}
|
|
/*
|
|
* ctime() adds a '\n' which we have to remove
|
|
* so that we get a one-line output for the QListViewItem
|
|
*/
|
|
mnttime = (time_t) atol( mnt.mnt_time );
|
|
if( (timetxt = ctime( &mnttime )) != NULL ) {
|
|
ptr = strrchr( timetxt, '\n' );
|
|
*ptr = '\0';
|
|
}
|
|
|
|
new TQListViewItem(
|
|
lBox,
|
|
mnt.mnt_special,
|
|
mnt.mnt_mountp,
|
|
mnt.mnt_fstype,
|
|
total,
|
|
avail,
|
|
TQString( timetxt ),
|
|
mnt.mnt_mntopts
|
|
);
|
|
}
|
|
fclose( mnttab );
|
|
|
|
lBox->setSorting( 0 );
|
|
// sorting_allowed = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GetInfo_XServer_and_Video( TQListView *lBox ) {
|
|
return GetInfo_XServer_Generic( lBox );
|
|
}
|
|
|
|
#ifdef HAVE_LIBDEVINFO_H
|
|
/*
|
|
* get Solaris' device configuration data through libdevinfo(3)
|
|
* and display it in a prtconf(1M) style tree
|
|
*
|
|
* NOTE: though the devinfo library seems to be present on earlier
|
|
* Solaris releases, this interface is documented to be available
|
|
* since Solaris 7 (libdevinfo.h is missing on pre-Solaris 7 systems)
|
|
*
|
|
* documentation for libdevinfo(3) including code samples on which
|
|
* this implementation is based on is available at
|
|
* http://soldc.sun.com/developer/support/driver/wps/libdevinfo/
|
|
*/
|
|
|
|
/*
|
|
* we start with various helper routines for GetInfo_Devices()
|
|
*/
|
|
|
|
/*
|
|
* mktree() -- break up the device path and place its components
|
|
* into the tree widget
|
|
*/
|
|
TQListViewItem *mktree( TQListViewItem *top, const char *path ) {
|
|
|
|
QListViewItem *parent,
|
|
*previous,
|
|
*result;
|
|
char *str = strdup( path ),
|
|
*token;
|
|
|
|
/*
|
|
* start at "/"
|
|
*/
|
|
parent = top;
|
|
result = (*top).firstChild();
|
|
previous = (*top).firstChild();
|
|
|
|
token = strtok( str, "/" );
|
|
while( token != NULL ) {
|
|
/*
|
|
* find insert pos:
|
|
* try to match the node at the current level
|
|
*
|
|
* NOTE: this implementation assumes that there are
|
|
* no two nodes with identical names at the
|
|
* same level of the device tree
|
|
*/
|
|
while( result != NULL ) {
|
|
if( strcmp( token, (*result).text( 0 ).latin1()) == 0 )
|
|
break;
|
|
previous = result;
|
|
result = (*result).nextSibling();
|
|
}
|
|
if( result == NULL ) {
|
|
/*
|
|
* we haven't found the node, create a new one
|
|
*/
|
|
result = new TQListViewItem( parent,
|
|
previous,
|
|
token );
|
|
} else {
|
|
/*
|
|
* we've found the node
|
|
*/
|
|
parent = result;
|
|
previous = NULL;
|
|
if( (*result).firstChild() == NULL ) {
|
|
/*
|
|
* create new node during next iteration
|
|
*/
|
|
result->setExpandable( true );
|
|
result->setOpen( false );
|
|
} else {
|
|
/*
|
|
* follow the child path
|
|
*/
|
|
result = (*result).firstChild();
|
|
}
|
|
}
|
|
token = strtok( NULL, "/" );
|
|
}
|
|
free( str );
|
|
|
|
return( result );
|
|
}
|
|
|
|
/*
|
|
* prop_type_str() -- return the property type as a string
|
|
*/
|
|
char *prop_type_str( di_prop_t prop ) {
|
|
|
|
switch( di_prop_type( prop )) {
|
|
case DI_PROP_TYPE_UNDEF_IT:
|
|
return( "undefined" );
|
|
case DI_PROP_TYPE_BOOLEAN:
|
|
return( "BOOL" );
|
|
case DI_PROP_TYPE_INT:
|
|
return( "INT" );
|
|
case DI_PROP_TYPE_STRING:
|
|
return( "STRING" );
|
|
case DI_PROP_TYPE_BYTE:
|
|
return( "BYTE" );
|
|
default:
|
|
return( "unknown" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
* prop_type_guess() -- guess the property type
|
|
*/
|
|
int prop_type_guess( uchar_t *data, int len ) {
|
|
|
|
int slen;
|
|
int guess;
|
|
int i, c;
|
|
|
|
if( len < 0 )
|
|
return( -1 );
|
|
else if( len == 0 )
|
|
return( DI_PROP_TYPE_BOOLEAN );
|
|
|
|
slen = 0;
|
|
guess = DI_PROP_TYPE_STRING;
|
|
|
|
for( i = 0; i < len; i++ ) {
|
|
c = (int) data[i];
|
|
switch( c ) {
|
|
case 0:
|
|
if( i == (len - 1 ))
|
|
break;
|
|
if( slen == 0 )
|
|
guess = DI_PROP_TYPE_BYTE;
|
|
else
|
|
guess = slen = 0;
|
|
break;
|
|
default:
|
|
if( ! isprint( c ))
|
|
guess = DI_PROP_TYPE_BYTE;
|
|
else
|
|
slen++;
|
|
}
|
|
if( guess != DI_PROP_TYPE_STRING )
|
|
break;
|
|
}
|
|
|
|
// if( (guess == DI_PROP_TYPE_BYTE) && (len % sizeof( int ) == 0 ))
|
|
// guess = DI_PROP_TYPE_INT;
|
|
|
|
return( guess );
|
|
}
|
|
|
|
/*
|
|
* dump_minor_node() -- examine a device minor node
|
|
* this routine gets passed to di_walk_node()
|
|
*/
|
|
int dump_minor_node( di_node_t node, di_minor_t minor, void *arg ) {
|
|
|
|
QListViewItem *item;
|
|
QString majmin;
|
|
char *type;
|
|
dev_t dev;
|
|
|
|
item = new TQListViewItem( (TQListViewItem *) arg,
|
|
di_minor_name( minor ));
|
|
item->setExpandable( true );
|
|
item->setOpen( false );
|
|
new TQListViewItem( item, i18n( "Spectype:" ),
|
|
(di_minor_spectype( minor ) == S_IFCHR)
|
|
? i18n( "character special" )
|
|
: i18n( "block special" ));
|
|
type = di_minor_nodetype( minor );
|
|
new TQListViewItem( item, i18n( "Nodetype:" ),
|
|
(type == NULL) ? "NULL" : type );
|
|
|
|
if( (dev = di_minor_devt( minor )) != DDI_DEV_T_NONE ) {
|
|
majmin.sprintf( "%ld/%ld", major( dev ), minor( dev ));
|
|
new TQListViewItem( item, i18n( "Major/Minor:" ), majmin );
|
|
}
|
|
|
|
if( di_minor_next( node, minor ) == DI_MINOR_NIL )
|
|
return( DI_WALK_TERMINATE );
|
|
else
|
|
return( DI_WALK_CONTINUE );
|
|
}
|
|
|
|
/*
|
|
* propvalue() -- return the property value
|
|
*/
|
|
TQString propvalue( di_prop_t prop ) {
|
|
|
|
int type;
|
|
int i, n;
|
|
char *strp;
|
|
int *intp;
|
|
uchar_t *bytep;
|
|
QString result;
|
|
|
|
/*
|
|
* Since a lot of printable strings seem to be tagged as 'byte',
|
|
* we're going to guess, if the property is not STRING or INT
|
|
* The actual type is shown in the info tree, though.
|
|
*/
|
|
type = di_prop_type( prop );
|
|
if( (type != DI_PROP_TYPE_STRING) && (type != DI_PROP_TYPE_INT) ) {
|
|
n = di_prop_bytes( prop, &bytep );
|
|
type = prop_type_guess( bytep, n );
|
|
}
|
|
|
|
result = "";
|
|
switch( type ) {
|
|
case DI_PROP_TYPE_STRING:
|
|
if( (n = di_prop_strings( prop, &strp )) < 0 ) {
|
|
result = "(error)";
|
|
} else {
|
|
for( i = 0; i < n; i++ ) {
|
|
result += "\"";
|
|
result += strp;
|
|
result += "\" ";
|
|
strp += strlen( strp ) + 1;
|
|
}
|
|
}
|
|
break;
|
|
case DI_PROP_TYPE_INT:
|
|
if( (n = di_prop_ints( prop, &intp )) < 0 ) {
|
|
result = "(error)";
|
|
} else {
|
|
for( i = 0; i < n; i++ ) {
|
|
TQString tmp;
|
|
tmp.setNum( intp[i] );
|
|
result += tmp;
|
|
result += " ";
|
|
}
|
|
}
|
|
break;
|
|
case DI_PROP_TYPE_BOOLEAN:
|
|
/*
|
|
* hmm, Sun's sample code handles the existence
|
|
* of a boolean property as "true", whereas
|
|
* prtconf(1M) obviously does not (Sol8, at least)
|
|
* -- we're doing the same and handle "bool" as "byte"
|
|
*/
|
|
case DI_PROP_TYPE_BYTE:
|
|
if( (n = di_prop_bytes( prop, &bytep )) < 0 ) {
|
|
result = "(error)";
|
|
} else {
|
|
if( n == 0 ) {
|
|
result = i18n( "(no value)" );
|
|
break;
|
|
}
|
|
result = "0x";
|
|
for( i = 0; i < n; i++ ) {
|
|
TQString tmp;
|
|
unsigned byte = (unsigned) bytep[i];
|
|
tmp.sprintf( "%2.2x", byte );
|
|
result += tmp;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
result = "???";
|
|
}
|
|
|
|
return( result );
|
|
}
|
|
|
|
/*
|
|
* dump_node() -- examine a device node and its children
|
|
* this routine gets passed to di_walk_node()
|
|
*/
|
|
int dump_node( di_node_t node, void *arg ) {
|
|
|
|
QListViewItem *top = (TQListViewItem *) arg,
|
|
*parent,
|
|
*previous;
|
|
char *path;
|
|
char *drivername;
|
|
char *names;
|
|
QString compatnames;
|
|
int i, n;
|
|
di_prop_t prop;
|
|
|
|
path = di_devfs_path( node );
|
|
|
|
/*
|
|
* if this is the root node ("/"), initialize the tree
|
|
*/
|
|
if( strlen( path ) == 1 ) {
|
|
top->setText( 0, TQString( di_binding_name( node )));
|
|
top->setPixmap( 0, SmallIcon( "kcmdevices" ));
|
|
top->setOpen( true );
|
|
top->setSelectable( false );
|
|
top->setExpandable( false );
|
|
}
|
|
|
|
/*
|
|
* place the node in the tree
|
|
*/
|
|
parent = mktree( top, path );
|
|
|
|
/*
|
|
* we have to handle the root node differently...
|
|
*/
|
|
if( strlen( path ) > 1 ) {
|
|
parent->setExpandable( true );
|
|
parent->setOpen( false );
|
|
} else {
|
|
previous = parent;
|
|
parent = top;
|
|
}
|
|
|
|
/*
|
|
* node name and physical device path
|
|
*/
|
|
drivername = di_driver_name( node );
|
|
previous = new TQListViewItem( parent,
|
|
i18n( "Driver Name:" ),
|
|
(drivername == NULL)
|
|
? i18n( "(driver not attached)" )
|
|
: drivername );
|
|
previous = new TQListViewItem( parent, previous,
|
|
i18n( "Binding Name:" ), di_binding_name( node ));
|
|
|
|
n = di_compatible_names( node, &names );
|
|
if( n < 1 ) {
|
|
compatnames = i18n( "(none)" );
|
|
} else {
|
|
for( i = 0; i < n; i++ ) {
|
|
compatnames += names;
|
|
compatnames += " ";
|
|
names += strlen( names ) + 1;
|
|
}
|
|
}
|
|
|
|
previous = new TQListViewItem( parent, previous,
|
|
i18n( "Compatible Names:" ), compatnames );
|
|
|
|
previous = new TQListViewItem( parent, previous,
|
|
i18n( "Physical Path:" ), TQString( path ));
|
|
|
|
/*
|
|
* dump the node's property list (if any)
|
|
*/
|
|
if( (prop = di_prop_next( node, DI_PROP_NIL )) != DI_PROP_NIL ) {
|
|
previous = new TQListViewItem( parent, previous, i18n( "Properties" ));
|
|
previous->setExpandable( true );
|
|
previous->setOpen( false );
|
|
do {
|
|
/*
|
|
* property type & value
|
|
*/
|
|
QListViewItem *tmp,
|
|
*prev;
|
|
tmp = new TQListViewItem( previous, di_prop_name( prop ));
|
|
tmp->setExpandable( true );
|
|
tmp->setOpen( false );
|
|
prev = new TQListViewItem( tmp, i18n( "Type:" ),
|
|
prop_type_str( prop ));
|
|
new TQListViewItem( tmp, prev, i18n( "Value:" ),
|
|
propvalue( prop ));
|
|
} while( (prop = di_prop_next( node, prop )) != DI_PROP_NIL );
|
|
}
|
|
|
|
/*
|
|
* if there are minor nodes, expand the tree appropriately
|
|
*/
|
|
if( di_minor_next( node, DI_MINOR_NIL ) != DI_MINOR_NIL ) {
|
|
previous = new TQListViewItem( parent, previous, i18n( "Minor Nodes" ));
|
|
previous->setExpandable( true );
|
|
previous->setOpen( false );
|
|
di_walk_minor( node, NULL, 0, previous, dump_minor_node );
|
|
}
|
|
|
|
return( DI_WALK_CONTINUE );
|
|
}
|
|
|
|
bool GetInfo_Devices( TQListView *lBox ) {
|
|
|
|
QListViewItem *top;
|
|
di_node_t root_node;
|
|
|
|
/*
|
|
* create a snapshot of the device tree
|
|
*/
|
|
if( (root_node = di_init( "/", DINFOCPYALL )) == DI_NODE_NIL ) {
|
|
return( false );
|
|
}
|
|
// XXX: might try to di_prom_init() here as well (if we're setgid sys)
|
|
|
|
/*
|
|
* prepare the tree widget
|
|
*/
|
|
lBox->addColumn( i18n( "Device Information" ));
|
|
lBox->addColumn( i18n( "Value" ));
|
|
|
|
top = new TQListViewItem( lBox );
|
|
|
|
/*
|
|
* traverse the device tree
|
|
*/
|
|
di_walk_node( root_node, DI_WALK_CLDFIRST, top, dump_node );
|
|
|
|
di_fini( root_node );
|
|
|
|
sorting_allowed = false;
|
|
return true;
|
|
}
|
|
|
|
#else /* ! HAVE_LIBDEVINFO_H */
|
|
bool GetInfo_Devices( TQListView * ) {
|
|
return false;
|
|
}
|
|
#endif /* ! HAVE_LIBDEVINFO_H */
|