vdr/tools.c
Klaus Schmidinger eb52e3004a Version 1.3.11
- In order to avoid problems on NPTL systems, VDR now checks for the presence
  of NPTL at program start, and if it is, exits and tells the user to do
  'export LD_ASSUME_KERNEL=2.4.1' before starting VDR.
- Revisited the "Fixed missing audio after replaying a DVD" change because it
  introduced a sound disturbance when switching between channels on the same
  transponder (thanks to Marco Schlüßler).
- In order to avoid problems on UTF-8 systems, VDR now checks for the presence
  of UTF-8 at program start, and if it is, exits and tells the user to turn off
  UTF-8 before starting VDR (thanks to Ludwig Nussel for pointing out a problem
  with systems that are set to use UTF-8). There are also problems in case the
  video partition is mounted with "iocharset=utf8" (thanks to Jörg Knitter for
  reporting this one).
  Please also read the "IMPORTANT NOTES" section in the INSTALL file!
- Some changes to the SPU decoder interface (thanks to Sven Goethel).
- Some improvements in cOsd creation (thanks to some suggestions by Jouni Karvo).
- Fixed calculating the OSD width and height (thanks to Olaf Henkel for reporting
  a problem with long event texts in the "Classic VDR" skin).
- Fixed switching channels while an encrypted channel is being recorded, because the
  channel was switched if the new channel was on the same transponder and was
  a radio channel or an unencrypted channel (thanks to Martin Dauskardt for reporting
  this one).
- No longer using the external 'find' command to scan the video directory for
  recordings (based on a suggestion by Mirko Dölle).
- The list of recordings is now kept statically in memory to avoid long delays
  when opening the "Recordings" menu. As a side effect, external modifications to
  the video directory are no longer immediately reflected in the "Recordings" menu.
  If a plugin manipulates the video directory in any way, it can call the function
  Recordings.TriggerUpdate() to trigger an update of the list of recordings.
  If some external tool manipulates the video directory, it can touch the file
  '.update' in the video directory to trigger an update of the list of recordings.
- Fixed a memory leak in theme description handling (thanks to Sascha Volkenandt).
- Added cDevice::Flush() to make sure that all data in the video card's buffers
  has been processed (thanks to Reinhard Nissl). Currently this is not yet actually
  implemented for FF DVB cards.
- Fixed handling the color button texts in cMenuEditStrItem (thanks to Maynard
  Cedric for reporting this one).
- Fixed the description of cRingBufferLinear (thanks to Ludwig Nussel for pointing
  out this one).
- Fixed cRingBufferLinear::Get() in case the buffer wraps around (thanks to Ludwig
  Nussel for reporting this one).
2004-06-19 18:00:00 +02:00

958 lines
22 KiB
C

