ごらくらいふ

プログラミングしたりゲームしたり

権限のクリーンなURL, タイトルコピー系chrome拡張を作った。

はい。

や、職場で「ガバガバ権限まじでやめて」って言われたので「じゃーもう自作しますわ」ってなってやりました。

久しぶりに作って頭の体操になった感じする。

chrome.google.com

「このサイトを共有したい」そう思ったときにタイトルとリンクが一度にコピーできると便利です。

この拡張は権限がクリーンであることを特徴としています。

"アクセスしたウェブサイト上にある自分の全データの読み取りと変更"を必要としません。 この権限ができることは広く、セキュリティ管理の都合から拡張を手放すよう指導されることもあります。

この権限問題を解消すべく、不安のない拡張を作りたいという思いから、この拡張は作られました。

ポップアップを開いた時点でクリップボードにコピーをしに行くので、拡張機能を起動するショートカットを登録するのがおすすめ。自分はcmd+Shift+Cにしてる。 設定はchrome://extensions/shortcutsにて。

コードはGitHubにあります。

github.com

完全に自分が必要なフォーマット${title}\n${url}だけの状態で公開してるので、Markdown用のやつがほしいとかあると、オプションページを作る動機になる。donate枠とか作れるし。

Pull-Requestお待ちしてます。

あとはぺろっとほしいものリストでも貼っておきますね。 https://www.amazon.co.jp/gp/registry/wishlist/10FZP34Z9U32Q

URLConnectionの通信をURLSessionに書き換えた

NSURLConnectionがiOS 9.0でDeprecatedになってから時間も経ち、もうiOS 12が出る時勢にNSURLSessionへの書き換え事案が発生した。

感想

NSURLConnectionDataDelegateとNSURLSessionDataDelegateの互換性はわりと確保されていて助かった。 なぜかcompletionHandlerにわたす引数の順序が変わってたけど。

NSURLConnectionにおけるdidFinishLoadingとdidFailWithErrorが、 didCompleteWithErrorにまとまったので、errorをちゃんと確認しないといけないのが「あーそうですかぁ」という感じ。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        // connection:didFailWithError: 相当の処理
        return;
    }
    // connectionDidFinishLoading: 相当の処理
}

だいたいどんなことをしたか。

サブスレッドでNSURLConnectionを動かすために、NSRunLoopとwhileを組み合わせて待機させていた。 また、そのサブスレッドの中で同期的な動きをするところに依存していた。

これをNSURLSessionに書き換えるとき、RunLoopを使う必要がないので、NSConditionでwaitをかけて再現した。

// before
- (void) download:(NSURLRequest *)request {
    // befor
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    while(!self.completed && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) ;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    self.completed = YES;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    self.completed = YES;
}
// after
- (void) download:(NSURLRequest *)request {
    NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
    self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    self.task = [self.session dataTaskWithRequest:request];
    [self.task resume];
    
    [self.condition lock];
    [self.condition wait];
    [self.condiiton unlock];
    self.condition = nil;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    [self.condition lock];
    [self.condition signal];
    [self.condition unlock];
}

delegateQueuenilを与えているのはどこでもいいから他のスレッドで処理してもらうため。 後続処理のwaitによってsessionを作るスレッドはスリープしてしまう。

Rust Vec<T>を特定の長さで分割する

背景

gifとかzipとかパースしたいなあ+Rust触りたいなあ! ということで、まずはバイナリダンプから始めようとした。

以下のようなフォーマットで標準出力に吐き出したい。

50 4b 03 04 00 00 00 00 00 00 00 00 00 00 00 00 | PK.............. 

一行出力するにあたり、バイナリのほか、一時的に格納した文字列情報が必要。

最初はfor文に食わせて、愚直にカウンタ制御でVecに文字を詰めたり改行して実装した。 当然ながら、どこまで出力されているかを意識してコードを書かなければならない。

これではしんどいので、octet単位ではなく行単位の出力で考えたい。

目的

Vec<u8> *1 を二次元配列的に扱えるように変換する。

結論

let lines: Vec<&[u8]> = binary.chunks(BYTES_IN_LINE).collect();

各行の長さをあとで変えたり、所有権も持たせたくてVec<Vec<u8>>が良いならば以下のとおり。

const BYTES_IN_LINE: usize = 16;
let lines: Vec<Vec<u8>> = binary.chunks(BYTES_IN_LINE).map(|chunk| chunk.to_vec()).collect();

余談

.chunksを探しに行く前に、愚直に作る処理を書いてみたのだけれど、これが数行で済むのだから便利だなぁ。

