Changeset - c6a12058c9da
[Not reviewed]
default
0 6 0
Nathan Brink (binki) - 15 years ago 2010-06-29 23:58:06
ohnobinki@ohnopublishing.net
- Fix up the client making outbound connections. It can now send a sort of ping request but not handle responses yet.
6 files changed with 109 insertions and 17 deletions:
0 comments (0 inline, 0 general)
src/common/protocol.c
Show inline comments
 
/*
 
  Copyright 2010 Nathan Phillip Brink
 

	
 
  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 "protocol.h"
 
#include "remoteio.h"
 

	
 
#include <malloc.h>
 
#include <stdio.h>
 
#include <string.h>
 

	
 
#define DISTREN_REQUEST_MAGIC (0x32423434)
 

	
 
int distren_request_new(struct distren_request **req, uint32_t len, enum distren_request_type type)
 
{
 
  struct distren_request *newreq;
 

	
 
  newreq = malloc(sizeof(struct distren_request));
 
  if(!newreq)
 
    {
 
      (*req) = NULL;
 
      return 1;
 
    }
 

	
 
  newreq->magic = DISTREN_REQUEST_MAGIC;
 
  newreq->len = len;
 
  newreq->type = type;
 

	
 
  (*req) = newreq;
 
  return 0;
 
}
 

	
 
int distren_request_send(struct remoteio *rem, struct distren_request *req, void *data)
 
{
 
  void *packet;
 
  void *packet_ptr;
 
  size_t len;
 
  size_t byteswritten;
 
  int write_err;
 

	
 
  if(req->magic != DISTREN_REQUEST_MAGIC)
 
    fprintf(stderr, "distren_request_send got a bad req\n");
 

	
 
  len = sizeof(struct distren_request) + req->len;
 

	
 
  packet = malloc(len);
 
  if(!packet)
 
    {
 
      fprintf(stderr, "Error allocating memory for packet\n");
 
      return 1;
 
    }
 
  memcpy(packet, req, sizeof(struct distren_request));
 
  memcpy(packet + sizeof(struct distren_request), data, req->len);
 

	
 
  write_err = 0;
 
  packet_ptr = packet;
 
  while(len
 
	&& !write_err)
 
    {
 
      write_err = remoteio_write(rem, packet_ptr, len, &byteswritten);
 
      len -= byteswritten;
 
      packet_ptr += byteswritten;
 
    }
 
  free(packet);
 

	
 
  return 0;
 
}
 

	
 
int distren_request_new_fromdata(struct distren_request **req, void *data, size_t len)
 
{
 
  struct distren_request *newreq;
 

	
 
  if(len < sizeof(struct distren_request))
 
    return 1;
 

	
 
  if( ((struct distren_request *)data)->magic != DISTREN_REQUEST_MAGIC )
 
    {
 
      fprintf(stderr, "packet doesn't match magic stuffs\n");
 
      return 1;
 
    }
 

	
 
  newreq = malloc(sizeof(struct distren_request));
 
  if(!newreq)
 
    {
 
      fprintf(stderr, "OOM\n");
 
      return 1;
 
    }
 

	
 
  memcpy(newreq, data, sizeof(struct distren_request));
 
  (*req) = newreq;
 
  return 0;
 
}
src/common/protocol.h
Show inline comments
 
@@ -18,91 +18,109 @@
 
*/
 

	
 
#ifndef DISTREN_PROTOCOL_H
 
#define DISTREN_PROTOCOL_H
 

	
 
#include <stddef.h>
 
#include <stdint.h>
 

	
 
/**
 
   Server types:
 
 */
 
#define DISTREN_SERVERTYPE_SUBMIT (0x1)
 
#define DISTREN_SERVERTYPE_DISTRIBUTE (0x2)
 
#define DISTREN_SERVERTYPE_RENDER (0x4)
 

	
 
/**
 
   This file defines the constants and structs that the client uses to talk to the server and that the servers use to talk to eachother.
 
 */
 

	
 
/**
 
   generic, shared requests
 
 */
 
