diff options
Diffstat (limited to 'lib/server/makeprotocol.pl.in')
-rwxr-xr-x | lib/server/makeprotocol.pl.in | 497 |
1 files changed, 324 insertions, 173 deletions
diff --git a/lib/server/makeprotocol.pl.in b/lib/server/makeprotocol.pl.in index a074b435..d6c0e216 100755 --- a/lib/server/makeprotocol.pl.in +++ b/lib/server/makeprotocol.pl.in @@ -78,7 +78,7 @@ sub add_type my ($protocol_name, $cpp_name, $header_file) = split /\s+/,$_[0]; $translate_type_info{$protocol_name} = [0, $cpp_name]; - push @extra_header_files, $header_file; + push @extra_header_files, $header_file if $header_file; } # check attributes @@ -158,7 +158,10 @@ print CPP <<__E; #include <sstream> #include "$filename_base.h" -#include "IOStream.h" +#include "CollectInBufferStream.h" +#include "MemBlockStream.h" +#include "SelfFlushingStream.h" +#include "SocketStream.h" __E print H <<__E; @@ -174,12 +177,10 @@ print H <<__E; #include <syslog.h> #endif +#include "autogen_ConnectionException.h" #include "Protocol.h" #include "Message.h" -#include "ServerException.h" - -class IOStream; - +#include "SocketStream.h" __E @@ -210,19 +211,26 @@ __E my $request_base_class = "${protocol_name}ProtocolRequest"; my $reply_base_class = "${protocol_name}ProtocolReply"; # the abstract protocol interface -my $protocol_base_class = $protocol_name."ProtocolBase"; +my $custom_protocol_subclass = $protocol_name."Protocol"; +my $client_server_base_class = $protocol_name."ProtocolClientServer"; my $replyable_base_class = $protocol_name."ProtocolReplyable"; +my $callable_base_class = $protocol_name."ProtocolCallable"; +my $send_receive_class = $protocol_name."ProtocolSendReceive"; print H <<__E; -class $protocol_base_class; +class $custom_protocol_subclass; +class $client_server_base_class; +class $callable_base_class; class $replyable_base_class; -class $reply_base_class; class $message_base_class : public Message { public: virtual std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, $context_class &rContext) const; + virtual std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, + $context_class &rContext, IOStream& rDataStream) const; + virtual bool HasStreamWithCommand() const = 0; }; class $reply_base_class @@ -233,17 +241,44 @@ class $request_base_class { }; +class $send_receive_class { +public: + virtual void Send(const $message_base_class &rObject) = 0; + virtual std::auto_ptr<$message_base_class> Receive() = 0; +}; + +class $custom_protocol_subclass : public Protocol +{ +public: + $custom_protocol_subclass(std::auto_ptr<SocketStream> apConn) + : Protocol(apConn) + { } + virtual ~$custom_protocol_subclass() { } + virtual std::auto_ptr<Message> MakeMessage(int ObjType); + virtual const char *GetProtocolIdentString(); + +private: + $custom_protocol_subclass(const $custom_protocol_subclass &rToCopy); +}; + __E print CPP <<__E; std::auto_ptr<$message_base_class> $message_base_class\::DoCommand($replyable_base_class &rProtocol, $context_class &rContext) const { - THROW_EXCEPTION(ConnectionException, Conn_Protocol_TriedToExecuteReplyCommand) + THROW_EXCEPTION(ConnectionException, Protocol_TriedToExecuteReplyCommand) +} + +std::auto_ptr<$message_base_class> $message_base_class\::DoCommand($replyable_base_class &rProtocol, + $context_class &rContext, IOStream& rDataStream) const +{ + THROW_EXCEPTION(ConnectionException, Protocol_TriedToExecuteReplyCommand) } __E -my %cmd_class; +my %cmd_classes; +my $error_message = undef; # output the classes foreach my $cmd (@cmd_list) @@ -262,7 +297,7 @@ foreach my $cmd (@cmd_list) my $cmd_base_class = join(", ", map {"public $_"} @cmd_base_classes); my $cmd_class = $protocol_name."Protocol".$cmd; - $cmd_class{$cmd} = $cmd_class; + $cmd_classes{$cmd} = $cmd_class; print H <<__E; class $cmd_class : $cmd_base_class @@ -294,18 +329,50 @@ __E if(obj_is_type($cmd,'IsError')) { - print H "\tbool IsError(int &rTypeOut, int &rSubTypeOut) const;\n"; - print H "\tstd::string GetMessage() const;\n"; + $error_message = $cmd; + my ($mem_type,$mem_subtype) = split /,/,obj_get_type_params($cmd,'IsError'); + my $error_type = $cmd_constants{"ErrorType"}; + print H <<__E; + $cmd_class(int SubType) : m$mem_type($error_type), m$mem_subtype(SubType) { } + bool IsError(int &rTypeOut, int &rSubTypeOut) const; + std::string GetMessage() const { return GetMessage(m$mem_subtype); }; + static std::string GetMessage(int subtype); +__E } - if(obj_is_type($cmd, 'Command')) + my $has_stream = obj_is_type($cmd, 'StreamWithCommand'); + + if(obj_is_type($cmd, 'Command') && $has_stream) + { + print H <<__E; + std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, + $context_class &rContext, IOStream& rDataStream) const; // IMPLEMENT THIS\n + std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, + $context_class &rContext) const + { + THROW_EXCEPTION_MESSAGE(CommonException, Internal, + "This command requires a stream parameter"); + } +__E + } + elsif(obj_is_type($cmd, 'Command') && !$has_stream) { print H <<__E; std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, $context_class &rContext) const; // IMPLEMENT THIS\n + std::auto_ptr<$message_base_class> DoCommand($replyable_base_class &rProtocol, + $context_class &rContext, IOStream& rDataStream) const + { + THROW_EXCEPTION_MESSAGE(CommonException, NotSupported, + "This command requires no stream parameter"); + } __E } + print H <<__E; + bool HasStreamWithCommand() const { return $has_stream; } +__E + # want to be able to read from streams? print H "\tvoid SetPropertiesFromStreamData(Protocol &rProtocol);\n"; @@ -442,9 +509,9 @@ bool $cmd_class\::IsError(int &rTypeOut, int &rSubTypeOut) const rSubTypeOut = m$mem_subtype; return true; } -std::string $cmd_class\::GetMessage() const +std::string $cmd_class\::GetMessage(int subtype) { - switch(m$mem_subtype) + switch(subtype) { __E foreach my $const (@{$cmd_constants{$cmd}}) @@ -459,7 +526,7 @@ __E print CPP <<__E; default: std::ostringstream out; - out << "Unknown subtype " << m$mem_subtype; + out << "Unknown subtype " << subtype; return out.str(); } } @@ -505,47 +572,44 @@ my $error_class = $protocol_name."ProtocolError"; # the abstract protocol interface print H <<__E; -class $protocol_base_class + +class $client_server_base_class { public: - $protocol_base_class(); - virtual ~$protocol_base_class(); - virtual const char *GetIdentString(); + $client_server_base_class(); + virtual ~$client_server_base_class(); + virtual std::auto_ptr<IOStream> ReceiveStream() = 0; bool GetLastError(int &rTypeOut, int &rSubTypeOut); + int GetLastErrorType() { return mLastErrorSubType; } protected: - void CheckReply(const std::string& requestCommand, - const $message_base_class &rReply, int expectedType); void SetLastError(int Type, int SubType) { mLastErrorType = Type; mLastErrorSubType = SubType; } + std::string mPreviousCommand; + std::string mPreviousReply; private: - $protocol_base_class(const $protocol_base_class &rToCopy); /* do not call */ + $client_server_base_class(const $client_server_base_class &rToCopy); /* do not call */ int mLastErrorType; int mLastErrorSubType; }; -class $replyable_base_class : public virtual $protocol_base_class +class $replyable_base_class : public virtual $client_server_base_class { public: - $replyable_base_class(); + $replyable_base_class() { } virtual ~$replyable_base_class(); - /* - virtual std::auto_ptr<$message_base_class> Receive() = 0; - virtual void Send(const ${message_base_class} &rObject) = 0; - */ - - virtual std::auto_ptr<IOStream> ReceiveStream() = 0; virtual int GetTimeout() = 0; void SendStreamAfterCommand(std::auto_ptr<IOStream> apStream); - + protected: std::list<IOStream*> mStreamsToSend; void DeleteStreamsToSend(); + virtual std::auto_ptr<$message_base_class> HandleException(BoxException& e) const; private: $replyable_base_class(const $replyable_base_class &rToCopy); /* do not call */ @@ -554,24 +618,47 @@ private: __E print CPP <<__E; -$protocol_base_class\::$protocol_base_class() +$client_server_base_class\::$client_server_base_class() : mLastErrorType(Protocol::NoError), mLastErrorSubType(Protocol::NoError) { } -$protocol_base_class\::~$protocol_base_class() +$client_server_base_class\::~$client_server_base_class() { } -const char *$protocol_base_class\::GetIdentString() +const char *$custom_protocol_subclass\::GetProtocolIdentString() { return "$ident_string"; } -$replyable_base_class\::$replyable_base_class() -{ } +std::auto_ptr<Message> $custom_protocol_subclass\::MakeMessage(int ObjType) +{ + switch(ObjType) + { +__E + +# do objects within this +for my $cmd (@cmd_list) +{ + print CPP <<__E; + case $cmd_id{$cmd}: + return std::auto_ptr<Message>(new $cmd_classes{$cmd}()); + break; +__E +} + +print CPP <<__E; + default: + THROW_EXCEPTION(ConnectionException, Protocol_UnknownCommandRecieved) + } +} $replyable_base_class\::~$replyable_base_class() -{ } +{ + // If there were any streams left over, there's no longer any way to + // access them, and we're responsible for them, so we'd better delete them. + DeleteStreamsToSend(); +} void $replyable_base_class\::SendStreamAfterCommand(std::auto_ptr<IOStream> apStream) { @@ -589,12 +676,14 @@ void $replyable_base_class\::DeleteStreamsToSend() mStreamsToSend.clear(); } -void $protocol_base_class\::CheckReply(const std::string& requestCommand, - const $message_base_class &rReply, int expectedType) +void $callable_base_class\::CheckReply(const std::string& requestCommandName, + const $message_base_class &rCommand, const $message_base_class &rReply, + int expectedType) { if(rReply.GetType() == expectedType) { // Correct response, do nothing + SetLastError(Protocol::NoError, Protocol::NoError); } else { @@ -605,8 +694,8 @@ void $protocol_base_class\::CheckReply(const std::string& requestCommand, { SetLastError(type, subType); THROW_EXCEPTION_MESSAGE(ConnectionException, - Conn_Protocol_UnexpectedReply, - requestCommand << " command failed: " + Protocol_UnexpectedReply, + requestCommandName << " command failed: " "received error " << (($error_class&)rReply).GetMessage()); } @@ -614,12 +703,18 @@ void $protocol_base_class\::CheckReply(const std::string& requestCommand, { SetLastError(Protocol::UnknownError, Protocol::UnknownError); THROW_EXCEPTION_MESSAGE(ConnectionException, - Conn_Protocol_UnexpectedReply, - requestCommand << " command failed: " + Protocol_UnexpectedReply, + requestCommandName << " command failed: " "received unexpected response type " << rReply.GetType()); } } + + // As a client, if we get an unexpected reply later, we'll want to know + // the last command that we executed, and the reply, to help debug the + // server. + mPreviousCommand = rCommand.ToString(); + mPreviousReply = rReply.ToString(); } // -------------------------------------------------------------------------- @@ -630,7 +725,7 @@ void $protocol_base_class\::CheckReply(const std::string& requestCommand, // Created: 2003/08/19 // // -------------------------------------------------------------------------- -bool $protocol_base_class\::GetLastError(int &rTypeOut, int &rSubTypeOut) +bool $client_server_base_class\::GetLastError(int &rTypeOut, int &rSubTypeOut) { if(mLastErrorType == Protocol::NoError) { @@ -653,13 +748,19 @@ __E # the callable protocol interface (implemented by Client and Local classes) # with Query methods that don't take a context parameter -my $callable_base_class = $protocol_name."ProtocolCallable"; print H <<__E; -class $callable_base_class : public virtual $protocol_base_class +class $callable_base_class : public virtual $client_server_base_class, + public $send_receive_class { public: - virtual std::auto_ptr<IOStream> ReceiveStream() = 0; virtual int GetTimeout() = 0; + +protected: + void CheckReply(const std::string& requestCommandName, + const $message_base_class &rCommand, + const $message_base_class &rReply, int expectedType); + +public: __E # add plain object taking query functions @@ -671,8 +772,8 @@ for my $cmd (@cmd_list) my $has_stream = obj_is_type($cmd,'StreamWithCommand'); my $argextra = $has_stream?', std::auto_ptr<IOStream> apStream':''; my $queryextra = $has_stream?', apStream':''; - my $request_class = $cmd_class{$cmd}; - my $reply_class = $cmd_class{obj_get_type_params($cmd,'Command')}; + my $request_class = $cmd_classes{$cmd}; + my $reply_class = $cmd_classes{obj_get_type_params($cmd,'Command')}; print H "\tvirtual std::auto_ptr<$reply_class> Query(const $request_class &rQuery$argextra) = 0;\n"; my @a; @@ -720,13 +821,15 @@ foreach my $type ('Client', 'Server', 'Local') { push @base_classes, $replyable_base_class; } + if (not $writing_server) { push @base_classes, $callable_base_class; } + if (not $writing_local) { - push @base_classes, "Protocol"; + push @base_classes, $custom_protocol_subclass; } my $base_classes_str = join(", ", map {"public $_"} @base_classes); @@ -735,6 +838,7 @@ foreach my $type ('Client', 'Server', 'Local') class $server_or_client_class : $base_classes_str { public: + virtual ~$server_or_client_class(); __E if($writing_local) @@ -743,18 +847,12 @@ __E $server_or_client_class($context_class &rContext); __E } - else - { - print H <<__E; - $server_or_client_class(IOStream &rStream); + + print H <<__E; + $server_or_client_class(std::auto_ptr<SocketStream> apConn); std::auto_ptr<$message_base_class> Receive(); void Send(const $message_base_class &rObject); __E - } - - print H <<__E; - virtual ~$server_or_client_class(); -__E if($writing_server) { @@ -775,37 +873,29 @@ __E my $has_stream = obj_is_type($cmd,'StreamWithCommand'); my $argextra = $has_stream?', std::auto_ptr<IOStream> apStream':''; my $queryextra = $has_stream?', apStream':''; - my $request_class = $cmd_class{$cmd}; - my $reply_class = $cmd_class{obj_get_type_params($cmd,'Command')}; + my $request_class = $cmd_classes{$cmd}; + my $reply_class = $cmd_classes{obj_get_type_params($cmd,'Command')}; print H "\tstd::auto_ptr<$reply_class> Query(const $request_class &rQuery$argextra);\n"; } } } - + if($writing_local) { print H <<__E; private: $context_class &mrContext; -__E - } - - print H <<__E; - -protected: - virtual std::auto_ptr<Message> MakeMessage(int ObjType); - -__E - - if($writing_local) - { - print H <<__E; - virtual void InformStreamReceiving(u_int32_t Size) { } - virtual void InformStreamSending(u_int32_t Size) { } - + std::auto_ptr<$message_base_class> mapLastReply; public: virtual std::auto_ptr<IOStream> ReceiveStream() { + if(mStreamsToSend.empty()) + { + THROW_EXCEPTION_MESSAGE(CommonException, Internal, + "Tried to ReceiveStream when none was sent or " + "made available"); + } + std::auto_ptr<IOStream> apStream(mStreamsToSend.front()); mStreamsToSend.pop_front(); return apStream; @@ -815,29 +905,33 @@ __E else { print H <<__E; - virtual void InformStreamReceiving(u_int32_t Size) - { - this->Protocol::InformStreamReceiving(Size); - } - virtual void InformStreamSending(u_int32_t Size) - { - this->Protocol::InformStreamSending(Size); - } + virtual std::auto_ptr<IOStream> ReceiveStream(); +__E -public: - virtual std::auto_ptr<IOStream> ReceiveStream() + print CPP <<__E; +std::auto_ptr<IOStream> $server_or_client_class\::ReceiveStream() +{ + try { - return this->Protocol::ReceiveStream(); - } -__E + return $custom_protocol_subclass\::ReceiveStream(); } - - print H <<__E; - virtual const char *GetProtocolIdentString() + catch(ConnectionException &e) { - return GetIdentString(); + if(e.GetSubType() == ConnectionException::Protocol_ObjWhenStreamExpected) + { + THROW_EXCEPTION_MESSAGE(ConnectionException, + Protocol_ObjWhenStreamExpected, + "Last exchange was " << mPreviousCommand << + " => " << mPreviousReply); + } + else + { + throw; + } } +} __E + } if($writing_local) { @@ -853,23 +947,13 @@ __E print H <<__E; virtual int GetTimeout() { - return this->Protocol::GetTimeout(); + return $custom_protocol_subclass\::GetTimeout(); } __E } - + print H <<__E; - /* - virtual void Handshake() - { - this->Protocol::Handshake(); - } - virtual bool GetLastError(int &rTypeOut, int &rSubTypeOut) - { - return this->Protocol::GetLastError(rTypeOut, rSubTypeOut); - } - */ - + private: $server_or_client_class(const $server_or_client_class &rToCopy); /* no copies */ }; @@ -890,8 +974,8 @@ __E else { print CPP <<__E; -$server_or_client_class\::$server_or_client_class(IOStream &rStream) -: Protocol(rStream) +$server_or_client_class\::$server_or_client_class(std::auto_ptr<SocketStream> apConn) +: $custom_protocol_subclass(apConn) { } __E } @@ -903,49 +987,58 @@ $server_or_client_class\::~$server_or_client_class() __E # write receive and send functions - print CPP <<__E; -std::auto_ptr<Message> $server_or_client_class\::MakeMessage(int ObjType) -{ - switch(ObjType) - { -__E - - # do objects within this - for my $cmd (@cmd_list) + if($writing_local) { print CPP <<__E; - case $cmd_id{$cmd}: - return std::auto_ptr<Message>(new $cmd_class{$cmd}()); - break; -__E - } - - print CPP <<__E; - default: - THROW_EXCEPTION(ConnectionException, Conn_Protocol_UnknownCommandRecieved) - } +std::auto_ptr<$message_base_class> $server_or_client_class\::Receive() +{ + return mapLastReply; +} +void $server_or_client_class\::Send(const $message_base_class &rObject) +{ + mapLastReply = rObject.DoCommand(*this, mrContext); } __E - - if(not $writing_local) + } + else { print CPP <<__E; std::auto_ptr<$message_base_class> $server_or_client_class\::Receive() { - std::auto_ptr<$message_base_class> preply(($message_base_class *) - Protocol::ReceiveInternal().release()); + std::auto_ptr<$message_base_class> apReply; + + try + { + apReply = std::auto_ptr<$message_base_class>( + static_cast<$message_base_class *> + ($custom_protocol_subclass\::ReceiveInternal().release())); + } + catch(ConnectionException &e) + { + if(e.GetSubType() == ConnectionException::Protocol_StreamWhenObjExpected) + { + THROW_EXCEPTION_MESSAGE(ConnectionException, + Protocol_StreamWhenObjExpected, + "Last exchange was " << mPreviousCommand << + " => " << mPreviousReply); + } + else + { + throw; + } + } if(GetLogToSysLog()) { - preply->LogSysLog("Receive"); + apReply->LogSysLog("Receive"); } if(GetLogToFile() != 0) { - preply->LogFile("Receive", GetLogToFile()); + apReply->LogFile("Receive", GetLogToFile()); } - return preply; + return apReply; } void $server_or_client_class\::Send(const $message_base_class &rObject) @@ -981,10 +1074,40 @@ void $server_or_client_class\::DoServer($context_class &rContext) { // Get an object from the conversation std::auto_ptr<$message_base_class> pobj = Receive(); + std::auto_ptr<$message_base_class> preply; // Run the command - std::auto_ptr<$message_base_class> preply = pobj->DoCommand(*this, rContext); - + try + { + try + { + if(pobj->HasStreamWithCommand()) + { + std::auto_ptr<IOStream> apDataStream = ReceiveStream(); + SelfFlushingStream autoflush(*apDataStream); + preply = pobj->DoCommand(*this, rContext, *apDataStream); + } + else + { + preply = pobj->DoCommand(*this, rContext); + } + } + catch(BoxException &e) + { + // First try a the built-in exception handler + preply = HandleException(e); + } + } + catch (...) + { + // Fallback in case the exception isn't a BoxException + // or the exception handler fails as well. This path + // throws the exception upwards, killing the process + // that handles the current client. + Send($cmd_classes{$error_message}(-1)); + throw; + } + // Send the reply Send(*preply); @@ -995,7 +1118,16 @@ void $server_or_client_class\::DoServer($context_class &rContext) { SendStream(**i); } - + + // As a server, if we get an unexpected message later, we'll + // want to know the last command that we received, and the + // reply, to help debug our response to it. + mPreviousCommand = pobj->ToString(); + std::ostringstream reply; + reply << preply->ToString() << " and " << + mStreamsToSend.size() << " streams"; + mPreviousReply = reply.str(); + // Delete these streams DeleteStreamsToSend(); @@ -1004,7 +1136,7 @@ void $server_or_client_class\::DoServer($context_class &rContext) { inProgress = false; } - } + } } __E @@ -1017,67 +1149,86 @@ __E { if(obj_is_type($cmd,'Command')) { - my $request_class = $cmd_class{$cmd}; + my $request_class = $cmd_classes{$cmd}; my $reply_msg = obj_get_type_params($cmd,'Command'); - my $reply_class = $cmd_class{$reply_msg}; + my $reply_class = $cmd_classes{$reply_msg}; my $reply_id = $cmd_id{$reply_msg}; my $has_stream = obj_is_type($cmd,'StreamWithCommand'); - my $argextra = $has_stream?', std::auto_ptr<IOStream> apStream':''; + my $argextra = $has_stream?', std::auto_ptr<IOStream> apDataStream':''; my $send_stream_extra = ''; - my $send_stream_method = $writing_client ? "SendStream" - : "SendStreamAfterCommand"; - + + print CPP <<__E; +std::auto_ptr<$reply_class> $server_or_client_class\::Query(const $request_class &rQuery$argextra) +{ +__E + if($writing_client) { if($has_stream) { $send_stream_extra = <<__E; // Send stream after the command - SendStream(*apStream); + try + { + SendStream(*apDataStream); + } + catch (BoxException &e) + { + BOX_WARNING("Failed to send stream after command: " << + rQuery.ToString() << ": " << e.what()); + throw; + } __E } print CPP <<__E; -std::auto_ptr<$reply_class> $server_or_client_class\::Query(const $request_class &rQuery$argextra) -{ // Send query Send(rQuery); - $send_stream_extra +$send_stream_extra // Wait for the reply - std::auto_ptr<$message_base_class> preply = Receive(); - - CheckReply("$cmd", *preply, $reply_id); - - // Correct response, if no exception thrown by CheckReply - return std::auto_ptr<$reply_class>(($reply_class *)preply.release()); -} + std::auto_ptr<$message_base_class> apReply = Receive(); __E } elsif($writing_local) { + print CPP <<__E; + std::auto_ptr<$message_base_class> apReply; + try + { +__E if($has_stream) { - $send_stream_extra = <<__E; - // Send stream after the command - SendStreamAfterCommand(apStream); + print CPP <<__E; + apReply = rQuery.DoCommand(*this, mrContext, *apDataStream); +__E + } + else + { + print CPP <<__E; + apReply = rQuery.DoCommand(*this, mrContext); __E } print CPP <<__E; -std::auto_ptr<$reply_class> $server_or_client_class\::Query(const $request_class &rQuery$argextra) -{ - // Send query - $send_stream_extra - std::auto_ptr<$message_base_class> preply = rQuery.DoCommand(*this, mrContext); - - CheckReply("$cmd", *preply, $reply_id); + } + catch(BoxException &e) + { + // First try a the built-in exception handler + apReply = HandleException(e); + } +__E + } + + # Common to both client and local + print CPP <<__E; + CheckReply("$cmd", rQuery, *apReply, $reply_id); // Correct response, if no exception thrown by CheckReply - return std::auto_ptr<$reply_class>(($reply_class *)preply.release()); + return std::auto_ptr<$reply_class>( + static_cast<$reply_class *>(apReply.release())); } __E - } } } } @@ -1110,7 +1261,7 @@ sub obj_get_type_params { return $1 if $_ =~ m/\A$ty\((.+?)\)\Z/; } - die "Can't find attribute $ty\n" + die "Can't find attribute $ty on command $c\n" } # returns (is basic type, typename) |