Changeset - 8b315b2b5d84
[Not reviewed]
default
0 1 0
Ethan Zonca (ethanzonca) - 15 years ago 2010-06-07 18:58:39
e@ethanzonca.com
Reorganized and commented code that binki needs to focus on
1 file changed with 148 insertions and 125 deletions:
0 comments (0 inline, 0 general)
src/server/slavefuncs.c
Show inline comments
 
/*
 
  Copyright 2010 Nathan Phillip Brink, Ethan Zonca, Matthew Orlando
 

	
 
  This file is a part of DistRen.
 

	
 
  DistRen is free software: you can redistribute it and/or modify
 
  it under the terms of the GNU Affero General Public License as published by
 
  the Free Software Foundation, either version 3 of the License, or
 
  (at your option) any later version.
 

	
 
  DistRen is distributed in the hope that it will be useful,
 
  but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  GNU Affero General Public License for more details.
 

	
 
  You should have received a copy of the GNU Affero General Public License
 
  along with DistRen.  If not, see <http://www.gnu.org/licenses/>.
 
*/
 

	
 
#include "distrenjob.h"
 
#include "slavefuncs.h"
 

	
 
#include "common/asprintf.h"
 
#include "common/execio.h"
 
#include "common/protocol.h"
 
#include "common/remoteio.h"
 

	
 
#include <curl/curl.h>
 
#include <curl/easy.h>
 
#include <curl/types.h>
 

	
 
#include <archive.h>
 
#include <archive_entry.h>
 

	
 
#include <stdio.h>
 
#include <string.h>
 
#include <unistd.h>
 
#include <stdlib.h>
 
#include <sys/stat.h>
 
#include <fcntl.h>
 

	
 
#include <time.h>
 

	
 
#define DEBUG 0
 
/**
 
   Sends the server a single request (see protocol.h)
 
*/
 
int sendSignal(struct remoteio *rem, char signal)
 
{
 
  size_t written;
 
  size_t towrite;
 
  char *ssignal;
 

	
 
  _distren_asprintf(&ssignal, "%c", signal);
 
  towrite = strlen(ssignal);
 
  while( towrite
 
	 && !remoteio_write(rem, ssignal, towrite, &written))
 
    {
 
      fprintf(stderr, "Sending request...\n");
 
      towrite -= written;
 
    }
 
  if(written)
 
    return 0;
 

	
 
  /**
 
     if remoteio_write returned 1, the connection
 
     is probably dead or there was a real error
 
   */
 
  return 1;
 
}
 

	
 
/**
 
   Sends the server an extended signal (request + data)
 
*/
 
int sendExtSignal(struct remoteio *rem, char signal, char *data){
 
  size_t written;
 
  size_t towrite;
 
  char *ssignal;
 
  _distren_asprintf(&ssignal, "%c%s", signal, data); // Just append the data FIXME: We should do this differently
 
  towrite = strlen(ssignal);
 
  while( towrite
 
          && !remoteio_write(rem, ssignal, towrite, &written))
 
     {
 
       fprintf(stderr, "Sending request...\n");
 
       towrite -= written;
 
     }
 
   if(written)
 
     return 0;
 

	
 
   /**
 
      if remoteio_write returned 1, the connection
 
      is probably dead or there was a real error
 
    */
 
   return 1;
 
}
 

	
 

	
 

	
 
/**
 
   Grabs the xml DOM node reached by an XPath.
 

	
 
   @param path an XPath that lead to DOM node
 
   @return the first node associated with the path or NULL if there is no match
 
 */
 
xmlNodePtr xml_quickxpath(xmlXPathContextPtr xpathctxt, xmlChar *path)
 
{
 
  xmlNodePtr toreturn;
 

	
 
  xmlXPathObjectPtr xmlxpathobjptr;
 
  xmlxpathobjptr = xmlXPathEval(path, xpathctxt);
 
  if(!xmlxpathobjptr
 
     || !xmlxpathobjptr->nodesetval->nodeNr)
 
    {
 
      fprintf(stderr, "XPath resolution failed for ``%s'' in ``%s'' (``%s'')\n", path, xpathctxt->doc->name, xpathctxt->doc->URL);
 
      return (xmlNodePtr)NULL;
 
    }
 

	
 
  toreturn = *(xmlxpathobjptr->nodesetval->nodeTab);
 

	
 
  xmlXPathFreeObject(xmlxpathobjptr);
 

	
 
  return toreturn;
 
}
 

	
 

	
 