enum distren_request_type
 
  {
 
    DISTREN_REQUEST_VERSION = 1, /*< identifies the version of software being
 
				   used by the sender and tells if it is a client or server */
 
    /**
 
       identifies the version of software being
 
       used by the sender and tells if it is a client or server.
 
       Just send PACKAGE_STRING.
 
    */
 
    DISTREN_REQUEST_VERSION = 1,
 
    DISTREN_REQUEST_PING = 2,
 
    DISTREN_REQUEST_PONG = 3,
 
    DISTREN_REQUEST_DISCONNECT = 4,
 

	
 
    /**
 
       client->server only requests
 
    */
 
    DISTREN_REQUEST_SUBMIT = 5,
 

	
 
    /**
 
       anything->server requests
 
     */
 
    DISTREN_REQUEST_JOBINFO = 6, /*< retrieves information about a job based on its number */
 

	
 
    /**
 
       server->anything
 
     */
 
    DISTREN_REQUEST_JOBINFO_RESPONSE = 7, /*< returns information about a job */
 

	
 
    /**
 
       server->server
 
    */
 
    DISTREN_REQUEST_RENDERFRAME = 8,
 
    DISTREN_REQUEST_DONEFRAME = 9, /* server should check to make sure the
 
slave is repoting on a frame it's actually assigned to */
 
    DISTREN_REQUEST_PROGRESS = 10, /*< tells another server of the progress of the first server's work at rendering */
 
    DISTREN_REQUEST_GETWORK = 11,
 
    DISTREN_REQUEST_GETVERSION = 12, /* returns version of software that slave
 
should be running */
 
    DISTREN_REQUEST_GETRENDERPOWER = 13, /* returns the render power of a
 
slave */
 
    DISTREN_REQUEST_GETVERSION = 12, /*< returns version of software that slave should be running */
 
    DISTREN_REQUEST_GETRENDERPOWER = 13, /* returns the render power of a slave */
 
    DISTREN_REQUEST_SETRENDERPOWER = 14, /* sets renderpower in server
 
database */
 
    DISTREN_REQUEST_RESETFRAME = 15, /* sets a frame back to unassigned,
 
    /**
 
       sets a frame back to unassigned,
 
happens if the slave quits for some reason. server code should only allow
 
resetting of a frame assigned to the slave calling the request (see php
 
code)*/ 
 
       code)
 
    */ 
 
    DISTREN_REQUEST_RESETFRAME = 15,
 

	
 
  };
 

	
 
struct distren_request
 
{
 
  uint32_t magic;
 
  uint32_t len;
 
  /** treat type as an enum distren_request_type using casting */
 
  uint32_t /* enum distren_request_type */ type;
 
};
 

	
 
/**
 
   initializes and allocates request
 
 */
 
int distren_request_new(struct distren_request **req, uint32_t len, enum distren_request_type type);
 

	
 
struct remoteio;
 
/**
 
   Takes a struct distren_request and its associated data, allocates
 
   a new block of data to hold the whole packet, and packets the req
 
   header and data together.
 

	
 
   @param rem A remoteio handle to ship this packet off to
 
   @param req Something you initialized with distren_request_new(). You are responsible for distren_request_free()ing this yourself.
 
   @param data A chunk of data the size of req->len. You are responsible for free()ing this yourself.
 
   @return 0 on success and 1 on failure.
 
 */
 
int distren_request_send(struct remoteio *rem, struct distren_request *req, void *data);
 

	
 
/**
 
   initializes and allocates request based on raw input data
 
   which includes the headers of the request.
 
 */
 
int distren_request_new_fromdata(struct distren_request **req, void *data, size_t len);
 

	
 
/**
 
   frees request
 
 */
 
int distren_request_free(struct distren_request *req);
 

	
 
#endif
src/common/remoteio.c
Show inline comments
 