const BYTES_IN_LINE: usize = 16;
let lines: &mut Vec<Vec<u8>> = &mut vec![];
let mut line: Vec<u8> = vec![];
for octet in binary {
    line.push(octet.clone());
    if line.len() >= BYTES_IN_LINE {
        lines.push(line);
        line = vec![];
    }
}
if line.len() > 0 {
    lines.push(line);
}

*1:軽量なデータから始めるので、fs.read(path) -> Vecを使う。

DeployGateのコマンドラインツールから Commands::Deploy Error: NoMethodError と怒られる件

環境

  • ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
  • deploygate-cli v0.6.4
  • Xcode 9.4.1

現象

dg deployを実行すると、適切にBundle Identifierを指定しているにも関わらず、Bundle Identifierの入力を求められたのち、 下記のエラーが発生する。

Status
deploygate-cli ver 0.6.4

Error message
undefined method `build_configuration_list' for nil:NilClass

Backtrace
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/xcode/analyze.rb:145:in `target_build_configration'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/xcode/analyze.rb:91:in `target_xcode_setting_provisioning_profile_uuid'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/commands/deploy/build.rb:50:in `ios'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/commands/deploy/build.rb:22:in `run'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/commands/deploy.rb:16:in `run'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/command_builder.rb:70:in `block (2 levels) in run'
/path/to/gemroot/gems/commander-4.4.5/lib/commander/command.rb:182:in `call'
/path/to/gemroot/gems/commander-4.4.5/lib/commander/command.rb:153:in `run'
/path/to/gemroot/gems/commander-4.4.5/lib/commander/runner.rb:446:in `run_active_command'
/path/to/gemroot/gems/fastlane-2.57.2/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:64:in `run!'
/path/to/gemroot/gems/commander-4.4.5/lib/commander/delegates.rb:15:in `run!'
/path/to/gemroot/gems/deploygate-0.6.4/lib/deploygate/command_builder.rb:128:in `run'
/path/to/gemroot/gems/deploygate-0.6.4/bin/dg:6:in `<top (required)>'
/path/to/gemroot/bin/dg:23:in `load'
/path/to/gemroot/bin/dg:23:in `<main>'
/path/to/gemroot/bin/ruby_executable_hooks:15:in `eval'
/path/to/gemroot/bin/ruby_executable_hooks:15:in `<main>'

解消方法

Xcodeのメニューバーから、[Product]> [Scheme]> [Manage Schemes ...] を開き、閉じる。

原因

  • lib/deploygate/xcode/analyze.rbが必要とする、*.xcschemeが存在しないため

調査記録

なんだか業務issue感さえ漂うので、何を調べたのか書き残す。余談。

Backtraceいわく、target_build_configrationの処理中に問題が発生したことがわかる。

def target_build_configration
  target_project_setting.build_configuration_list.build_configurations.reject{|conf| conf.name != @build_configuration}.first
end

https://github.com/DeployGate/deploygate-cli/blob/v0.6.4/lib/deploygate/xcode/analyze.rb#L145

undefined method 'build_configuration_list' for nil:NilClassということから、target_project_settingの結果がnilであることが直接の問題であるとわかる。

def target_project_setting
  scheme_file = find_xcschemes
  xs = Xcodeproj::XCScheme.new(scheme_file)
  target_name = xs.profile_action.buildable_product_runnable.buildable_reference.target_name


  target_project.native_targets.reject{|target| target.name != target_name}.first
end

https://github.com/DeployGate/deploygate-cli/blob/v0.6.4/lib/deploygate/xcode/analyze.rb#L153-L155

ではtarget_project_settingの中身で何が起きているのか確認するため、scheme_file, xs, target_nameをprintしてみたところ、すべてnilであることが確認できた。

この時点で、まずfind_xcschemesが怪しいと直感を得た。(悪い癖)

native_targets.reject{|target| target.name != target_name}を見て、「ああ、target_nameがnilになっちゃって正しいtargetも捨てちゃってるんだな」という認識を挟むのが正しい怪しみ方だろう。

def find_xcschemes
  shared_schemes = Dir[File.join(@xcodeproj, 'xcshareddata', 'xcschemes', '*.xcscheme')].reject do |scheme|
    @scheme != File.basename(scheme, '.xcscheme')
  end
  user_schemes = Dir[File.join(@xcodeproj, 'xcuserdata', '*.xcuserdatad', 'xcschemes', '*.xcscheme')].reject do |scheme|
    @scheme != File.basename(scheme, '.xcscheme')
  end


  shared_schemes.concat(user_schemes).first
end

https://github.com/DeployGate/deploygate-cli/blob/v0.6.4/lib/deploygate/xcode/analyze.rb#L165-L170

