Seems as if every other week the Engineer Insight article discusses byte swapping. Must be that time because that's my topic today. Up until now we've always written about what one should do, in theory. Finishing up the first Intel release has given us all more practical experience and I'd like to share some of it with you.
There are several sections of the API, including the BMessage
class and
file attributes, where dealing with endian issues might effect your
coding habits. I'll discuss the BMessage
class in this article and leave
the file attributes case for a later article.
The BMessage
class does lots of work for you in terms of byte ordering.
If you are just reading and writing standard data types to BMessage
s then
you needn't fret about byte ordering. By "standard" I mean all the data
types (with three exceptions) defined in
support/TypeConstants.h
.
For example, if you have code like the following:
// code that writes your data into a messagemsg
->AddString
("label",text
);msg
->AddInt32
("weight",weight
);msg
->AddMessenger
("return_ref",some_messenger
); // let's save this message to a filemsg
->Flatten
(&some_file
); ... // That file can now be moved to another platform. What // happens isn't under your program's control. ... // let's read that message out of a filemsg
->Unflatten
(&some_file
); // code that reads your data out of a messagemsg
->FindString
("label", &text
);msg
->FindInt32
("weight", &weight
);msg
->FindMessenger
("return_ref", &some_messenger
);
In the above code you don't have to worry about byte ordering. If the
file is moved across platforms the system (the
Flatten
/Unflatten
calls
themselves) will handle any byte ordering issues. The same is true if the
message is flattened into any sort of buffer and then moved to another
platform. An example of this would be if you flattened a message and
saved it in an attribute of some file.
The three exceptions mentioned above are B_ANY_TYPE
,
B_RAW_TYPE
, and
B_OBJECT_TYPE
. The first type, B_ANY_TYPE
, isn't really a type. The
latter two types have arbitrary and undefined formats. They can be
anything, so there is no way that the system can properly swap bytes
between Intel and PPC platforms.
Also, it is the developers responsibility to swap any custom data types that you add to a message. Here's an example:
struct my_info { int32weight
; charbx
[2]; floatpercent_done
; }; ... my_infoinfo
; // info gets filled in with data // now you add data of this type to a messagemsg
->AddData
("data", 'myin', &info
,sizeof
(my_info
));
Now if msg
is flattened and moved to platform with differing endianness
you'll have to deal with byte swapping:
my_infoinfo
; void *raw
; ssize_tsize
;msg
->FindData
("data", 'myin', &raw
, &size
); // raw points to raw/unswapped data, not necessarily a // valid my_info structure. If endianness of this platform // differs from source platform then work needs to be done.
One option is to add a field to the my_info struct specifying the endianness of the data as written. The other option is to redo the data structure so that it is endian insensitive. For more details on dealing with vanilla C structures such as my_info see Brad Taylor's Engineering Insights articles in issue 2-#9 and issue 2-#45 of the Newsletter:
Another alternative that I'll discuss in detail is using the BFlattenable
class. Using this class can make handling custom data structures somewhat
easier. Let's redo the above example of a C structure (my_info) using the
BFlattenable
class:
structTMyData
: publicBFlattenable
{ // here's my data int32weight
; charbx
[2]; floatpercent_done
; virtual status_tFlatten
(void *buffer
, ssize_tsize
) const; virtual status_tUnflatten
(type_codec
, constvoid *buf
, ssize_tsize
); // overrides for other functions are omitted to keep example small };
The BFlattenable
class gives you nice bottlenecks for moving data in and
out of BMessages
, files, or file attributes. These bottlenecks give you a
framework for dealing with byte ordering. As always you have two options
for dealing with this issue:
Write out data in natural order and swap when needed on read.
Write out data in canonical format and swap when needed on read.
In this scenario the flatten code will save the flag indicating the endianness of the source platform. The unflatten code will compare that against the current platform's endianness and swap when necessary.
status_tTMyData
::Flatten
(void *buffer
, ssize_tsize
) const { char *p
= (char *) buffer; // need to save a flag indicating the current platform. So // we remember if this platform is little endian. The // FlattenSize function must account for this extra space. *p
++ =B_HOST_IS_LENDIAN
; // now save the rest of the data in natural format *((int32 *)p
) =weight
;p
+=sizeof
(int32); *p
++ =bx
[0]; *p
++ =bx
[1]; *((float *)p
) =percent_done
; returnB_OK
; } status_tTMyData
::Unflatten
(type_codec
, constvoid *buf
, ssize_tsize
) { const char *p
= (const char *)buf
; uint8endian
; boolmust_swap
; // read the endian flag saved by the Flatten callendian
= *((uint8 *)p
++); // compared the saved value with current valuemust_swap
= (endian
==B_HOST_IS_LENDIAN
); // must_swap will only be true in the source and // destination have different byte ordering // now simply read out the dataweight
= *((int32 *)p
);p
+=sizeof
(int32);bx
[0] = *p
++;bx
[1] = *p
++;percent_done
= *((float *)p
); // now swap the data if needed if (must_swap
) {weight
=B_SWAP_INT32
(weight
);percent_done
=B_SWAP_FLOAT
(percent_done
); // don't need to swap the bx chars. } returnB_OK
; }
In this scenario the code always writes out the data in little endian
format (choosing between big or little endian is up to the developer). In
this case an extra flag
field isn't needed.
status_tTMyData
::Flatten
(void *buffer
, ssize_tsize
) const { char *p
= (char *)buffer
; // Decided to always write the data in little endian // format. So we'll write data using the HOST_TO_LENDIAN // macros. *((int32 *)p
) =B_HOST_TO_LENDIAN_INT32
(weight
);p
+=sizeof
(int32); *p
++ =bx
[0]; *p
++ =bx
[1]; *((float *)p
) =B_HOST_TO_LENDIAN_FLOAT
(percent_done
); returnB_OK
; } status_tTMyData
::Unflatten
(type_codec
, constvoid *buf
, ssize_tsize
) { const char *p
= (const char *)buf
; uint8endian
; boolmust_swap
; // We know that the data was saved in little endian // format. So read in the data using the appropriate // macros from support/ByteOrder.hweight
=B_LENDIAN_TO_HOST_INT32
(*((int32 *)p
));p
+=sizeof
(int32);bx
[0] = *p
++;bx
[1] = *p
++;percent_done
=B_LENDIAN_TO_HOST_INT32
(*((float *)p
)); returnB_OK
; }
And there you have it. Hopefully this helps clarify how to use BMessage
s
in a multi-endian world. It's the PC thing to do.
One of the great features of the BeOS is the ability to query information about files' attributes. This is both useful in programming and for the general user wanting to find files that meet certain criteria. The mechanism behind queries is a series of attribute indices that list the files that can be searched given a particular attribute and value.
Indices are volume-specific, meaning that different volumes connected to your machine can have different indexed attributes. Furthermore, an attribute index is only updated whenever a matching attribute is written. So any files on the volume before the index is created are not listed. The BeOS does not step through and examine all of the files on the volume every time an index is created (that could be a lengthy process on a large volume.)
This week's sample code allows you to selectively re-index files to make
sure that all of their attributes are up-to-date in the attribute
indices. Indexer is comprised of a BApplication
class that examines
entry_refs, various InfoView
classes
that process and display information
about these refs, and some global indexing functions. There is an
InfoView
for each type of entry: File, SymLink, Directory and Volume.
These classes combine to give you detailed information about each dropped
ref in addition to re-indexing any referenced files that need it.
When a reference to a file, directory, symbolic link, or volume enters
Indexer, more often than not it arrives in the form of an entry_ref. Full
details about entry_refs can be found in
Entry.h
, but quickly an
entry_ref specifies the location of a potential file on disk by
specifying a device number, a directory number, and a name. Paths also
specify location in a similar manner, and throughout Indexer you will see
various translations between these two entry formats. One thing to note,
however, is that entry_refs should not be stored on disk, as the device
number can change between boots. entry_refs saved to disk, and then
copied in a file to another disk will just not work when unpacked on the
other side. Use paths if you need to save the location of an entry to
disk.
When items are selected and dropped on Indexer's icon or one of its
windows from the Tracker, the refs come bundled in either a
B_REFS_RECEIVED
or B_SIMPLE_DATA
message. Both of these messages are
similar: The real meat of the message is bundled into the "refs" members.
Parsing the message to get the references is the first task.
voidIndexer
::RefsReceived
(BMessage
*msg
) { uint32type
; int32count
; entry_refref
;msg
->GetInfo
("refs", &type
, &count
); if (type
!=B_REF_TYPE
) return; for (int32i
= --count
;i
>= 0;i
--) { if (msg
->FindRef
("refs",i
, &ref
) ==B_OK
) {EvaluateRef
(ref
); } } }
Each entry_ref pulled out of the message is passed
along to EvaluateRef()
to start our real work. Indexer also accepts arguments from the
command-line which are processed in its ArgvReceived()
function. Any
paths present are translated into entry_refs with the
get_ref_for_path()
function, and the resulting refs are also passed to EvaluateRef()
.
status_tIndexer
::EvaluateRef
(entry_ref &ref
) { struct statst
;BEntry
entry
; if (entry
.SetTo
(&ref
,false
) !=B_OK
) returnB_ERROR
; if (entry
.GetStat
(&st
) !=B_OK
) returnB_ERROR
; if (S_ISLNK
(st
.st_mode
)) returnHandleLink
(ref
,st
); else if (S_ISREG
(st
.st_mode
)) returnHandleFile
(ref
,st
); else if (S_ISDIR
(st
.st_mode
)) {BDirectory
dir
; if (dir
.SetTo
(&ref
) !=B_OK
) returnB_ERROR
; if (dir
.IsRootDirectory
()) returnHandleVolume
(ref
,st
,dir
); else returnHandleDirectory
(ref
,st
,dir
); } }
The first thing we want to do is determine exactly what type of entry_ref
we have gotten. To do this we need to get a stat structure. The stat
structure provides a host of information about entries, including the
flavor of the node, and creation and modification times. To get the stat
we need an instance of the BStatable
class, and
BEntry
is an easy one to
get. As we want to be able to tell if we have a symbolic link, we inform
BEntry
::SetTo()
to not traverse any links it finds.
We can then proceed to get the stat and discover what type of entry we have. If we discover a file or a symlink, we simple start to handle them. If we find a directory we need to do a little bit more examination to determine whether we have a normal directory or a volume. Volumes are represented as directories that live in the root of the file system. To determine which type we have we create a BDirectory and ask it if it is a root directory. If it is, we handle it as a volume, if not we handle it as a directory.
As Indexer is a fairly large bit of sample code, I won't go into all of the details of processing each entry type. Instead I'll simply point out some of the more interesting bits, and leave you to examine the rest of the sample code in more depth later.
As I mentioned above, I determine that I have a volume when I receive a reference to a root directory. The next step is to discover exactly what volume I am supposed to examine.
This is trickier than you might think, as there is not a convenient
GetVolume()
function in the BDirectory
class. My first thoughts were to
use the device ID contained in the entry_ref. While this might seem to be
a good idea, it doesn't actually work. The problem is that root is a
virtual device, and the device ID listed corresponds to it, and not to
the volume I want to explore. So I turned to the BVolumeRoster
class so I
could step through each mounted volume and see if I can find a root
directory that matches mine. The code looks like this:
status_tIndexer
::HandleVolume
(entry_ref &ref
, struct stat &st
,BDirectory
&dir
) {BVolumeRoster
vol_roster
;BVolume
vol
;BDirectory
root_dir
; dev_tdevice
; while (vol_roster
.GetNextVolume
(&vol
) ==B_NO_ERROR
) {vol
.GetRootDirectory
(&root_dir
); if (root_dir
==dir
) break; } // build the info window and the like ... }
I then proceed to create my InfoView
, with gathers up a ton of
information about the volume (essentially every detail that the BVolume
class can muster) and displays it the user. Among the information
presented is a list of all of the indexed attributes on the volume. I
collected this with a global function I defined called
get_attribute_indices()
.
extern status_tget_attribute_indices
(dev_tdevice
,BList
&index_list
) { DIR *index_dir
; struct dirent *index_ent
;index_dir
=fs_open_index_dir
(device
); if (!index_dir
) returnB_ERROR
; while (index_ent
=fs_read_index_dir
(index_dir
)) { char *text
=strdup
(index_ent
->d_name
);index_list
.AddItem
(text
); }fs_close_index_dir
(index_dir
); returnB_OK
; }
This function opens the volume's index directory, gets the name of every attribute found there, adds them to the list, and then closes the directory. Note that it is very important to close the index directory, or you will be unable to unmount its volume.
You might also notice that I did not traverse the entire volume indexing
every file. I could easily do this, but I figured that it made more sense
to just display a list of all of the indexed attributes instead. The code
could easily be modified to do so by borrowing the appropriate functions
from dInfoView
.
I do traverse directories and re-index all of the files found there, as
well as collect a bunch of information about the contents of the
directory. The two functions of most interest in the dInfoView class are
the TraverseDirectory()
and
EvaluateRef()
functions. TraverseDirectory()
is a very standard way to step through all of the items in a directory.
It gets an entry_ref for each item found in the directory and calls
EvaluateRef()
on it. I could also achieve the same result by iterating
through the directory with GetNextEntry()
(or GetNextDirent()
for that
matter), I simply chose to use entry_refs.
voiddInfoView
::TraverseDirectory
(BDirectory
&dir
) { entry_refref
; while(dir
.GetNextRef
(&ref
) !=B_ENTRY_NOT_FOUND
) {EvaluateRef
(ref
); } }
EvaluateRef()
is called for every item found in the directory. The type
of entry is discovered and processed accordingly: sub-directories are
traversed, files are indexed, and symlinks are noted. Statistics are kept
for each type of item, including a count of how successfully a file was
re-indexed. I'll get to the reindex_node()
call when I discuss files a
little later.
voiddInfoView
::EvaluateRef
(entry_ref &ref
) { struct statst
;BEntry
entry
; if (entry
.SetTo
(&ref
,false
) !=B_OK
) return;fEntryCount
++;entry
.GetStat
(&st
); if (S_ISLNK
(st
.st_mode
))fLinkCount
++; else if (S_ISREG
(st
.st_mode
)) { if (ref
.device
!=fRef
.device
) {fInvalidCount
++; return; }fFileCount
++;BNode
node
; if (node
.SetTo
(&ref
) !=B_NO_ERROR
) {fInvalidCount
++; return; } status_tstatus
=B_OK
;status
=reindex_node
(node
, *fIndexList
); if (status
==INDEXED
)fIndexed
++; else if (status
==PARTIAL_INDEXED
)fPartialIndexed
++; elsefNotIndexed
++; } else if (S_ISDIR
(st
.st_mode
)) {BDirectory
dir
; if (dir
.SetTo
(&ref
) !=B_OK
) {fInvalidCount
++; return; } if (dir
.IsRootDirectory
())fInvalidCount
++; else {fSubDirCount
++;TraverseDirectory
(dir
); } } }
Symlinks are by far the easiest class to deal with. A BSymLink
is created
and information about it is displayed. The main bit of information is
whether the link is relative or absolute, and the actual path to the
linked to node. This information is retrieved with the ReadLink()
function.
I'll also note that all of the InfoView
s collect some generic information
about the entries they receive, namely the creation and modification time
of the entry, its path, and its name.
Finally I'll note some peculiarities about processing links. When the
Tracker bundles up a B_SIMPLE_DATA
message to drop on the window of an
application, it simply gets the entry_ref of all of the items selected.
When dropping onto the icon of an application, however, the Tracker looks
at a lot of information about the file, in an attempt to discover whether
the application knows how to handle that type. In the process of doing
this, the entry_ref for a link is traversed. This means that to get an
lInfoView
in Indexer
you need to reference the link through the command
line, or to drop the link onto a window. The current behavior is being
looked at internally, and we hope to have some sort of resolution for
Release 4.
Finally we come to the heart of the original program design, the
re-indexing of a file. To complete this action we need to get a list of
the attributes that are indexed on the volume (the get_volume_indices()
function described above). We then create a BNode
from the ref in order
to have access to the node's attributes, and pass it to the global
reindex_node()
function.
extern status_treindex_node
(BNode
&node
,BList
&index_list) { attr_infoinfo
; status_tstatus
=B_OK
; int32to_be_indexed
= 0; int32indexed
= 0; int32not_indexed
= 0; int32size
= 1024; char *value = (char *)malloc
(size
*sizeof
(char)); //rewrite all of the appropriate attributes for (int32i
= 0;i
<index_list
.CountItems
();i
++) { char *attr
= (char *)index_list
.ItemAt
(i
); if (node
.GetAttrInfo
(attr
, &info
) ==B_OK
) {to_be_indexed
++; // adjust the size of our static buffer if necessary if (info
.size
>size
) {value
= (char *)realloc
(value
,info
.size
);size
=info
.size
; } if (node
.ReadAttr
(attr
,info
.type
, 0,value
,info
.size
) > 0) { if (node
.WriteAttr
(attr
,info
.type
, 0,value
,info
.size
) > 0)indexed
++; elsenot_indexed
++; } elsenot_indexed
++; } }free
(value
);value
=NULL
; if (to_be_indexed
> 0) { if (indexed
> 0) { if (not_indexed
> 0) returnPARTIAL_INDEXED
; else returnINDEXED
; } else returnNOT_INDEXED
; } else returnNOT_INDEXED
; }
reindex_node()
steps through the list of indexed attributes and calls
BNode
::GetAttrInfo()
to see if the node has a matching attribute. If so,
ReadAttr()
is used to read the value into the buffer, and if successful,
the same value is written back with WriteAttr()
. Simply rewriting the
attribute will cause it to be added to the index if not already present.
A running count of the matching attributes and whether they were successfully rewritten is kept, and the function returns a status detailing whether the file was indexed successfully, partially, or not at all.
The fInfoView
also compiles a list of all of the file's attributes for
display purposes. GetAttributes()
steps through the file's attribute
directory and adds the name of each attribute to a list.
voidfInfoView
::GetAttributes
(BNode
&node
,BList
&list
) { charattr_buf
[B_ATTR_NAME_LENGTH
];node
.RewindAttrs
(); while (node
.GetNextAttrName
(attr_buf
) ==B_NO_ERROR
) { char *string
=strdup
(attr_buf
);list
.AddItem
(string
); } }
As a last note on files, the fInfoView
also discovers the MIME-type of
the file through use of BNodeInfo
::GetMimeType()
.
Finally, some last notes, suggestions, caveats and plans. There are a
series of volumes that cannot be accessed through the Tracker (such as
/dev
or
/pipe
) that information can be gotten about through the
command-line. Take a look at a couple of them and see what can be seen
(although not much will be seen in the virtual file systems in the way of
files, the info windows usually fail.)
I also want to note that the code for displaying the information is probably not how the interface guys would want to see it done. Use the sample code to look at the Storage Kit classes, not as a pristine example of Interface Kit code.
I'll continue to update Indexer in the future, and I would love to hear
of any improvements you make. Upcoming features might include code to
remove and create indexes, as well as a look into whether passing BEntry
s
or entry_refs is more efficient.
In the meantime, Indexer can be found at: ftp://ftp.be.com/pub/samples/storage_kit/obsolete/Indexer.zip.
I'll be back next week with Victor Tsou from the Doc Team. We'll be filling you in on many of the things you'll want to know about the upcoming Release 3 release of the BeOS for Intel. Until then...
This isn't about my college-bound son and the education of our auto insurance company. It's an attempt to put some thoughts on electronic paper concerning the sea of driver trouble ahead of us.
Once upon a time, we had totally proprietary hardware. This was at the very beginning of the company, when multiprocessor hardware was the province of high-end servers and workstations, not down to the $2000-$3000 level as it is today (see the Ming specials at http://www.be.com/support/guides/ming-specials.html).
In the beginning, we were in the position of having to design I/O cards and write the drivers. Now though, we are in the retroactively obvious situation where we can benefit from other people's investment in chipsets, motherboards, and I/O devices, and focus our efforts on the OS.
But the abundance of riches comes at a price: how can we get drivers to support this wonderful wacky world of PC add-ons? Put another way, some observers have attributed OS/2's failure to a paucity of drivers. Customers eager for an alternative to Windows were disappointed to find that their interface cards and peripherals weren't always supported by IBM or by the hardware add-on manufacturer. Are we going to disappoint hopeful BeOS users in the same way? Are we going to bleed to death attempting to write drivers or cajoling vendors for support?
The hidden pivot in the argument is the comparison to OS/2. If indeed, we come out of nowhere representing ourselves as a general-purpose OS ready for mainstream consumption, then we face an impossible challenge. Put another way, OS/2 positioned itself as an alternative to Windows and failed to deliver what turned out to be an impossible proposition, better DOS than DOS, better Windows than Windows.
The BeOS is not a general-purpose OS, it is a complement or a supplement to Windows. The BeOS coexists with the general-purpose Windows as a specialized OS, just as Linux does. Fine, but what does it mean for the sea of drivers issue?
First, it means we have to keep setting expectations, just as we did in earlier days with tongue-in-cheek but serious surgeon general warnings: this product is unfit for consumption by normal humans. It still is and, just like Linux, will be for a long time—if not for ever.
As an industry such as ours mature, it will diversify, segment and specialize, and the thought that any OS has to be or do "everything" doesn't have to apply. More specifically, as we represent ourselves as the media OS, best fit for real-time WYSIWYG, the reality-based positioning, a Be, Inc. exclusive, translates into a focus set of hardware targets. This, in turn, translates into a finite set of drivers. That's what we have to define, that's what we have to communicate.
BeDevTalk is an unmonitored discussion group in which technical information is shared by Be developers and interested parties. In this column, we summarize some of the active threads, listed by their subject lines as they appear, verbatim, in the mail.
To subscribe to BeDevTalk, visit the mailing list page on our web site: http://www.be.com/aboutbe/mailinglists.html.
The file descriptor limit (128) was less controversial than the "how many threads should I spawn?" subtopic. A statement by Dominic Giampaolo...
“Unless you know what you're doing, creating more threads than there are CPUs in the system is probably not wise.”
...was jumped on (and, despite Sander Stoks plea for caution) taken out of context. The specifics of an app (what it's doing, how fast it needs to do it) will temper Dominic's rule of thumb. For example (from Peter Mogensen)...
“The reason I have to use lots of threads is that I have a lot of identical objects and I *need* one 'concurrency context' per object. ...and for now I do *not* care about performance.”
Client management (one thread per client) was also cited as a legitimate use of "too many" threads.
More menu talk: Should all documents have a main menu bar, and should that bar have a reliable set of submenus? (And should those submenus have submenus? etc.) Should the user be able to choose the menu style as an emulation of some other OS (gimme Mac menus... gimme Windows menus... gimme Amiga menus)? Taken out of context, Tyler Riti supplied the quote of the week:
“People think that if they can't see something, then it isn't there.”
(Is this not so?)
The discussion teetered on the verge of the old Dionysian/Apollonian argument that pits GUI customizability against consistency. Despite the high noise level, a number of valid suggestions were made, some of them bordering on the interesting.
“The global be_app variable could return a pointer to a BMenu
or
BMenuItem
allowing the programmer to add application menu items or
submenus to it's Icon/Label in the DeskBar.” (Scott Ahten)
“Somebody should create a set of standard translations [OK, Cancel, Open, etc.] that everyone can put in their apps to provide at least _some_ internationality.” (Sean Gies)
“A floating app-level menu is not a terrible idea...we could have application level menus that are only visible when the application is in the foreground.” (Claude I. Denton)
“Make every toolbar dockable. If it's floating, all of the app's windows use the floating toolbar; if it's docked to a part of the window, each window has its own copy.” (Andi Payn)
And so on.
You make an icon-sized image in IconWorld and add the image file to your BeIDE project expecting to be able to access the data as a resource of the app. But you can't. What's wrong? Brian Stern offered the following IDE guidelines:
“You need the resource to be in a file whose target info indicates that it has resources. The default is for any file with an extension of .rsrc to have its resources copied into the executable during link. Just add the resource files to the project and make. You can add as many resource files as you like to a project.
If that didn't work then either the target info in the Target prefs panel doesn't indicate that that file kind has resources, or there are no resources in the files.”
But, as Wendell Beckwith pointed out, IconWorld saves images as attributes, not resources. Mr. Beckwith has created an Attribute Convertor app that converts attributes to resources for just such situations.
SO_REUSEADDR
—is it necessary? What is it? Scott Andrew says...
“[SO_REUSEADDR
]... is what allows many users to connect the same IP. It's
not necessary for FTP... [but is for a server].”
The right spirit, but, according to Howard Berkey, the wrong letter:
“You can still have many clients connect()
to the same IP address/port
without using SO_REUSEADDR
by using
accept()
and listen()
.”
Agreement all around.
Pixel/coordinate confusion. Remember: 0+1 == 2. A rectangle that has a top left at pixel (0,0) and a bottom right at (1,1) touches 2 pixels on each side—but, nonetheless, it's a 1x1 rectangle. We've seen this discussion before, but never without some objection to the status quo coordination. You're all getting soft.