Created
April 27, 2009 06:51
-
-
Save moriyoshi/102366 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ifneq ($(KERNELRELEASE),) | |
obj-m := my_vfs.o | |
my_vfs-objs := my_vfs_module.o | |
else | |
KVER = $(shell uname -r) | |
KDIR = /lib/modules/$(KVER)/build | |
modules:: | |
$(MAKE) -C $(KDIR) M=$(shell pwd) modules | |
clean:: | |
$(MAKE) -C $(KDIR) M=$(shell pwd) clean | |
endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <linux/module.h> | |
#include <linux/init.h> | |
#include <linux/fs.h> | |
#include <linux/dcache.h> | |
#include <linux/string.h> | |
#include <linux/pagemap.h> | |
#include <linux/wait.h> | |
#include <linux/poll.h> | |
#include <linux/sched.h> | |
#include <linux/timer.h> | |
#include <asm/uaccess.h> /* for copy_to_user */ | |
struct my_vfs_inode { | |
struct inode inode; | |
struct timer_list timer; | |
wait_queue_head_t wait; | |
atomic_t flagged; | |
}; | |
static struct inode *my_vfs_get_inode(struct super_block *sb, unsigned int ino); | |
/** | |
* でっち上げたディレクトリエントリ | |
*/ | |
static const char *dir_entries[] = { | |
"a", | |
"b", | |
"c", | |
"d", | |
"e", | |
}; | |
/** | |
* タイマーの時間間隔 (数値は適当) | |
*/ | |
static const unsigned long timer_interval = 400ul; | |
/** | |
* struct my_vfs_inode 用スラブアロケータ | |
*/ | |
static struct kmem_cache *my_vfs_inode_cache __read_mostly; | |
/** | |
* このモジュールで内部的に使うだけの関数。 | |
* dir_entries に、name で指定されたファイルがあるかどうかを調べて、 | |
* 見つかった場合は inode 番号を返す。 | |
* | |
* @param name 探すファイル名 | |
* @param name_sz 探すファイル名の長さ | |
* @return inode 番号 | |
*/ | |
static unsigned long my_vfs_internal_lookup(const char *name, size_t name_sz) | |
{ | |
int i; | |
for (i = 0; i < sizeof(dir_entries) / sizeof(*dir_entries); i++) { | |
if (0 == strncmp(dir_entries[i], name, name_sz)) { | |
/* とりあえず文字列へのポインタ == inode 番号 | |
* ということにしてしまう */ | |
return (unsigned long)dir_entries[i]; | |
} | |
} | |
return 0; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。read(2) / pread(2) に対応する処理。 | |
* @file: ディレクトリのディスクリプタ | |
* @buf: 読み込み先であるユーザ空間のバッファへのポインタ | |
* @buf_sz: バッファのサイズ | |
* @pos: ファイルポインタ | |
*/ | |
static ssize_t my_vfs_dir_read(struct file *file, char __user *buf, size_t buf_sz, loff_t *pos) | |
{ | |
/* ディレクトリに対する read 操作は無効 */ | |
return -EISDIR; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。readdir(3) に対応する処理。 | |
* @file: ディレクトリのディスクリプタ | |
* @dirent: ディレクトリエントリを格納するバッファへのポインタ | |
* @filldir: 指定されたバッファにデータを追加するために呼び出し側から | |
* 提供される実装のコールバック関数ポインタ | |
*/ | |
static int my_vfs_dir_readdir(struct file *file, void *dirent, filldir_t filldir) | |
{ | |
unsigned int i = file->f_pos; | |
while (i < 2 + sizeof(dir_entries) / sizeof(*dir_entries)) { | |
if (i == 0) { | |
/* "." の処理 */ | |
if (filldir(dirent, ".", sizeof(".") - 1, i, 1, DT_DIR)) | |
break; | |
} else if (i == 1) { | |
/* ".." の処理 */ | |
if (filldir(dirent, "..", sizeof("..") - 1, i, | |
parent_ino(file->f_path.dentry), DT_DIR)) | |
break; | |
} else { | |
/* それ以外 */ | |
if (filldir(dirent, dir_entries[i - 2], strlen(dir_entries[i - 2]), | |
i, (unsigned long)dir_entries[i - 2], DT_REG)) | |
break; | |
} | |
++i; | |
} | |
file->f_pos = i; | |
return 0; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。lseek(2) に対応する処理。 | |
* @file: ディレクトリのディスクリプタ | |
* @off: オフセット | |
* @origin: SEEK_SET / SEEK_CUR / SEEK_END のいずれか | |
*/ | |
static loff_t my_vfs_dir_lseek(struct file *file, loff_t off, int origin) | |
{ | |
loff_t new_off = file->f_pos; | |
switch (origin) { | |
case SEEK_SET: | |
new_off = off; | |
break; | |
case SEEK_CUR: | |
new_off += off; | |
break; | |
default: | |
return -EINVAL; | |
} | |
if (new_off > sizeof(dir_entries) / sizeof(*dir_entries)) | |
file->f_pos = sizeof(dir_entries) / sizeof(*dir_entries); | |
file->f_pos = new_off; | |
return new_off; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。open(2) か creat(2) に対応する処理。 | |
* @inode: ディレクトリのノードを表す struct inode へのポインタ | |
* @dentry: 作成されるファイルを表す struct dentry へのポインタ | |
* @mode: ファイルモード | |
* @nd: [out] 解決されたパス名? | |
*/ | |
static int my_vfs_dir_create(struct inode *inode, struct dentry *entry, int mode, struct nameidata *nd) | |
{ | |
return -ENOSPC; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。stat(2) に対応する処理。 | |
* @mnt: マウントポイントを表す struct vfsmount へのポインタ | |
* @dentry: 対象のファイルを表す struct dentry へのポインタ | |
* @mode: ファイルの属性を返す struct kstat へのポインタ | |
*/ | |
static int my_vfs_dir_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat) | |
{ | |
generic_fillattr(dentry->d_inode, stat); | |
return 0; | |
} | |
/** | |
* dentry 用のコールバック関数。 | |
* @dentry: 対象の dentry を表すポインタ。 | |
*/ | |
static int my_vfs_dir_dentry_delete(struct dentry *dentry) | |
{ | |
return 1; | |
} | |
/* XXX: why not const'ified? */ | |
static struct dentry_operations my_vfs_dir_dentry_operations = { | |
.d_delete = my_vfs_dir_dentry_delete | |
}; | |
/** | |
* ディレクトリノード用のコールバック関数。ディレクトリから指定された | |
* エントリを探す。 | |
* @inode: 検索対象のディレクトリノードを表す struct inode へのポインタ | |
* @entry: 検索するエントリの名前 | |
* @nd: 何の操作で呼ばれたかが intent に入ってたりとか | |
*/ | |
static struct dentry *my_vfs_dir_lookup(struct inode *inode, struct dentry *entry, struct nameidata *nd) | |
{ | |
(void)nd; /* unused */ | |
if (entry->d_name.len > NAME_MAX) | |
return ERR_PTR(-ENAMETOOLONG); | |
entry->d_op = &my_vfs_dir_dentry_operations; | |
{ | |
unsigned long file_inode_n; | |
struct inode *file_inode; | |
file_inode_n = my_vfs_internal_lookup(entry->d_name.name, | |
entry->d_name.len); | |
if (!file_inode_n) { | |
d_add(entry, NULL); | |
return NULL; | |
} | |
file_inode = my_vfs_get_inode(inode->i_sb, file_inode_n); | |
if (!file_inode) | |
return ERR_PTR(-ENOENT); | |
d_add(entry, file_inode); | |
} | |
return NULL; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。シンボリックリンクを作成する。 | |
* @inode: 作成対象のディレクトリノードを表す struct inode へのポインタ | |
* @entry: 作成するシンボリックリンクの名前 | |
* @name: リンク先 | |
*/ | |
static int my_vfs_dir_symlink(struct inode *inode, struct dentry *entry, const char *name) | |
{ | |
if (!my_vfs_internal_lookup(entry->d_name.name, entry->d_name.len)) | |
return -ENOENT; | |
return -ENOSPC; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。新しいノードを作成する | |
* @inode: 作成対象のディレクトリノードを表す struct inode へのポインタ | |
* @entry: 作成するノードの名前 | |
* @mode: ファイルモード | |
* @dev: デバイス番号 | |
*/ | |
static int my_vfs_dir_mknod(struct inode *inode, struct dentry *entry, int mode, dev_t dev) | |
{ | |
return -ENOSPC; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。新しいディレクトリノードを作成する。 | |
* @inode: 作成対象のディレクトリノードを表す struct inode へのポインタ | |
* @entry: 作成するディレクトリの名前 | |
* @mode: ファイルモード | |
*/ | |
static int my_vfs_dir_mkdir(struct inode *inode, struct dentry *entry, int mode) | |
{ | |
return -ENOSPC; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。ディレクトリノードを削除する。 | |
* @inode: 削除するディレクトリが含まれるディレクトリノードを表す | |
* struct inode へのポインタ | |
* @entry: 削除するディレクトリの名前 | |
*/ | |
static int my_vfs_dir_rmdir(struct inode *inode, struct dentry *entry) | |
{ | |
return -EACCES; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。ハードリンクを作成する | |
* @old_entry: リンク元のするファイルを表す struct dentry へのポインタ | |
* @inode: ハードリンクを作成するディレクトリノードを表す | |
* struct inode へのポインタ | |
* @entry: ハードリンク名 | |
*/ | |
static int my_vfs_dir_link(struct dentry *old_entry, struct inode *inode, struct dentry *entry) | |
{ | |
return -ENOSPC; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。ノードをアンリンクする。 | |
* @inode: アンリンクを作成するエントリを含むディレクトリノードを表す | |
* struct inode へのポインタ | |
* @entry: アンリンクするファイルを表す struct dentry へのポインタ | |
*/ | |
static int my_vfs_dir_unlink(struct inode *inode, struct dentry *entry) | |
{ | |
return my_vfs_internal_lookup(entry->d_name.name, entry->d_name.len) ? | |
-EACCES: -ENOENT; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。エントリを移動する。 | |
* @old_inode: 移動元のエントリを含むディレクトリノードを表す | |
* struct inode へのポインタ | |
* @old_entry: 移動元のエントリを表す struct dentry へのポインタ | |
* @inode: 移動先のエントリを含むディレクトリノードを表す | |
* struct inode へのポインタ | |
* @entry: 移動先のエントリを表す struct dentry へのポインタ | |
*/ | |
static int my_vfs_dir_rename(struct inode *old_inode, struct dentry *old_entry, struct inode *new_inode, struct dentry *new_entry) | |
{ | |
if (!my_vfs_internal_lookup(old_entry->d_name.name, old_entry->d_name.len)) | |
return -ENOENT; | |
if (my_vfs_internal_lookup(new_entry->d_name.name, new_entry->d_name.len)) | |
return -EEXIST; | |
return -EACCES; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。close(2) の後、 | |
* ディスクリプタの参照カウントが 0 になると呼ばれる。 | |
* @inode: ディレクトリノードを表す struct inode へのポインタ | |
* @file: ディレクトリのディスクリプタ | |
*/ | |
static int my_vfs_dir_release(struct inode *inode, struct file *file) | |
{ | |
return 0; | |
} | |
/** | |
* ディレクトリノード用のコールバック関数。 | |
* ディレクトリのディスクリプタを開く | |
* @inode: ディレクトリノードを表す struct inode へのポインタ | |
* @file: ディレクトリのディスクリプタ | |
*/ | |
static int my_vfs_dir_open(struct inode *inode, struct file *file) | |
{ | |
return 0; | |
} | |
static const struct file_operations my_vfs_dir_ops = { | |
.llseek = my_vfs_dir_lseek, | |
.read = my_vfs_dir_read, | |
.readdir = my_vfs_dir_readdir, | |
.release = my_vfs_dir_release, | |
.open = my_vfs_dir_open | |
}; | |
static const struct inode_operations my_vfs_dir_inode_ops = { | |
.create = my_vfs_dir_create, | |
.getattr = my_vfs_dir_getattr, | |
.lookup = my_vfs_dir_lookup, | |
.mknod = my_vfs_dir_mknod, | |
.link = my_vfs_dir_link, | |
.unlink = my_vfs_dir_unlink, | |
.symlink = my_vfs_dir_symlink, | |
.rename = my_vfs_dir_rename, | |
.mkdir = my_vfs_dir_mkdir, | |
.rmdir = my_vfs_dir_rmdir | |
}; | |
/** ファイルの内容を模した文字列 */ | |
static const char str[] = "quick brown fox jumps over the lazy dog"; | |
/** | |
* ファイルノード用のコールバック関数。read(2) / pread(2) に対応する処理。 | |
* @file: ファイルのディスクリプタ | |
* @buf: 読み込み先であるユーザ空間のバッファへのポインタ | |
* @buf_sz: バッファのサイズ | |
* @pos: ファイルポインタ | |
*/ | |
static ssize_t my_vfs_file_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) | |
{ | |
if (*ppos >= sizeof(str) - 1) | |
return 0; | |
{ | |
const ssize_t nbytes = ((loff_t)(sizeof(str) - 1) < *ppos + count ? | |
sizeof(str) - 1: *ppos + count) - *ppos; | |
if (copy_to_user(buf, str + *ppos, nbytes)) | |
return -EFAULT; | |
*ppos += nbytes; | |
return nbytes; | |
} | |
} | |
/** | |
* ファイルノード用のコールバック関数。lseek(2) に対応する処理。 | |
* @file: ファイルのディスクリプタ | |
* @off: オフセット | |
* @origin: SEEK_SET / SEEK_CUR / SEEK_END のいずれか | |
*/ | |
static loff_t my_vfs_file_lseek(struct file *file, loff_t off, int origin) | |
{ | |
loff_t new_off = file->f_pos; | |
switch (origin) { | |
case SEEK_SET: | |
new_off = off; | |
break; | |
case SEEK_CUR: | |
new_off += off; | |
break; | |
default: | |
return -EINVAL; | |
} | |
if (new_off > sizeof(str) - 1) { | |
file->f_pos = sizeof(str) - 1; | |
} | |
file->f_pos = new_off; | |
return new_off; | |
} | |
/** | |
* ファイルノード用のコールバック関数。close(2) の後、 | |
* ディスクリプタの参照カウントが 0 になると呼ばれる。 | |
* @file: ファイルのディスクリプタ | |
* @off: オフセット | |
* @origin: SEEK_SET / SEEK_CUR / SEEK_END のいずれか | |
*/ | |
static int my_vfs_file_release(struct inode *inode, struct file *file) | |
{ | |
return 0; | |
} | |
/** | |
* ファイルノード用のコールバック関数。poll(2) / select(2) に対応する処理 | |
* @file: ファイルのディスクリプタ | |
* @pts: もしイベントが発生していなかったときに通知する先の | |
* 待ち行列を指定する struct poll_table_struct | |
*/ | |
static unsigned int my_vfs_file_poll(struct file *file, struct poll_table_struct *pts) | |
{ | |
struct my_vfs_inode *inode = file->private_data; | |
BUG_ON(!inode); | |
/* まずは呼出側のwait_queueを登録 */ | |
poll_wait(file, &inode->wait, pts); | |
/* もしフラグがすでに立っていたら、リセットして POLLIN を返す */ | |
if (atomic_add_unless(&inode->flagged, -1, 0)) | |
return POLLIN; | |
else | |
return 0; | |
} | |
/** | |
* ファイルノード用のコールバック関数。open(2) に対応する処理 (一部)。 | |
* @inode: ディレクトリのノードを表す struct inode へのポインタ | |
* @file: 開かれるファイルのディスクリプタ | |
*/ | |
static int my_vfs_file_open(struct inode *inode, struct file *file) | |
{ | |
file->private_data = inode; | |
return 0; | |
} | |
static const struct file_operations my_vfs_file_ops = { | |
.read = my_vfs_file_read, | |
.llseek = my_vfs_file_lseek, | |
.release = my_vfs_file_release, | |
.poll = my_vfs_file_poll, | |
.open = my_vfs_file_open | |
}; | |
static const struct inode_operations my_vfs_file_inode_ops = {}; | |
/** | |
* inode を inode 番号から引く。 | |
* @sb: マウントされている実体を表す struct super_block へのポインタ | |
* @ino: inode 番号 | |
*/ | |
static struct inode *my_vfs_get_inode(struct super_block *sb, unsigned int ino) | |
{ | |
struct inode *retval; | |
retval = iget_locked(sb, ino); | |
if (!retval) | |
return 0; | |
retval->i_uid = 0; | |
retval->i_gid = 0; | |
retval->i_mode = 0644; | |
if (ino == 1) { | |
retval->i_fop = &my_vfs_dir_ops; | |
retval->i_op = &my_vfs_dir_inode_ops; | |
retval->i_mode |= 0111 | S_IFDIR; | |
inc_nlink(retval); | |
} else { | |
retval->i_fop = &my_vfs_file_ops; | |
retval->i_op = &my_vfs_file_inode_ops; | |
retval->i_mode |= S_IFREG; | |
retval->i_size = sizeof(str) - 1; | |
} | |
unlock_new_inode(retval); | |
return retval; | |
} | |
/** | |
* ルートディレクトリノードを返す。 | |
* @sb: マウントされている実体を表す struct super_block へのポインタ | |
*/ | |
static struct inode *my_vfs_get_root(struct super_block *sb) | |
{ | |
return my_vfs_get_inode(sb, 1); | |
} | |
/** | |
* アンマウントされるときに呼ばれるコールバック関数 | |
* @sb: マウントされている実体を表す struct super_block へのポインタ | |
*/ | |
static void my_vfs_put_super(struct super_block *sb) | |
{ | |
/* do nothing for now */ | |
} | |
/** | |
* タイマー用のコールバック。 | |
*/ | |
static void my_vfs_inode_timer_cb(unsigned long inode_ptr) | |
{ | |
struct my_vfs_inode *inode = (struct my_vfs_inode *)inode_ptr; | |
/* フラグを立てる */ | |
if (atomic_add_unless(&inode->flagged, 1, 1)) { | |
/* pollしてる奴を全部起こす */ | |
wake_up_interruptible_all(&inode->wait); | |
} | |
/* タイマーを再度セットする */ | |
del_timer(&inode->timer); | |
inode->timer.expires = jiffies + timer_interval; | |
add_timer(&inode->timer); | |
} | |
/** | |
* inode 構造体用のメモリを確保するときに呼ばれる | |
* @sb: マウントされている実体を表す struct super_block へのポインタ | |
*/ | |
static struct inode *my_vfs_inode_alloc(struct super_block *sb) | |
{ | |
struct my_vfs_inode *inode; | |
/* 自分用のスラブアロケータから確保する */ | |
inode = kmem_cache_alloc(my_vfs_inode_cache, GFP_KERNEL); | |
if (!inode) | |
return NULL; | |
/* 本来はここで初期化すべきなのだろうか... */ | |
/* 待ち行列を初期化 */ | |
init_waitqueue_head(&inode->wait); | |
/* タイマーを初期化 */ | |
setup_timer(&inode->timer, my_vfs_inode_timer_cb, (unsigned long)inode); | |
inode->timer.expires = jiffies + timer_interval; | |
add_timer(&inode->timer); | |
atomic_set(&inode->flagged, 0); | |
return (struct inode *)inode;; | |
} | |
/** | |
* inode 構造体用のメモリを解放するときに呼ばれる | |
* @sb: マウントされている実体を表す struct super_block へのポインタ | |
*/ | |
static void my_vfs_inode_destroy(struct inode *_inode) | |
{ | |
struct my_vfs_inode *inode = (struct my_vfs_inode *)_inode; | |
del_timer_sync(&inode->timer); | |
kmem_cache_free(my_vfs_inode_cache, (inode)); | |
} | |
static const struct super_operations my_vfs_ops = { | |
.alloc_inode = my_vfs_inode_alloc, | |
.destroy_inode = my_vfs_inode_destroy, | |
.statfs = simple_statfs, | |
.put_super = my_vfs_put_super | |
}; | |
/** | |
* inode 構造体用のスラブアロケータが新たにメモリを確保したときに | |
* 呼ばれるコールバック | |
* @cache: アロケータを表すポインタ | |
* @data: 確保されたメモリ (sizeof(struct my_vfs_inode)) | |
*/ | |
static void my_vfs_inode_cache_init_once(struct kmem_cache *cache, void *data) | |
{ | |
inode_init_once(&((struct my_vfs_inode *)data)->inode); | |
} | |
/** | |
* マウントされるときに呼ばれるコールバック関数。 | |
* マウントされる実体 (スーパーブロック) を表す struct super_block を | |
* 初期化する。 | |
* @sb: マウントされる実体を表す struct super_block へのポインタ | |
* @data: マウントパラメータ | |
* @silent: エラーメッセージを表示するか否か | |
*/ | |
static int my_vfs_fill_super(struct super_block *sb, void *data, int silent) | |
{ | |
sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_CACHE_SIZE; | |
sb->s_blocksize_bits = PAGE_CACHE_SHIFT; | |
sb->s_magic = 0x484f4745; /* 'HOGE' */ | |
sb->s_op = &my_vfs_ops; | |
/* sb->s_flags |= MS_RDONLY; */ | |
{ | |
struct inode *inode = my_vfs_get_root(sb); struct dentry *root = 0; | |
if (!inode) | |
return -ENOMEM; | |
root = d_alloc_root(inode); | |
if (!root) { | |
iput(inode); | |
return -ENOMEM; | |
} | |
sb->s_root = root; | |
} | |
return 0; | |
} | |
/** | |
* マウントされるときに呼ばれるコールバック関数。 | |
* マウントポイントとマウントされる実体 (スーパーブロック) を関連付ける。 | |
* @fs: ファイルシステム | |
* @flags: マウントフラグ | |
* @dev_name: マウントされるデバイスの名前 | |
* @data: マウントオプション | |
* @mnt: マウントポイントを表す struct vfsmount へのポインタ | |
*/ | |
static int my_vfs_get_sb(struct file_system_type *fs, | |
int flags, const char *dev_name, void *data, | |
struct vfsmount *mnt) | |
{ | |
return get_sb_nodev(fs, flags, data, my_vfs_fill_super, mnt); | |
} | |
static struct file_system_type my_vfs_type = { | |
.name = "my_vfs", | |
.get_sb = my_vfs_get_sb, | |
.kill_sb = kill_litter_super, | |
.owner = THIS_MODULE | |
}; | |
static int __init my_vfs_module_init(void) | |
{ | |
/* スラブアロケータを確保する */ | |
my_vfs_inode_cache = kmem_cache_create( | |
"my_vfs_inode_cache", | |
sizeof(struct my_vfs_inode), | |
0, | |
SLAB_HWCACHE_ALIGN | SLAB_RECLAIM_ACCOUNT | |
| SLAB_MEM_SPREAD, | |
my_vfs_inode_cache_init_once); | |
if (!my_vfs_inode_cache) | |
return -ENOMEM; | |
/* ファイルシステムを登録する */ | |
register_filesystem(&my_vfs_type); | |
return 0; | |
} | |
static void __exit my_vfs_module_exit(void) | |
{ | |
/* ファイルシステムの登録を解除する */ | |
unregister_filesystem(&my_vfs_type); | |
/* スラブアロケータを削除する */ | |
kmem_cache_destroy(my_vfs_inode_cache); | |
} | |
module_init(my_vfs_module_init) | |
module_exit(my_vfs_module_exit) | |
MODULE_LICENSE("GPL"); | |
/* | |
* vim: sw=8 sts=8 ts=8 noet | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment