Author: sayer
Date: 2009-10-22 19:11:11 +0200 (Thu, 22 Oct 2009)
New Revision: 1561

Added:
   trunk/apps/dsm/doc/examples/test_byehdr.dsm
Modified:
   trunk/apps/dsm/DSMCall.cpp
   trunk/apps/dsm/DSMCoreModule.cpp
   trunk/apps/dsm/DSMSession.h
   trunk/apps/dsm/DSMStateEngine.h
   trunk/apps/dsm/doc/dsm_syntax.txt
   trunk/apps/dsm/mods/mod_utils/Readme.mod_utils.txt
Log:
updated core module with exceptions instead of errno



Modified: trunk/apps/dsm/DSMCall.cpp
===================================================================
--- trunk/apps/dsm/DSMCall.cpp  2009-10-22 16:42:40 UTC (rev 1560)
+++ trunk/apps/dsm/DSMCall.cpp  2009-10-22 17:11:11 UTC (rev 1561)
@@ -57,10 +57,6 @@
   for (set<AmPromptCollection*>::iterator it=
         used_prompt_sets.begin(); it != used_prompt_sets.end(); it++)
     (*it)->cleanup((long)this);
-
-//   for (map<string, AmPromptCollection*>::iterator it=
-//      prompt_sets.begin(); it != prompt_sets.end(); it++)
-//     it->second->cleanup((long)this);
 }
 
 /** returns whether var exists && var==value*/
@@ -192,7 +188,10 @@
 void DSMCall::onBye(const AmSipRequest& req)
 {
   DBG("onBye\n");
-  engine.runEvent(this, DSMCondition::Hangup, NULL);
+  map<string, string> params;
+  params["headers"] = req.hdrs;
+ 
+  engine.runEvent(this, DSMCondition::Hangup, &params);
 }
 
 void DSMCall::process(AmEvent* event)
@@ -246,14 +245,14 @@
     if ((var["prompts.default_fallback"] != "yes") ||
       default_prompts->addToPlaylist(name,  (long)this, playlist, 
                                    /*front =*/ false, loop)) {
-       DBG("checked [%p]\n", default_prompts);
-      SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+      DBG("checked [%p]\n", default_prompts);
+      throw DSMException("prompt", "name", name);
     } else {
       used_prompt_sets.insert(default_prompts);
-      SET_ERRNO(DSM_ERRNO_OK);    
+      SET_RES(DSM_RES_OK);    
     }      
   } else {
-    SET_ERRNO(DSM_ERRNO_OK);
+    SET_RES(DSM_RES_OK);
   }
 }
 
@@ -273,7 +272,9 @@
     ERROR("audio file '%s' could not be opened for reading.\n", 
          name.c_str());
     delete af;
-    SET_ERRNO(DSM_ERRNO_FILE);
+    
+    throw DSMException("file", "path", name);
+
     return;
   }
   if (loop) 
@@ -285,7 +286,7 @@
     playlist.addToPlaylist(new AmPlaylistItem(af, NULL));
 
   audiofiles.push_back(af);
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
 }
 
 void DSMCall::recordFile(const string& name) {
@@ -299,28 +300,28 @@
          name.c_str());
     delete rec_file;
     rec_file = NULL;
-    SET_ERRNO(DSM_ERRNO_FILE);
+    throw DSMException("file", "path", name);
     return;
   }
   setInput(rec_file); 
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
 }
 
 unsigned int DSMCall::getRecordLength() {
   if (!rec_file) {
-    SET_ERRNO(DSM_ERRNO_FILE);
+    SET_RES(DSM_RES_SCRIPT);
     return 0;
   }
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
   return rec_file->getLength();
 }
 
 unsigned int DSMCall::getRecordDataSize() {
   if (!rec_file) {
-    SET_ERRNO(DSM_ERRNO_FILE);
+    SET_RES(DSM_RES_SCRIPT);
     return 0;
   }
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
   return rec_file->getDataSize();
 }
 
@@ -330,10 +331,10 @@
     rec_file->close();
     delete rec_file;
     rec_file = NULL;