/** Stub for deleting job data from the disk. @TODO: unstubify me! */
 
int delete_jobdata(int jobnum, char *datadir)
 
{
 
  char *jobpath;
 
  _distren_asprintf(&jobpath, "%s/%d", datadir, jobnum);
 
  // rmdir(jobpath);
 
  fprintf(stderr, "Please manually remove %s. Automatic removal is currently not implemented.\n", jobpath);
 
  return 0;
 
}
 

	
 
/** Function referenced by curlget() to write data to disk. */
 
size_t curl_writetodisk(void *ptr, size_t size, size_t nmemb, FILE *stream)
 
 {
 
    return fwrite(ptr, size, nmemb, stream);
 
  }
 

	
 
/** Helper function for cURL's progress display */
 
int curl_progress(char *Bar, double t, double d, double ultotal, double ulnow)
 
@@ -209,104 +158,96 @@ CURLcode curlpost(char *filename, char *
 
               &lastptr,
 
               CURLFORM_COPYNAME, "framenum",
 
               CURLFORM_COPYCONTENTS, sframenum, CURLFORM_END);
 
  /* slavekey field... */
 
  curl_formadd(&formpost,
 
               &lastptr,
 
               CURLFORM_COPYNAME, "slavekey",
 
               CURLFORM_COPYCONTENTS, sslavekey, CURLFORM_END);
 
  /* upload field... */
 
  curl_formadd(&formpost,
 
               &lastptr,
 
               CURLFORM_COPYNAME, targetname,
 
               CURLFORM_FILE, filename,
 
               CURLFORM_END);
 
  /* filename field... */
 
  curl_formadd(&formpost,
 
               &lastptr,
 
               CURLFORM_COPYNAME, "filename",
 
               CURLFORM_COPYCONTENTS, filename,
 
               CURLFORM_END);
 
  /* submit field, not usually needed, just in case... */
 
  curl_formadd(&formpost,
 
               &lastptr,
 
               CURLFORM_COPYNAME, "submit",
 
               CURLFORM_COPYCONTENTS, "send",
 
               CURLFORM_END);
 

	
 
  res = CURLE_FAILED_INIT;
 
  curl = curl_easy_init();
 
  headerlist = curl_slist_append(headerlist, buf);
 
  if(curl)
 
    {
 
    /* Setting the URL to get the post, and the contents of the post */
 
    curl_easy_setopt(curl, CURLOPT_URL, url);
 
    curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
 
    res = curl_easy_perform(curl);
 

	
 
    curl_easy_cleanup(curl);
 
    /* cleanup the formpost junk */
 
    curl_formfree(formpost);
 
    curl_slist_free_all (headerlist);
 
    free(sjobnum);
 
    free(sframenum);
 
    free(sslavekey);
 
    }
 
  return res;
 
}
 

	
 

	
 
/** Logs the user into the server after ensuring that keys exist */
 
int login_user(char *username)
 
{
 
  // @TODO: Put some telnet-style auth code here unless this is obselete
 
  return 1; // success
 
}
 

	
 
/** Replaces wordtoreplace with replacewith in conffile (relative to SYSCONFDIR) */
 
int conf_replace(char *conffile, char *wordtoreplace, char *replacewith){
 
  int maxlinelen = 120;
 
  char *fileOrig;
 
  char *fileRepl;
 
  _distren_asprintf(&fileOrig, "%s/%s", SYSCONFDIR, conffile);
 
  _distren_asprintf(&fileRepl, "%s%s.edited", SYSCONFDIR, conffile);
 
  char buffer[maxlinelen+2];
 
  char *buff_ptr, *find_ptr;
 
  FILE *fp1, *fp2;
 
  size_t find_len = strlen(wordtoreplace);
 
  fp1 = fopen(fileOrig,"r");
 
  fp2 = fopen(fileRepl,"w");
 
  if (fp1 ==NULL){
 
    fprintf(stderr, "%s doesn't exist\n",fileOrig);
 
    return 0;
 
  }
 
  else if(fp2 ==NULL){
 
    fprintf(stderr, "Can't write a file to disk! Check permissions.\n");
 
    return 0;
 
  }
 
  else{
 
    while(fgets(buffer,maxlinelen+2,fp1))
 
      {
 
        buff_ptr = buffer;
 
        while ((find_ptr = strstr(buff_ptr,wordtoreplace)))
 
        {
 
           while(buff_ptr < find_ptr)
 
             fputc((int)*buff_ptr++,fp2);
 
           fputs(replacewith,fp2);
 
           buff_ptr += find_len;
 
         }
 
         fputs(buff_ptr,fp2);
 
      }
 
    rename(fileRepl, fileOrig);
 
  }
 
  fclose(fp2);
 
  fclose(fp1);
 
  fprintf(stderr,"Wrote conf file...\n");
 
return 1; // Success
 
}
 

	
 

	
 