@@ -92,49 +92,49 @@ int remoteio_config(cfg_t *cfg, struct r
 

	
 
  opts->servers = list_init();
 
  if(!opts->servers)
 
    {
 
      fprintf(stderr, "@todo cleanup!\n");
 
      abort();
 
    }
 
  
 
  numservers = cfg_size(cfg, "server");
 
  for(counter = 0; counter < numservers; counter ++)
 
    {
 
      cfg_t *cfg_aserver;
 
      char *method;
 
      
 
      cfg_aserver = cfg_getnsec(cfg, "server", counter);
 
      
 
      aserver.name = strdup(cfg_title(cfg_aserver));
 
      aserver.hostname = strdup(cfg_getstr(cfg_aserver, "hostname"));
 
      aserver.username = strdup(cfg_getstr(cfg_aserver, "username"));
 

	
 
      aserver.method = REMOTEIO_METHOD_MAX;
 
      method = cfg_getstr(cfg_aserver, "method");
 
      for(counter2 = 0; funcmap[counter2].name; counter2 ++)
 
	if(strcmp(method, funcmap[counter2].name) == 0)
 
	  aserver.method = REMOTEIO_METHOD_SSH;
 
	  aserver.method = funcmap[counter2].method;
 
      if(aserver.method == REMOTEIO_METHOD_MAX)
 
	{
 
	  fprintf(stderr, "No such method as %s\n", method);
 
	  if(!haslisted_methods)
 
	    {
 
	      fprintf(stderr, "Available methods:\n");
 
	      for(counter2 = 0; funcmap[counter2].name; counter2 ++)
 
		fprintf(stderr, "\t%s\n", funcmap[counter2].name);
 
	      
 
	      haslisted_methods ++;
 
	    }
 
	  abort();
 
	}
 
      list_insert_after(opts->servers, &aserver, sizeof(struct remoteio_server));
 
    }
 
  
 
  return 0;
 
}
 

	
 

	
 

	
 
int remoteio_open(struct remoteio **remoteio, struct remoteio_opts *opts, const char *servername)
 
{
 
  struct remoteio_server *theserver;
src/server/slave.c
Show inline comments
 
@@ -126,49 +126,49 @@ int main(int argc, char *argv[])
 
      fprintf(stderr, "password not set\n");
 
      return 1;
 
    }
 
  if(!hostname)
 
    {
 
      fprintf(stderr, "hostname not set\n");
 
      return 1;
 
    }
 

	
 
  /* Notifies the user if there no username in .conf */
 
  if(checkUsername(username))
 
    return 1;
 
  if(!strncmp(password, "!password",10))
 
    {
 
      fprintf(stderr, "You haven't specified a password. Please edit distrenslave.conf!\n");
 
      return 1;
 
    }
 

	
 
  fprintf(stderr, "Connecting to server...\n");
 
  if(remoteio_open(&comm_slave, commonopts->remoteio, server))
 
    {
 
      fprintf(stderr, "Error connecting to server; exiting\n");
 
      return 1;
 
    }
 

	
 
  greet_server(comm_slave);
 

	
 
  // Variables needed for main loop
 
  int jobnum = 0;
 
  int framenum = 0;
 
  int slavekey = atoi(username); // @TODO: Make this more friendly
 

	
 
  char *urltoTar;      /* Full URL to the server-side location of job#.tgz */
 
  char *pathtoTar;     /* Full path to the location of the job#.tgz */
 
  char *pathtoTardir;
 

	
 
  char *urltoOutput;   /* Full URL where output is posted */
 
  char *pathtoOutput;  /* Full path to the output (rendered) file */
 
  char *pathtoOutdir;  /* Full path to output directory */
 
  char *pathtoRenderOutput;  /* Contains blender framenum placeholder */
 

	
 
  char *urltoJobfile; /* No longer used, url to .blend on server */
 

	
 
  char *pathtoJob;     /* Full path to job data folder */
 
  char *pathtoJobfile; /* Full path to the job's main file */
 
  char *outputExt = "jpg";     /* Output Extension (e.g., JPG) */
 

	
 
  int haveWork = 0;
 
  int quit = 0;
 

	
 