/*
* tools.c: Various tools
*
* See the main source file 'vdr.c' for copyright information and
* how to reach the author.
*
* $Id: tools.c 1.80 2004/06/13 14:36:41 kls Exp $
*/
#include "tools.h"
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/vfs.h>
#include <time.h>
#include <unistd.h>
#include "i18n.h"
int SysLogLevel = 3;
int BCD2INT(int x)
{
return ((1000000 * BCDCHARTOINT((x >> 24) & 0xFF)) +
(10000 * BCDCHARTOINT((x >> 16) & 0xFF)) +
(100 * BCDCHARTOINT((x >> 8) & 0xFF)) +
BCDCHARTOINT( x & 0xFF));
}
ssize_t safe_read(int filedes, void *buffer, size_t size)
{
for (;;) {
ssize_t p = read(filedes, buffer, size);
if (p < 0 && errno == EINTR) {
dsyslog("EINTR while reading from file handle %d - retrying", filedes);
continue;
}
return p;
}
}
ssize_t safe_write(int filedes, const void *buffer, size_t size)
{
ssize_t p = 0;
ssize_t written = size;
const unsigned char *ptr = (const unsigned char *)buffer;
while (size > 0) {
p = write(filedes, ptr, size);
if (p < 0) {
if (errno == EINTR) {
dsyslog("EINTR while writing to file handle %d - retrying", filedes);
continue;
}
break;
}
ptr += p;
size -= p;
}
return p < 0 ? p : written;
}
void writechar(int filedes, char c)
{
safe_write(filedes, &c, sizeof(c));
}
char *readline(FILE *f)
{
static char buffer[MAXPARSEBUFFER];
if (fgets(buffer, sizeof(buffer), f) > 0) {
int l = strlen(buffer) - 1;
if (l >= 0 && buffer[l] == '\n')
buffer[l] = 0;
return buffer;
}
return NULL;
}
char *strcpyrealloc(char *dest, const char *src)
{
if (src) {
int l = max(dest ? strlen(dest) : 0, strlen(src)) + 1; // don't let the block get smaller!
dest = (char *)realloc(dest, l);
if (dest)
strcpy(dest, src);
else
esyslog("ERROR: out of memory");
}
else {
free(dest);
dest = NULL;
}
return dest;
}
char *strn0cpy(char *dest, const char *src, size_t n)
{
char *s = dest;
for ( ; --n && (*dest = *src) != 0; dest++, src++) ;
*dest = 0;
return s;
}
char *strreplace(char *s, char c1, char c2)
{
char *p = s;
while (p && *p) {
if (*p == c1)
*p = c2;
p++;
}
return s;
}
char *strreplace(char *s, const char *s1, const char *s2)
{
char *p = strstr(s, s1);
if (p) {
int of = p - s;
int l = strlen(s);
int l1 = strlen(s1);
int l2 = strlen(s2);
if (l2 > l1)
s = (char *)realloc(s, strlen(s) + l2 - l1 + 1);
if (l2 != l1)
memmove(s + of + l2, s + of + l1, l - of - l1 + 1);
strncpy(s + of, s2, l2);
}
return s;
}
char *skipspace(const char *s)
{
while (*s && isspace(*s))
s++;
return (char *)s;
}
char *stripspace(char *s)
{
if (s && *s) {
for (char *p = s + strlen(s) - 1; p >= s; p--) {
if (!isspace(*p))
break;
*p = 0;
}
}
return s;
}
char *compactspace(char *s)
{
if (s && *s) {
char *t = stripspace(skipspace(s));
char *p = t;
while (p && *p) {
char *q = skipspace(p);
if (q - p > 1)
memmove(p + 1, q, strlen(q) + 1);
p++;
}
if (t != s)
memmove(s, t, strlen(t) + 1);
}
return s;
}
const char *strescape(const char *s, const char *chars)
{
static char *buffer = NULL;
const char *p = s;
char *t = NULL;
while (*p) {
if (strchr(chars, *p)) {
if (!t) {
buffer = (char *)realloc(buffer, 2 * strlen(s) + 1);
t = buffer + (p - s);
s = strcpy(buffer, s);
}
*t++ = '\\';
}
if (t)
*t++ = *p;
p++;
}
if (t)
*t = 0;
return s;
}
bool startswith(const char *s, const char *p)
{
while (*p) {
if (*p++ != *s++)
return false;
}
return true;
}
bool endswith(const char *s, const char *p)
{
const char *se = s + strlen(s) - 1;
const char *pe = p + strlen(p) - 1;
while (pe >= p) {
if (*pe-- != *se-- || (se < s && pe >= p))
return false;
}
return true;
}
bool isempty(const char *s)
{
return !(s && *skipspace(s));
}
int numdigits(int n)
{
char buf[16];
snprintf(buf, sizeof(buf), "%d", n);
return strlen(buf);
}
int time_ms(void)
{
static time_t t0 = 0;
struct timeval t;
if (gettimeofday(&t, NULL) == 0) {
if (t0 == 0)
t0 = t.tv_sec; // this avoids an overflow (we only work with deltas)
return (t.tv_sec - t0) * 1000 + t.tv_usec / 1000;
}
return 0;
}
void delay_ms(int ms)
{
int t0 = time_ms();
while (time_ms() - t0 < ms)
;
}
bool isnumber(const char *s)
{
if (!*s)
return false;
while (*s) {
if (!isdigit(*s))
return false;
s++;
}
return true;
}
const char *itoa(int n)
{
static char buf[16];
snprintf(buf, sizeof(buf), "%d", n);
return buf;
}
const char *AddDirectory(const char *DirName, const char *FileName)
{
static char *buf = NULL;
free(buf);
asprintf(&buf, "%s/%s", DirName && *DirName ? DirName : ".", FileName);
return buf;
}
int FreeDiskSpaceMB(const char *Directory, int *UsedMB)
{
if (UsedMB)
*UsedMB = 0;
int Free = 0;
struct statfs statFs;
if (statfs(Directory, &statFs) == 0) {
double blocksPerMeg = 1024.0 * 1024.0 / statFs.f_bsize;
if (UsedMB)
*UsedMB = int((statFs.f_blocks - statFs.f_bfree) / blocksPerMeg);
Free = int(statFs.f_bavail / blocksPerMeg);
}
else
LOG_ERROR_STR(Directory);
return Free;
}
bool DirectoryOk(const char *DirName, bool LogErrors)
{
struct stat ds;
if (stat(DirName, &ds) == 0) {
if (S_ISDIR(ds.st_mode)) {
if (access(DirName, R_OK | W_OK | X_OK) == 0)
return true;
else if (LogErrors)
esyslog("ERROR: can't access %s", DirName);
}
else if (LogErrors)
esyslog("ERROR: %s is not a directory", DirName);
}
else if (LogErrors)
LOG_ERROR_STR(DirName);
return false;
}
bool MakeDirs(const char *FileName, bool IsDirectory)
{
bool result = true;
char *s = strdup(FileName);
char *p = s;
if (*p == '/')
p++;
while ((p = strchr(p, '/')) != NULL || IsDirectory) {
if (p)
*p = 0;
struct stat fs;
if (stat(s, &fs) != 0 || !S_ISDIR(fs.st_mode)) {
dsyslog("creating directory %s", s);
if (mkdir(s, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == -1) {
LOG_ERROR_STR(s);
result = false;
break;
}
}
if (p)
*p++ = '/';
else
break;
}
free(s);
return result;
}
bool RemoveFileOrDir(const char *FileName, bool FollowSymlinks)
{
struct stat st;
if (stat(FileName, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
DIR *d = opendir(FileName);
if (d) {
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
char *buffer;
asprintf(&buffer, "%s/%s", FileName, e->d_name);
if (FollowSymlinks) {
int size = strlen(buffer) * 2; // should be large enough
char *l = MALLOC(char, size);
int n = readlink(buffer, l, size);
if (n < 0) {
if (errno != EINVAL)
LOG_ERROR_STR(buffer);
}
else if (n < size) {
l[n] = 0;
dsyslog("removing %s", l);
if (remove(l) < 0)
LOG_ERROR_STR(l);
}
else
esyslog("ERROR: symlink name length (%d) exceeded anticipated buffer size (%d)", n, size);
free(l);
}
dsyslog("removing %s", buffer);
if (remove(buffer) < 0)
LOG_ERROR_STR(buffer);
free(buffer);
}
}
closedir(d);
}
else {
LOG_ERROR_STR(FileName);
return false;
}
}
dsyslog("removing %s", FileName);
if (remove(FileName) < 0) {
LOG_ERROR_STR(FileName);
return false;
}
}
else if (errno != ENOENT) {
LOG_ERROR_STR(FileName);
return false;
}
return true;
}
bool RemoveEmptyDirectories(const char *DirName, bool RemoveThis)
{
DIR *d = opendir(DirName);
if (d) {
bool empty = true;
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..") && strcmp(e->d_name, "lost+found")) {
char *buffer;
asprintf(&buffer, "%s/%s", DirName, e->d_name);
struct stat st;
if (stat(buffer, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
if (!RemoveEmptyDirectories(buffer, true))
empty = false;
}
else
empty = false;
}
else {
LOG_ERROR_STR(buffer);
free(buffer);
return false;
}
free(buffer);
}
}
closedir(d);
if (RemoveThis && empty) {
dsyslog("removing %s", DirName);
if (remove(DirName) < 0) {
LOG_ERROR_STR(DirName);
return false;
}
}
return empty;
}
else
LOG_ERROR_STR(DirName);
return false;
}
char *ReadLink(const char *FileName)
{
char RealName[PATH_MAX];
const char *TargetName = NULL;
int n = readlink(FileName, RealName, sizeof(RealName) - 1);
if (n < 0) {
if (errno == ENOENT || errno == EINVAL) // file doesn't exist or is not a symlink
TargetName = FileName;
else // some other error occurred
LOG_ERROR_STR(FileName);
}
else if (n < int(sizeof(RealName))) { // got it!
RealName[n] = 0;
TargetName = RealName;
}
else
esyslog("ERROR: symlink's target name too long: %s", FileName);
return TargetName ? strdup(TargetName) : NULL;
}
bool SpinUpDisk(const char *FileName)
{
static char *buf = NULL;
for (int n = 0; n < 10; n++) {
free(buf);
if (DirectoryOk(FileName))
asprintf(&buf, "%s/vdr-%06d", *FileName ? FileName : ".", n);
else
asprintf(&buf, "%s.vdr-%06d", FileName, n);
if (access(buf, F_OK) != 0) { // the file does not exist
timeval tp1, tp2;
gettimeofday(&tp1, NULL);
int f = open(buf, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
// O_SYNC doesn't work on all file systems
if (f >= 0) {
close(f);
system("sync");
remove(buf);
gettimeofday(&tp2, NULL);
double seconds = (((long long)tp2.tv_sec * 1000000 + tp2.tv_usec) - ((long long)tp1.tv_sec * 1000000 + tp1.tv_usec)) / 1000000.0;
if (seconds > 0.5)
dsyslog("SpinUpDisk took %.2f seconds\n", seconds);
return true;
}
else
LOG_ERROR_STR(buf);
}
}
esyslog("ERROR: SpinUpDisk failed");
return false;
}
time_t LastModifiedTime(const char *FileName)
{
struct stat fs;
if (stat(FileName, &fs) == 0)
return fs.st_mtime;
return 0;
}
const char *WeekDayName(int WeekDay)
{
static char buffer[4];
WeekDay = WeekDay == 0 ? 6 : WeekDay - 1; // we start with monday==0!
if (0 <= WeekDay && WeekDay <= 6) {
const char *day = tr("MonTueWedThuFriSatSun");
day += WeekDay * 3;
strncpy(buffer, day, 3);
return buffer;
}
else
return "???";
}
const char *WeekDayName(time_t t)
{
struct tm tm_r;
return WeekDayName(localtime_r(&t, &tm_r)->tm_wday);
}
const char *DayDateTime(time_t t)
{
static char buffer[32];
if (t == 0)
time(&t);
struct tm tm_r;
tm *tm = localtime_r(&t, &tm_r);
snprintf(buffer, sizeof(buffer), "%s %2d.%02d %02d:%02d", WeekDayName(tm->tm_wday), tm->tm_mday, tm->tm_mon + 1, tm->tm_hour, tm->tm_min);
return buffer;
}
// --- cPoller ---------------------------------------------------------------
cPoller::cPoller(int FileHandle, bool Out)
{
numFileHandles = 0;
Add(FileHandle, Out);
}
bool cPoller::Add(int FileHandle, bool Out)
{
if (FileHandle >= 0) {
for (int i = 0; i < numFileHandles; i++) {
if (pfd[i].fd == FileHandle)
return true;
}
if (numFileHandles < MaxPollFiles) {
pfd[numFileHandles].fd = FileHandle;
pfd[numFileHandles].events = Out ? POLLOUT : POLLIN;
numFileHandles++;
return true;
}
esyslog("ERROR: too many file handles in cPoller");
}
return false;
}
bool cPoller::Poll(int TimeoutMs)
{
if (numFileHandles) {
if (poll(pfd, numFileHandles, TimeoutMs) != 0)
return true; // returns true even in case of an error, to let the caller
// access the file and thus see the error code
}
return false;
}
// --- cFile -----------------------------------------------------------------
bool cFile::files[FD_SETSIZE] = { false };
int cFile::maxFiles = 0;
cFile::cFile(void)
{
f = -1;
}
cFile::~cFile()
{
Close();
}
bool cFile::Open(const char *FileName, int Flags, mode_t Mode)
{
if (!IsOpen())
return Open(open(FileName, Flags, Mode));
esyslog("ERROR: attempt to re-open %s", FileName);
return false;
}
bool cFile::Open(int FileDes)
{
if (FileDes >= 0) {
if (!IsOpen()) {
f = FileDes;
if (f >= 0) {
if (f < FD_SETSIZE) {
if (f >= maxFiles)
maxFiles = f + 1;
if (!files[f])
files[f] = true;
else
esyslog("ERROR: file descriptor %d already in files[]", f);
return true;
}
else
esyslog("ERROR: file descriptor %d is larger than FD_SETSIZE (%d)", f, FD_SETSIZE);
}
}
else
esyslog("ERROR: attempt to re-open file descriptor %d", FileDes);
}
return false;
}
void cFile::Close(void)
{
if (f >= 0) {
close(f);
files[f] = false;
f = -1;
}
}
bool cFile::Ready(bool Wait)
{
return f >= 0 && AnyFileReady(f, Wait ? 1000 : 0);
}
bool cFile::AnyFileReady(int FileDes, int TimeoutMs)
{
fd_set set;
FD_ZERO(&set);
for (int i = 0; i < maxFiles; i++) {
if (files[i])
FD_SET(i, &set);
}
if (0 <= FileDes && FileDes < FD_SETSIZE && !files[FileDes])
FD_SET(FileDes, &set); // in case we come in with an arbitrary descriptor
if (TimeoutMs == 0)
TimeoutMs = 10; // load gets too heavy with 0
struct timeval timeout;
timeout.tv_sec = TimeoutMs / 1000;
timeout.tv_usec = (TimeoutMs % 1000) * 1000;
return select(FD_SETSIZE, &set, NULL, NULL, &timeout) > 0 && (FileDes < 0 || FD_ISSET(FileDes, &set));
}
bool cFile::FileReady(int FileDes, int TimeoutMs)
{
fd_set set;
struct timeval timeout;
FD_ZERO(&set);
FD_SET(FileDes, &set);
if (TimeoutMs >= 0) {
if (TimeoutMs < 100)
TimeoutMs = 100;
timeout.tv_sec = TimeoutMs / 1000;
timeout.tv_usec = (TimeoutMs % 1000) * 1000;
}
return select(FD_SETSIZE, &set, NULL, NULL, (TimeoutMs >= 0) ? &timeout : NULL) > 0 && FD_ISSET(FileDes, &set);
}
bool cFile::FileReadyForWriting(int FileDes, int TimeoutMs)
{
fd_set set;
struct timeval timeout;
FD_ZERO(&set);
FD_SET(FileDes, &set);
if (TimeoutMs < 100)
TimeoutMs = 100;
timeout.tv_sec = 0;
timeout.tv_usec = TimeoutMs * 1000;
return select(FD_SETSIZE, NULL, &set, NULL, &timeout) > 0 && FD_ISSET(FileDes, &set);
}
// --- cSafeFile -------------------------------------------------------------
cSafeFile::cSafeFile(const char *FileName)
{
f = NULL;
fileName = ReadLink(FileName);
tempName = fileName ? MALLOC(char, strlen(fileName) + 5) : NULL;
if (tempName)
strcat(strcpy(tempName, fileName), ".$$$");
}
cSafeFile::~cSafeFile()
{
if (f)
fclose(f);
unlink(tempName);
free(fileName);
free(tempName);
}
bool cSafeFile::Open(void)
{
if (!f && fileName && tempName) {
f = fopen(tempName, "w");
if (!f)
LOG_ERROR_STR(tempName);
}
return f != NULL;
}
bool cSafeFile::Close(void)
{
bool result = true;
if (f) {
if (ferror(f) != 0) {
LOG_ERROR_STR(tempName);
result = false;
}
if (fclose(f) < 0) {
LOG_ERROR_STR(tempName);
result = false;
}
f = NULL;
if (result && rename(tempName, fileName) < 0) {
LOG_ERROR_STR(fileName);
result = false;
}
}
else
result = false;
return result;
}
// --- cLockFile -------------------------------------------------------------
#define LOCKFILENAME ".lock-vdr"
#define LOCKFILESTALETIME 600 // seconds before considering a lock file "stale"
cLockFile::cLockFile(const char *Directory)
{
fileName = NULL;
f = -1;
if (DirectoryOk(Directory))
asprintf(&fileName, "%s/%s", Directory, LOCKFILENAME);
}
cLockFile::~cLockFile()
{
Unlock();
free(fileName);
}
bool cLockFile::Lock(int WaitSeconds)
{
if (f < 0 && fileName) {
time_t Timeout = time(NULL) + WaitSeconds;
do {
f = open(fileName, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (f < 0) {
if (errno == EEXIST) {
struct stat fs;
if (stat(fileName, &fs) == 0) {
if (abs(time(NULL) - fs.st_mtime) > LOCKFILESTALETIME) {
esyslog("ERROR: removing stale lock file '%s'", fileName);
if (remove(fileName) < 0) {
LOG_ERROR_STR(fileName);
break;
}
continue;
}
}
else if (errno != ENOENT) {
LOG_ERROR_STR(fileName);
break;
}
}
else {
LOG_ERROR_STR(fileName);
break;
}
if (WaitSeconds)
sleep(1);
}
} while (f < 0 && time(NULL) < Timeout);
}
return f >= 0;
}
void cLockFile::Unlock(void)
{
if (f >= 0) {
close(f);
remove(fileName);
f = -1;
}
}
// --- cListObject -----------------------------------------------------------
cListObject::cListObject(void)
{
prev = next = NULL;
}
cListObject::~cListObject()
{
}
void cListObject::Append(cListObject *Object)
{
next = Object;
Object->prev = this;
}
void cListObject::Insert(cListObject *Object)
{
prev = Object;
Object->next = this;
}
void cListObject::Unlink(void)
{
if (next)
next->prev = prev;
if (prev)
prev->next = next;
next = prev = NULL;
}
int cListObject::Index(void)
{
cListObject *p = prev;
int i = 0;
while (p) {
i++;
p = p->prev;
}
return i;
}
// --- cListBase -------------------------------------------------------------
cListBase::cListBase(void)
{
objects = lastObject = NULL;
}
cListBase::~cListBase()
{
Clear();
}
void cListBase::Add(cListObject *Object, cListObject *After)
{
if (After && After != lastObject) {
After->Next()->Insert(Object);
After->Append(Object);
}
else {
if (lastObject)
lastObject->Append(Object);
else
objects = Object;
lastObject = Object;
}
}
void cListBase::Ins(cListObject *Object, cListObject *Before)
{
if (Before && Before != objects) {
Before->Prev()->Append(Object);
Before->Insert(Object);
}
else {
if (objects)
objects->Insert(Object);
else
lastObject = Object;
objects = Object;
}
}
void cListBase::Del(cListObject *Object, bool DeleteObject)
{
if (Object == objects)
objects = Object->Next();
if (Object == lastObject)
lastObject = Object->Prev();
Object->Unlink();
if (DeleteObject)
delete Object;
}
void cListBase::Move(int From, int To)
{
Move(Get(From), Get(To));
}
void cListBase::Move(cListObject *From, cListObject *To)
{
if (From && To) {
if (From->Index() < To->Index())
To = To->Next();
if (From == objects)
objects = From->Next();
if (From == lastObject)
lastObject = From->Prev();
From->Unlink();
if (To) {
if (To->Prev())
To->Prev()->Append(From);
From->Append(To);
}
else {
lastObject->Append(From);
lastObject = From;
}
if (!From->Prev())
objects = From;
}
}
void cListBase::Clear(void)
{
while (objects) {
cListObject *object = objects->Next();
delete objects;
objects = object;
}
objects = lastObject = NULL;
}
cListObject *cListBase::Get(int Index) const
{
if (Index < 0)
return NULL;
cListObject *object = objects;
while (object && Index-- > 0)
object = object->Next();
return object;
}
int cListBase::Count(void) const
{
int n = 0;
cListObject *object = objects;
while (object) {
n++;
object = object->Next();
}
return n;
}
void cListBase::Sort(void)
{
bool swapped;
do {
swapped = false;
cListObject *object = objects;
while (object) {
if (object->Next() && *object->Next() < *object) {
Move(object->Next(), object);
swapped = true;
}
object = object->Next();
}
} while (swapped);
}