-    SET_ERRNO(DSM_ERRNO_OK);
+    SET_RES(DSM_RES_OK);
   } else {
     WARN("stopRecord: we are not recording\n");
-    SET_ERRNO(DSM_ERRNO_FILE);
+    SET_RES(DSM_RES_SCRIPT);
     return;
   }
 }
@@ -343,8 +344,10 @@
   if (prompt_set) {
     DBG("adding prompt set '%s'\n", name.c_str());
     prompt_sets[name] = prompt_set;
+    SET_RES(DSM_RES_OK);
   } else {
     ERROR("trying to add NULL prompt set\n");
+    SET_RES(DSM_RES_INTERNAL);
   }
 }
 
@@ -359,21 +362,21 @@
 
   if (it == prompt_sets.end()) {
     ERROR("prompt set %s unknown\n", name.c_str());
-    SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+    throw DSMException("prompt", "name", name);
     return;
   }
 
   DBG("setting prompt set '%s'\n", name.c_str());
   used_prompt_sets.insert(prompts);
   prompts = it->second;
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
 }
 
 
 void DSMCall::addSeparator(const string& name, bool front) {
   unsigned int id = 0;
   if (str2i(name, id)) {
-    SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+    SET_RES(DSM_RES_UNKNOWN_ARG);
     return;
   }
 
@@ -384,7 +387,7 @@
     playlist.addToPlaylist(new AmPlaylistItem(sep, sep));
   // for garbage collector
   audiofiles.push_back(sep);
-  SET_ERRNO(DSM_ERRNO_OK);
+  SET_RES(DSM_RES_OK);
 }
 
 void DSMCall::transferOwnership(DSMDisposable* d) {

Modified: trunk/apps/dsm/DSMCoreModule.cpp
===================================================================
--- trunk/apps/dsm/DSMCoreModule.cpp    2009-10-22 16:42:40 UTC (rev 1560)
+++ trunk/apps/dsm/DSMCoreModule.cpp    2009-10-22 17:11:11 UTC (rev 1561)
@@ -183,9 +183,9 @@
 
   DBG("posting event to session '%s'\n", sess_id.c_str());
   if (!AmSessionContainer::instance()->postEvent(sess_id, ev))
-    sc_sess->SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
   else 
-    sc_sess->SET_ERRNO(DSM_ERRNO_OK);
+    sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCPlayFileAction, ',', true);
@@ -219,6 +219,7 @@
   if (varname.empty())
     varname = "record_length";
   sc_sess->var[varname]=int2str(sc_sess->getRecordLength());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCGetRecordDataSizeAction) {
@@ -232,31 +233,38 @@
   bool notify = 
     resolveVars(arg, sess, sc_sess, event_params) == "true";
   sc_sess->closePlaylist(notify);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCConnectMediaAction) {
   sc_sess->connectMedia();
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCDisconnectMediaAction) {
   sc_sess->disconnectMedia();
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCMuteAction) {
   sc_sess->mute();
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCUnmuteAction) {
   sc_sess->unmute();
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 
 EXEC_ACTION_START(SCEnableDTMFDetection) {
   sess->setDtmfDetectionEnabled(true);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCDisableDTMFDetection) {
   sess->setDtmfDetectionEnabled(false);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCThrowAction, ',', true);
@@ -328,11 +336,13 @@
   unsigned int lvl;
   if (str2i(resolveVars(par1, sess, sc_sess, event_params), lvl)) {
     ERROR("unknown log level '%s'\n", par1.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   }
   string l_line = resolveVars(par2, sess, sc_sess, event_params).c_str();
   _LOG((int)lvl, "FSM: %s '%s'\n", (par2 != l_line)?par2.c_str():"",
        l_line.c_str());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCLogVarsAction) {
@@ -348,6 +358,7 @@
     _LOG((int)lvl, "FSM:  $%s='%s'\n", it->first.c_str(), it->second.c_str());
   }
   _LOG((int)lvl, "FSM: variables end ---\n");
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCSetAction,'=', false);
@@ -358,6 +369,7 @@
   sc_sess->var[var_name] = resolveVars(par2, sess, sc_sess, event_params);
   DBG("set $%s='%s'\n", 
       var_name.c_str(), sc_sess->var[var_name].c_str());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCSetVarAction,'=', false);