@@ -182,49 +182,49 @@ int main(int argc, char *argv[])
 
    {
 
      if(slaveBenchmark(datadir, &benchmarkTime, &renderPower))
 
        {
 
          fprintf(stderr,"Benchmark failed! Exiting.\n");
 
          return 1;
 
        }
 
      else
 
        {
 
          fprintf(stderr,"Benchmark successful, time taken was %d seconds, giving you a render power of %d.\n",
 
                  benchmarkTime, renderPower);
 
          _web_setrenderpower(slavekey, password, renderPower);
 
          return 0;
 
        }
 
    }
 

	
 
  if(!DEBUG)
 
    fprintf(stderr, "Running..");
 

	
 

	
 
  // Main loop
 
  while(!quit)
 
    {
 

	
 
    // request work
 
    fprintf(stderr,"Requesting work...\n");
 
    fprintf(stderr, "Waiting...\n");
 
    haveWork = getwork(comm_slave, &jobnum, &framenum);
 

	
 
    /* If we got a frame */
 
    if(haveWork)
 
      {
 
        fprintf(stderr,"Got work from server...\n");
 
        /* @TODO: Add remotio hooks */
 
        // jobnum = remoteio_read(jobnum); /* Set jobnum from remoteio (we could use info from struct, but we need this info to download the xmlfile */
 
        // framenum = remoteio_read(jobnum); /* Set framenum from remoteio */
 
        // outputExt = remotio)read(outputExt); /* Set output extension from remotio */
 

	
 
        if(DEBUG)
 
          fprintf(stderr, "Preparing to render frame %d in job %d\n", framenum, jobnum);
 

	
 
        prepareJobPaths(jobnum, framenum, outputExt, datadir, &urltoTar, &pathtoTar,&pathtoTardir,&pathtoJob, &pathtoJobfile, &urltoJobfile, &urltoOutput, &pathtoOutput, &pathtoRenderOutput, &pathtoOutdir);
 
        free(outputExt);
 

	
 
        int dlret = downloadTar(urltoTar, pathtoTar);
 
        if(dlret == 0)
 
          fprintf(stderr,"Data retrieved successfully!\n");
 
        else if(dlret == 3){
 
          resetframe(comm_slave, jobnum, framenum);  // Unassign the frame on the server so other slaves can render it
 
          return 0; // ouput dir doesn't exist
 
        }
 
@@ -280,52 +280,56 @@ int main(int argc, char *argv[])
 
          return 1;
 
        }
 
        else{
 
          /* Post-execution */
 
          if(DEBUG)
 
            fprintf(stderr, "Finished frame %d in job %d, uploading...\n", framenum, jobnum);
 
          else
 
            fprintf(stderr,"Finished frame.\n");
 
          uploadOutput(pathtoOutput, urltoOutput, jobnum, framenum, slavekey); // @TODO: Handle return value
 

	
 
          free(urltoOutput);
 
          free(pathtoOutput);
 
          urltoOutput = NULL;
 
          pathtoOutput = NULL;
 

	
 
          // Tell the server that rendering and upload are complete of "jobjum.framenum"
 
          finishframe(comm_slave, jobnum, framenum);
 
        }
 
     }
 
    else{
 
      if(DEBUG)
 
        fprintf(stderr,"Nothing to do. Idling...\n");
 
      else
 
        fprintf(stderr,".");
 
      sleep(300); // Poll every 300 seconds @TODO: remove polling
 

	
 
      /**
 
	 to prevent infinite loops from burning CPU, we just sleep(1) ;-)
 
      */
 
      sleep(1);
 
    }
 

	
 
    // @TODO: If the server says that every frame for the last jobnum is finished, OR if the data is getting old
 
    /* @TODO: If the server says that every frame for the last jobnum is finished, OR if the data is getting old */
 
    if(1 == 0)
 
      {
 
        // Note: individual frames are already deleted after uploading,
 
        // except for ones that couldn't be uploaded
 
        delete_jobdata(jobnum, datadir);
 
      }
 

	
 
    sleep(5); // Poll 5 seconds. @TODO: Remove all polling
 
  }
 

	
 
  fprintf(stderr,"Closing connection to server...\n");
 
  remoteio_close(comm_slave);
 
  free(my_cfg);
 
  free(outputExt);
 
  free(datadir);
 
  free(urltoTar);
 
  free(pathtoTar);
 
  free(pathtoTardir);
 
  free(pathtoJob);
 
  free(pathtoJobfile);
 
  free(urltoJobfile);
 
  free(urltoOutput);
 
  free(pathtoRenderOutput);
 
  free(pathtoOutdir);
src/server/slavefuncs.c
Show inline comments
 
