From aef02eddce5753fb3c0904df8aefecec2ba01949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 27 Dec 2009 03:51:54 +0000 Subject: [PATCH] use of memory barriers in the shm server and client code --- library/DFHackAPI.cpp | 4 + library/DFProcess-linux-SHM.cpp | 94 +++++++++++++++++-- library/DFProcessEnumerator-linux.cpp | 11 ++- library/DFProcessEnumerator-windows.cpp | 11 ++- library/DFProcessEnumerator.h | 1 + library/shmserver/df-hacked | 14 +++ library/shmserver/dfconnect.c | 116 +++++++++++++++++++++--- library/shmserver/dfconnect.h | 8 ++ library/shmserver/dfconnect.so | Bin 0 -> 12100 bytes library/shmserver/readme.txt | 3 + 10 files changed, 232 insertions(+), 30 deletions(-) create mode 100755 library/shmserver/df-hacked create mode 100755 library/shmserver/dfconnect.so diff --git a/library/DFHackAPI.cpp b/library/DFHackAPI.cpp index 264cf1a25..aa5537119 100644 --- a/library/DFHackAPI.cpp +++ b/library/DFHackAPI.cpp @@ -969,6 +969,10 @@ bool API::Attach() { d->pm = new ProcessEnumerator (d->xml); // FIXME: handle bad XML better } + else + { + d->pm->purge(); + } // find a process (ProcessManager can find multiple when used properly) if (!d->pm->findProcessess()) diff --git a/library/DFProcess-linux-SHM.cpp b/library/DFProcess-linux-SHM.cpp index 195801df5..01d8a3c59 100644 --- a/library/DFProcess-linux-SHM.cpp +++ b/library/DFProcess-linux-SHM.cpp @@ -28,8 +28,50 @@ distribution. #include #include #include "shmserver/dfconnect.h" +#include +#include +#include +#include using namespace DFHack; +// a full memory barrier! better be safe than sorry. +#define gcc_barrier asm volatile("" ::: "memory"); __sync_synchronize(); + +/* +* wait for futex +* futex has to be aligned to 4 bytes +* futex has to be equal to val (returns EWOULDBLOCK otherwise) +* wait can be broken by arriving signals (returns EINTR) +* returns 0 when broken by futex_wake +*/ +inline int futex_wait(int * futex, int val) +{ + return syscall(SYS_futex, futex, FUTEX_WAIT, val, 0, 0, 0); +} +/* +* wait for futex +* futex has to be aligned to 4 bytes +* futex has to be equal to val (returns EWOULDBLOCK otherwise) +* wait can be broken by arriving signals (returns EINTR) +* returns 0 when broken by futex_wake +* returns ETIMEDOUT on timeout +*/ +inline int futex_wait_timed(int * futex, int val, const struct timespec *timeout) +{ + return syscall(SYS_futex, futex, FUTEX_WAIT, val, timeout, 0, 0); +} +/* +* wake up futex. returns number of waked processes +*/ +inline int futex_wake(int * futex) +{ + return syscall(SYS_futex, futex, FUTEX_WAKE, 1, 0, 0, 0); +} +static timespec one_second = { 1,0 }; +static timespec five_second = { 5,0 }; + + + class SHMProcess::Private { public: @@ -38,6 +80,8 @@ class SHMProcess::Private my_datamodel = NULL; my_descriptor = NULL; my_pid = 0; + my_shm = 0; + my_shmid = -1; my_window = NULL; attached = false; suspended = false; @@ -54,6 +98,7 @@ class SHMProcess::Private bool attached; bool suspended; bool identified; + bool validate(char * exe_file, uint32_t pid, vector & known_versions); bool waitWhile (DF_PINGPONG state); bool DF_TestBridgeVersion(bool & ret); @@ -71,6 +116,7 @@ bool SHMProcess::Private::waitWhile (DF_PINGPONG state) shmctl(my_shmid, IPC_STAT, &descriptor); if(descriptor.shm_nattch == 1)// DF crashed? { + gcc_barrier ((shm_cmd *)my_shm)->pingpong = DFPP_RUNNING; attached = suspended = false; return false; @@ -96,8 +142,10 @@ bool SHMProcess::Private::waitWhile (DF_PINGPONG state) bool SHMProcess::Private::DF_TestBridgeVersion(bool & ret) { ((shm_cmd *)my_shm)->pingpong = DFPP_VERSION; + gcc_barrier if(!waitWhile(DFPP_VERSION)) return false; + gcc_barrier ((shm_cmd *)my_shm)->pingpong = DFPP_SUSPENDED; ret =( ((shm_retval *)my_shm)->value == PINGPONG_VERSION ); return true; @@ -106,8 +154,10 @@ bool SHMProcess::Private::DF_TestBridgeVersion(bool & ret) bool SHMProcess::Private::DF_GetPID(pid_t & ret) { ((shm_cmd *)my_shm)->pingpong = DFPP_PID; + gcc_barrier if(!waitWhile(DFPP_PID)) return false; + gcc_barrier ((shm_cmd *)my_shm)->pingpong = DFPP_SUSPENDED; ret = ((shm_retval *)my_shm)->value; return true; @@ -139,11 +189,12 @@ SHMProcess::SHMProcess(vector & known_versions) /* * Check if there are two processes connected to the segment */ - struct shmid_ds descriptor; + shmid_ds descriptor; shmctl(d->my_shmid, IPC_STAT, &descriptor); if(descriptor.shm_nattch != 2)// badness { - fprintf(stderr,"dfhack: no DF or different client already connected\n"); + fprintf(stderr,"dfhack: %d : invalid no. of processes connected\n", descriptor.shm_nattch); + fprintf(stderr,"detach: %d",shmdt(d->my_shm)); return; } @@ -182,8 +233,10 @@ SHMProcess::SHMProcess(vector & known_versions) d->validate(target_name, d->my_pid, known_versions); d->my_window = new DFWindow(this); } + gcc_barrier // at this point, DF is stopped and waiting for commands. make it run again ((shm_cmd *)d->my_shm)->pingpong = DFPP_RUNNING; + fprintf(stderr,"detach: %d",shmdt(d->my_shm)); } bool SHMProcess::isSuspended() @@ -239,6 +292,10 @@ SHMProcess::~SHMProcess() { delete d->my_window; } + if(d->my_shm) + { + fprintf(stderr,"detach: %d",shmdt(d->my_shm)); + } delete d; } @@ -341,15 +398,23 @@ bool SHMProcess::attach() cerr << "there's already a different process attached" << endl; return false; } - d->attached = true; - if(suspend()) + /* + * Attach the segment + */ + if ((d->my_shm = (char *) shmat(d->my_shmid, NULL, 0)) != (char *) -1) { - d->suspended = true; - g_pProcess = this; - return true; + d->attached = true; + if(suspend()) + { + d->suspended = true; + g_pProcess = this; + return true; + } + d->attached = false; + cerr << "unable to suspend" << endl; + return false; } - d->attached = false; - cerr << "unable to suspend" << endl; + cerr << "unable to attach" << endl; return false; } @@ -375,6 +440,7 @@ void SHMProcess::read (const uint32_t offset, const uint32_t size, uint8_t *targ assert (size < (SHM_SIZE - sizeof(shm_read))); ((shm_read *)d->my_shm)->address = offset; ((shm_read *)d->my_shm)->length = size; + gcc_barrier ((shm_read *)d->my_shm)->pingpong = DFPP_READ; d->waitWhile(DFPP_READ); memcpy (target, d->my_shm + sizeof(shm_ret_data),size); @@ -383,6 +449,7 @@ void SHMProcess::read (const uint32_t offset, const uint32_t size, uint8_t *targ uint8_t SHMProcess::readByte (const uint32_t offset) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_BYTE; d->waitWhile(DFPP_READ_BYTE); return ((shm_retval *)d->my_shm)->value; @@ -391,6 +458,7 @@ uint8_t SHMProcess::readByte (const uint32_t offset) void SHMProcess::readByte (const uint32_t offset, uint8_t &val ) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_BYTE; d->waitWhile(DFPP_READ_BYTE); val = ((shm_retval *)d->my_shm)->value; @@ -399,6 +467,7 @@ void SHMProcess::readByte (const uint32_t offset, uint8_t &val ) uint16_t SHMProcess::readWord (const uint32_t offset) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_WORD; d->waitWhile(DFPP_READ_WORD); return ((shm_retval *)d->my_shm)->value; @@ -407,6 +476,7 @@ uint16_t SHMProcess::readWord (const uint32_t offset) void SHMProcess::readWord (const uint32_t offset, uint16_t &val) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_WORD; d->waitWhile(DFPP_READ_WORD); val = ((shm_retval *)d->my_shm)->value; @@ -415,6 +485,7 @@ void SHMProcess::readWord (const uint32_t offset, uint16_t &val) uint32_t SHMProcess::readDWord (const uint32_t offset) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_DWORD; d->waitWhile(DFPP_READ_DWORD); return ((shm_retval *)d->my_shm)->value; @@ -422,6 +493,7 @@ uint32_t SHMProcess::readDWord (const uint32_t offset) void SHMProcess::readDWord (const uint32_t offset, uint32_t &val) { ((shm_read_small *)d->my_shm)->address = offset; + gcc_barrier ((shm_read_small *)d->my_shm)->pingpong = DFPP_READ_DWORD; d->waitWhile(DFPP_READ_DWORD); val = ((shm_retval *)d->my_shm)->value; @@ -435,6 +507,7 @@ void SHMProcess::writeDWord (uint32_t offset, uint32_t data) { ((shm_write_small *)d->my_shm)->address = offset; ((shm_write_small *)d->my_shm)->value = data; + gcc_barrier ((shm_write_small *)d->my_shm)->pingpong = DFPP_WRITE_DWORD; d->waitWhile(DFPP_WRITE_DWORD); } @@ -444,6 +517,7 @@ void SHMProcess::writeWord (uint32_t offset, uint16_t data) { ((shm_write_small *)d->my_shm)->address = offset; ((shm_write_small *)d->my_shm)->value = data; + gcc_barrier ((shm_write_small *)d->my_shm)->pingpong = DFPP_WRITE_WORD; d->waitWhile(DFPP_WRITE_WORD); } @@ -452,6 +526,7 @@ void SHMProcess::writeByte (uint32_t offset, uint8_t data) { ((shm_write_small *)d->my_shm)->address = offset; ((shm_write_small *)d->my_shm)->value = data; + gcc_barrier ((shm_write_small *)d->my_shm)->pingpong = DFPP_WRITE_BYTE; d->waitWhile(DFPP_WRITE_BYTE); } @@ -461,6 +536,7 @@ void SHMProcess::write (uint32_t offset, uint32_t size, const uint8_t *source) ((shm_write *)d->my_shm)->address = offset; ((shm_write *)d->my_shm)->length = size; memcpy(d->my_shm+sizeof(shm_write),source, size); + gcc_barrier ((shm_write *)d->my_shm)->pingpong = DFPP_WRITE; d->waitWhile(DFPP_WRITE); } diff --git a/library/DFProcessEnumerator-linux.cpp b/library/DFProcessEnumerator-linux.cpp index d477a1a7d..6755bf9e7 100644 --- a/library/DFProcessEnumerator-linux.cpp +++ b/library/DFProcessEnumerator-linux.cpp @@ -110,14 +110,19 @@ ProcessEnumerator::ProcessEnumerator( string path_to_xml ) d->meminfo = new MemInfoManager(path_to_xml); } - -ProcessEnumerator::~ProcessEnumerator() +void ProcessEnumerator::purge() { - // delete all processes for(uint32_t i = 0;i < d->processes.size();i++) { delete d->processes[i]; } + d->processes.clear(); +} + +ProcessEnumerator::~ProcessEnumerator() +{ + // delete all processes + purge(); delete d->meminfo; delete d; } diff --git a/library/DFProcessEnumerator-windows.cpp b/library/DFProcessEnumerator-windows.cpp index d6156f196..50532e42b 100644 --- a/library/DFProcessEnumerator-windows.cpp +++ b/library/DFProcessEnumerator-windows.cpp @@ -115,14 +115,19 @@ ProcessEnumerator::ProcessEnumerator( string path_to_xml ) d->meminfo = new MemInfoManager(path_to_xml); } - -ProcessEnumerator::~ProcessEnumerator() +ProcessEnumerator::purge() { - // delete all processes for(uint32_t i = 0;i < d->processes.size();i++) { delete d->processes[i]; } + d->processes.clear(); +} + +ProcessEnumerator::~ProcessEnumerator() +{ + // delete all processes + purge(); delete d->meminfo; delete d; } diff --git a/library/DFProcessEnumerator.h b/library/DFProcessEnumerator.h index f369330e7..601ac7f41 100644 --- a/library/DFProcessEnumerator.h +++ b/library/DFProcessEnumerator.h @@ -48,6 +48,7 @@ namespace DFHack bool findProcessess(); uint32_t size(); Process * operator[](uint32_t index); + void purge(void); }; } #endif // PROCESSMANAGER_H_INCLUDED diff --git a/library/shmserver/df-hacked b/library/shmserver/df-hacked new file mode 100755 index 000000000..bf56b33e2 --- /dev/null +++ b/library/shmserver/df-hacked @@ -0,0 +1,14 @@ +#!/bin/sh +DF_DIR=$(dirname "$0") +cd "${DF_DIR}" +export SDL_DISABLE_LOCK_KEYS=1 # Work around for bug in Debian/Ubuntu SDL patch. +#export SDL_VIDEO_CENTERED=1 # Centre the screen. Messes up resizing. +ldd dwarfort.exe | grep SDL_image | grep -qv "not found$" +if [ $? -eq 0 ]; then + mkdir unused_libs + mv libs/libSDL* unused_libs/ +fi +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./libs" # Update library search path. +export LD_PRELOAD="./libs/dfconnect.so" # Hack DF! +./dwarfort.exe $* # Go, go, go! :) + diff --git a/library/shmserver/dfconnect.c b/library/shmserver/dfconnect.c index cfb93e0c7..655611250 100644 --- a/library/shmserver/dfconnect.c +++ b/library/shmserver/dfconnect.c @@ -25,8 +25,6 @@ distribution. /** * This is the source for the DF <-> dfhack shm bridge */ -#define _GNU_SOURCE - #include #include #include @@ -37,27 +35,77 @@ distribution. #include #include #include "dfconnect.h" +#include +#include +#include +#include + +// a full memory barrier! better be safe than sorry. +#define gcc_barrier asm volatile("" ::: "memory"); __sync_synchronize(); + +/* + * wait for futex + * futex has to be aligned to 4 bytes + * futex has to be equal to val (returns EWOULDBLOCK otherwise) + * wait can be broken by arriving signals (returns EINTR) + * returns 0 when broken by futex_wake + */ +inline int futex_wait(int * futex, int val) +{ + return syscall(SYS_futex, futex, FUTEX_WAIT, val, 0, 0, 0); +} +/* + * wait for futex + * futex has to be aligned to 4 bytes + * futex has to be equal to val (returns EWOULDBLOCK otherwise) + * wait can be broken by arriving signals (returns EINTR) + * returns 0 when broken by futex_wake + * returns ETIMEDOUT on timeout + */ +inline int futex_wait_timed(int * futex, int val, const struct timespec *timeout) +{ + return syscall(SYS_futex, futex, FUTEX_WAIT, val, timeout, 0, 0); +} +/* + * wake up futex. returns number of waked processes + */ +inline int futex_wake(int * futex) +{ + return syscall(SYS_futex, futex, FUTEX_WAKE, 1, 0, 0, 0); +} +static timespec one_second = { 1,0 }; +static timespec five_second = { 5,0 }; + // ptr to the real functions static void (*_SDL_GL_SwapBuffers)(void) = 0; static void (*_SDL_Quit)(void) = 0; static int (*_SDL_Init)(uint32_t flags) = 0; +static int (*_SDL_Flip)(void * some_ptr) = 0; // various crud int counter = 0; int errorstate = 0; char *shm; int shmid; -static unsigned char * BigFat; - void SHM_Init ( void ) { // name for the segment key_t key = 123466; - BigFat = (unsigned char *) malloc(SHM_SIZE); - // create the segment - if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0600)) < 0) + // find previous segment, check if it's used by some processes. if it isn't, kill it with fire + if ((shmid = shmget(key, SHM_SIZE, 0600)) != -1) + { + shmid_ds descriptor; + shmctl(shmid, IPC_STAT, &descriptor); + if(descriptor.shm_nattch == 0) + { + shmctl(shmid,IPC_RMID,NULL); + fprintf(stderr,"dfhack: killed dangling resources from crashed DF.\n"); + } + } + // create the segment, make sure only ww are really creating it + if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0600)) < 0) { perror("shmget"); errorstate = 1; @@ -71,30 +119,40 @@ void SHM_Init ( void ) errorstate = 1; return; } + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_RUNNING; // make sure we don't stall or do crazy stuff } void SHM_Destroy ( void ) { - // blah, I don't care + shmid_ds descriptor; + shmctl(shmid, IPC_STAT, &descriptor); + shmdt(shm); + while(descriptor.shm_nattch != 0) + { + shmctl(shmid, IPC_STAT, &descriptor); + } + shmctl(shmid,IPC_RMID,NULL); + fprintf(stderr,"dfhack: destroyed shared segment.\n"); } void SHM_Act (void) { - struct shmid_ds descriptor; - uint32_t numwaits = 0; if(errorstate) { return; } + shmid_ds descriptor; + uint32_t numwaits = 0; uint32_t length; uint32_t address; check_again: // goto target!!! if(numwaits == 10000) { - shmctl(shmid, IPC_STAT, &descriptor); + shmctl(shmid, IPC_STAT, &descriptor); if(descriptor.shm_nattch == 1)// other guy crashed { + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_RUNNING; fprintf(stderr,"dfhack: Broke out of loop, other process disappeared.\n"); return; @@ -117,23 +175,26 @@ void SHM_Act (void) numwaits++; goto check_again; case DFPP_SUSPEND: + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_SUSPENDED; goto check_again; - + /* case DFPP_BOUNCE: length = ((shm_bounce *)shm)->length; memcpy(BigFat,shm + sizeof(shm_bounce),length); memcpy(shm + sizeof(shm_ret_data),BigFat,length); ((shm_cmd *)shm)->pingpong = DFPP_RET_DATA; goto check_again; - + */ case DFPP_PID: ((shm_retval *)shm)->value = getpid(); + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_RET_PID; goto check_again; case DFPP_VERSION: ((shm_retval *)shm)->value = PINGPONG_VERSION; + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_RET_VERSION; goto check_again; @@ -141,24 +202,28 @@ void SHM_Act (void) length = ((shm_read *)shm)->length; address = ((shm_read *)shm)->address; memcpy(shm + sizeof(shm_ret_data), (void *) address,length); + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_RET_DATA; goto check_again; case DFPP_READ_DWORD: address = ((shm_read_small *)shm)->address; ((shm_retval *)shm)->value = *((uint32_t*) address); + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_RET_DWORD; goto check_again; case DFPP_READ_WORD: address = ((shm_read_small *)shm)->address; ((shm_retval *)shm)->value = *((uint16_t*) address); + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_RET_WORD; goto check_again; case DFPP_READ_BYTE: address = ((shm_read_small *)shm)->address; ((shm_retval *)shm)->value = *((uint8_t*) address); + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_RET_BYTE; goto check_again; @@ -166,21 +231,25 @@ void SHM_Act (void) address = ((shm_write *)shm)->address; length = ((shm_write *)shm)->length; memcpy((void *)address, shm + sizeof(shm_write),length); + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_SUSPENDED; goto check_again; case DFPP_WRITE_DWORD: (*(uint32_t*)((shm_write_small *)shm)->address) = ((shm_write_small *)shm)->value; + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_SUSPENDED; goto check_again; case DFPP_WRITE_WORD: (*(uint16_t*)((shm_write_small *)shm)->address) = ((shm_write_small *)shm)->value; + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_SUSPENDED; goto check_again; case DFPP_WRITE_BYTE: (*(uint8_t*)((shm_write_small *)shm)->address) = ((shm_write_small *)shm)->value; + gcc_barrier ((shm_cmd *)shm)->pingpong = DFPP_SUSPENDED; goto check_again; @@ -191,12 +260,13 @@ void SHM_Act (void) default: ((shm_retval *)shm)->value = DFEE_INVALID_COMMAND; + gcc_barrier ((shm_retval *)shm)->pingpong = DFPP_SV_ERROR; break; } } -// hook - called every tick +// hook - called every tick in OpenGL mode of DF extern "C" void SDL_GL_SwapBuffers(void) { if(_SDL_GL_SwapBuffers) @@ -210,6 +280,20 @@ extern "C" void SDL_GL_SwapBuffers(void) } } +// hook - called every tick in the 2D mode of DF +extern "C" int SDL_Flip(void * some_ptr) +{ + if(_SDL_Flip) + { + if(((shm_cmd *)shm)->pingpong != DFPP_RUNNING) + { + SHM_Act(); + } + counter ++; + return _SDL_Flip(some_ptr); + } +} + // hook - called at program exit extern "C" void SDL_Quit(void) { @@ -217,7 +301,8 @@ extern "C" void SDL_Quit(void) { _SDL_Quit(); } - fprintf(stderr,"dfhack: DF called SwapBuffers %d times, lol\n", counter); + fprintf(stderr,"dfhack: DF called SwapBuffers %d times\n", counter); + SHM_Destroy(); } // hook - called at program start, initialize some stuffs we'll use later @@ -226,6 +311,7 @@ extern "C" int SDL_Init(uint32_t flags) // find real functions _SDL_GL_SwapBuffers = (void (*)( void )) dlsym(RTLD_NEXT, "SDL_GL_SwapBuffers"); _SDL_Init = (int (*)( uint32_t )) dlsym(RTLD_NEXT, "SDL_Init"); + _SDL_Flip = (int (*)( void * )) dlsym(RTLD_NEXT, "SDL_Flip"); _SDL_Quit = (void (*)( void )) dlsym(RTLD_NEXT, "SDL_Quit"); // check if we got them diff --git a/library/shmserver/dfconnect.h b/library/shmserver/dfconnect.h index a75b3ea09..a86e7f4cf 100644 --- a/library/shmserver/dfconnect.h +++ b/library/shmserver/dfconnect.h @@ -12,6 +12,14 @@ * end - sent to DF for breaking out of the wait */ +enum DF_SHM_ERRORSTATE +{ + SHM_OK, // all OK + SHM_CANT_GET_SHM, // getting the SHM ID failed for some reason + SHM_CANT_ATTACH, // we can't attach the shm for some reason + SHM_SECOND_DF // we are a second DF process, can't use SHM at all +}; + enum DF_PINGPONG { DFPP_RUNNING = 0, // no command, normal server execution diff --git a/library/shmserver/dfconnect.so b/library/shmserver/dfconnect.so new file mode 100755 index 0000000000000000000000000000000000000000..17960daa83ea8f51cdd681ebf913f00b1dd88c87 GIT binary patch literal 12100 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