@@ -366,6 +378,7 @@
   sc_sess->var[var_name] = resolveVars(par2, sess, sc_sess, event_params);
   DBG("set $%s='%s'\n", 
       var_name.c_str(), sc_sess->var[var_name].c_str());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 
@@ -386,6 +399,7 @@
     arg.substr(1) : arg;
   DBG("clear variable '%s'\n", var_name.c_str());
   sc_sess->var.erase(var_name);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 
@@ -398,6 +412,7 @@
 
   DBG("$%s now '%s'\n", 
       var_name.c_str(), sc_sess->var[var_name].c_str());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCSubStrAction,',', false);
@@ -408,19 +423,20 @@
   if (str2i(resolveVars(par2, sess, sc_sess, event_params), pos)) {
     ERROR("substr length '%s'\n",
          resolveVars(par2, sess, sc_sess, event_params).c_str());
-    sc_sess->SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   }
   try {
     sc_sess->var[var_name] = sc_sess->var[var_name].substr(pos);
   } catch(...) {
     ERROR("in substr\n");
-    sc_sess->SET_ERRNO(DSM_ERRNO_UNKNOWN_ARG);
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   }
 
   DBG("$%s now '%s'\n", 
       var_name.c_str(), sc_sess->var[var_name].c_str());
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCIncAction) {
@@ -432,6 +448,8 @@
 
   DBG("inc: $%s now '%s'\n", 
       var_name.c_str(), sc_sess->var[var_name].c_str());
+
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCSetTimerAction,',', false);
@@ -441,6 +459,7 @@
   if (str2i(resolveVars(par1, sess, sc_sess, event_params), timerid)) {
     ERROR("timer id '%s' not decipherable\n", 
          resolveVars(par1, sess, sc_sess, event_params).c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   }
 
@@ -448,6 +467,7 @@
   if (str2i(resolveVars(par2, sess, sc_sess, event_params), timeout)) {
     ERROR("timeout value '%s' not decipherable\n", 
          resolveVars(par2, sess, sc_sess, event_params).c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   }
 
@@ -457,11 +477,13 @@
 
   if(!user_timer_fact) {
     ERROR("load sess_timer module for timers.\n");
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
   AmDynInvoke* user_timer = user_timer_fact->getInstance();
   if(!user_timer) {
     ERROR("load sess_timer module for timers.\n");
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
 
@@ -471,6 +493,7 @@
   di_args.push(sess->getLocalTag().c_str());
   user_timer->invoke("setTimer", di_args, ret);
 
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 
@@ -480,6 +503,7 @@
   if (str2i(resolveVars(arg, sess, sc_sess, event_params), timerid)) {
     ERROR("timer id '%s' not decipherable\n", 
          resolveVars(arg, sess, sc_sess, event_params).c_str());
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
 
@@ -488,11 +512,13 @@
     AmPlugIn::instance()->getFactory4Di("user_timer");
 
   if(!user_timer_fact) {
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     ERROR("load sess_timer module for timers.\n");
     return false;
   }
   AmDynInvoke* user_timer = user_timer_fact->getInstance();
   if(!user_timer) {
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     ERROR("load sess_timer module for timers.\n");
     return false;
   }
@@ -502,6 +528,7 @@
   di_args.push(sess->getLocalTag().c_str());
   user_timer->invoke("removeTimer", di_args, ret);
 
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 EXEC_ACTION_START(SCRemoveTimersAction) {
@@ -513,11 +540,13 @@
 
   if(!user_timer_fact) {
     ERROR("load sess_timer module for timers.\n");
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
   AmDynInvoke* user_timer = user_timer_fact->getInstance();
   if(!user_timer) {
     ERROR("load sess_timer module for timers.\n");
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
 
@@ -525,6 +554,7 @@
   di_args.push(sess->getLocalTag().c_str());
   user_timer->invoke("removeUserTimers", di_args, ret);
 
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 
@@ -648,6 +678,7 @@
   if (params.size() < 2) {
     ERROR("DI needs at least: mod_name, "
          "function_name (in '%s'\n", name.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;    
   }
 
@@ -658,11 +689,13 @@
 
   if(!fact) {
     ERROR("load module for factory '%s'.\n", fact_name.c_str());
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
   AmDynInvoke* di_inst = fact->getInstance();
   if(!di_inst) {
     ERROR("load module for factory '%s'\n", fact_name.c_str());
+    sc_sess->SET_RES(DSM_RES_ADMIN);
     return false;
   }
   p_it++; 
@@ -686,6 +719,7 @@
       } else {
        ERROR("converting value '%s' to int\n", 
              p.c_str());
+       sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
        return false;
       }
     } else {
@@ -701,21 +735,26 @@
   } catch (const AmDynInvoke::NotImplemented& ni) {
     ERROR("not implemented DI function '%s'\n", 
          ni.what.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   } catch (const AmArg::OutOfBoundsException& oob) {
     ERROR("out of bounds in  DI call '%s'\n", 
          name.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   } catch (const AmArg::TypeMismatchException& oob) {
     ERROR("type mismatch  in  DI call '%s'\n", 
          name.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN_ARG);
     return false;
   } catch (...) {
     ERROR("unexpected Exception  in  DI call '%s'\n", 
          name.c_str());
+    sc_sess->SET_RES(DSM_RES_UNKNOWN);
     return false;
   }
 
+  bool flag_error = false;
   if (get_res) {
     // rudimentary variables conversion...
     if (isArgCStr(sc_sess->di_res)) 
@@ -736,13 +775,21 @@
        } break;
        default: {
          ERROR("unsupported AmArg return type!");
+         flag_error = true;
+         sc_sess->SET_RES(DSM_RES_UNKNOWN);
        }
        }
       }
     } else {
       ERROR("unsupported AmArg return type!");
+      flag_error = true;
+      sc_sess->SET_RES(DSM_RES_UNKNOWN);
     }
   }
+  if (!flag_error) {
+    sc_sess->SET_RES(DSM_RES_OK);
+  }
+
 } EXEC_ACTION_END;
   
 
@@ -751,14 +798,17 @@
   string remote_party = resolveVars(par1, sess, sc_sess, event_params);
   string remote_uri = resolveVars(par2, sess, sc_sess, event_params);
   sc_sess->B2BconnectCallee(remote_party, remote_uri);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
  
 EXEC_ACTION_START(SCB2BTerminateOtherLegAction) {
   sc_sess->B2BterminateOtherLeg();
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;
 
 CONST_ACTION_2P(SCB2BReinviteAction,',', true);
 EXEC_ACTION_START(SCB2BReinviteAction) {
   bool updateSDP = par1=="true";
   sess->sendReinvite(updateSDP, par2);
+  sc_sess->SET_RES(DSM_RES_OK);
 } EXEC_ACTION_END;

Modified: trunk/apps/dsm/DSMSession.h
===================================================================
--- trunk/apps/dsm/DSMSession.h 2009-10-22 16:42:40 UTC (rev 1560)
+++ trunk/apps/dsm/DSMSession.h 2009-10-22 17:11:11 UTC (rev 1561)
@@ -54,6 +54,17 @@
 #define SET_ERRNO(new_errno) \
     var["errno"] = new_errno
 
+#define DSM_RES_OK          ""
+#define DSM_RES_UNKNOWN     "1" // unknown error
+#define DSM_RES_UNKNOWN_ARG "2" // unknown argument/argument error
+#define DSM_RES_ADMIN       "3" // configuration error
+#define DSM_RES_SCRIPT      "4" // DSM script error
+#define DSM_RES_INTERNAL    "5" // internal error
+
+#define SET_RES(new_res) \
+    var["res"] = new_res
+
+
 class DSMDisposable;
 class AmPlaylistItem;
 

Modified: trunk/apps/dsm/DSMStateEngine.h
===================================================================
--- trunk/apps/dsm/DSMStateEngine.h     2009-10-22 16:42:40 UTC (rev 1560)
+++ trunk/apps/dsm/DSMStateEngine.h     2009-10-22 17:11:11 UTC (rev 1561)
@@ -167,6 +167,18 @@
   DSMException(const string& e_type) 
     { params["type"] = e_type; }
 
+  DSMException(const string& e_type, 
+              const string& key1, const string& val1) 
+    { params["type"] = e_type; 
+      params[key1] = val1; }
+
+  DSMException(const string& e_type, 
+              const string& key1, const string& val1,
+              const string& key2, const string& val2) 
+    { params["type"] = e_type; 
+      params[key1] = val1; 
+      params[key2] = val2; }
+
   DSMException(map<string, string>& params)
     : params(params) { }
 

Modified: trunk/apps/dsm/doc/dsm_syntax.txt
===================================================================
--- trunk/apps/dsm/doc/dsm_syntax.txt   2009-10-22 16:42:40 UTC (rev 1560)
+++ trunk/apps/dsm/doc/dsm_syntax.txt   2009-10-22 17:11:11 UTC (rev 1561)
@@ -40,14 +40,23 @@
    from promptCollection, e.g. playPrompt("hello");
    if $prompts.default_fallback=yes, default prompt set is tried if
    prompt not found in current prompt set
+   Throws "prompt" exeption with #name if prompt not found.
+
  playPromptLooped(param)
 
  setPromptSet(name)
-   if more prompt sets are loaded
+   select active prompt set if more prompt sets are loaded
+   Throws "prompt" exeption with #name if prompt set not found
 
  playFile(filename)
+   Throws "file" exeption with #path if file can not be opened
+
  playFileFront(filename)
+   Throws "file" exeption with #path if file can not be opened
+
  recordFile(filename)
+   Throws "file" exeption with #path if file can not be opened for recording
+
  stopRecord()
  getRecordLength([dst_varname])   -- only while recording! default dst var: 
record_length
  getRecordDataSize([dst_varname]) -- only while recording! default dst var: 
record_data_size

Added: trunk/apps/dsm/doc/examples/test_byehdr.dsm
===================================================================
--- trunk/apps/dsm/doc/examples/test_byehdr.dsm 2009-10-22 16:42:40 UTC (rev 
1560)
+++ trunk/apps/dsm/doc/examples/test_byehdr.dsm 2009-10-22 17:11:11 UTC (rev 
1561)
@@ -0,0 +1,7 @@
+
+initial state START 
+       enter {
+         playFile(wav/default_en.wav);
+       };
+transition "got bye" START - hangup / log(1, #headers) -> end;
+state end;
\ No newline at end of file

Modified: trunk/apps/dsm/mods/mod_utils/Readme.mod_utils.txt
===================================================================
--- trunk/apps/dsm/mods/mod_utils/Readme.mod_utils.txt  2009-10-22 16:42:40 UTC 
(rev 1560)
+++ trunk/apps/dsm/mods/mod_utils/Readme.mod_utils.txt  2009-10-22 17:11:11 UTC 
(rev 1561)
@@ -3,13 +3,23 @@
 
  utils.playCountRight(int cnt [, string basedir])
     play count for laguages that have single digits after the 10s (like 
english)
+    Throws "file" exeption with #path if file can not be opened
 
  utils.playCountLeft(int cnt [, string basedir])
     play count for laguages that have single digits befire the 10s (like 
german)  
+    Throws "file" exeption with #path if file can not be opened
 
  utils.spell(string word[, string basedir])
   plays each character in the word (e.g. utils.spell(321,wav/digits/) plays
     wav/digits/3.wav, wav/digits/2.wav, wav/digits/1.wav 
-  like SayDigits 
+  (like SayDigits from *)
+  Throws "file" exeption with #path if file can not be opened
 
+
+ utils.rand(string varname [, int modulo])
+  generates random number: $varname=rand()%modulo or $varname = rand()
+
+ utils.srand() 
+  seed the RNG with time().
+
 Conditions: 

_______________________________________________
Semsdev mailing list
[email protected]
http://lists.iptel.org/mailman/listinfo/semsdev

Reply via email to