@@ -846,53 +846,57 @@ int sendSignal(struct remoteio *rem, cha
 
  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){
 
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
 
  /**
 
     Just append the data FIXME: We should do this differently
 
  */
 
  _distren_asprintf(&ssignal, "%c%s", signal, data);
 
  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
 

	
 
   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.
 
@@ -903,52 +907,80 @@ void finishframe(struct remoteio *rem, i
 
  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);
 

	
 
}
 

	
 
/**
 
   Greets the server.
 

	
 
   We send PACKAGE_STRING as our version ping thing.
 
 */
 
int greet_server(struct remoteio *rem)
 
{
 
  int err;
 
  struct distren_request *req;
 

	
 
  err = 0;
 

	
 
  fprintf(stderr, "Saying hello to the server ;-)...\n");
 

	
 
  err += distren_request_new(&req, strlen(PACKAGE_STRING), DISTREN_REQUEST_VERSION);
 
  err += distren_request_send(rem, req, PACKAGE_STRING);
 
  err += distren_request_free(req);
 

	
 
  return err;
 
}
 

	
 
/** retrieves job from server */
 
int getwork(struct remoteio *rem, int *jobnum, int *framenum){
 
int getwork(struct remoteio *rem, int *jobnum, int *framenum)
 
{
 
  struct distren_request *req;
 
  char* data;
 
  _distren_asprintf(&data, "%d%d", jobnum, framenum);
 
  int len; /*< asprintf() uses int, not size_t */
 
  len = _distren_asprintf(&data, "%d.%d", jobnum, framenum);
 

	
 
  //distren_request_new(&req, 
 

	
 

	
 
  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;
src/server/slavefuncs.h
Show inline comments
 
@@ -37,44 +37,45 @@ int software_updatecheck();
 
int delete_jobdata(int jobnum, char *datadir);
 
size_t curl_writetodisk(void *ptr, size_t size, size_t nmemb, FILE *stream);
 
CURLcode curlget(char *url, char *out);
 
CURLcode curlpost(char *filename, char *url, int jobnum, int framenum, int slavekey);
 
int ssh_keygen();
 
int register_user(char *username, char *email);
 
int login_user(char *username);
 
int conf_replace(char *conffile, char *wordtoreplace, char *replacewith);
 
int exec_blender(char *input, char *output, int frame);
 
void xmlinit();
 
void xmlcleanup();
 
int distren_mkdir_recurse(char *dirname);
 
int job_build_path(char *filename, unsigned int jobnum);
 
int downloadTar(char *url, char *destinationPath);
 
int uploadOutput(char *pathtoOutput, char *urltoOutput, int jobnum, int framenum, int slavekey);
 
int unpackJob(char *outdir, char *pathtoTar);
 
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);
 
int checkUsername(char *username);
 
void slaveTest();
 

	
 
/* Standard slave */
 
void finishframe(struct remoteio *rem, int jobnum, int framenum);
 
void resetframe(struct remoteio *rem, int jobnum, int framenum);
 
void startframe(struct remoteio *rem, int jobnum, int framenum);
 
int greet_server(struct remoteio *rem);
 
int getwork(struct remoteio *rem, int *jobnum, int *framenum);
 
void setrenderpower(struct remoteio *rem, int renderpower);
 
int getrenderpower(struct remoteio *rem);
 
int checkslaveversion(struct remoteio *rem);
 

	
 
/* Simple slave */
 
struct _web_memorystruct;
 
void *_web_myrealloc(void *ptr, size_t size);
 
size_t _web_writememorycallback(void *ptr, size_t size, size_t nmemb, void *data);
 
struct _web_memorystruct _web_getrequest(char *url);
 
void _web_finishframe(int slavekey, char *slavepass, int jobnum, int framenum);
 
void _web_startframe(int slavekey, char *slavepass, int jobnum, int framenum);
 
void _web_resetframe(int slavekey, char *slavepass, int jobnum, int framenum);
 
int _web_getwork(int slavekey, char *slavepass, int *jobnum, int *framenum);
 
void _web_setrenderpower(int slavekey, char *slavepass, int renderpower);
 
int slaveBenchmark(char *datadir, int *benchmarkTime, int *renderPower);
 

	
 

	
 

	
 
#endif
0 comments (0 inline, 0 general)