diff --git a/include/ulib/base/utility.h b/include/ulib/base/utility.h index 797f7ee6..f13a2b3d 100644 --- a/include/ulib/base/utility.h +++ b/include/ulib/base/utility.h @@ -261,6 +261,7 @@ U_EXPORT char* u_memoryDump( char* restrict bp, unsigned char* restrict cp, u U_EXPORT uint32_t u_memory_dump(char* restrict bp, unsigned char* restrict cp, uint32_t n); U_EXPORT uint8_t u_get_loadavg(void); /* Get the load average of the system (over last 1 minute) */ +U_EXPORT uint16_t u_crc16(const char* a, uint32_t len); /* CRC16 implementation according to CCITT standards */ U_EXPORT uint32_t u_printSize(char* restrict buffer, uint64_t bytes); /* print size using u_calcRate() */ U_EXPORT int u_getScreenWidth(void) __pure; /* Determine the width of the terminal we're running on */ diff --git a/include/ulib/net/client/client.h b/include/ulib/net/client/client.h index 9d9f5577..8ab65de6 100644 --- a/include/ulib/net/client/client.h +++ b/include/ulib/net/client/client.h @@ -316,7 +316,7 @@ protected: ~UClient_Base(); private: - U_DISALLOW_COPY_AND_ASSIGN(UClient_Base) +// U_DISALLOW_COPY_AND_ASSIGN(UClient_Base) static USocket* csocket; static vPFu resize_response_buffer; diff --git a/include/ulib/net/client/redis.h b/include/ulib/net/client/redis.h index 16c0af29..f3fe69d1 100644 --- a/include/ulib/net/client/redis.h +++ b/include/ulib/net/client/redis.h @@ -15,6 +15,7 @@ #define ULIB_REDIS_H 1 #include +#include #include #include @@ -62,6 +63,8 @@ typedef void (*vPFcs) (const UString&); typedef void (*vPFcscs)(const UString&,const UString&); +class UREDISClusterClient; + class U_EXPORT UREDISClient_Base : public UClient_Base, UEventFd { public: @@ -189,6 +192,45 @@ public: bool connect(const char* host = U_NULLPTR, unsigned int _port = 6379); + // by Victor Stewart + + UString single(const UString& pipeline) + { + U_TRACE(0, "UREDISClient_Base::single(%V)", pipeline.rep) + + (void) processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(pipeline)); + + return vitem[0]; + } + + void silencedSingle(UString& pipeline) + { + U_TRACE(0, "UREDISClient_Base::silencedSingle(%V)", pipeline.rep) + + (void) pipeline.insert(0, U_CONSTANT_TO_PARAM("CLIENT REPLY SKIP \r\n")); + + (void) processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(pipeline)); + } + + const UVector& multi(const UString& pipeline) + { + U_TRACE(0, "UREDISClient_Base::multi(%V)", pipeline.rep) + + (void) processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(pipeline)); + + return vitem; + } + + void silencedMulti(UString& pipeline) + { + U_TRACE(0, "UREDISClient_Base::silencedMulti(%V)", pipeline.rep) + + (void) pipeline.insert(0, U_CONSTANT_TO_PARAM("CLIENT REPLY OFF \r\n")); + (void) pipeline.append(U_CONSTANT_TO_PARAM("CLIENT REPLY ON \r\n")); + + (void) processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(pipeline)); + } + // STRING (@see http://redis.io/commands#string) bool get(const char* key, uint32_t keylen) // Get the value of a key @@ -857,7 +899,9 @@ protected: private: bool getResponseItem() U_NO_EXPORT; - U_DISALLOW_COPY_AND_ASSIGN(UREDISClient_Base) + friend class UREDISClusterClient; + +// U_DISALLOW_COPY_AND_ASSIGN(UREDISClient_Base) }; template class U_EXPORT UREDISClient : public UREDISClient_Base { @@ -882,7 +926,7 @@ public: #endif private: - U_DISALLOW_COPY_AND_ASSIGN(UREDISClient) +// U_DISALLOW_COPY_AND_ASSIGN(UREDISClient) }; template <> class U_EXPORT UREDISClient : public UREDISClient_Base { @@ -931,4 +975,109 @@ public: private: U_DISALLOW_COPY_AND_ASSIGN(UREDISClient) }; + +// by Victor Stewart + +#if defined(U_STDCPP_ENABLE) && defined(HAVE_CXX17) +# include + +class U_EXPORT UREDISClusterClient : public UREDISClient { +private: + struct RedisNode { + UString ipAddress; + UREDISClient client; + uint16_t lowHashSlot, highHashSlot; + }; + + enum class ClusterError : uint8_t { + none, + moved, + ask, + tryagain + }; + + ClusterError error; + UString temporaryASKip; + std::vector redisNodes; + + uint16_t hashslotForKey(const UString& hashableKey) { return u_crc16(U_STRING_TO_PARAM(hashableKey)); } + + uint16_t hashslotFromCommand(const UString& command) + { + U_TRACE(0, "UREDISClusterClient::hashslotFromCommand(%V)", command.rep) + + // expects hashable keys to be delivered as abc{hashableKey}xyz value blah \r\n + + uint32_t beginning = command.find('{') + 1, + end = command.find('}', beginning) - 1; + + return hashslotForKey(command.substr(beginning, end - beginning)); + } + + UREDISClient& clientForHashslot(uint16_t hashslot) + { + U_TRACE(0, "UREDISClusterClient::clientForHashslot(%u)", hashslot) + + for (RedisNode& workingNode : redisNodes) + { + if ((workingNode.lowHashSlot <= hashslot) || (workingNode.highHashSlot >= hashslot)) return workingNode.client; + } + + return redisNodes[0].client; + } + + UREDISClient& clientForASKip() + { + for (RedisNode& workingNode : redisNodes) + { + if (temporaryASKip == workingNode.ipAddress) return workingNode.client; + } + + return redisNodes[0].client; + } + + UREDISClient& clientForHashableKey(const UString& hashableKey) { return clientForHashslot(hashslotForKey(hashableKey)); } + +public: + UREDISClusterClient() : UREDISClient() + { + U_TRACE_CTOR(0, UREDISClusterClient, "") + } + + ~UREDISClusterClient() + { + U_TRACE_DTOR(0, UREDISClusterClient) + } + + void processResponse(); + void calculateNodeMap(); + + const UVector& processPipeline(UString& pipeline, bool silence); + + // all of these multis require all keys to exist within a single hash slot (on the same node isn't good enough) + + UString clusterSingle(const UString& hashableKey, const UString& pipeline) { return clientForHashableKey(hashableKey).single(pipeline); } + const UVector& clusterMulti( const UString& hashableKey, const UString& pipeline) { return clientForHashableKey(hashableKey).multi(pipeline); } + + void clusterSilencedMulti( const UString& hashableKey, UString& pipeline) { clientForHashableKey(hashableKey).silencedMulti(pipeline); } + void clusterSilencedSingle(const UString& hashableKey, UString& pipeline) { clientForHashableKey(hashableKey).silencedSingle(pipeline); } + + // anon multis are pipelined commands of various keys that might belong to many nodes. always processed in order. commands always delimined by \r\n + + const UVector& clusterAnonMulti( UString& pipeline) { return processPipeline(pipeline, false); } + void clusterSilencedAnonMulti(UString& pipeline) { (void) processPipeline(pipeline, true); } + + bool clusterUnsubscribe(const UString& hashableKey, const UString& channel) { return clientForHashableKey(hashableKey).unsubscribe(channel); } + bool clusterSubscribe( const UString& hashableKey, const UString& channel, vPFcscs callback) { return clientForHashableKey(hashableKey).subscribe(channel, callback); } + + // DEBUG + +#if defined(U_STDCPP_ENABLE) && defined(DEBUG) + const char* dump(bool _reset) const { return UREDISClient_Base::dump(_reset); } +#endif + +private: + U_DISALLOW_COPY_AND_ASSIGN(UREDISClusterClient) +}; +#endif #endif diff --git a/include/ulib/string.h b/include/ulib/string.h index 141a04f0..54929f6b 100644 --- a/include/ulib/string.h +++ b/include/ulib/string.h @@ -566,6 +566,22 @@ public: // EXTENSION + bool isNumber(uint32_t pos) const + { + U_TRACE(0, "UStringRep::isNumber(%u)", pos) + + U_CHECK_MEMORY + + if (_length) + { + U_INTERNAL_ASSERT_MINOR(pos, _length) + + if (u_isNumber(str + pos, _length - pos)) U_RETURN(true); + } + + U_RETURN(false); + } + bool isBinary(uint32_t pos) const { U_TRACE(0, "UStringRep::isBinary(%u)", pos) @@ -2098,6 +2114,7 @@ public: bool isText(uint32_t pos = 0) const { return rep->isText(pos); } bool isUTF8(uint32_t pos = 0) const { return rep->isUTF8(pos); } bool isUTF16(uint32_t pos = 0) const { return rep->isUTF16(pos); } + bool isNumber(uint32_t pos = 0) const { return rep->isNumber(pos); } bool isBinary(uint32_t pos = 0) const { return rep->isBinary(pos); } bool isBase64(uint32_t pos = 0) const { return rep->isBase64(pos); } bool isBase64Url(uint32_t pos = 0) const { return rep->isBase64Url(pos); } diff --git a/src/ulib/base/utility.c b/src/ulib/base/utility.c index 4db163f2..fb99cf28 100644 --- a/src/ulib/base/utility.c +++ b/src/ulib/base/utility.c @@ -821,6 +821,68 @@ __pure uint32_t u_findEndHeader(const char* restrict str, uint32_t n) return endHeader; } +/** + * CRC16 implementation according to CCITT standards + * + * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the following parameters: + * + * Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN" + * Width : 16 bit + * Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1) + * Initialization : 0000 + * Reflect Input byte : False + * Reflect Output CRC : False + * Xor constant to output CRC : 0000 + * Output for "123456789" : 31C3 + */ + +uint16_t u_crc16(const char* buf, uint32_t len) +{ + static uint16_t crc16tab[256]= { + 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, + 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, + 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, + 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, + 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, + 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, + 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, + 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, + 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, + 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, + 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, + 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, + 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, + 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, + 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, + 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, + 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, + 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, + 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, + 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, + 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, + 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, + 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, + 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, + 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, + 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, + 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, + 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, + 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, + 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, + 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, + 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 + }; + + uint16_t crc = 0; + uint32_t counter; + + U_INTERNAL_TRACE("u_crc16(%.*s,%u)", U_min(len,128), buf, len) + + for (counter = 0; counter < len; ++counter) crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++) & 0x00FF]; + + return crc; +} + /* Determine the width of the terminal we're running on */ __pure int u_getScreenWidth(void) @@ -1456,7 +1518,7 @@ __pure bool u_isNumber(const char* restrict s, uint32_t n) ((*(const unsigned char* restrict)s) >> 4) == 0x03 && vdigit[(*(const unsigned char* restrict)s) & 0x0f]) { - U_INTERNAL_PRINT("*s = %c, *s >> 4 = %c ", *s, (*(char* restrict)s) >> 4) + U_INTERNAL_PRINT("*s = %c, *s >> 4 = %c", *s, (*(char* restrict)s) >> 4) ++s; } diff --git a/src/ulib/net/client/redis.cpp b/src/ulib/net/client/redis.cpp index 096643bb..54a8835f 100644 --- a/src/ulib/net/client/redis.cpp +++ b/src/ulib/net/client/redis.cpp @@ -617,9 +617,201 @@ int UREDISClient_Base::handlerRead() U_RETURN(U_NOTIFIER_OK); } +#if defined(U_STDCPP_ENABLE) + +// by Victor Stewart + +# if defined(HAVE_CXX17) +void UREDISClusterClient::processResponse() +{ + U_TRACE_NO_PARAM(0, "UREDISClusterClient::processResponse()") + + if (UClient_Base::response.find("MOVED", 0, 5) != U_NOT_FOUND) + { + // MOVED 3999 127.0.0.1:6381 => the hashslot has been moved to another master node + + error = ClusterError::moved; + + calculateNodeMap(); + } + else if (UClient_Base::response.find("ASK", 0, 3) != U_NOT_FOUND) + { + // ASK 3999 127.0.0.1:6381 => this means that one of the hash slots is being migrated to another server + + error = ClusterError::ask; + + uint32_t _start = UClient_Base::response.find(' ', 8) + 1, + end = UClient_Base::response.find(':', _start); + + (void) temporaryASKip.assign(UClient_Base::response.substr(_start, end - _start)); + } + + else if (UClient_Base::response.find("TRYAGAIN", 0, 8) != U_NOT_FOUND) + { + /** + * during a resharding the multi-key operations targeting keys that all exist and are all still in the same node (either the source or destination node) are still available. + * Operations on keys that don't exist or are - during the resharding - split between the source and destination nodes, will generate a -TRYAGAIN error. The client can try + * the operation after some time, or report back the error. As soon as migration of the specified hash slot has terminated, all multi-key operations are available again for + * that hash slot + */ + + error = ClusterError::tryagain; + + UTimeVal(0L, 1000L).nanosleep(); // 0 sec, 1000 microsec = 1ms + } + else + { + error = ClusterError::none; + + UREDISClient::processResponse(); + } +} + +const UVector& UREDISClusterClient::processPipeline(UString& pipeline, bool silence) +{ + U_TRACE(0, "UREDISClusterClient::processPipeline(%V,%b)", pipeline.rep, silence) + + uint16_t hashslot = 0, workingHashslot; + UString command, workingString(U_CAPACITY); + UVector commands(pipeline, "\r\n"); + + for (uint32_t count = 0, index = 0, n = commands.size(); index < n; ++index) + { + command = commands[index]; + + workingHashslot = hashslotFromCommand(command); + + if (workingHashslot == hashslot) + { + (void) workingString.append(command + "\r\n"); + + ++count; + + if ((index + 1) < n) continue; + } + + hashslot = workingHashslot; + + if (silence) + { + if (count > 1) + { + (void) workingString.insert(0, U_CONSTANT_TO_PARAM("CLIENT REPLY OFF \r\n")); + (void) workingString.append(U_CONSTANT_TO_PARAM("CLIENT REPLY ON \r\n")); + } + else + { + (void) pipeline.insert(0, U_CONSTANT_TO_PARAM("CLIENT REPLY SKIP \r\n")); + } + } + + UREDISClient& client = clientForHashslot(hashslot); + +replay: + (void) client.processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(workingString)); + + switch (error) + { + case ClusterError::moved: + case ClusterError::tryagain: + { + goto replay; + } + break; + + case ClusterError::ask: + { + UREDISClient& temporaryClient = clientForASKip(); + + (void) temporaryClient.processRequest(U_RC_MULTIBULK, U_STRING_TO_PARAM(workingString)); + } + break; + + case ClusterError::none: break; + } + + if (silence == false) vitem.move(client.vitem); + } + + return vitem; +} + +void UREDISClusterClient::calculateNodeMap() +{ + U_TRACE_NO_PARAM(0, "UREDISClusterClient::calculateNodeMap()") + + /* + 127.0.0.1:30001> cluster slots + 1) 1) (integer) 0 + 2) (integer) 5460 + 3) 1) "127.0.0.1" + 2) (integer) 30001 + 3) "09dbe9720cda62f7865eabc5fd8857c5d2678366" + 4) 1) "127.0.0.1" + 2) (integer) 30004 + 3) "821d8ca00d7ccf931ed3ffc7e3db0599d2271abf" + 2) 1) (integer) 5461 + 2) (integer) 10922 + 3) 1) "127.0.0.1" + 2) (integer) 30002 + 3) "c9d93d9f2c0c524ff34cc11838c2003d8c29e013" + 4) 1) "127.0.0.1" + 2) (integer) 30005 + 3) "faadb3eb99009de4ab72ad6b6ed87634c7ee410f" + 3) 1) (integer) 10923 + 2) (integer) 16383 + 3) 1) "127.0.0.1" + 2) (integer) 30003 + 3) "044ec91f325b7595e76dbcb18cc688b6a5b434a1" + 4) 1) "127.0.0.1" + 2) (integer) 30006 + 3) "58e6e48d41228013e5d9c1c37c5060693925e97e" + */ + + bool findHashSlots = true; + uint16_t workingLowHashSlot; + uint16_t workingHighHashSlot; + + (void) UREDISClient_Base::processRequest(U_RC_MULTIBULK, U_CONSTANT_TO_PARAM("CLUSTER SLOTS")); + + const UVector& rawNodes = UREDISClient_Base::vitem; + + for (uint32_t a = 0, b = rawNodes.size(); a < b; ++a) + { + if (findHashSlots) + { + if (rawNodes[a].isNumber() && + rawNodes[a+1].isNumber()) + { + workingLowHashSlot = rawNodes[a++].strtoul(); + workingHighHashSlot = rawNodes[a].strtoul(); + + findHashSlots = false; + } + } + else + { + // the immediate next after hash slot is the master + + RedisNode workingNode; + + workingNode.lowHashSlot = workingLowHashSlot; + workingNode.highHashSlot = workingHighHashSlot; + (void) workingNode.ipAddress.assign(rawNodes[a]); + + workingNode.client.connect(workingNode.ipAddress.c_str(), rawNodes[++a].strtoul()); + + redisNodes.push_back(std::move(workingNode)); + + findHashSlots = true; + } + } +} +# endif + // DEBUG -#if defined(U_STDCPP_ENABLE) && defined(DEBUG) +# if defined(DEBUG) const char* UREDISClient_Base::dump(bool _reset) const { UClient_Base::dump(false); @@ -637,4 +829,5 @@ const char* UREDISClient_Base::dump(bool _reset) const return U_NULLPTR; } +# endif #endif diff --git a/tests/ulib/err/application.err b/tests/ulib/err/application.err new file mode 100644 index 00000000..a450a30c --- /dev/null +++ b/tests/ulib/err/application.err @@ -0,0 +1 @@ +../.function: line 259: ../../examples/application/application: No such file or directory diff --git a/tests/ulib/out/application.out b/tests/ulib/out/application.out new file mode 100644 index 00000000..e69de29b diff --git a/tests/ulib/plugin/.deps/product1.Plo b/tests/ulib/plugin/.deps/product1.Plo index 83f80bd3..c7ac8ff3 100644 --- a/tests/ulib/plugin/.deps/product1.Plo +++ b/tests/ulib/plugin/.deps/product1.Plo @@ -206,8 +206,8 @@ plugin/product1.lo: plugin/product1.cpp /usr/include/stdc-predef.h \ /usr/include/unicode/utf16.h /usr/include/unicode/utf_old.h \ /usr/include/unicode/uenum.h /usr/include/unicode/localpointer.h \ /usr/include/libxml2/libxml/xmlIO.h \ - /usr/include/libxml2/libxml/globals.h /usr/include/libxml2/libxml/SAX.h \ - /usr/include/libxml2/libxml/xlink.h /usr/include/libxml2/libxml/SAX2.h \ + /usr/include/libxml2/libxml/globals.h /usr/include/libxml2/libxml/SAX2.h \ + /usr/include/libxml2/libxml/xlink.h \ /usr/include/libxml2/libxml/xmlmemory.h \ /usr/include/libxml2/libxml/threads.h \ ../../include/ulib/internal/macro.h \ @@ -809,12 +809,10 @@ plugin/product.h: /usr/include/libxml2/libxml/globals.h: -/usr/include/libxml2/libxml/SAX.h: +/usr/include/libxml2/libxml/SAX2.h: /usr/include/libxml2/libxml/xlink.h: -/usr/include/libxml2/libxml/SAX2.h: - /usr/include/libxml2/libxml/xmlmemory.h: /usr/include/libxml2/libxml/threads.h: diff --git a/tests/ulib/plugin/.deps/product2.Plo b/tests/ulib/plugin/.deps/product2.Plo index 157bd13f..52342caa 100644 --- a/tests/ulib/plugin/.deps/product2.Plo +++ b/tests/ulib/plugin/.deps/product2.Plo @@ -206,8 +206,8 @@ plugin/product2.lo: plugin/product2.cpp /usr/include/stdc-predef.h \ /usr/include/unicode/utf16.h /usr/include/unicode/utf_old.h \ /usr/include/unicode/uenum.h /usr/include/unicode/localpointer.h \ /usr/include/libxml2/libxml/xmlIO.h \ - /usr/include/libxml2/libxml/globals.h /usr/include/libxml2/libxml/SAX.h \ - /usr/include/libxml2/libxml/xlink.h /usr/include/libxml2/libxml/SAX2.h \ + /usr/include/libxml2/libxml/globals.h /usr/include/libxml2/libxml/SAX2.h \ + /usr/include/libxml2/libxml/xlink.h \ /usr/include/libxml2/libxml/xmlmemory.h \ /usr/include/libxml2/libxml/threads.h \ ../../include/ulib/internal/macro.h \ @@ -809,12 +809,10 @@ plugin/product.h: /usr/include/libxml2/libxml/globals.h: -/usr/include/libxml2/libxml/SAX.h: +/usr/include/libxml2/libxml/SAX2.h: /usr/include/libxml2/libxml/xlink.h: -/usr/include/libxml2/libxml/SAX2.h: - /usr/include/libxml2/libxml/xmlmemory.h: /usr/include/libxml2/libxml/threads.h: diff --git a/tests/ulib/tmp/c b/tests/ulib/tmp/c new file mode 100644 index 00000000..6e7ada21 --- /dev/null +++ b/tests/ulib/tmp/c @@ -0,0 +1,5 @@ +c +c +c +c +c