で、find_xcschemesの中にあるshared_schemes, user_schemesもprintしてみればこっちもnilだと確認した。 というかブロックの中に入っていなかった。

File.joinで組んでいるパターンをターミナルからも確かめた結果、*.xcschemeが生成されていないことがわかった。 おそらくこれが原因だろうと、schemeを作る手段を求めて上記解消方法を試したところ、*.xcschemeが生成されたことを確認できた。

関連リンク

解消手順を報告させてもらったissue

UITextFieldの内容をUserDefaultsに自動保存する。(+ 保存用のキーをInterfaceBuilderで設定する)

寄り道を経てカスタムクラスの最適解に降り立った気がする。

This UITextField is save to UserDefaults when edit ...

編集終了のタイミングでUserDefaultsに書き込んでくれる。

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textField: RestorableTextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.textField.restoreByUserDefaults()
    }
}

Interface-Builderで紐づけたら、あとは呼び出すだけ。

リファクタリング「リトル・レディ」 #1 画像ボタンをホバーで差し替える処理

約3年も前の話、「リトル・レディ」という作品をChromeで動くようにしたい! という思いのもと、勝手に復活開発日誌を始めた。

yajamon.hatenablog.com

yajamon.hatenablog.com

この開発は一気に進み、2年弱放置の後に完結宣言が行われた。

この記事は?

この記事はいわば番外編。今風のコードに書き換えていく活動の上で何をしたか記録していく。

目下目的はHTML5に馴染む形に書き換えること。野心はSingle Page Applicationにすること。 自分でアプリケーションを設計して、脚本を読み込むようにする。 そして、リトル・レディの脚本を抽出・変換ツールを用意することで、自分以外の人に体験してもらえる状況に持ち込むのが到達点。

画像の差し替え処理を書き換える

スタート画面では、<a>タグをボタンに見立て、中に画像を配置している。

/index.htm

<script language="JavaScript">
// プリロード
if(document.images) {
    var imgPreload1 = new Image() ; imgPreload1.src = "image/basis/menu1-2.png";
    // ...つづく
}
</script>
<a href="javascript:topS()" onMouseOver="menu1.src='image/basis/menu1-2.png'" onMouseOut="menu1.src='image/basis/menu1-1.png'"><img SRC="image/basis/menu1-1.png" border=0 name="menu1"></a>
<!-- つづく -->

<img>name属性で識別され、ボタンへのマウスオーバー(アウト)、で差し替えている。 スムーズに差し替えられるよう、ホバー用の画像はコード上で食わせて、プリロードとしている。

body以下のツリーに足したりしていないので、現代のブラウザでプリロードが行われるのか不安だったが、問題なくプリロードされているようだった。

修正後

<style>
    .btn-base, .btn-hover {
        border : 0;
    }
    .btn .btn-base {
        display: inline-block;
    }
    .btn:hover .btn-base {
        display: none;
    }
    .btn .btn-hover {
        display: none;
    }
    .btn:hover .btn-hover {
        display: inline-block;
    }
</style>
<a href="javascript:topS()" class="btn" ><img class="btn-base" src="image/basis/menu1-1.png"><img class="btn-hover" src="image/basis/menu1-2.png"></a>
<!-- つづく -->

<a>の中に通常時、ホバー時の画像を指定してしまい、CSS:hover疑似クラスとdisplay:要素で表示制御するようにした。

行数は多少増えてしまった感があるが、HTMLにハードコードされたonMouseOver, onMouseOutを排除することができた。 また、ボタンの数に対する編集箇所が一箇所に集まったのでこの方法が好きかな。(今後増えたりする見込みはないけれど…)

ほかにもCSSでボタンごとにa.btn-start:hover { background-image: url(...) }とかも考えたけれど、気にするところが増えたのでやめた。*1, *2

余談

完結宣言から1年経って手を付け始めたのだけれど、タイトルが予言されてて笑ってしまった。

これから手を加えるのは、より現代ブラウザ向けの修正になるので、"リファクタリング:リトル・レディ"とかのタイトルでやろうかな。

yajamon.hatenablog.com

*1:ボタンの数だけスタイルとHTMLに対処を書き込む必要があったり

*2:widthどうするん…画像サイズに合わせなきゃ…など

Chromeの開発者ツールで選択した要素のHTMLElementを教えてくれるやつ、Ctrl+Shift+C がショートカットキーだった。

Chrome 67 で確認。 MacだとCmd+Shift+Cね。

URLとタイトルをひとまとめにクリップボードへ突っ込んでくれるChrome拡張がお亡くなりになったみたいで、 自作するしかないかー、そういえば空いてるショートカットキーはあるかななどと触ってたら動いて驚いた。