/* Executors */
 

	
 
/** Executor function for Blender operations */
 
int exec_blender(char *input, char *output, int frame)
 
{
 
@@ -393,160 +334,96 @@ int downloadTar(char *url, char *destina
 
  int fstatus = stat(destinationPath, &buffer);
 
  if(fstatus == -1)
 
    {
 
      // Download the Tar
 
      int ret = curlget(url, destinationPath);
 
      if(ret == 0){
 
        fprintf(stderr, "Job data retrieved successfully\n");
 
        // free(url); @FIXME: causes doublefree! Curl must free the url?
 
        return 0;
 
      }
 
      else if(ret == 1){
 
        fprintf(stderr, "Downloading job data from %s failed. Check your network connection.\n",url);
 
        free(url);
 
        return 1; // Eventually make a retry loop
 
      }
 
    }
 
  fprintf(stderr, "Tar already exists! Download cancelled.\n");
 
  return 2;
 
}
 

	
 

	
 
int uploadOutput(char *pathtoOutput, char *urltoOutput, int jobnum, int framenum, int slavekey){
 
  //fprintf(stderr,"Uploading output %s to url %s for job %d and frame %d for slavekey %d", pathtoOutput, urltoOutput, jobnum, framenum, slavekey);
 
  if( !curlpost(pathtoOutput, urltoOutput, jobnum, framenum, slavekey)) // Uploads output
 
    {
 
      if(DEBUG)
 
        fprintf(stderr,"Upload successful, removing old output...\n");
 
      else
 
        fprintf(stderr,"Upload completed\n");
 

	
 
      remove(pathtoOutput); // Delete the file after its uploaded
 
      return 0;
 
    }
 
  else
 
    {
 
      fprintf(stderr,"Upload failed. Check your network connection. Retrying upload...\n");
 
      int tries=1;
 
      while(tries<=10 && curlpost(pathtoOutput, urltoOutput, jobnum, framenum, slavekey))
 
        {
 
          fprintf(stderr, "Upload failed. Trying again in 10 seconds... (attempt %d of 10)\n", tries);
 
          tries++;
 
          sleep(10);
 
        }
 
      return 1; // Upload failed after multiple tries
 
      // @FUTURE: Keep track of files that we were unable to upload, and upload them later
 
    }
 
}
 

	
 
/**
 
   Extracts archive to the specified directory, creating this directory
 
   if necessary (but this directory will not be recursively created).
 
   @param outdir output directory
 
   @param pathtoTar filename of the archive to extract
 
 */
 
int unpackJob(char *outdir, char *pathtoTar)
 
{
 
  int ret;
 

	
 
  struct archive *a;
 
  struct archive_entry *ae;
 
  int astatus;
 

	
 
  /* ignore return because directory may exist already */
 
  mkdir(outdir, 0700);
 
  ret = chdir(outdir);
 
  if(ret == -1)
 
    {
 
      perror("chdir");
 
      return 1;
 
    }
 

	
 
  a = archive_read_new();
 
  ae = archive_entry_new();
 

	
 
  archive_read_support_compression_all(a);
 
  archive_read_support_format_raw(a);
 
  astatus = archive_read_open_filename(a, pathtoTar, 8192);
 
  if (astatus != ARCHIVE_OK)
 
    {
 
      fprintf(stderr, "error opening archive\n");
 
      return 1;
 
    }
 

	
 
  for(astatus = ARCHIVE_OK;
 
      astatus == ARCHIVE_OK
 
	|| astatus == ARCHIVE_WARN;
 
      )
 
    {
 
      astatus = archive_read_next_header2(a, ae);
 
      if(astatus == ARCHIVE_WARN)
 
	fprintf(stderr, "Encountered nonfatal read error somehow\n");
 

	
 
      if(astatus == ARCHIVE_OK
 
	 || astatus == ARCHIVE_WARN)
 
	astatus = archive_read_extract(a, ae,
 
				       ARCHIVE_EXTRACT_NO_OVERWRITE
 
				       | ARCHIVE_EXTRACT_SECURE_SYMLINKS
 
				       | ARCHIVE_EXTRACT_SECURE_NODOTDOT);
 
    }
 
  archive_entry_free(ae);
 
  archive_read_finish(a);
 
  
 
  if(astatus != ARCHIVE_EOF)
 
    {
 
      fprintf(stderr, "Error reading archive\n");
 
      return 1;
 
    }
 

	
 
  return 0;
 
}
 

	
 

	
 
void prepareJobPaths(int jobnum, int framenum, char *outputExt, char *datadir, char **urltoTar,char **pathtoTar,char **pathtoTardir,char **pathtoJob, char **pathtoJobfile, char **urltoJobfile, char **urltoOutput,char **pathtoOutput, char **pathtoRenderOutput, char **pathtoOutdir)
 
{
 
  // Variable Preparation
 
  char *jobdatapath;
 
   _distren_asprintf(&jobdatapath, "job%d", jobnum);
 
   _distren_asprintf(urltoTar, "http://data.distren.org/job%d/job%d.tar.gz", jobnum, jobnum); // Prepares URL to download from
 
   _distren_asprintf(urltoJobfile, "http://data.distren.org/job%d/job.blend", jobnum); // Prepares URL to download from
 

	
 
   _distren_asprintf(pathtoTar, "%s/%s/job%d.tar.gz", datadir, jobdatapath, jobnum); // Prepares destination to save to
 

	
 
   _distren_asprintf(pathtoTardir, "%s/%s/", datadir, jobdatapath); // Prepares destination to save to
 

	
 
   _distren_asprintf(pathtoJob, "%s/%s/", datadir, jobdatapath);
 

	
 
   _distren_asprintf(pathtoJobfile, "%s/%s/job.blend", datadir, jobdatapath ); // Prepares the path to the jobfile
 
   _distren_asprintf(urltoOutput, "http://dev.distren.org/slaveUpload.php?jobkey=%d&framekey=%d",jobnum,framenum); // Prepares the URL where output is posted
 

	
 
   _distren_asprintf(pathtoRenderOutput, "%s/%s/output/job%d-frame#.%s", datadir, jobdatapath, jobnum, outputExt ); // Note: the # is for blender to put in framenum in output file
 
   _distren_asprintf(pathtoOutput, "%s/%s/output/job%d-frame%d.%s", datadir, jobdatapath, jobnum, framenum, outputExt ); // Note: the # is for blender to put in framenum in output file
 

	
 
   _distren_asprintf(pathtoOutdir, "%s/%s/output", datadir, jobdatapath);
 
   free(jobdatapath);
 
}
 

	
 
int checkUsername(char *username)
 
{
 
  if(username == NULL || strcmp(username, "!username") == 0 )
 
    {
 
     fprintf(stderr, "\nPlease ensure that your username is present in distrenslave.conf\n");
 
     return 1;
 
   }
 
   else
 
     if( username != NULL || strcmp(username, "!username") != 0 )
 
       {
 
         // Log in the user
 
         if(login_user(username) == 1){
 
           // Logged in OK
 
           return 0;
 
         }
 
         else
 
           {
 
             fprintf(stderr, "Login failed, please check your username. If you have not registered, please do so on the DistRen website.\n");
 
             return 1;
 
           }
 
       }
 
     else
 
       {
 
         fprintf(stderr, "Please check your distrenslave.conf, it appears to be incorrectly formatted.\n");
 
@@ -826,103 +703,249 @@ int slaveBenchmark(char *datadir, int *b
 
  fprintf(stderr,"Running benchmark...\n");
 

	
 
  long startTime;
 
  long endTime;
 

	
 
  time(&startTime);
 

	
 
  char buf[20];
 
  struct execio *testrem;
 
  size_t readlen;
 

	
 
  ret = execio_open(&testrem, command, cmd);
 
  if(ret)
 
    {
 
      fprintf(stderr, "Error executing blender\n");
 
      return 1;
 
    }
 

	
 
  buf[19] = '\0';
 
  while(!execio_read(testrem, buf, 19, &readlen))
 
    {
 
       buf[readlen] = '\0';
 
       fprintf(stderr, "read \"%s\"\n", buf);
 
    }
 
  execio_close(testrem);
 

	
 
  time(&endTime);
 

	
 
  struct stat buffer;
 
  int ostatus = stat(realOutput, &buffer);
 
  if(ostatus == -1){
 
    ret = 1; // Return error if output wasn't generated
 
  }
 
  else
 
    remove(output);
 

	
 
  *benchmarkTime = abs(difftime(startTime,endTime));
 
  float tmp = *benchmarkTime;
 
  tmp = (1/tmp) * 50000;
 
  *renderPower = (int)tmp;
 
  
 
free(realOutput);
 
  free(frame_str);
 
  free(input);
 
  free(output);
 
  return ret;
 
}
 

	
 

	
 

	
 
/** *********************************************************************************************************/
 
/** Why hello ohnobinki! Normaldotcom has graciously prepared this portion of code for you to work on! Yay! */
 
/** *********************************************************************************************************/
 

	
 

	
 
/**
 
   Extracts archive to the specified directory, creating this directory
 
   if necessary (but this directory will not be recursively created).
 
   @param outdir output directory
 
   @param pathtoTar filename of the archive to extract
 

	
 
   ohnobinki: please make me work :-\
 
 */
 
int unpackJob(char *outdir, char *pathtoTar)
 
{
 
  int ret;
 

	
 
  struct archive *a;
 
  struct archive_entry *ae;
 
  int astatus;
 

	
 
  /* ignore return because directory may exist already */
 
  mkdir(outdir, 0700);
 
  ret = chdir(outdir);
 
  if(ret == -1)
 
    {
 
      perror("chdir");
 
      return 1;
 
    }
 

	
 
  a = archive_read_new();
 
  ae = archive_entry_new();
 

	
 
  archive_read_support_compression_all(a);
 
  archive_read_support_format_raw(a);
 
  astatus = archive_read_open_filename(a, pathtoTar, 8192);
 
  if (astatus != ARCHIVE_OK)
 
    {
 
      fprintf(stderr, "error opening archive\n");
 
      return 1;
 
    }
 

	
 
  for(astatus = ARCHIVE_OK;
 
      astatus == ARCHIVE_OK
 
        || astatus == ARCHIVE_WARN;
 
      )
 
    {
 
      astatus = archive_read_next_header2(a, ae);
 
      if(astatus == ARCHIVE_WARN)
 
        fprintf(stderr, "Encountered nonfatal read error somehow\n");
 

	
 
      if(astatus == ARCHIVE_OK
 
         || astatus == ARCHIVE_WARN)
 
        astatus = archive_read_extract(a, ae,
 
                                       ARCHIVE_EXTRACT_NO_OVERWRITE
 
                                       | ARCHIVE_EXTRACT_SECURE_SYMLINKS
 
                                       | ARCHIVE_EXTRACT_SECURE_NODOTDOT);
 
    }
 
  archive_entry_free(ae);
 
  archive_read_finish(a);
 

	
 
  if(astatus != ARCHIVE_EOF)
 
    {
 
      fprintf(stderr, "Error reading archive\n");
 
      return 1;
 
    }
 

	
 
  return 0;
 
}
 

	
 

	
 
/** Logs the user into the server after ensuring that keys exist
 
    ohnobinki: I assume you could use this for remoteio, or just kill it
 
*/
 
int login_user(char *username)
 
{
 
  // @TODO: Put some telnet-style auth code here unless this is obselete
 
  return 1; // success
 
}
 

	
 
/**
 
   Sends the server a single request (see protocol.h)
 
   ohnobinki: This should hopefully work, maybe ;D
 
*/
 
int sendSignal(struct remoteio *rem, char signal)
 
{
 
  size_t written;
 
  size_t towrite;
 
  char *ssignal;
 

	
 
  _distren_asprintf(&ssignal, "%c", signal);
 
  towrite = strlen(ssignal);
 
  while( towrite
 
         && !remoteio_write(rem, ssignal, towrite, &written))
 
    {
 
      fprintf(stderr, "Sending request...\n");
 
      towrite -= written;
 
    }
 
  if(written)
 
    return 0;
 

	
 
  /**
 
     if remoteio_write returned 1, the connection
 
     is probably dead or there was a real error
 
   */
 
  return 1;
 
}
 

	
 
/**
 
   Sends the server an extended signal (request + data)
 
   ohnobinki: I have no clue how you really want to handle this. Please clarify/edit
 
*/
 
int sendExtSignal(struct remoteio *rem, char signal, char *data){
 
  size_t written;
 
  size_t towrite;
 
  char *ssignal;
 
  _distren_asprintf(&ssignal, "%c%s", signal, data); // Just append the data FIXME: We should do this differently
 
  towrite = strlen(ssignal);
 
  while( towrite
 
          && !remoteio_write(rem, ssignal, towrite, &written))
 
     {
 
       fprintf(stderr, "Sending request...\n");
 
       towrite -= written;
 
     }
 
   if(written)
 
     return 0;
 

	
 
   /**
 
      if remoteio_write returned 1, the connection
 
      is probably dead or there was a real error
 
    */
 
   return 1;
 
}
 

	
 

	
 
/* Port of web functions for standard code
 

	
 
   Currently, most functions are stubs due to lack
 
   of socket reading code */
 
   of socket reading code
 

	
 
   ohnobinki: I can take care of a fair amount of this, but the remotio reading and writing is where you should really lay down some code.
 
*/
 

	
 
/** marks frame finished on server */
 
void finishframe(struct remoteio *rem, int jobnum, int framenum){
 
  char* data;
 
  _distren_asprintf(&data, "%d%d", jobnum, framenum);
 
  sendExtSignal(rem, DISTREN_REQUEST_DONEFRAME, data);
 
}
 

	
 
/** resets frame to unassigned on server */
 
void resetframe(struct remoteio *rem, int jobnum, int framenum){
 
  fprintf(stderr,"Resetting frame %d in job %d on server... ",framenum,jobnum);
 
  char* data;
 
  _distren_asprintf(&data, "%d%d", jobnum, framenum);
 
 sendExtSignal(rem, DISTREN_REQUEST_RESETFRAME, data);
 

	
 
}
 

	
 
/** marks frame assigned on server */
 
void startframe(struct remoteio *rem, int jobnum, int framenum){
 
  if(DEBUG)
 
    fprintf(stderr,"Marking frame %d started on server... ",framenum);
 
  char* data;
 
  _distren_asprintf(&data, "%d%d", jobnum, framenum);
 
  sendExtSignal(rem, DISTREN_REQUEST_RENDERFRAME, data);
 

	
 
}
 

	
 
/** retrieves job from server */
 
int getwork(struct remoteio *rem, int *jobnum, int *framenum){
 
  char* data;
 
  _distren_asprintf(&data, "%d%d", jobnum, framenum);
 
  sendExtSignal(rem, DISTREN_REQUEST_GETWORK, data);
 
  return 0;
 
}
 

	
 
/** sets render power of slave on server */
 
void setrenderpower(struct remoteio *rem, int renderpower){
 
  fprintf(stderr,"Setting render power on server... ");
 
  char* data;
 
  _distren_asprintf(&data, "%d", renderpower);
 
  sendExtSignal(rem,DISTREN_REQUEST_SETRENDERPOWER, data);
 
}
 

	
 

	
 
/** retrieves render power of slave from server */
 
int getrenderpower(struct remoteio *rem){
 
  sendSignal(rem, DISTREN_REQUEST_GETRENDERPOWER);
 
  return 0;
 
}
 

	
 
/** compares slave software version with server software version */
 
int checkslaveversion(struct remoteio *rem){
 
  sendSignal(rem, DISTREN_REQUEST_GETVERSION);
 
  char* serverVersionFromRemotio = "9000";
 
  if(!strcmp(PACKAGE_VERSION, serverVersionFromRemotio)) // compare versions
 
    return 1;
 
  return 0;
 
}
0 comments (0 inline, 0 general)